【Python/tkinter】横スクロールアクションゲームを作る(敵キャラクターの作成)

横スクロールアクションゲームを作る(敵キャラクターの作成)の解説ページアイキャッチ

このページでは、Python で tkinter を利用した簡単な「横スクロールアクションゲーム」の作り方を解説していきます。

このページは「横スクロールアクションゲームの作り方の解説」の7ページ目となります。

6ページ目は下記ページとなり、6ページ目では主にゴールの作成と大雑把な当たり判定&ゲームクリア画面表示の解説を行なっています。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

また、このページでは、上記ページの このページで作成したスクリプト で紹介しているスクリプトを変更していきながらゲームを作成していきます。

ですので、事前に上記のページを読んでおくことをオススメします。

この「横スクロールアクションゲームの作り方の解説」の7ページ目では、まず敵キャラクターの作成を行い、操作キャラクターが敵キャラクターを倒したときの処理や敵キャラクターに操作キャラクターが倒されたときの処理、さらには操作キャラクターが倒された際のゲームオーバー画面の表示等の機能を実現していきたいと思います。

このページは基本編の最後の解説ページであり、横スクロールアクションゲームの基本的な部分はこのページで全て作成完了となります!

敵キャラクターの作成

まずは敵キャラクターを作成していたいと思います!

今まで作り込んできたキャラクターを実現する Character クラスのサブクラスとして、敵キャラクターを実現する Enemy クラス、さらにその Enemy クラスのサブクラスとして CatEnemyDogEnemy クラスを用意しています。

クラス構成を示す図

わざわざ CatEnemy クラスと DogEnemy クラスに分けているのは、描画されるキャラクターの画像やキャラクターのサイズ、ジャンプの高さ等を別々に設定できるようにするためです。

ただし、このページでスクリプトを変更していくベースとなる 下記ページの このページで作成したスクリプト では、Enemy クラス、CatEnemy クラス、DogEnemy クラスはまだ空っぽのクラスとなっています。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

まずは、主にこれらの 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__ は下記のように変更したいと思います。

CatEnemyクラスの作成
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_height70 に設定しています。

操作キャラクターの jump_height150 ですので、操作キャラクターに比べると猫型敵キャラクターのジャンプの高さは半分以下ということになります。

こんな感じで操作キャラクターと比べながら、猫型敵キャラクターとしてどのように動作して欲しいかを考えていくとカスタマイズしやすいと思います。また、上記のカスタマイズは一例ですので、ご自身でご自由に設定していただいて問題ないです。

また、上記では 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__ は下記のように変更したいと思います。

DogEnemyクラスの作成
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__ は下記のように変更したいと思います。

Enemyクラスの作成
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 メソッドが実行されています(他のキャラクターと当たった場合は)。

現状の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 メソッドを利用して敵キャラクターの移動を実現していきたいと思います。

横スクロールアクションゲームを作る(キャラクターの移動)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(キャラクターの移動)

ただし、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 メソッドが実行されるようになっています。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

前述でも紹介しましたが、この collide メソッドは現状下記のようになっています。

現状のcollisionメソッド
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)
		#↑これを追加

ゴールは移動しないので、引数 characteGoal クラスのオブジェクトになることはないかもしれませんが、一応その場合も考慮して 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 が重なった領域の始点の縦方向の座標となります。この座標の求め方を忘れてしまった方は下記ページの キャラクター同士の当たり判定 を参照していただければと思います。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

さらに、ゲーム画面(キャンバス)においては、縦方向の正方向は下方向になります。ですので、縦方向の座標が大きいほど下側の座標であることになります。したがって、下記が成立した際は、self(メソッドを実行したオブジェクト)の中心座標が sy よりも上側にあると判断することができます(この時は selfopponent を踏みつけたことになる)。

実行オブジェクトの中心がsyより上にあるかを判断
if self.y + self.height / 2 < sy:

縦方向の座標の扱いが若干ややこしいので注意してください。

操作キャラクターが敵キャラクターを倒す

先程作成した Charactr クラスの isTrampling メソッドにより、操作キャラクターが敵キャラクターを踏みつけたかどうかを判断することができるようになりました。

続いては、操作キャラクターが敵キャラクターを踏みつけた時の処理を実装していきたいと思います。

この場合、敵キャラクターは踏みつけられたので、操作キャラクターによって倒されたことになります。

操作キャラクターが敵キャラクターを踏みつけた様子を示す図

キャラクターの状態を倒された状態に遷移

この際には、まず踏みつけられた敵キャラクターの状態を “倒された状態” に遷移させるようにしたいと思います。

この辺りの話は下記ページの キャラクターの状態管理 で解説しているのですが、Character クラスではキャラクターの状態を管理するデータ属性 state を持っています。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

さらに、このデータ属性 state には、Character クラスの下記の3つの状態を表すクラス変数を設定するようになっています。

  • Character.STATE_NORMAL:通常状態
  • Character.STATE_CLEAR:ゲームクリア状態
  • Character.STATE_DEFEATED:倒された状態

敵キャラクターが操作キャラクターによって踏みつけられた際には、その敵キャラクターは倒されたことになるので、このキャラクターのデータ属性 stateCharacter.STATE_DEFEATED に遷移させるようにしていきます。

まずは前準備として、このキャラクターの状態を遷移させるメソッドを用意したいと思います。

具体的には、Character クラスに下記の defeated メソッドを用意します。

倒された状態にするメソッド
class Character:

	#↓これを追加
	def defeated(self):
		self.state = Character.STATE_DEFEATED
	#↑これを追加

以降で操作キャラクターも敵キャラクターに倒されるようになることを見越して、defeated メソッドは Character クラスに追加しています。

敵キャラクターが踏みつけられた時の処理の実装

続いて Game クラスの collide メソッドの変更を行い、操作キャラクターが敵キャラクターを踏みつけた時の処理を実装していきたいと思います(collide メソッドはキャラクター同士が当たった時のみ実行されるようになっています)。

ここではとりあえず、踏みつけられた敵キャラクターの状態を “倒された状態” に遷移させる処理を行うようにしたいと思います。

操作キャラクターが敵キャラクターを踏みつけたかどうかは、操作キャラクターに Character クラスの isTrampling を実行させることで判断することができます(isTrampling の引数には当たった敵キャラクターのオブジェクトを指定する)。

また、isTramplingTrue を返却した場合、引数に指定した敵キャラクターは倒されたことになりますので、この際には引数で指定した敵キャラクターに 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 のオブジェクトに対しては移動や当たり判定が行われないようにしました。

横スクロールアクションゲームを作る(ゴールの作成)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)

この時と同様の方法で、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】横スクロールアクションゲームを作る(ゴールの作成)

上記ページの ゲームクリア画面の表示 では、主に次のことを行なっています。

  1. “GAME CLEAR” メッセージの表示
    • “GAME CLEAR” の文字列を描画する Screen クラスの message メソッドを作成
    • 操作キャラクターの状態がゲームクリア状態の際に Screen クラスの message メソッドを実行
  2. 操作キャラクターの移動と当たり判定の停止
  3. 定期更新処理とキー入力受付の停止

ただ、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 メソッドを、引数 typeScreen.TYPE_GAMEOVER の場合に “GAME OVER” という文字列を描画するように変更します。

すなわち、Screen クラスの message メソッドを下記のように変更します。

GAME OVERの文字列描画
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 メソッドになります。

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_stateCharacter.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 メソッドを下記のような感じで変更すれば、敵を踏みつけた時に上方向に移動する動作を実現することができます。

変更後の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 メソッドの中で実行される isTramplingTrue を返却したかどうかで判断できます。したがって、この場合に 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_stateCharacter.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_stateCharacter.JUMP_TRAMPLE に更新され、さらに同時に踏みつけた位置(縦方向)が trample_y に記録されます。

その後に上記の update メソッドが実行されると、キャラクターが上方向に移動していくことになります。そしてその後、trample_height 分移動した際には jump_stateCharacter.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】横スクロールアクションゲームを作る(より詳細な当たり判定)