このページでは、Python で tkinter を利用した簡単な「横スクロールアクションゲーム」の作り方を解説していきます。
このページは「横スクロールアクションゲームの作り方の解説」の7ページ目となります。
6ページ目は下記ページとなり、6ページ目では主にゴールの作成と大雑把な当たり判定&ゲームクリア画面表示の解説を行なっています。
【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)また、このページでは、上記ページの このページで作成したスクリプト で紹介しているスクリプトを変更していきながらゲームを作成していきます。
ですので、事前に上記のページを読んでおくことをオススメします。
この「横スクロールアクションゲームの作り方の解説」の7ページ目では、まず敵キャラクターの作成を行い、操作キャラクターが敵キャラクターを倒したときの処理や敵キャラクターに操作キャラクターが倒されたときの処理、さらには操作キャラクターが倒された際のゲームオーバー画面の表示等の機能を実現していきたいと思います。
このページは基本編の最後の解説ページであり、横スクロールアクションゲームの基本的な部分はこのページで全て作成完了となります!
Contents
敵キャラクターの作成
まずは敵キャラクターを作成していたいと思います!
今まで作り込んできたキャラクターを実現する Character
クラスのサブクラスとして、敵キャラクターを実現する Enemy
クラス、さらにその Enemy
クラスのサブクラスとして CatEnemy
と DogEnemy
クラスを用意しています。
わざわざ CatEnemy
クラスと DogEnemy
クラスに分けているのは、描画されるキャラクターの画像やキャラクターのサイズ、ジャンプの高さ等を別々に設定できるようにするためです。
ただし、このページでスクリプトを変更していくベースとなる 下記ページの このページで作成したスクリプト では、Enemy
クラス、CatEnemy
クラス、DogEnemy
クラスはまだ空っぽのクラスとなっています。
まずは、主にこれらの Enemy
クラス、さらには CatEnemy
クラスと DogEnemy
クラスを作り込んでいくことで、敵キャラクターを作成していきたいと思います。
CatEnemy
クラスの作成
まずはサブクラス側の CatEnemy
クラスを作成したいと思います。
この CatEnemy
は猫型の敵キャラクターを実現することを想定したクラスとなります。
CatEnemy
クラスの作り方
CatEnemy
クラスに関しても基本は Goal
クラスの時と同様で、__init__
の中でまずスーパークラスの __init__
を実行し、猫型敵キャラクター表示用の画像オブジェクトの生成を行います。
そしてその後、必要に応じてデータ属性の上書きを行うことで、このキャラクター独自のカスタマイズ(動作する速度やジャンプの高さ等の設定)を行なっていけば良いです。
現状どのようなカスタマイズができるについては、下記ページの サブクラスのカスタマイズ で解説していますので、忘れてしまった方はこちらを参照していただければと思います。
【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)猫型敵キャラクター用の画像の準備
また、これもゴールの時と同様で、画面に描画する際に用いる猫型敵キャラクター用の画像を事前に準備しておく必要があります。
詳細は下記のページで解説しますが、背景の透明度を利用すれば詳細な当たり判定が行えるため、背景は透明に設定されている画像が望ましいです(透明度が設定されていない場合は、大雑把な当たり判定のみが行われることになります)。
【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)このページでは いらすとや の下記 URL の画像を猫型敵キャラクター用の画像として利用していきたいと思います。
https://www.irasutoya.com/2019/09/blog-post_188.html
下の図は上記 URL の画像を転載させていただいたものになります。
転載元:いらすとや
__init__
の変更
画像が準備できれば __init__
の変更を行なっていきましょう!
今回は、CatEnemy
クラスの __init__
は下記のように変更したいと思います。
class CatEnemy(Enemy):
def __init__(self):
#↓これは削除
#pass
#↑これは削除
#↓これを追加
super().__init__("cat_wink_brown.png", (80, 80))
self.jump_height = 70
self.speed_x = 20
#↑これを追加
super().__init__
の第1引数の "cat_wink_brown.png"
は猫型敵キャラクターの画像のファイルパスになりますので、ご自身が用意された画像のファイル名や保存したフォルダ名に合わせて修正してください。
また第2引数は、第1引数で指定した画像の拡大縮小に用いる矩形のサイズとなります。この矩形に合わせて画像の縦横比を保ったまま画像が拡大縮小され、それが画面に描画されることになります。
操作キャラクターの拡大縮小時に用いる矩形のサイズは (100, 100)
なので、上記のように指定することで猫型敵キャラクターは操作キャラクターよりもちょっと小さめの画像として描画されることとなります。
さらに、super().__init__
実行後は猫型敵キャラクター専用のカスタマイズを行なっており、例えば jump_height
を 70
に設定しています。
操作キャラクターの jump_height
は 150
ですので、操作キャラクターに比べると猫型敵キャラクターのジャンプの高さは半分以下ということになります。
こんな感じで操作キャラクターと比べながら、猫型敵キャラクターとしてどのように動作して欲しいかを考えていくとカスタマイズしやすいと思います。また、上記のカスタマイズは一例ですので、ご自身でご自由に設定していただいて問題ないです。
また、上記では super().__init__
を実行しており、これによりスーパークラスの __init__
が実行されることになります。
CatEnemy
クラスのスーパークラスは Enemy
であり、Enemy
クラスの __init__
はまだ実装していませんので、上記の変更を行なってもスクリプトがうまく動作しない(エラーになる)ので注意してください。次の DogEnemy
クラスを作成した後に、Enemy
クラスの __init__
を実装していきたいと思います。
スポンサーリンク
DogEnemy
クラスの作成
次はもう1つのサブクラスである DogEnemy
クラスを作成したいと思います。
この DogEnemy
は犬型の敵キャラクターを実現することを想定したクラスとなります。
DogEnemy
クラスの作り方
DogEnemy
クラスは CatEnemy
クラスとほぼ同様の作り方で作成しますので、ここでの解説は省略させていただきます。
犬型敵キャラクター用の画像の準備
これも猫型敵キャラクター同様、犬型敵キャラクター用の画像を事前に準備しておく必要があります。
このページでは いらすとや の下記 URL の画像を犬型敵キャラクター用の画像として利用していきたいと思います。
https://www.irasutoya.com/2014/06/blog-post_5759.html
下の図は上記 URL の画像を転載させていただいたものになります。
転載元:いらすとや
__init__
の変更
画像が準備できれば __init__
の変更を行なっていきます。
今回は、DogEnemy
クラスの __init__
は下記のように変更したいと思います。
class DogEnemy(Enemy):
def __init__(self):
#↓これは削除
#pass
#↑これは削除
#↓これを追加
super().__init__("jitensya_inu.png", (80, 80), False)
self.jump_height = 50
self.speed_x = 25
#↑これを追加
ポイントや注意事項に関しては CatEnemy
クラスの時と同様ですが、前述で用意した犬型敵キャラクター用の画像が左を向いているため、super().__init__
の第3引数に False
を設定するようにしています。
Enemy
クラスの作成
続いて Enemy
クラスを作成していきます。
Enemy
クラスの作り方
まず前提として、Enemy
クラスは前述の通り CatEnemy
クラスと DogEnemy
クラスのスーパークラスであり、Character
クラスのサブクラスになります。
CatEnemy
クラスと DogEnemy
クラスの __init__
から Enemy
クラスの __init__
が実行されますが、本当にここで実行したいのは Character
クラスの __init__
です(Character
クラスの __init__
で画像オブジェクトが生成され、各種データ属性の共通設定が行われる)。
ですので Enemy
クラスでは、サブクラスから __init__
が実行された際に引数で受け取ったデータをそのまま引数に指定して Character
クラスの __init__
を実行するようにしたいと思います。
さらに Character
クラスの __init__
実行後に Enemy
クラスの __init__
でデータ属性の設定を行い、敵キャラクター共通のカスタマイズを行うようにしたいと思います。
また、敵キャラクター共通で必要、かつ、他のキャラクターに不要なメソッドに関しては、この Enemy
クラスのメソッドとして作成していくようにしたいと思います。
__init__
の変更
まずは、__init__
の変更のみを行いたいと思います。
今回は、Enemy
クラスの __init__
は下記のように変更したいと思います。
class Enemy(Character):
def __init__(self, path, size, is_right=True): #←ここを変更
#↓これは削除
#pass
#↑これは削除
#↓これを追加
super().__init__(path, size, is_right)
self.x = random.randrange(300, GAME_WIDTH - 300)
self.direction = Character.DIRECTION_LEFT
#↑これを追加
引数を変更しているので注意してください。この引数は Character
クラスの __init__
と同様のものになります。
また、キャラクターの横方向の位置を示すデータ属性 x
に関しては random モジュールを利用してランダムに設定するようにしています。これはゲーム開始直後の敵キャラクターの位置にランダム性を持たせるためです。
また、敵キャラクターと操作キャラクターがいきなりぶつからないようにするため、ある程度中心部分に敵キャラクターの位置が設定されるよう randrange
関数の引数を調整しています(GAME_WIDTH
はゲーム画面の幅を表す値になります。300
はてきとうです…)。
敵キャラクターオブジェクトの生成
各クラスの __init__
が大体出来上がりましたので、次は実際に CatEnemy
クラスと DogEnemy
クラスのオブジェクトの生成を行なっていきたいと思います。
具体的には、Game
クラスの __init__
を下記のように変更します。
class Game:
def __init__(self, master):
#略
self.characters = []
self.player = Player()
self.characters.append(self.player)
goal = Goal()
self.characters.append(goal)
#↓これを追加
for _ in range(3):
enemy = CatEnemy()
self.characters.append(enemy)
for _ in range(3):
enemy = DogEnemy()
self.characters.append(enemy)
#↑これを追加
#略
上記の変更により、Game
クラスのオブジェクト生成時に CatEnemy
クラスと DogEnemy
クラスのオブジェクトがそれぞれ 3
つずつ生成されることになります。range
の引数を変更すれば、生成されるオブジェクトの数を変更することも可能です。が、あまり多いと処理が重くなってゲームの動きが遅くなるので注意してください。
また、生成されたオブジェクトを Game
クラスのデータ属性のリスト characters
に追加しているところがポイントになります。
このリスト characters
は、全てのキャラクターのオブジェクトを管理するリストになり、このリストに格納されているオブジェクトに対して下記の処理が実行されるようになっています。
- 画面への描画(
Game
クラスのupdate
メソッド) - キャラクターの状態の更新(
Game
クラスのupdate
メソッド) - キャラクターの当たり判定(
Game
クラスのcollisionDetect
メソッド)- (判定のみで当たった時の処理は未実装)
ですので、ここまでの変更を行なっただけで、敵キャラクターの画像が画面に描画されるようになっています。
ただし、リスト characters
に敵キャラクターのオブジェクトを追加したとしても、下記の処理は実行されません。
- 敵キャラクターの移動
- 敵キャラクターが当たったときの処理
前者に関していうと、現状移動するキャラクターは操作キャラクターのみであり、さらにこのキャラクターはキーボードのキー入力に応じて移動するようになっています。
流石に敵キャラクターもキー入力で移動してしまうのはおかしいので、他の手段で敵キャラクターを移動させるようにしていく必要があります。
また後者に関していうと、既にリスト characters
で管理されているオブジェクトのキャラクター全てに対して当たり判定が行われ、”当たっている” と判断された際には下記の collide
メソッドが実行されるようになっています。
つまり、上記の変更で生成した敵キャラクターのオブジェクトに対しても下記 collide
メソッドが実行されています(他のキャラクターと当たった場合は)。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
ですが、この collide
メソッドを見ていただければわかる通り、Player
クラスと Goal
クラスのオブジェクトが当たった時の処理しか記述されていないので、それ以外のオブジェクトが当たった場合は何も行われないことになります。
ですので、敵キャラクターと他のキャラクターが当たった際に何かしらの処理が実行されるようにするためには、Enemy
クラスのオブジェクトが当たった際の処理を上記の collide
に追記してやる必要があります。
以降では、まず敵キャラクターが移動できるようにし、その後敵キャラクターが当たった際の処理の実装を行なっていきたいと思います。
スポンサーリンク
敵キャラクターの移動
では敵キャラクターの移動を実現していきたいと思います。
操作キャラクターはキーボードのキー入力で移動を行うようにしましたが、敵キャラクターに関しては定期的に移動を行うようにしたいと思います。
定期的な処理は after
メソッドを用いれば実現することができるのですが、下記ページでの キャラクターのジャンプ で既に Character
クラスの update
メソッドが定期的に実行されるようになっていますので、この update
メソッドを利用して敵キャラクターの移動を実現していきたいと思います。
ただし、Character
クラスの update
メソッドは当然 Character
クラスのサブクラスで共通のメソッドとなります。従って、この update
メソッドを変更してキャラクターの移動を行うようにしてしまうと、操作キャラクターやゴールまで定期的に移動するようになってしまいます。
そのため、Character
クラスの update
メソッドを直接変更するのではなく、この update
メソッドをオーバーライドする形で Enemy
クラスに update
メソッドを追加したいと思います。
Enemy
クラスのメソッドで敵キャラクターの移動を実現するわけですから、CatEnemy
クラスと DogEnemy
クラスのオブジェクトは同様の方法で移動が行われることになります。もし個別に移動方法を変更したいのであれば、CatEnemy
クラスと DogEnemy
クラスそれぞれに update
メソッドを追加すれば良いです。
また、キャラクターの移動は Character
クラスの move
メソッドにより行うことができますので、とりあえず敵キャラクターを現在向いている方向に移動するのであれば、敵キャラクターの移動は Enemy
クラスに下記のような update
メソッドを追加すれば良いことになります。
class Enemy(Character):
#↓これを追加
def update(self):
self.move(self.direction)
super().update()
#↑これを追加
前述の通り、各キャラクターのオブジェクトに対する update
メソッドは既に定期的に実行されるようになっているため、上記の変更を行うだけで敵キャラクターが移動するようになります。
データ属性 direction
は、現在キャラクターが向いている方向を示すものになりますので、このメソッドが実行されるたびに敵キャラクターが向いてる方向に移動していくことになります。
ただし、これだと同じ方向にずっと移動するので、敵キャラクターが全てゲーム画面の端に集まってしまうことになります。
これだとイマイチなので、敵キャラクターは画面の端まで移動したら逆方向に移動するようにしていきたいと思います。さらにランダムなタイミングでジャンプも行うようにしたいと思います。
このような移動を行わせるためには、先ほど追加した Enemy
クラスの update
メソッドを下記のように変更すれば良いです。
class Enemy(Character):
def update(self):
self.move(self.direction)
#↓これを追加
if random.randrange(10) % 10 == 0:
self.move(Character.DIRECTION_UP)
if self.x == 0:
self.direction = Character.DIRECTION_RIGHT
elif self.x == GAME_WIDTH - self.width:
self.direction = Character.DIRECTION_LEFT
#↑これを追加
super().update()
ゲーム画面の端まで敵キャラクターが到達した際に移動方向を反転させるようにしています。
また、random モジュールを利用し、10
回に 1
回くらいの割合でジャンプするような制御も行なっています。
さらに、上記の Enemy
クラスの update
メソッドの最後では、スーパークラスである Character
クラスの update
を実行していますので、ジャンプした際の敵キャラクターの縦方向の位置変更も同時に実行されることになります。
他の敵キャラクターと当たった時の処理
敵キャラクターが移動できるようになったので、次は敵キャラクターが当たった時の処理を実装していきたいと思います。
前述でも触れましたが、現状敵キャラクターに対しても当たり判定が行われ、当たった際には下記ページの ゲームクリアの判定 で追加した Game
クラスの collide
メソッドが実行されるようになっています。
前述でも紹介しましたが、この collide
メソッドは現状下記のようになっています。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
第1引数は1つ目のキャラクターのオブジェクトであり、第2引数は2つ目のキャラクターのオブジェクトです。もう少し具体的に言えば、第1引数には移動直後のキャラクターのオブジェクト、第2引数にはその移動直後のキャラクターに当たったキャラクターのオブジェクトが指定されるようになっています。
さらに、この2つのオブジェクトがどのクラスのオブジェクトであるかを調べ、クラスに応じた処理を実行するようにしています。
同様にして、敵キャラクターが当たった時の処理を実装していくのですが、敵キャラクターが当たる相手としては下記の3パターンが考えられます。
- 他の敵キャラクター
- ゴール
- 操作キャラクター
ここからは、上記の3パターンそれぞれに対し、当たった時の動作が実行されるよう上記の collide
メソッドを変更していきたいと思います。
まずは、敵キャラクターが他の敵キャラクターと当たった時の処理を実装していきます。
敵キャラクターが他の敵キャラクターと当たった時は、それぞれのキャラクターが遠のく方向に移動するようにしたいと思います。
キャラクター同士が向き合っている場合
もし当たったキャラクター同士が向き合っている場合は、両方のキャラクターを現在の移動方向とは逆の方向に移動するようにしたいと思います。
キャラクターの移動は Character
クラスの move
メソッドにより行えますので、このような動作は、collide
メソッドを下記のように変更することで実現することができます。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
#↓これを追加
elif isinstance(character, Enemy) and isinstance(opponent, Enemy):
if character.direction != opponent.direction:
if character.direction == Character.DIRECTION_LEFT:
character.move(Character.DIRECTION_RIGHT)
opponent.move(Character.DIRECTION_LEFT)
else:
character.move(Character.DIRECTION_LEFT)
opponent.move(Character.DIRECTION_RIGHT)
#↑これを追加
ポイントは、キャラクターが向いている方向をデータ属性 direction
から判断し、その方向とは逆の方向を move
メソッドに指定するところだと思います。
実は、上記の判断だけだとそっぽ向き合った状態で敵キャラクターと当たった時も追記した処理が行われてしまいます。が、そのようなケースは少ないと思いますので、今回は上記の条件のみで判断するようにしたいと思います。
また、実際にスクリプトを実行してみると分かると思いますが、敵キャラクター同士が当たっていないのに逆方向に移動し始めるように見える場合があります。なので、本当はもう少し座標等の調整を行った方が良いのですが、こだわり出すとキリがないので、今回はここは妥協させていただこうと思います。
スポンサーリンク
キャラクター同士が同じ方向に移動している場合
また、キャラクター同士が同じ方向に移動している場合に当たった際は、移動方向に対して後ろ側に存在するキャラクターを逆方向に移動させるようにしたいと思います。
このような動作は、collide
メソッドを下記のように変更することで実現することができます。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
elif isinstance(character, Enemy) and isinstance(opponent, Enemy):
if character.direction != opponent.direction:
if character.direction == Character.DIRECTION_LEFT:
character.move(Character.DIRECTION_RIGHT)
opponent.move(Character.DIRECTION_LEFT)
else:
character.move(Character.DIRECTION_LEFT)
opponent.move(Character.DIRECTION_RIGHT)
#↓これを追加
else:
if character.direction == Character.DIRECTION_LEFT:
if character.x < opponent.x:
opponent.move(Character.DIRECTION_RIGHT)
else:
character.move(Character.DIRECTION_RIGHT)
else:
if character.x > opponent.x:
opponent.move(Character.DIRECTION_LEFT)
else:
character.move(Character.DIRECTION_LEFT)
#↑これを追加
今回は方向だけでなく、データ属性 x
を比較して、どちらのキャラクターが後ろ側にいるのかを判断してから move
メソッドを実行するようにしています。
かなり collide
メソッドが複雑になってきたので別メソッドに処理を切り出すようにした方が良いのですが、今回は解説を進めることを優先し、このまま collide
メソッドに処理を追記していきたいと思います。
ここまでの collide
メソッドの変更を加えた状態でスクリプトを実行すれば、敵キャラクター同士が当たった時にお互いが遠のくのように移動するようになったことが確認できると思います。
ゴールと当たった時の処理
次は敵キャラクターがゴールと当たった時の処理を実装していきたいと思います。
ゴールは移動しない想定なので、敵キャラクターがゴールとぶつかった場合は、敵キャラクターを移動方向の逆方向に移動させるようにしたいと思います。
このような動作は、collide
メソッドを下記のように変更することで実現することができます。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
elif isinstance(character, Enemy) and isinstance(opponent, Enemy):
#略
#↓これを追加
elif isinstance(character, Enemy) and isinstance(opponent, Goal):
if character.direction == Character.DIRECTION_LEFT:
character.move(Character.DIRECTION_RIGHT)
else:
character.move(Character.DIRECTION_LEFT)
elif isinstance(character, Goal) and isinstance(opponent, Enemy):
if opponent.direction == Character.DIRECTION_LEFT:
opponent.move(Character.DIRECTION_RIGHT)
else:
opponent.move(Character.DIRECTION_LEFT)
#↑これを追加
ゴールは移動しないので、引数 characte
が Goal
クラスのオブジェクトになることはないかもしれませんが、一応その場合も考慮して elif
文を2つ追加しています。
上記の collide
メソッドの変更を行なった後にスクリプトを実行すれば、敵キャラクターがゴールに当たった際に敵キャラクターがゴールとは反対の方向に移動するようになったことが確認できると思います。
ただ、この当たった時の処理の作りも甘くて、敵キャラクターが一度ゴールにめり込んでしまうと抜け出せなくなる可能性があります…。
操作キャラクターと当たった時の処理
敵キャラクターが当たった時の処理として、最後に操作キャラクターと当たった時の処理を実装していきたいと思います。
ここまで敵キャラクター側の視点から考えてきましたが、ここからは操作キャラクター側の視点で考えていきたいと思います(おそらくこっちの方がわかりやすいと思いますので)。
操作キャラクターが敵キャラクターと当たった時の処理としては下記の2つを実装したいと思います。
- 操作キャラクターが敵キャラクターを倒す
- 操作キャラクターが敵キャラクターに倒される
前者は操作キャラクターが敵キャラクターを踏みつける形で当たった時の処理とし、後者はそれ以外の形で当たった時の処理としたいと思います。
スポンサーリンク
踏みつけたかどうかの判断
さて、上記のように2つの場合の処理を切り替えて実行するためには、2つのキャラクターが当たった時に、操作キャラクターが敵キャラクターを踏みつけたかどうかを判断できるようにする必要があります。
今回は簡単に下記の2条件を満たした際に、操作キャラクターが敵キャラクターを踏みつけたと判断するようにしたいと思います。
- 操作キャラクターがジャンプ中(上昇中・降下中のいずれか)
- 敵キャラクターと当たったのが操作キャラクターの下半分のみ
前者に関しては、Characte
クラスの jump_state
により判断することができます。
また、2つのキャラクターが重なった領域の始点と終点は下記ページの キャラクター同士の当たり判定 で解説している方法で求めることができます。
【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)ですので、後者の “当たったのが操作キャラクターの下半分のみ” であるかどうかは、操作キャラクターの中心座標が重なった領域の始点よりも上側に存在するかどうかで判断することができます。
したがって、操作キャラクターが他のキャラクターを踏みつけたかどうかは、下記の isTrampling
によって判断することができます。
class Character:
#↓これを追加
def isTrampling(self, opponent):
if self.jump_state == Character.JUMP_NO:
return False
sy = max(self.y, opponent.y)
if self.y + self.height / 2 < sy:
return True
else:
return False
#↑これを追加
isTrampling
は、メソッドを実行したオブジェクトが引数 opponent
のオブジェクトを踏みつけたかどうかを判断するメソッドで、踏みつけた場合は True
を、踏みつけられなかった場合は False
を返却します。
今回他のキャラクターを踏みつけるのは操作キャラクターのみを想定していますが、別に敵キャラクターが他の敵キャラクターを踏みつけるような動作もあり得ないこともないので、上記のように isTrampling
メソッドは Character
クラスに追加しています。
また、上記における sy
が重なった領域の始点の縦方向の座標となります。この座標の求め方を忘れてしまった方は下記ページの キャラクター同士の当たり判定 を参照していただければと思います。
さらに、ゲーム画面(キャンバス)においては、縦方向の正方向は下方向になります。ですので、縦方向の座標が大きいほど下側の座標であることになります。したがって、下記が成立した際は、self
(メソッドを実行したオブジェクト)の中心座標が sy
よりも上側にあると判断することができます(この時は self
が opponent
を踏みつけたことになる)。
if self.y + self.height / 2 < sy:
縦方向の座標の扱いが若干ややこしいので注意してください。
操作キャラクターが敵キャラクターを倒す
先程作成した Charactr
クラスの isTrampling
メソッドにより、操作キャラクターが敵キャラクターを踏みつけたかどうかを判断することができるようになりました。
続いては、操作キャラクターが敵キャラクターを踏みつけた時の処理を実装していきたいと思います。
この場合、敵キャラクターは踏みつけられたので、操作キャラクターによって倒されたことになります。
キャラクターの状態を倒された状態に遷移
この際には、まず踏みつけられた敵キャラクターの状態を “倒された状態” に遷移させるようにしたいと思います。
この辺りの話は下記ページの キャラクターの状態管理 で解説しているのですが、Character
クラスではキャラクターの状態を管理するデータ属性 state
を持っています。
さらに、このデータ属性 state
には、Character
クラスの下記の3つの状態を表すクラス変数を設定するようになっています。
Character.STATE_NORMAL
:通常状態Character.STATE_CLEAR
:ゲームクリア状態Character.STATE_DEFEATED
:倒された状態
敵キャラクターが操作キャラクターによって踏みつけられた際には、その敵キャラクターは倒されたことになるので、このキャラクターのデータ属性 state
を Character.STATE_DEFEATED
に遷移させるようにしていきます。
まずは前準備として、このキャラクターの状態を遷移させるメソッドを用意したいと思います。
具体的には、Character
クラスに下記の defeated
メソッドを用意します。
class Character:
#↓これを追加
def defeated(self):
self.state = Character.STATE_DEFEATED
#↑これを追加
以降で操作キャラクターも敵キャラクターに倒されるようになることを見越して、defeated
メソッドは Character
クラスに追加しています。
敵キャラクターが踏みつけられた時の処理の実装
続いて Game
クラスの collide
メソッドの変更を行い、操作キャラクターが敵キャラクターを踏みつけた時の処理を実装していきたいと思います(collide
メソッドはキャラクター同士が当たった時のみ実行されるようになっています)。
ここではとりあえず、踏みつけられた敵キャラクターの状態を “倒された状態” に遷移させる処理を行うようにしたいと思います。
操作キャラクターが敵キャラクターを踏みつけたかどうかは、操作キャラクターに Character
クラスの isTrampling
を実行させることで判断することができます(isTrampling
の引数には当たった敵キャラクターのオブジェクトを指定する)。
また、isTrampling
が True
を返却した場合、引数に指定した敵キャラクターは倒されたことになりますので、この際には引数で指定した敵キャラクターに Character
クラスの defeated
を実行させて状態を “倒された状態” に遷移させます。
これらの処理は、Game
クラスの collide
メソッドを下記のように変更することで実現することができます。
class Game:
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
#略
elif isinstance(character, Goal) and isinstance(opponent, Enemy):
if opponent.direction == Character.DIRECTION_LEFT:
opponent.move(Character.DIRECTION_RIGHT)
else:
opponent.move(Character.DIRECTION_LEFT)
#↓これを追加
elif isinstance(character, Player) and isinstance(opponent, Enemy):
if character.isTrampling(opponent):
opponent.defeated()
else:
pass
elif isinstance(character, Enemy) and isinstance(opponent, Player):
if opponent.isTrampling(character):
character.defeated()
else:
pass
#↑これを追加
collide
の引数 character
には移動直後のキャラクターのオブジェクト、さらに引数 opponent
にはその移動直後のキャラクターと当たったオブジェクトが指定されます。したがって、character
が操作キャラクターの場合もありますが、character
が敵キャラクターの場合もあります。
その一方で、isTrampling
メソッドを実行するのは操作キャラクターのオブジェクトである必要があります。そのため、どちらの引数が操作キャラクターのオブジェクトであるかによって isTrampling
メソッドを実行させるオブジェクトを character
or opponent
で切り替える必要があるので注意が必要です。
また、上記の collide
メソッドの中で pass
のみを記述している部分がありますが、ここは操作キャラクターが倒された時の処理になります。この部分は後ほど実装していきます。
以上の変更により、操作キャラクターが敵キャラクターを踏みつけた際に、踏みつけられた敵キャラクターの状態が倒された状態に遷移するようになります。
ただ、これだけだと状態が変わるだけなので視覚的には何の変化もありません。視覚的に敵キャラクターが倒されたことが分かるよう、次は倒された状態のキャラクターの移動と当たり判定、さらには画面への描画を停止するようにしていきたいと思います。
倒された敵キャラクターの移動と当たり判定の停止
まずは、倒された敵キャラクターの移動と当たり判定が行われないようにしていきます。
下記ページの ゲームクリア画面の表示 では、state
が Character.STATE_CLEAR
のオブジェクトに対しては移動や当たり判定が行われないようにしました。
この時と同様の方法で、state
が Character.STATE_DEFEATED
のオブジェクトに対しては移動や当たり判定が行われないようにしていきたいと思います。
具体的には、Character
クラスの move
メソッド、update
メソッド、isCollided
メソッドを下記のように変更し、state
が Character.STATE_DEFEATED
の場合にメソッド内の処理を行わないように変更します。
class Character: def move(self, direction): if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED: #←ここを変更 return #略 def update(self): if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED: #←ここを変更 return #略
def isCollided(self, opponent): #↓これを追加 if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED: #←ここを変更 return False if opponent.state == Character.STATE_CLEAR or opponent.state == Character.STATE_DEFEATED: #←ここを変更 return False #略
以上の変更により、操作キャラクターに踏みつけられて state
が Character.STATE_DEFEATED
に設定された敵キャラクターは、それ以降は移動と当たり判定が行われないようになります。
この変更を加えたスクリプトを実行し、操作キャラクターを操作して敵キャラクターを踏みつければ、その敵キャラクターが動かなくなること&その敵キャラクターに対して当たり判定が行われなくなることが確認できると思います。
倒されたキャラクターの描画の停止
先程の変更により、踏みつけられた敵キャラクターが動かなくなりましたので、その敵キャラクターが倒されていることは一応視覚的にも分かるとは思います。
が、倒されたキャラクターは画面外から消えてしまった方がより自然かなぁと思いますので、ここから倒された敵キャラクターの描画を行わないようにしていきたいと思います。
キャラクターの描画は Screen
クラスの update
メソッドで行われますが、このメソッドは引数 image_infos
のリストに格納されている画像を描画するように作っています。
ですので、倒されたキャラクターの画像を image_infos
に格納しないようにすれば、自然と倒された状態のキャラクターが描画されないようになります。
上記のリストの作成は Game
クラスの update
メソッドで行っており、この update
メソッドを下記のように変更することで、倒された状態のキャラクターの描画を行わないようにすることができます。
class Game:
def update(self):
#略
image_infos = []
for character in self.characters:
#↓これを追加
if character.state != Character.STATE_DEFEATED:
image = character.getImage()
image_info = (image, character.x, character.y)
image_infos.append(image_info)
#↑これを追加
#↓これは削除
#image = character.getImage()
#image_info = (image, character.x, character.y)
#image_infos.append(image_info)
#↑これは削除
self.screen.update(image_infos, self.player.x + self.player.width // 2)
#略
上記の変更を加えたスクリプトを実行して敵キャラクターを踏みつければ、その敵キャラクターが画面に表示されなくなることが確認できると思います。
一応これで操作キャラクターが敵キャラクターを倒す際の処理の実装は完了ですが、これだとあまり踏みつけた感がないので、後述の より踏みつけた感を出すための工夫 でもう少し敵キャラクターを踏みつけた感が出るように工夫していきたいと思います。
操作キャラクターが敵キャラクターに倒される
次は、逆に操作キャラクターが敵キャラクターによって倒された時の処理を実装していきます。
要は、操作キャラクターと敵キャラクターが当たったにも関わらず、操作キャラクターが敵キャラクターを踏みつけられなかった時の処理の実装を行なっていきます。
この場合は、まず操作キャラクターの状態を倒された状態に遷移し、さらに操作キャラクターの状態が倒された状態の場合にゲームオーバー画面を表示するようにしていきたいと思います。
操作キャラクターの状態を倒された状態に遷移するためには、前述で作成した defeated
メソッドを実行すれば良いです。
また、ゲームオーバー画面の表示に関しては、基本的には下記ページの ゲームクリア画面の表示 で行ったことと同様のこと行えば良いです。
【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)上記ページの ゲームクリア画面の表示 では、主に次のことを行なっています。
- “GAME CLEAR” メッセージの表示
- “GAME CLEAR” の文字列を描画する
Screen
クラスのmessage
メソッドを作成 - 操作キャラクターの状態がゲームクリア状態の際に
Screen
クラスのmessage
メソッドを実行
- “GAME CLEAR” の文字列を描画する
- 操作キャラクターの移動と当たり判定の停止
- 定期更新処理とキー入力受付の停止
ただ、2. に関しては、既に前述の 倒された敵キャラクターの移動と当たり判定の停止 での変更により、キャラクターの状態が倒された状態に遷移した際に停止するようになっていますので、本節では何も行う必要はありません。
また、3. に関しては、既に上記ページの ゲームクリア画面の表示 での変更により、操作キャラクターの状態が通常状態から他の状態に変化した際に停止するようになっていますので、これに関しても本節では何も行う必要はありません。
したがって、ゲームオーバー画面の表示に関しては、本節では上記の 1. 2. 3. のうち、1. と同様のことだけを実現すれば良いことになります。
まとめると、操作キャラクターが敵キャラクターを踏みつけられなかった際にゲームオーバー画面を表示するようにするためには、下記の3つを実装していけば良いことになります。
- 操作キャラクターの状態を倒された状態に遷移
- “GAME OVER” の文字列を描画する
Screen
クラスのmessage
メソッドを作成 - 操作キャラクターの状態がゲームクリア状態の際に
Screen
クラスのmessage
メソッドを実行
操作キャラクターの状態を倒された状態に遷移
ということで、まずは操作キャラクターが倒された時に状態を更新するようにしていきたいと思います。
前述の通り、”操作キャラクターが倒された” とは、操作キャラクターが敵キャラクターに当たった&操作キャラクターが敵キャラクターを踏みつけられなかった場合のことです。
踏みつけたかどうかは Character
クラスの isTrampling
により判断できますし、操作キャラクターの倒された状態への遷移は defeated
メソッドを実行することで実現できます。
したがって、collide
メソッドを下記のように変更すれば、操作キャラクターが倒された時に状態を更新することができるようになります。
class Game:
def collide(self, character, opponent):
#略
elif isinstance(character, Player) and isinstance(opponent, Enemy):
if character.isTrampling(opponent):
opponent.defeated()
else:
#↓これは削除
#pass
#↑これは削除
#↓これを追加
character.defeated()
#↑これを追加
elif isinstance(character, Enemy) and isinstance(opponent, Player):
if opponent.isTrampling(character):
character.defeated()
else:
#↓これは削除
#pass
#↑これは削除
#↓これを追加
opponent.defeated()
#↑これを追加
“GAME OVER” の描画
続いて、”GAME OVER” の文字列の描画が行えるようにしていきます。
基本的には、ゲームクリア画面の表示 で “GAME CLEAR” という文字列の描画を行なった時と同様のことを行えば良いです。
具体的には、message
メソッドを、引数 type
が Screen.TYPE_GAMEOVER
の場合に “GAME OVER” という文字列を描画するように変更します。
すなわち、Screen
クラスの message
メソッドを下記のように変更します。
class Screen:
def message(self, type, player_x):
#略
if type == Screen.TYPE_GAMECLEAR:
self.canvas.create_text(
x, y,
font=("", 40),
fill="blue",
text="GAME CLEAR",
anchor=tkinter.CENTER
)
#↓これを追加
elif type == Screen.TYPE_GAMEOVER:
self.canvas.create_text(
x, y,
font=("", 40),
fill="red",
text="GAME OVER",
anchor=tkinter.CENTER
)
#↑これを追加
ゲームオーバー画面表示の実行
次は、操作キャラクターが倒された状態になった際に先ほど変更した Screen
クラスの message
メソッドを実行するようにしていきたいと思います。
Game
クラスの update
メソッドで既にゲームクリア時に message
メソッドを実行するようになっていますので、それに倣って下記のように Game
クラスの update
メソッドの最後に message
メソッドの呼び出しを追加します。
class Game:
def update(self):
#略
self.screen.update(image_infos, self.player.x + self.player.width / 2)
if self.player.state == Character.STATE_CLEAR:
self.screen.message(Screen.TYPE_GAMECLEAR, self.player.x + self.player.width // 2)
#↓これを追加
elif self.player.state == Character.STATE_DEFEATED:
self.screen.message(Screen.TYPE_GAMEOVER, self.player.x + self.player.width // 2)
#↑これを追加
以上の変更を加えたスクリプトを実行すれば、操作キャラクターが敵キャラクターに倒された際に、画面に “GAME OVER” と表示されるようになったことが確認できると思います。
スポンサーリンク
より踏みつけた感を出すための工夫
このページの最後として、より踏みつけた感を出すようにちょっとした工夫をしてきたいと思います。
現状、操作キャラクターが敵キャラクターを踏みつけても、その反動を受けずに下方向に移動してしまうのであまり踏みつけた感がないかなぁと思います。
ですので、踏みつけた際にその反動を受けて少し上方向に移動させるようにすることで、より踏みつけた感が出るようにしたいと思います。
踏みつけた時の反動によるジャンプの実現方法
このような動作は、下記ページの キャラクターのジャンプ で解説しているジャンプを応用して実現していきたいと思います。
【Python/tkinter】横スクロールアクションゲームを作る(キャラクターの移動)キャラクターのジャンプを実現しているのは主に Character
クラスの move
メソッドと、下記の update
メソッドになります。
Class Character:
def update(self):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return
if self.jump_state == Character.JUMP_UP:
self.y -= self.speed_y
if self.y <= self.base_y - self.jump_height:
self.jump_state = Character.JUMP_DOWN
self.y = self.base_y - self.jump_height
elif self.jump_state == Character.JUMP_DOWN:
self.y += self.speed_y
if self.y >= self.base_y:
self.jump_state = Character.JUMP_NO
self.y = self.base_y
操作キャラクターの場合で考えると、まず上キーが入力された際には move
メソッドで jump_state
が Character.JUMP_UP
に更新されます。
そして、その後に上記の update
メソッドが実行されれば、self.y -= self.speed_y
が実行されてキャラクターの位置が上方向に移動します(update
メソッドは定期的に実行されている)。
この時、キャラクターが jump_height
分上方向に移動したのであれば、すなわち self.y <= self.base_y - self.jump_height
が成立したのであれば、次は下方向に移動するように jump_state
を Character.JUMP_DOWN
に変更します。
そしてその後に上記の update
メソッドが実行されれば、self.y += self.speed_y
が実行されてキャラクターの位置が下方向に移動されていきます。
このような処理を行うことで、下の図のようなジャンプを実現しています。
これに対し、今回実現したいのは、下の図のように敵キャラクターを踏みつけた際にその位置から一定量上方向に移動する動作になります。
「上方向に移動するタイミング」「上方向に移動を開始する位置」「そこから上昇する高さ」はジャンプの時とは異なるものの、ジャンプと同様の処理で実現できそうなことが確認できると思います。さらに言えば、下降中の動作はジャンプの時と全く同じです。
もう少し具体的に言えば、先程紹介した update
メソッドを下記のような感じで変更すれば、敵を踏みつけた時に上方向に移動する動作を実現することができます。
Class Character:
def update(self):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return
if self.jump_state == Character.JUMP_UP:
self.y -= self.speed_y
if self.y <= self.base_y - self.jump_height:
self.jump_state = Character.JUMP_DOWN
self.y = self.base_y - self.jump_height
#↓これを追加
if self.jump_state == 敵を踏みつけた後の状態:
self.y -= self.speed_y
if self.y <= 敵を踏みつけた位置 - 踏みつけた後に上昇する高さ:
self.jump_state = Character.JUMP_DOWN
self.y = 敵を踏みつけた位置 - 踏みつけた後に上昇する高さ
#↑これを追加
elif self.jump_state == Character.JUMP_DOWN:
self.y += self.speed_y
if self.y >= self.base_y:
self.jump_state = Character.JUMP_NO
self.y = self.base_y
あとは、上記で日本語で記述している部分を「状態を表すクラス変数」や「位置や高さを表すデータ属性」に変更し、さらに敵キャラクターを踏みつけた際に、状態を 敵を踏みつけた後の状態
に変化させ、さらにその際に 敵を踏みつけた位置
を記憶しておくようにすれば、敵を踏みつけた反動によるジャンプを実現することができます。
踏みつけた後であることを示す状態の追加
ということで、上記のような update
メソッドを実現できるよう、敵を踏みつけた後の状態
を表すクラス変数を新たに追加したいと思います。
具体的には、Character
クラスを下記のように変更し、ジャンプの状態の1つとして 敵を踏みつけた後の状態
を表すクラス変数 JUMP_TRAMPLE
を追加します。
class Character:
#略
JUMP_NO = 0
JUMP_UP = 1
JUMP_DOWN = 2
#↓これを追加
JUMP_TRAMPLE = 3
#↑これを追加
スポンサーリンク
必要なデータ属性の追加
さらに、Character
クラスに敵キャラクターを踏みつけた後に上方向に移動する量と、敵キャラクターを踏みつけた位置(縦方向の位置)を示すためのデータ属性 trample_height
と trample_y
を追加します。
class Character:
def __init__(self, path, size, is_right=True):
#略
self.state = Character.STATE_NORMAL
#↓これを追加
self.trample_height = 50
self.trample_y = 0
#↑これを追加
踏みつけた時の処理の変更
続いて、敵キャラクターを踏みつけた際に実行するメソッドを作成したいと思います。このメソッドでは、キャラクターのデータ属性 jump_state
を先ほど追加したクラス変数 Character.JUMP_TRAMPLE
に更新し、さらに踏みつけた位置(縦方向)をデータ属性 trample_y
に記憶させます。
具体的には、下記のような trample
メソッドを Character
クラスに追加します。
class Character:
#↓これを追加
def trample(self):
self.jump_state = Character.JUMP_TRAMPLE
self.trample_y = self.y
#↑これを追加
今回は他のキャラクターを踏みつけるのが操作キャラクターのみなので Player
クラスに trample
メソッドを追加しても良かったのですが、ゲームによっては敵キャラクターが敵キャラクターを踏みつけた際に反動で上方向に移動するようなこともあり得るかなぁと考え、今回は Character
クラスに追加を行なっています。
さらに今度は、実際に操作キャラクターが敵キャラクターを踏みつけた際に、上記で追加した trample
メソッドを実行するように変更を行います。
踏みつけたかどうかは、Game
クラスの collide
メソッドの中で実行される isTrampling
が True
を返却したかどうかで判断できます。したがって、この場合に trample
メソッドが実行されるよう、下記のように collide
メソッドを変更します。
class Game:
def collide(self, character, opponent):
#略
elif isinstance(character, Player) and isinstance(opponent, Enemy):
if character.isTrampling(opponent):
opponent.defeated()
#↓これを追加
character.trample()
#↑これを追加
else:
character.defeated()
elif isinstance(character, Enemy) and isinstance(opponent, Player):
if opponent.isTrampling(character):
character.defeated()
#↓これを追加
opponent.trample()
#↑これを追加
else:
opponent.defeated()
縦方向の移動処理の変更
上記の変更により、操作キャラクターが敵キャラクターを踏みつけた際に、データ属性 jump_state
が Character.JUMP_TRAMPLE
に更新されるようになりました。
あとは、踏みつけた時の反動によるジャンプの実現方法 で示した変更後の update
メソッドのイメージを、ここまで追加したクラス変数やデータ属性に合わせて変更すれば良いだけです。
より具体的には、Character
クラスの update
メソッドを下記のように変更すれば、敵を踏みつけた反動によるジャンプを実現することができます。
class Character:
def update(self):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return
if self.jump_state == Character.JUMP_UP:
#略
#↓これを追加
elif self.jump_state == Character.JUMP_TRAMPLE:
self.y -= self.speed_y
if self.y <= self.trample_y - self.trample_height:
self.jump_state = Character.JUMP_DOWN
self.y = self.trample_y - self.trample_height
#↑これを追加
elif self.jump_state == Character.JUMP_DOWN:
#略
前述の通り、敵を踏みつけた際に jump_state
が Character.JUMP_TRAMPLE
に更新され、さらに同時に踏みつけた位置(縦方向)が trample_y
に記録されます。
その後に上記の update
メソッドが実行されると、キャラクターが上方向に移動していくことになります。そしてその後、trample_height
分移動した際には jump_state
が Character.JUMP_DOWN
に変化し、通常のジャンプの時と同様にキャラクターが下方向に移動していくことになります。
以上のように変更を行なったスクリプトを実行し、敵キャラクターを操作キャラクターで踏みつけてみると、踏みつけた際に反動で操作キャラクターが上方向に移動するようになったことが確認できると思います。ちょっと踏みつけた感が出たかなぁと思うのですがいかがでしょうか?
スポンサーリンク
このページで作成したスクリプト
以上で、このページの解説は終了です。
最後に、ここまでの解説を踏まえて作成したスクリプトの全体を下記に掲載しておきます。次のページではこのスクリプトをベースに解説を進めていきたいと思います。
import tkinter
from PIL import Image, ImageTk, ImageOps
import random
# アプリの設定
VIEW_WIDTH = 600
VIEW_HEIGHT = 400
GAME_WIDTH = 1500
UPDATE_TIME = 100
NUM_CAT_ENEMY = 3
NUM_DOG_ENEMY = 3
BG_IMAGE_PATH = "bg_natural_sougen.jpeg"
PLAYER_IMAGE_PATH = "hashiru_boy.png"
GOAL_IMAGE_PATH = "car_animals_flag.png"
CAT_ENEMY_IMAGE_PATH = "cat_wink_brown.png"
DOG_ENEMY_IMAGE_PATH = "jitensya_inu.png"
class Character:
DIRECTION_LEFT = 0
DIRECTION_RIGHT = 1
DIRECTION_UP = 2
JUMP_NO = 0
JUMP_UP = 1
JUMP_DOWN = 2
JUMP_TRAMPLE = 3
STATE_NORMAL = 0
STATE_CLEAR = 1
STATE_DEFEATED = 2
def __init__(self, path, size, is_right=True):
self.prepareImage(path, size, is_right)
self.base_y = VIEW_HEIGHT - self.right_image.height()
self.x = 0
self.y = self.base_y
self.speed_x = 30
self.speed_y = 20
self.jump_state = Character.JUMP_NO
self.jump_height = 200
self.direction = Character.DIRECTION_RIGHT
self.state = Character.STATE_NORMAL
self.trample_height = 50
self.trample_y = 0
def getImage(self):
if self.direction == Character.DIRECTION_RIGHT:
return self.right_image
elif self.direction == Character.DIRECTION_LEFT:
return self.left_image
def prepareImage(self, path, size, is_right=True):
image = Image.open(path)
width, height = size
ratio = min(width / image.width, height / image.height)
resize_size = (round(ratio * image.width), round(ratio * image.height))
resized_image = image.resize(resize_size)
mirrored_image = ImageOps.mirror(resized_image)
if is_right:
self.right_image = ImageTk.PhotoImage(resized_image)
self.left_image = ImageTk.PhotoImage(mirrored_image)
else:
self.left_image = ImageTk.PhotoImage(resized_image)
self.right_image = ImageTk.PhotoImage(mirrored_image)
self.width = self.right_image.width()
self.height = self.right_image.height()
def move(self, direction):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return
if direction == Character.DIRECTION_LEFT:
self.x = max(0, self.x - self.speed_x)
self.direction = Character.DIRECTION_LEFT
elif direction == Character.DIRECTION_RIGHT:
self.x = min(GAME_WIDTH - self.right_image.width(), self.x + self.speed_x)
self.direction = Character.DIRECTION_RIGHT
elif direction == Character.DIRECTION_UP:
if self.jump_state == Character.JUMP_NO:
self.jump_state = Character.JUMP_UP
def update(self):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return
if self.jump_state == Character.JUMP_UP:
self.y -= self.speed_y
if self.y <= self.base_y - self.jump_height:
self.jump_state = Character.JUMP_DOWN
self.y = self.base_y - self.jump_height
elif self.jump_state == Character.JUMP_TRAMPLE:
self.y -= self.speed_y
if self.y <= self.trample_y - self.trample_height:
self.jump_state = Character.JUMP_DOWN
self.y = self.trample_y - self.trample_height
elif self.jump_state == Character.JUMP_DOWN:
self.y += self.speed_y
if self.y >= self.base_y:
self.jump_state = Character.JUMP_NO
self.y = self.base_y
def isCollided(self, opponent):
if self.state == Character.STATE_CLEAR or self.state == Character.STATE_DEFEATED:
return False
if opponent.state == Character.STATE_CLEAR or opponent.state == Character.STATE_DEFEATED:
return False
sx = max(self.x, opponent.x)
sy = max(self.y, opponent.y)
ex = min(self.x + self.width, opponent.x + opponent.width)
ey = min(self.y + self.height, opponent.y + opponent.height)
if sx < ex and sy < ey:
return True
else:
return False
def defeated(self):
self.state = Character.STATE_DEFEATED
def trample(self):
self.jump_state = Character.JUMP_TRAMPLE
self.trample_y = self.y
def isTrampling(self, opponent):
if self.jump_state == Character.JUMP_NO:
return False
sy = max(self.y, opponent.y)
if self.y + self.height / 2 < sy:
return True
else:
return False
class Player(Character):
def __init__(self):
super().__init__(PLAYER_IMAGE_PATH, (100, 100))
def gameClear(self):
self.state = Character.STATE_CLEAR
class Enemy(Character):
def __init__(self, path, size, is_right=True):
super().__init__(path, size, is_right)
self.x = random.randrange(300, GAME_WIDTH - 300)
self.direction = Character.DIRECTION_LEFT
def update(self):
self.move(self.direction)
if random.randrange(10) % 10 == 0:
self.move(Character.DIRECTION_UP)
if self.x == 0:
self.direction = Character.DIRECTION_RIGHT
elif self.x == GAME_WIDTH - self.width:
self.direction = Character.DIRECTION_LEFT
super().update()
class CatEnemy(Enemy):
def __init__(self):
super().__init__(CAT_ENEMY_IMAGE_PATH, (80, 80))
self.jump_height = 70
self.speed_x = 20
class DogEnemy(Enemy):
def __init__(self):
super().__init__(DOG_ENEMY_IMAGE_PATH, (80, 80), False)
self.jump_height = 50
self.speed_x = 25
class Goal(Character):
def __init__(self):
super().__init__(GOAL_IMAGE_PATH, (200, 200), False)
self.direction = Character.DIRECTION_LEFT
self.x = GAME_WIDTH - self.width
class Screen:
TYPE_GAMECLEAR = 0
TYPE_GAMEOVER = 1
def __init__(self, master):
self.master = master
self.view_width = VIEW_WIDTH
self.view_height = VIEW_HEIGHT
self.game_width = GAME_WIDTH
self.game_height = self.view_height
self.draw_images = []
self.createWidgets()
self.drawBackground()
def createWidgets(self):
self.canvas = tkinter.Canvas(
self.master,
width=self.view_width,
height=self.view_height,
scrollregion=(
0, 0, self.game_width, self.game_height
),
highlightthickness=0
)
self.canvas.grid(column=0, row=0)
def drawBackground(self):
image = Image.open(BG_IMAGE_PATH)
size = (self.game_width, self.game_height)
resized_image = image.resize(size)
self.bg_image = ImageTk.PhotoImage(resized_image)
self.canvas.create_image(
0, 0,
anchor=tkinter.NW,
image=self.bg_image
)
def update(self, image_infos, player_x):
for draw_image in self.draw_images:
self.canvas.delete(draw_image)
self.draw_images.clear()
for image, x, y in image_infos:
draw_image = self.canvas.create_image(
x, y,
anchor=tkinter.NW,
image=image
)
self.draw_images.append(draw_image)
scroll_x = (player_x - self.view_width / 2) / self.game_width
self.canvas.xview_moveto(max(0, scroll_x))
def message(self, type, player_x):
if player_x < self.view_width / 2:
x = self.view_width // 2
elif player_x >= self.game_width - self.view_width / 2:
x = self.game_width - self.view_width // 2
else:
x = player_x
y = self.game_height // 2
if type == Screen.TYPE_GAMECLEAR:
self.canvas.create_text(
x, y,
font=("", 40),
fill="blue",
text="GAME CLEAR",
anchor=tkinter.CENTER
)
elif type == Screen.TYPE_GAMEOVER:
self.canvas.create_text(
x, y,
font=("", 40),
fill="red",
text="GAME OVER",
anchor=tkinter.CENTER
)
class Game:
def __init__(self, master):
self.master = master
self.screen = Screen(self.master)
self.characters = []
self.player = Player()
self.characters.append(self.player)
goal = Goal()
self.characters.append(goal)
for _ in range(NUM_CAT_ENEMY):
enemy = CatEnemy()
self.characters.append(enemy)
for _ in range(NUM_DOG_ENEMY):
enemy = DogEnemy()
self.characters.append(enemy)
self.master.bind("<KeyPress-Left>", self.press)
self.master.bind("<KeyPress-Right>", self.press)
self.master.bind("<KeyPress-Up>", self.press)
self.update()
def update(self):
if self.player.state == Character.STATE_NORMAL:
self.master.after(UPDATE_TIME, self.update)
else:
self.master.unbind("<KeyPress-Left>")
self.master.unbind("<KeyPress-Right>")
self.master.unbind("<KeyPress-Up>")
for character in self.characters:
character.update()
self.collisionDetect(character)
image_infos = []
for character in self.characters:
if character.state != Character.STATE_DEFEATED:
image = character.getImage()
image_info = (image, character.x, character.y)
image_infos.append(image_info)
self.screen.update(image_infos, self.player.x + self.player.width / 2)
if self.player.state == Character.STATE_CLEAR:
self.screen.message(Screen.TYPE_GAMECLEAR, self.player.x + self.player.width // 2)
elif self.player.state == Character.STATE_DEFEATED:
self.screen.message(Screen.TYPE_GAMEOVER, self.player.x + self.player.width // 2)
def press(self, event):
if event.keysym == "Left":
self.player.move(Character.DIRECTION_LEFT)
elif event.keysym == "Right":
self.player.move(Character.DIRECTION_RIGHT)
elif event.keysym == "Up":
self.player.move(Character.DIRECTION_UP)
self.collisionDetect(self.player)
def collide(self, character, opponent):
if isinstance(character, Player) and isinstance(opponent, Goal):
character.gameClear()
elif isinstance(character, Goal) and isinstance(opponent, Player):
opponent.gameClear()
elif isinstance(character, Enemy) and isinstance(opponent, Enemy):
if character.direction != opponent.direction:
if character.direction == Character.DIRECTION_LEFT:
character.move(Character.DIRECTION_RIGHT)
opponent.move(Character.DIRECTION_LEFT)
else:
character.move(Character.DIRECTION_LEFT)
opponent.move(Character.DIRECTION_RIGHT)
else:
if character.direction == Character.DIRECTION_LEFT:
if character.x < opponent.x:
opponent.move(Character.DIRECTION_RIGHT)
else:
character.move(Character.DIRECTION_RIGHT)
else:
if character.x > opponent.x:
opponent.move(Character.DIRECTION_LEFT)
else:
character.move(Character.DIRECTION_LEFT)
elif isinstance(character, Enemy) and isinstance(opponent, Goal):
if character.direction == Character.DIRECTION_LEFT:
character.move(Character.DIRECTION_RIGHT)
else:
character.move(Character.DIRECTION_LEFT)
elif isinstance(character, Goal) and isinstance(opponent, Enemy):
if opponent.direction == Character.DIRECTION_LEFT:
opponent.move(Character.DIRECTION_RIGHT)
else:
opponent.move(Character.DIRECTION_LEFT)
elif isinstance(character, Player) and isinstance(opponent, Enemy):
if character.isTrampling(opponent):
opponent.defeated()
character.trample()
else:
character.defeated()
elif isinstance(character, Enemy) and isinstance(opponent, Player):
if opponent.isTrampling(character):
character.defeated()
opponent.trample()
else:
opponent.defeated()
def collisionDetect(self, character):
for opponent in self.characters:
if opponent is character:
continue
if character.isCollided(opponent):
self.collide(character, opponent)
def main():
app = tkinter.Tk()
game = Game(app)
app.mainloop()
if __name__ == "__main__":
main()
基本的には、ここまで紹介してきたスクリプトを寄せ集めてきたものですが、画像のファイルパスや敵キャラクターの数をグローバル変数として定義するようにしています。
今回定義したグローバル変数の意味合いは下記のようになります。
CAT_ENEMY_IMAGE_PATH
:猫型敵キャラクターの画像のファイルパスDOG_ENEMY_IMAGE_PATH
:犬型敵キャラクターの画像のファイルパスNUM_CAT_ENEMY
:猫型敵キャラクターの数NUM_DOG_ENEMY
:犬型敵キャラクターの数
もし、ここまでの解説の中でこれらを自身の環境に合わせて変更された方は、こちらの定義値も変更しておく必要があるので注意してください。
まとめ
このページでは、横スクロールアクションゲームにおける「敵キャラクターの作成」および「敵キャラクターの移動」「敵キャラクターが他のキャラクターと当たった時の処理」等について解説しました。
操作キャラクターが敵キャラクターを踏みつけられるようになったり、敵キャラクター同士が当たった時にお互いに遠のくように移動するようになったりしたことで、アクションゲームっぽく仕立てられたのではないかと思います。
一応これでゲームの基本的な枠組みは作成完了したことになりますので、「横スクロールアクションゲームの作り方」の解説の基本編は終了です!
あとは、ちょっとした応用編として、下記ページで詳細な当たり判定を実現したり、
【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)さらに下記ページで作成してきたゲームをカスタマイズする例について紹介していきます。
【Python/tkinter】横スクロールアクションゲームを作る(カスタマイズ例)おそらく、ここまで作成してきたゲームを実際にプレイしてみると、気に入らない点やもっと発展させてみたい点などが出てくるのではないかと思います。そういった点に関しましては、是非ご自身でプログラミングを行なって改良してみていただければと思います。
そうすることで、あなたご自身のプログラミングの力を育むことができると思います。また、ゲームの開発なので、それなりに楽しくプログラミングの力を育むことができるのではないかと思います。
そういった改良を行う際に、特に上記の詳細な当たり判定の実現やカスタマイズ例が参考になると思いますので。是非次のページ以降のページも読んでみていただければと思います!
次のページでは、前述の通り、より詳細な当たり判定を実現していきます!
【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!