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

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

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

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

5ページ目は下記ページとなり、5ページ目では画面の自動スクロールの解説を行なっています。

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

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

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

この「横スクロールアクションゲームの作り方の解説」の6ページ目では、まず新たなキャラクターである「ゴールを作成」し、続いて「ゴール判定及びゴール画面の表示」を行なっていきたいと思います。

Character クラスとサブクラスの整理

まずゴールを作成していく前に、Character クラスと Character クラスのサブクラスの整理をしていきたいと思います。

Player クラスの作成

この辺りの話は下記ページで簡単に説明しているのですが、今回は PlayerGoal、さらには以降で登場する Enemy クラスのスーパークラスとして Character クラスを用意しています。

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

ここまでの解説では主に、Player で実現すべき操作キャラクターを、スーパークラスの Character クラスで実現してきました。

Characterクラスで操作プレイヤーを作成してきたことを示す図

今まで登場するキャラクターが操作キャラクターだけだったので不便はなかったですが、ここでいよいよ新たなキャラクターであるゴールが登場することになります。

ゴールと操作キャラクターを別のクラスとして扱えるよう、Character クラスのサブクラスとして Player クラスを作成し、操作プレイヤーを本来のクラスである Player で実現するように変更を行なっていきたいと思います。そしてその後、Goal クラスを作成し、ゴールをGoal クラスで作成していきたいと思います。

操作キャラクターをPlayerクラス、ゴールをGoalクラスで作成していくことを示す図

まず、現状では操作キャラクターのオブジェクトを生成する際に、Game クラスから Character クラスのコンストラクタが実行されるようになっているため、Player クラスのオブジェクトを生成するように変更します。

具体的には、Game クラスの __init__ を下記のように変更します。

Playerクラスのオブジェクト生成
class Game:

	def __init__(self, master):
		self.master = master
		self.screen = Screen(self.master)

		self.characters = []

		#↓これを追加
		self.player = Player()
		#↑これを追加

		#↓これは削除
		#self.player = Character()
		#↑これは削除

		self.characters.append(self.player)

		#略

さらに、下記のように、Player クラスの __init__ でスーパークラス(すなわち Character クラス)の __init__ を実行するように変更します。

Playerクラス
class Player(Character):

	def __init__(self):
		#↓これは削除
		#pass
		#↑これは削除

		#↓これを追加
		super().__init__()
		#↑これを追加

変更後にスクリプトを実行すれば、今まで通りに操作キャラクターの表示や移動が行えることを確認できると思います。

ただし、現状では、上記のように Character クラスのサブクラスから Character クラスの __init__ を実行した際に、必ずキャラクターの画像として操作キャラクター表示用の画像のオブジェクトが生成されるようになってしまっています。

これは、Character クラスの __init__prepareImage メソッドを実行する際に、操作キャラクター表示用の画像のパスを指定するようになってしまっているからです。

他のクラスのオブジェクト生成時には異なる画像が指定できるよう、Character クラスの __init__ を画像に関するパラメータは引数で受け取るようにし、さらに受け取った引数を用いて prepareImage メソッドを実行するように変更したいと思います。

具体的には、下記のように、prepareImage メソッドで受け取る引数を Character クラスの __init__ でも受け取るように変更します。さらに __init__ が受け取った引数を prepareImage メソッドに渡すように変更します。

__init__への引数の追加
class Character:

	def __init__(self, path, size, is_right=True): #←ここを変更
		self.prepareImage(path, size, is_right) #←ここを変更

		# 略

続いて Player クラスの __init__ を、スーパークラスの __init__ を実行する際に画像に関する引数を指定するように変更します。具体的には、今まで Character クラスの __init__prepareImage メソッドに渡していた引数をここで指定するように変更します。

__init__実行時の引数の追加
class Player(Character):

	def __init__(self):
		super().__init__(PLAYER_IMAGE_PATH, (100, 100)) #←ここを変更

以上の変更により、各サブクラスの __init__ から、そのクラスに応じた画像を設定できるようになったことになります。

つまり、ゴールを作成する場合はゴール用の画像のファイルパスとサイズを、さらに敵キャラクターを作成する場合は敵キャラクター用の画像のファイルパスとサイズを指定してスーパークラスの __init__ を実行してやれば、キャラクターに応じた画像を画面上に表示させることができるようになったことになります(必要に応じて第3引数に is_rightの設定も行ってください)。

スポンサーリンク

サブクラスのカスタマイズ

以降では、キャラクターの各クラスに共通のデータ属性の設定は Character クラスの __init__ で行い、サブクラス特有の設定は、そのサブクラスの __init__super().__init__() 実行後にデータ属性を上書きする形で行うようにしたいと思います。

各クラス固有のデータ属性の設定方法を示す図

例えば、現状 Character クラスでは、ジャンプの高さを表すデータ属性 jump_height200 に設定していますが、例えば操作キャラクターのジャンプの高さを高くしたいような場合、Player クラスの __init__jump_height をもっと大きな値で上書きするようにします。

Player特有のデータ属性の設定例
class Player(Character):

	def __init__(self):
		super().__init__(PLAYER_IMAGE_PATH, (100, 100))

		#以降でサブクラス固有のデータ属性の上書き
		self.jump_height = 500

このデータ属性の上書きにより、操作キャラクターのみ他のキャラクターに比べて高くジャンプできるようになります。

こんな感じで、各キャラクターに特有のデータ属性を設定することで、キャラクターごとに特徴を持たせた動作などが可能になります。

また、上書きだけでなく、そのサブクラスにのみ必要なデータ属性は、そのサブクラスの __init__ でのみ設定するようにしていきたいと思います。逆に各サブクラス共通のデータ属性は Character クラスの __init__ で設定を行うようにしていきます。

上記のようなサブクラス特有のデータ属性の設定を行うためには、データ属性の意味を理解しておいた方が良いため、ここで復習として Character クラスの __init__ で設定しているデータ属性の意味をまとめておきます。

  • base_y:キャラクターの基準位置
  • x:ゲーム開始時のキャラクターの位置(横方向)
  • y:ゲーム開始時のキャラクターの位置(縦方向)
  • speed_x:キャラクターの移動量(横方向)
  • speed_y:キャラクターの移動量(縦方向)
  • jump_state:ゲーム開始時のキャラクターのジャンプの状態
  • jump_height:キャラクターのジャンプの高さ
  • direction:ゲーム開始時のキャラクターの向き

これらの属性は下記の2つのページで追加したものですので、詳しく知りたい方は下記ページをご参照いただければと思います。

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

ゴールの作成と表示

ここまでの変更により、キャラクターを実現するためのスーパークラスとして Character クラスが準備できたことになります。

次は実際にゴールを作成するために Goal クラスを作成していきたいと思います。

(復習)キャラクターが表示される仕組み

まずはゴールが画面に表示されるように Goal クラスを作成してきたいと思います。

その前に、ここで一旦、操作キャラクターが画面に表示される仕組みについておさらいしておきます。

操作キャラクターはまず、Game クラスの __init__ からの Player クラスのコンストラクタの実行によりオブジェクトが生成されます。

操作キャラクターが画面に表示される仕組み1

このオブジェクト生成時には Player クラスの __init__ が実行されます。さらにこの Player クラスの __init__ からはスーパークラス Character__init__ が実行され、操作キャラクターの画像のオブジェクトの生成やサブクラス間で共通のデータ属性の設定が行われます。

特に画像のオブジェクトの生成を行うために、Character__init__ にはキャラクター用の画像のパスと矩形のサイズを指定する必要があります(その矩形に合わせて画像が拡大縮小される)。

また、生成された操作キャラクターのオブジェクトは Game クラスのデータ属性のリスト characters に格納されます。

操作キャラクターが画面に表示される仕組み2

さらに、Game クラスの update メソッドからリスト characters に格納されている全オブジェクトのキャラクターの画像の描画を Screen クラスに依頼することで、各キャラクターの画像がキャンバスに描画され、画面に表示されるようになっています。

操作キャラクターが画面に表示される仕組み3

つまり、すでにリスト characters に格納されている全オブジェクトのキャラクターの画像が描画されるようになっていますので、ゴールにおいても、Game クラスから Goal クラスのコンストラクタを実行してオブジェクトを生成し、そのオブジェクトをリスト characters に格納してやれば自動的に表示されるようになります。

ゴールを表示するために必要なことを示す図

したがって、ゴールを表示するだけであれば、下記の3つのみ実施してやれば良いことになります。

  • Goal クラスのオブジェクト生成時に実行される __init__ の作成
  • Game クラスからの Goal クラスのオブジェクト生成の実行(コンストラクタの実行)
  • 作成されたオブジェクトのリスト characers への格納

1点目の __init__ においても、Player クラスと同様に作成すれば良いのですが、操作キャラクターと異なる画像を表示したいので、別途ゴール用の画像を準備し、ファイルパスを指定する必要があります。

スポンサーリンク

ゴール用の画像の準備

ということで、まずはゴール用の画像を準備しましょう!

ゴール用の画像はどんなものでも特に問題はないですが、これも操作キャラクターの時と同様に背景が透明に設定されているものが望ましいです。

このページでは いらすとや の下記 URL の画像をゴール用の画像として利用していきたいと思います。

http://www.irasutoya.com/2016/12/blog-post_99.html

下の図は上記 URL の画像を転載させていただいたものになります。

ゴール用の画像

転載元:いらすとや

__init__ の作成

画像が準備できれば、Goal クラスの __init__ でスーパークラス(Character クラス)の __init__ を実行するように変更します。このスーパークラス(Character クラス)の __init__ には、下記の3つの引数を指定する必要があるので注意してください。

  • 第1引数:画像のファイルパス
  • 第2引数:矩形のサイズ
  • 第3引数:画像のキャラクターの向きが右向きかどうか(省略化)

第1引数と第2引数の意味は下記ページの キャラクターの画像オブジェクト生成 で、

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

第3引数の意味は下記ページの 画像の向きに応じた画像オブジェクトの生成 で解説していますので、忘れてしまった方はこれらのページを参照していただければと思います。

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

例えば、ゴール用の画像のファイルパスが "car_animals_flag.png" で、200 x 200 ピクセルの矩形に合わせて画像の拡大縮小を行うのであれば、Goal クラスの __init__ を下記のように変更します。

ゴール用のコンストラクタの作成
class Goal(Character):

	def __init__(self):
		#↓これを削除
		#pass
		#↑これを削除

		#↓これを追加
		super().__init__("car_animals_flag.png", (200, 200), False)
		#↑これを追加

第3引数に False を指定しているのは、"car_animals_flag.png" の画像(前述で紹介したゴール用の画像)の向きが左向きだからです。

Goal クラスのオブジェクト生成

現状では、Goal クラスのオブジェクトの生成が行われていませんので、続いて Goal クラスのオブジェクトを生成するように変更していきたいと思います。

各クラスのオブジェクトを生成するのは Game クラスの役割ですので、次は Game クラスを変更していきます。

具体的には、Game クラスの __init__ を下記のように変更します。

Goalクラスのオブジェクト生成
class Game:

	def __init__(self, master):

		# 略

		self.characters = []
		self.player = Player()
		self.characters.append(self.player)

		#↓これを追加
		goal = Goal()
		self.characters.append(goal)
		#↑これを追加

		# 略

以上の変更により、Goal クラスのオブジェクトが生成され、そのオブジェクトが Game クラスのデータ属性のリスト characters に追加されるようになります。

前述の通り、Game クラスの update メソッドは、リスト characters の全要素のオブジェクトの画像を表示するように作成していますので、上記により Goal クラスのオブジェクトが自動的に表示されるようになります。

ということで、ここまで変更したスクリプトを実行してみましょう!ゴールが画面に表示されるようになったことが確認できるはずです。

ゴールが画面の左端におかしな方向を向いた状態で表示されてしまう様子

スポンサーリンク

ゴール用のカスタマイズ

ゴールが表示されるようになったことは確認できると思いますが、ゴールの位置が左端&ゴールの向きが右向きでちょっと違和感があるのではないかと思います。

このような表示になっているのは、スーパークラスの Character クラスの __init__ において、データ属性 x0 に、データ属性 directionCharacter.DIRECTION_RIGHT に設定されているからです。

なので、これらはゴール用のカスタマイズとして Goal クラスの __init__ の中で上書き変更しようと思います。

具体的には、Goal クラスの __init__を下記のように変更します。

ゴール用のカスタマイズ
class Goal(Character):

	def __init__(self):
		super().__init__("car_animals_flag.png", (200, 200), False)

		#↓これを追加
		self.direction = Character.DIRECTION_LEFT
		self.x = GAME_WIDTH - self.width
		#↑これを追加

x に関しては、ゲーム画面の左端に表示するために上記のような計算を行なって位置の調整を行なっています(GAME_WIDTH はゲーム画面の幅)。

上記のように変更してスクリプトを実行し、ゲーム画面の右端までキャラクターを移動すれば、左向きのゴールが表示されることが確認できると思います。

ゴール用のカスタマイズでゴールが正常に表示されるようになった様子

ゲームクリアの判定

ゴールが作成できましたので、続いては操作キャラクターがゴールと当たった際に “ゲームクリアした” と判定できるようにしていきたいと思います(次の章の ゲームクリア画面の表示 で、ゲームクリアしたと判定されている際にゲームクリア画面を表示するようにしていきます)。

まずは、操作キャラクターの状態管理を行うようにし、操作キャラクターがゲームクリアしたかどうかを判断できるようにしていきます。

キャラクターの状態を管理する様子

続いて操作キャラクターがゴールと当たったかどうかを判断できるようにしていきます。いわゆる当たり判定というやつですね!

キャラクター同士が当たったかどうかを判断する様子

さらに、操作キャラクターとゴールが当たった時に、操作キャラクターの状態をゲームクリア状態に変更するようにしていきます。

操作キャラクターが当たった際に状態をゲームクリア状態に変化させる様子

次の章の ゲームクリア画面の表示 では、この操作キャラクターの状態がゲームクリア状態に変化した際にゲームクリア画面を表示していくことになります。

このページでは、ゲームクリア画面表示までの流れの実現を優先したいため、当たり判定に関しては簡単に&大雑把にのみ行うようにしたいと思います。より詳細な当たり判定に関しては、下記のページで解説させていただきます。

横スクロールアクションゲームを作る(より詳細な当たり判定)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)

解説の順番的には、次のページで敵キャラクターの作成を行い、さらにその次のページで詳細な当たり判定を実現していくことになります。

キャラクターの状態管理

ということで、まずは Character クラスでキャラクターの状態管理を行うようにしていきたいと思います。この状態管理を行うことで、キャラクターが既にゲームクリアした状態であるどうかを判断できるようにしていきます。

状態の定義

今回作成するゲームでは、まずはキャラクターの状態として下記の3つを用意したいと思います。

  • 通常状態
    • 下記の2つ以外の状態
  • ゲームクリア状態
    • ゴールに到達した操作キャラクターの状態
  • 倒された状態
    • 操作キャラクターによって踏みつけられた敵キャラクターの状態
    • 敵キャラクターにぶつかられた操作キャラクターの状態

最後の “倒された状態” に関しては、敵キャラクターを扱う下記のページから使用するようになりますが、ついでにこの状態もここで追加しておこうと思います。

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

では、これらの状態をクラス変数として扱えるよう、まずは Character クラスを下記のように変更します。

キャラクターの状態を表すクラス変数
class Character:

	#略

	JUMP_NO = 0
	JUMP_UP = 1
	JUMP_DOWN = 2

	#↓これを追加
	STATE_NORMAL = 0
	STATE_CLEAR = 1
	STATE_DEFEATED = 2
	#↑これを追加

これらは、それぞれ下記の状態を表すクラス変数となります。

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

さらに、Character クラスにキャラクターの状態を管理できるようデータ属性を追加したいと思います。そのため、Character クラスの __init__ を下記のように変更します。

キャラクターの状態の管理を行うデータ属性
class Character:

	def __init__(self, path, size, is_right=True):

		#略

		self.direction = Character.DIRECTION_RIGHT

		#↓これを追加
		self.state = Character.STATE_NORMAL
		#↑これを追加

上記で追加した state がキャラクターの状態を管理するデータ属性になります。オブジェクト生成時は通常状態になるよう stateCharacter.STATE_NORMAL を設定しています。

現状 state を設定するのは上記の __init__ のみですが、操作キャラクターとゴールが当たった時に操作キャラクターの state をCharacter.STATE_CLEAR に設定し、さらに操作キャラクターと敵キャラクターが当たった時に、踏みつけられた方のキャラクターの state をCharacter.STATE_DEFEATED に設定するようにしていけば、キャラクターがどのような状態であるかを state によって判断することができるようになります。

MEMO

Character クラスはデータ属性 jump_state も持っていますが、こちらはジャンプ中の状態を示すデータ属性になります

それに対してここで追加したデータ属性 state は、キャラクターそのものの状態を示すデータ属性になります

ゲームクリア状態への遷移

続いて、操作キャラクターの状態をゲームクリア状態に遷移するメソッドを用意したいと思います。ゲームクリアするのは操作キャラクターだけですので、Player クラスにこのメソッドを用意したいと思います。

具体的には、Player クラスに下記の gameClear メソッドを追加します。

ゲームクリア状態への遷移
class Player:

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

ゴールに当たった際に操作キャラクターに上記メソッドを実行させれば、操作キャラクターの状態がゲームクリアに遷移することになります。

スポンサーリンク

キャラクター同士の当たり判定

キャラクターの状態管理が行えるようになりましたので、次は当たり判定を行なっていきたいと思います。前述の通り、まずは大雑把に当たり判定を行います。

キャラクター同士が当たったかどうかの判定

まず、操作キャラクターに関しても、ゴールに関しても、表示されているのは画像です。

さらにこれらの画像は、キャンバスの create_image メソッドにより、始点を (x, y)、終点を (x + width, y + height) とする矩形の中に描画されています(xywidthheight はいずれもデータ属性で保持している値になります)。

キャラクターの画像が矩形内に描画されていることを示す図

ですので、このキャラクターが描画されている矩形とゴールが描画されている矩形の2つの矩形が重なっているかどうかが判断できれば、大雑把ではありますが当たり判定を行うことができることになります。

矩形の重なりの有無により当たり判定を行う様子

この2つの矩形が重なっているかどうかは、下記から求まる始点と終点の位置関係から判断することができます。

  • 始点の x 座標:下記の2つの座標の大きい方
    • 1つ目の矩形の始点の x 座標
    • 2つ目の矩形の始点の x 座標
  • 始点の y 座標:下記の2つの座標の大きい方
    • 1つ目の矩形の始点の y 座標
    • 2つ目の矩形の始点の y 座標
  • 終点の x 座標:下記の2つの座標の小さい方
    • 1つ目の矩形の終点の x 座標
    • 2つ目の矩形の終点の x 座標
  • 終点の y 座標:下記の2つの座標の小さい方
    • 1つ目の矩形の終点の y 座標
    • 2つ目の矩形の終点の y 座標

以降では、上記の座標を始点と終点とする矩形を「重なり矩形」と呼ばせていただきます。

このようにして求めた座標の始点と終点を持つ重なり矩形においては、1つ目の矩形と2つ目の矩形が重なっている時、始点が必ず終点に対して左上方向の位置に存在します。

3つ目の矩形の始点と終点の位置関係から当たり判定を行う様子1

逆に1つ目の矩形と2つ目の矩形が重なっていない時、重なり矩形において始点は終点に対して左上方向の位置には存在しません。

3つ目の矩形の始点と終点の位置関係から当たり判定を行う様子2

したがって、上記のように作成した重なり矩形の座標において、下記が成立するかどうかを判断すれば、1つ目の矩形と2つ目の矩形が重なっているかどうかを判定することができます。

また、下記の式が成立するとき、その始点と終点を持つ矩形、すなわち重なり矩形が2つの矩形の重なった領域となります。

当たったかどうかの判定式
始点のx座標 < 終点のx座標 and 始点のy座標 < 終点のy座標

今回は操作キャラクターとゴールとが当たったかどうかの判定を行うわけですので、上記が成立した場合、操作キャラクターがゲームクリアしたと判断することができます。

今回作成するゲームにおいては、上記のような当たり判定は Character クラスで行うようにしたいと思います。当たり判定を行うメソッドを isCollided とすれば、下記のように Character クラスを変更することで、ここまで解説した当たり判定を行う機能を Chracter クラスに持たせることができます。

当たり判定の処理
class Character:

	#↓これを追加
	def isCollided(self, opponent):
		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
	#↑これを追加

この isCollided メソッドは、実行したオブジェクトのキャラクターと引数 opponent で指定されたオブジェクトのキャラクターが当たったかどうかを判定するメソッドになります。

当たっている場合は True を、当たっていない場合は False を返却します。

まだ大雑把ではありますが当たり判定が行えるようになりましたので、キャラクターが移動する度に上記の isCollided を実行するようにすれば、移動した際にキャラクター同士が当たったかどうかを判定できるようになります。

他の全てのキャラクターとの当たり判定の実行

ただし、この当たり判定は、移動後のキャラクターと当たる可能性がある全てのキャラクターに対して行う必要があります。

現状では登場するキャラクターが操作キャラクターとゴールのみなので、操作キャラクターとゴールとの当たり判定のみ行うのでも問題ありません。

ですが、今後敵キャラクターを作成してゲーム内に登場するキャラクターが増えていくことになりますので、それを見据えて “移動したキャラクター以外の全キャラクター” と当たり判定を行うようにしていきたいと思います。

移動してキャラクターとそれ以外の全てのキャラクターに対して当たり判定を行う様子

MEMO

本当に当たり判定を行う必要があるのは当たる可能性のあるキャラクター同士のみです

ですので、遠くにいるキャラクター同士の当たり判定は省いても良いです

ただ、省くためには遠くにいるかどうかの判断を行う必要があり、それはそれで処理が複雑になるため、今回は “移動したキャラクター” と “移動したキャラクター以外の全キャラクター” との当たり判定を行うようにしています

Game クラスではデータ属性のリスト characters で全キャラクターのオブジェクトを管理するようにしているため、上記のような “移動したキャラクター” と “移動したキャラクター以外の全キャラクター” との当たり判定は、Game クラスに下記の collisionDetect メソッドを追加することで実現することができます。

他のキャラクター全てと当たり判定を行う
class Game:

	#↓これを追加
	def collisionDetect(self, character):
		for opponent in self.characters:
			if opponent is character:
				continue
			
			if character.isCollided(opponent):
				pass
	#↑これを追加

引数 character は、当たり判定を行う際の1つ目のキャラクターとなります。より具体的には、直前に移動を行ったキャラクターのオブジェクトを指定することを想定しています。

また、isCollided は前述で追加した、実行オブジェクトと引数で指定したオブジェクトとの当たり判定を行うメソッドになります。

したがって、上記のように Game クラスのデータ属性 characters からオブジェクト opponent を1つ1つ取得しながら character.isCollided(opponent) を実行するようにループを組めば、移動直後のキャラクターと全キャラクターとの当たり判定を実施することができることになります。

ただし、同じオブジェクト同士の当たり判定は不要ですので(必ず当たったと判定されてしまう)、characteropponent が同じオブジェクトの場合は continue で当たり判定をスキップするようにしています。

isCollided メソッドが True の場合の処理が pass になっていますが、ここには後述の キャラクター同士が当たった時の処理 で作成する「当たった時の処理を実行するメソッドの実行」を追加することになります。

今後の解説ページで敵キャラクターを作成した場合にも、敵キャラクターのオブジェクトをデータ属性 characters に追加さえすれば、自動的に上記の collisionDetect メソッドで当たり判定の対象として扱われるようになります。

キャラクター移動後の当たり判定の実行

あとは、先ほど追加した collisionDetect メソッドをキャラクターの移動直後に実行するようにすれば、キャラクターが移動するたびにキャラクター同士の当たり判定が行われるようになります。

キャラクターが移動するのは、Character クラスの move メソッド(左右上キー入力時にキャラクターを移動させるメソッド)と Character クラスの update メソッド(ジャンプ中にキャラクターを上下方向に移動させるメソッド)の2つですので、これらのメソッド実行直後に collisionDetect を実行するようにしていきます。

具体的には、まず Game クラスの press メソッドを下記のように変更します。

キー入力直後の当たり判定
class Game:

	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)
		#↑これを追加

さらに続いて、Game クラスの update メソッドを下記のように変更します。

定期処理の中での当たり判定
class Game:

	def update(self):
		for character in self.characters:
			character.update()

			#↓これを追加
			self.collisionDetect(character)
			#↑これを追加

			#略

キャラクター同士が当たった時の処理

ここまでの変更により、キャラクターが移動するたびに当たり判定が実行されるようになりました。続いては、当たり判定によりキャラクター同士が当たったと判断された際の処理を実現していきたいと思います。

操作キャラクターとゴールが当たった時の処理

今回は、操作キャラクターとゴールとが当たった時の処理を実現していきます。

操作キャラクターとゴールが当たったのですから、この場合はゲームクリアということになります。ですので、この際には操作キャラクターの状態をゲームクリア状態に更新するようにしていきたいと思います(ここでは状態の更新のみを行い、次の ゲームクリア画面の表示 で、ゲームクリア画面を表示するようにしていきます)。

まず、キャラクター同士が当たった時の処理は Game クラスに行わせるようにしたいと思います。

この辺りは主に敵キャラクターが登場してからの話になるのですが、キャラクター同士が当たった時にはキャラクターの移動等のキャラクターのオブジェクトに対する依頼が発生します。

各キャラクターのオブジェクトへの処理の依頼は Game クラスから行うようにしたいため、今回はキャラクター同士が当たった時の処理は Game クラスで行うものとしたいと思います。

このキャラクター同士が当たった時の処理を実行するメソッドを collide とすれば、Game クラスを下記のように変更することで、collide メソッド実行時に操作キャラクターの状態をゲームクリア状態に更新することができるようになります。

当たった時の処理
class Game:

	#↓これを追加
	def collide(self, character):
		character.gameClear()
	#↑これを追加

ただし、これだと collide メソッドが実行されると必ず操作キャラクターの状態がゲームクリア状態に更新されることになります。

この collide メソッドは、他のキャラクター同士が当たった時の処理も実行するメソッドとしたいので、当たったキャラクターが操作キャラクターとゴールである場合のみ操作キャラクターの状態をゲームクリアにするようにしたいと思います。

これを行うためには、上記の 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, Player):
			opponent.gameClear()
		#↑これを追加

		#↓これは削除
		#character.gameClear()
		#↑これは削除

collide メソッドの引数 character は1つ目のキャラクターのオブジェクト、引数 opponent は2つ目のキャラクターのオブジェクトとなります。

現状だと当たり判定を行うキャラクターは操作キャラクターとゴールだけですが、以降で敵キャラクターが登場した際にも同じメソッドで当たり判定が行えるよう、オブジェクトは引数で受け取るようにしています。

また、isinstance 関数は第1引数で指定したオブジェクトが第2引数で指定したクラス(もしくはそのサブクラス)のインスタンスであるかどうかを判断する関数になります。

上記のように判断を加えることで、Player クラスのオブジェクトと Goal クラスのオブジェクトが当たった時のみ、gameClear メソッドが実行され、操作キャラクターの状態 stateCharacter.STATE_CLEAR に更新されるようになります。

ただし、あくまでも gameClear メソッドを実行するのは Player クラスのオブジェクトであることに注意してください。

当たった時の処理の実行

先程作成した collide メソッドはキャラクター同士が当たった時の処理を行うメソッドですので、次は実際にキャラクター同士が当たったと判定された時に collide メソッドが実行されるようにしていきます。

当たり判定は isCollided メソッドで実施されており、この isCollided メソッドは Game クラスの collisionDetect メソッドから実行されています。

ですので、この Game クラスの collisionDetect メソッドを下記のように変更すれば、キャラクター同士が当たったと判定された時に collide メソッドが実行されるようになります。

当たった時の処理の実行
class Game:

	def collisionDetect(self, character):
		for opponent in self.characters:
			if opponent is character:
				continue
			
			if character.isCollided(opponent):
				#↓これは削除
				#pass
				#↓これは削除

				#↓これを追加
				self.collide(character, opponent)
				#↑これを追加

ここまでの変更により、操作キャラクターが移動してゴールに当たった際には、操作キャラクターの状態がゲームクリア状態に変化するようになります。

ただし、状態が変化するだけで、現状ではまだ画面には何も変化がありません。

次は、キャラクターの状態がゲームクリア状態に変わった際にゲームクリアの画面を表示し、さらにゲームの動作を停止させるようにしていきたいと思います。

ゲームクリア画面の表示

では、キャラクターの状態がゲームクリアに遷移した際にゲームクリア画面の表示を行うようにしていきたいと思います。

ゲームクリア画面といっても、今回は簡単のため、”GAME CLEAR” というメッセージを画面に表示させるだけにさせていただきたいと思います。

ゲームクリア画面の例

スポンサーリンク

メッセージの表示

まずは “GAME CLEAR” というメッセージを画面に表示させる機能を作成していきたいと思います。

メッセージの表示方法

こういったメッセージの表示は、tkinter のキャンバスを利用した場合、create_text メソッドによる文字列の描画により行うことができます。今回もこの create_text メソッドを利用して “GAME CLEAR” というメッセージを表示させるようにしていきたいと思います。

create_text メソッドに関しては下記ページで詳しく解説していますので、詳細を知りたい方はこちらをご参照いただければと思います。

tkinterキャンバスに図形を描画する方法解説ページのアイキャッチTkinterの使い方:Canvasクラスで図形を描画する

文字列の描画位置

create_text メソッドでは、第1引数と第2引数に文字列を描画する座標を指定する必要があります。今回作成するゲームにおいては、”GAME CLEAR” の文字列は表示領域の中央に描画するように座標を指定するようにしたいと思います。

表示領域の中央ですので、縦方向の座標はゲーム画面の高さを2で割れば求めることが可能です。

また下記ページで紹介した自動スクロールの導入により、ゲーム画面の端付近でない限り、操作キャラクターは表示領域の中心に描画されるように画面が自動スクロールされるようになっています。

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

したがって、横方向の座標には操作キャラクターの中央の座標を指定してやれば良いことになります。ただし、前述のようにゲーム画面の端付近に操作キャラクターが存在する場合は操作キャラクターが表示領域の中央に表示されないため、端付近のみ下の図のように横方向の座標を調整するようにしたいと思います。

GAME CLEARの文字列の表示位置の説明図

今回ゴールはゲーム画面の右端に設置するようにしていますので、「表示領域がゲーム画面の右端にある場合」の座標である ゲーム画面の幅 - 表示領域の幅 / 2 を毎回 create_text メソッドの第1引数に指定しても良いかもしれません。

ですが、ゴールの位置がカスタマイズで変更されることや今後ゲームオーバー画面の表示を行うようにすることを見据え、右端以外でも文字列を描画できるよう、あらゆるケースの描画位置を計算できるようにしておきたいと思います。

メッセージ表示の実装

ここまでの考え方に基づき、メッセージの表示を行うメソッドを Screen クラスの message メソッドとして作成したいと思います。

具体的には、Screen クラスを下記のように変更し、クラス変数の追加と messege メソッドの追加を行います。

メッセージを表示する
class Screen:

	#↓これを追加
	TYPE_GAMECLEAR = 0
	TYPE_GAMEOVER = 1
	#↑これを追加

	#↓これを追加
	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
			)
	#↑これを追加

上記の message メソッドは、引数 type で指定されたタイプに応じたメッセージを表示領域の中央に表示するメソッドになります。

表示領域の中央の座標は、引数 player_x で指定された操作キャラクターの中央の座標(横方向)に基づいて計算を行なっています。

また、引数 type には上記で追加したクラス変数のいずれかを指定されることを想定しており、さらに、この type に応じて描画する文字列を切り替えるようにすることを想定しています。

  • Screen.TYPE_GAMECLEAR:”GAME CLEAR” を描画
  • Screen.TYPE_GAMEOVER:”GAME OVER” を描画

このページでは “GAME CLEAR” の描画のみを行うので typeScreen.TYPE_GAMECLEAR の場合の処理のみを実装していますが、typeScreen.TYPE_GAMEOVER の場合の処理は、下記ページで敵キャラクターと当たった際の処理を実現する際に実装していきます。

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

ゲームクリア画面表示の実行

次は操作キャラクターの状態がゲームクリアになった際にゲームクリア画面が表示できるよう、先ほど作成した message メソッドの実行部分の実装を行なっていきたいと思います。

今回作成するゲームにおいては、各キャラクターの画像の描画後、操作キャラクターの状態がゲームクリア状態の場合のみ message メソッドを実行するようにしていきたいと思います。

各キャラクターの画像の描画は Screen クラスの update メソッドで行われており、さらにこの Screen クラスの update メソッドは Game クラスの update メソッドで実行されています(さらにこのメソッドは 100 ms ごとに定期実行されている)。

したがって、Game クラスの update メソッドの最後部分を下記のように変更すれば、各キャラクターの画像の描画が終わった後に message メソッドを実行してゲームクリア画面を表示することができるようになります(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)
		#↑これを追加

message メソッドの第1引数には、先程追加した Screen クラスのクラス変数を描画したい文字列に合わせて指定します。今回は “GAME CLEAR” を描画したいので、Screen.TYPE_GAMECLEAR を指定しています。

また、第2引数には操作キャラクターの中央の座標(横方向)を指定しています(Player クラスのデータ属性 xwidth を用いて中央の座標を計算して指定)。

ゲームクリアの判定 で解説したように、操作キャラクターの状態がゲームクリアに遷移するのは操作キャラクターとゴールが当たった時です(Game クラスの collide メソッドで状態が変更される)。

操作キャラクターの状態がゲームクリアに遷移している場合は、上記で追加した条件文が YES になるので message メソッドが実行され、ゲームクリア画面が表示されることになります。

実際に、変更後のスクリプトを実行して操作キャラクターを右方向に移動し、さらに操作キャラクターをゴールに当ててやれば、画面に “GAME CLEAR” と表示されることが確認できると思います!

ゲームクリア画面の例

まだ大雑把にしか当たり判定を行なっていないので、実際にはゴールに触れる前にゲームクリア画面が表示されてしまうと思います。より詳細な当たり判定は下記ページで実現しますので、これによりもうちょっと当たり判定の精度は上がるはずです。

横スクロールアクションゲームを作る(より詳細な当たり判定)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(より詳細な当たり判定)

ゲームクリア後のキャラクターの移動の停止

ただし、現状では画面に “GAME CLEAR” が表示されても操作キャラクターが移動できてしまいます(さらに操作キャラクターの移動に伴って “GAME CLEAR” の文字列がどんどん描画されていく…)。

ゲームクリア後も操作キャラクターが移動できてしまう様子を示す図

これだとあまりゲームクリアした感じがしないので、キャラクターの状態がゲームクリア状態になった後は、キャラクターの移動、さらには移動後の当たり判定が行われないようにしていきたいと思います。

移動の停止

まず、キャラクターの移動を行なっているのは、下記の2つの Character クラスのメソッドになります。

  • move メソッド
  • update メソッド

したがって、上記の2つのメソッドの先頭を下記のように変更すれば、キャラクターの状態 stateCharacter.STATE_CLEAR の場合はキャラクターが移動しなくなります(即座にメソッドが終了するようになる)。

キャラクターの移動の停止
class Character:

	def move(self, direction):
		#↓これを追加
		if self.state == Character.STATE_CLEAR:
			return
		#↑これを追加

		#略

	def update(self):
		#↓これを追加
		if self.state == Character.STATE_CLEAR:
			return
		#↑これを追加

		#略

当たり判定の停止

また、当たり判定を行なっているのは、下記の Character クラスのメソッドになります。

  • isCollided メソッド

したがって、isCollided メソッドの先頭を下記のように変更すれば、キャラクターの状態 stateCharacter.STATE_CLEAR の場合は当たり判定が行われなくなります(即座にメソッドが終了するようになる)。

当たり判定の停止
class Character:

	def isCollided(self, opponent):
		#↓これを追加
		if self.state == Character.STATE_CLEAR:
			return False
		if opponent.state == Character.STATE_CLEAR:
			return False
		#↑これを追加

		#略

キャラクターの状態 stateCharacter.STATE_CLEAR の場合は必ず False を返却するようになっています。つまり、必ずキャラクター同士が当たっていないと判断されるので、当たった時の処理である Game クラスの collide メソッドも必ず実行されないようになります。

また、isCollided の場合、このメソッドを実行したオブジェクトだけでなく、当たり判定相手のオブジェクト opponentstateCharacter.STATE_CLEAR の場合も当たり判定を行わないようにしています(念のため)。

ここまでの変更を行なったスクリプトを実行すれば、操作キャラクターがゴールに当たった際に、操作キャラクターが移動しなくなるようになったことが確認できると思います。

ゲームクリア後に操作キャラクターが移動しなくなる様子を示す図

スポンサーリンク

ゲームクリア後の定期更新とキー入力の停止

上記の変更により、操作キャラクターがゴールに当たった際にゲームクリア画面が表示され、さらにキャラクターが移動できなくなるようになり、それ以降画面表示が変化しなくなりました。

画面表示は変化しませんが、実はまだ画像の描画は定期的に実行されています。同じ位置に同じ画像が 100 ms 毎(after メソッドの第1引数に指定した時間の間隔)に描画されています。

これだとゲームとしては終了しているものの、CPU(パソコン)への負荷がかかったままになってしまいますので、このページの最後にゲームクリア後に画像の定期的な描画を停止させるようにしたいと思います。

また同様に、キーボードのキー入力も受け付けられる状態になっていますので(受け付けても Game クラスの press メソッドで即座に関数は終了するようにはなっていますが)、ゲームクリア後にキーボードのキー入力も受け付けないようにしていきたいと思います。

定期更新の停止

まずは画像の定期的な描画を停止させていきたいと思います。

画像の描画が定期的に実行されているのは、Game クラスの update メソッドが定期的に実行されているためです(この update メソッドから Screen クラスに画像の描画が依頼されている)。

さらに、Game クラスの update メソッドが定期的に実行されているのは、Game クラスの update メソッドの中の下記の処理により、UPDATE_TIME ms 後に再度 Game クラスの update メソッドが実行されるよう after メソッドに依頼しているからです

定期的処理の実行
self.master.after(UPDATE_TIME, self.update)

ですので、操作キャラクターの状態が通常状態から変化した際に上記の after メソッドを実行しないようにすれば、ゲームクリア後に Game クラスの update メソッドの定期的な実行を停止することができます。

つまり、Game クラスの update メソッドを下記のように変更すれば、ゲームクリア後の画像の定期的な描画を停止させることができるようになります。

定期的処理の停止
class Game:

	def update(self):

		#↓これは削除
		#self.master.after(UPDATE_TIME, self.update)
		#↑これは削除

		#↓これを追加
		if self.player.state == Character.STATE_NORMAL:
			self.master.after(UPDATE_TIME, self.update)
		#↑これを追加

		#略

キー入力受付の停止

続いてキーボードのキー入力の受付の停止を行なっていきたいと思います。

このゲームでキーボードのキー入力が受け付けられているのは、下記のように bind メソッドを実行しているためです。

キー入力の受付設定
class Game:

	def __init__(self, master):

		#略

		self.master.bind("<KeyPress-Left>", self.press)
		self.master.bind("<KeyPress-Right>", self.press)
		self.master.bind("<KeyPress-Up>", self.press)

		#略

bind メソッドで設定したイベントの受付を停止するためには、unbind メソッドを実行する必要があります。unbind メソッドの引数には、受け付けを停止したいイベントシーケンスを指定します。要は、上記の bind メソッドの第1引数に指定している文字列を指定すれば良いです。

キー入力の受付を停止するのは、操作キャラクターの状態がゲームクリアに変化した時(通常状態から変化した時)ですので、先ほど変更した Game クラスの update メソッドをさらに下記のように変更すれば、ゲームクリア後にキー入力の受付を停止することができます。

キー入力の受付停止
class Game:

	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>")
		#↑これを追加

		#略

以上の変更により、ゲームクリア後の定期的な処理とキーボードのキー入力を停止させることができたことになります。

見た目が変化しないので効果が分かりにくかもしれませんが、アプリ毎の CPU 使用率を計測するツール(Windows だとタスクマネージャ、MacOSX だとアクティビティモニタなど)を使用すれば、ゲームクリア後に CPU 使用率が一気に低下することが確認できると思います。

例えば私の環境では、アプリ起動後からゲームクリアまでにおけるこのアプリの CPU 使用率は 8 % 程度ですが、ゲームクリア後には 1 % 以下まで低下することが確認できました。おそらくイベントの受付はそこまで負荷が高くないと思うので、画像の定期的な描画を停止することで CPU 負荷が低下するようになったのだと考えられます。

このページで作成したスクリプト

以上で、このページの解説は終了です。

最後に、ここまでの解説を踏まえて作成したスクリプトの全体を下記に掲載しておきます。次のページではこのスクリプトをベースに解説を進めていきたいと思います。

このページで作成したスクリプト
import tkinter
from PIL import Image, ImageTk, ImageOps
import random

# アプリの設定
VIEW_WIDTH = 600
VIEW_HEIGHT = 400
GAME_WIDTH = 1500

UPDATE_TIME = 100

BG_IMAGE_PATH = "bg_natural_sougen.jpeg"
PLAYER_IMAGE_PATH = "hashiru_boy.png"
GOAL_IMAGE_PATH = "car_animals_flag.png"

class Character:

	DIRECTION_LEFT = 0
	DIRECTION_RIGHT = 1
	DIRECTION_UP = 2

	JUMP_NO = 0
	JUMP_UP = 1
	JUMP_DOWN = 2

	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

	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:
			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:
			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

	def isCollided(self, opponent):
		if self.state == Character.STATE_CLEAR:
			return False
		if opponent.state == Character.STATE_CLEAR:
			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

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):
		pass


class CatEnemy(Enemy):

	def __init__(self):
		pass


class DogEnemy(Enemy):

	def __init__(self):
		pass


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
			)

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)

		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:

			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)

	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()

	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()

基本的には、ここまで紹介してきたスクリプトを寄せ集めてきたものですが、ゴールの画像のファイルパスをグローバル変数として定義するようにしています。

今回定義したグローバル変数の意味合いは下記のようになります。

  • GOAL_IMAGE_PATH:ゴールの画像のファイルパス

もし、ここまでの解説の中でこれらを自身の環境に合わせて変更された方は、こちらの定義値も変更しておく必要があるので注意してください。

まとめ

このページでは、横スクロールアクションゲームにおける「ゴールの作成」および「ゲームクリアの判定」「ゲームクリア画面の表示」について解説しました。

もっと細かく言えば、スーパークラスとサブクラスの分割や当たり判定の実装、当たった時の処理の実装、状態管理の実装、文字列の描画、ゲームクリア後の移動と当たり判定およびキー入力と定期処理実行の停止を行なったことになります。

かなり多くのことを行なってきたので解説の分量も多くなってしまいましたが、特に当たり判定や当たった時の処理を実現したことで、よりゲームらしさが出てきたのでは無いかと思います。

現状では、当たり判定は矩形同士の重なりの有無のみで大雑把に行なっていますが、矩形同士の当たり判定自体は色んなゲームでも利用できると思いますので、当たり判定を行う際の考え方は覚えておくと良いと思います。

次のページでは、ついに基本編の最後となる敵キャラクターの作成を行なっていきます!次のページでも色んな事が学べると思いますので、是非次のページも読んでみてください!

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