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

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

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

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

3ページ目は下記ページとなり、3ページ目では主に操作キャラクターの表示の解説を行なっています。

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

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

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

この「横スクロールアクションゲームの作り方の解説」の4ページ目では、主に操作キャラクターの移動について解説をしていきたいと思います。

今回作成するゲームでは、キーボードのキー入力に応じて操作キャラクターの移動を行うようにしていきたいと思います。

左右キーが押された時には、そのキーに応じた方向にキャラクターを移動させ、さらに上キーが押された時には、キャラクターをジャンプさせるようにしていきます。

まずはキーボードのキー入力受付を行い、続いて左右方向の移動を実現し、その後ジャンプを実現させるという流れで解説していきます。

キーボード入力の受付

それでは、操作キャラクターの移動を実現していきたいと思います。

前述の通り、今回作成するゲームでは、操作キャラクターに関してはキーボードのキー入力により移動させるようにしたいと思います。具体的には、左右キーが押された時には、そのキーに応じた方向にキャラクターを移動させ、さらに上キーが押された時にはキャラクターをジャンプさせるようにしていきます。

キー入力された時に操作キャラクターが移動する方向

で、これを実現するためには、まずアプリがキーボードのキー入力を受け付けられるようにする必要があります。

tkinter では、bind メソッドを利用することで、キーボードのキー入力などのイベントを受け付けることができます。

もう少し具体的にいうと、bind メソッドを実行すれば、それ以降 bind メソッドの第1引数に指定したイベントが発生した際に第2引数に指定した関数やメソッドが自動的に実行されるようにすることができます。第1引数に指定する文字列はイベントシーケンスと呼ばれます。

bind
widget.bind("イベントシーケンス", 関数名 or メソッド名)

ですので、例えば第1引数にキーボードの左キー入力を表すイベントシーケンスを指定し、さらに第2引数にキャラクターを左に移動する関数を指定してやれば、左キー入力された時に自動的にキャラクターの位置が左に移動するようになります。

このイベント関連の解説は下記ページで解説していますので、詳しく知りたい方は別途下記ページを参照していただければと思います。

イベント処理解説ページのアイキャッチTkinterの使い方:イベント処理を行う

今回作成するゲームでは、キーボードの左キー入力時には操作キャラクターを左に移動し、キーボードの右キー入力時には操作キャラクターを右に移動し、さらにキーボードの上キー入力時には操作キャラクターをジャンプさせるようにしたいため、下記の3つのイベントの受付を行う必要があります。

  • キーボードの左キー入力("<KeyPress-Left>"
  • キーボードの右キー入力("<KeyPress-Right>"
  • キーボードの上キー入力("<KeyPress-Up>"

上記の括弧内で記述したものは、bind メソッドの第1引数に指定するイベントシーケンスになります。

今回はアプリ起動直後からキーボードのキー入力を受け付けるようにするため、アプリ起動時に行われる Game クラスのオブジェクト生成時、すなわち Game クラスの __init__ 実行時に bind メソッドを実行するようにしたいと思います。

これを行うために、まずは Game クラスの __init__ を下記のように変更します。

キーボード入力の受付
class Game:

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

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

		#↓これを追加
		self.master.bind("<KeyPress-Left>", self.press)
		self.master.bind("<KeyPress-Right>", self.press)
		self.master.bind("<KeyPress-Up>", self.press)
		#↑これを追加

		self.update()

bind メソッドは、tkinter のウィジェットに実行させる必要があるので注意してください。上記では self.master、すなわち今回作成するゲームのメインウィンドウウィジェットに実行させています。

上記のように変更を行えば、キーボードの左キー・右キー・上キー入力時に Game クラスの press メソッドが自動的に実行されるようになります。

ただし、この press メソッドは Game クラスにまだ用意されていません。そのため、この press を下記のように Game クラスに追加したいと思います。

キー入力時に実行するメソッド
class Game:

	#↓これを追加
	def press(self, event):
		pass
	#↑これを追加

press はまだ空のメソッドなのでキーを入力しても何も実行されませんが、ひとまずキーボードのキー入力を受け付ける枠組みは出来上がりです。

あとは、press メソッドの中から操作キャラクターの位置を変更するようにすれば、キー入力時にキャラクターが移動するようになります。

ただし、キャラクターの位置を管理するクラスは Character クラス(およびそのサブクラス)としたいので、キャラクターを移動するためのメソッドを Character クラスに追加し、そのメソッドを上記で用意した press メソッドから実行させるような構成にしていきたいと思います。

キャラクターの移動(左右)

ということで、次はキャラクターを移動するためのメソッドを Character クラスに実装していきます。まずはこのメソッドを追加し、操作キャラクターを左右に移動できるようにしていきたいと思います(ジャンプについては次の章で解説します)。

キャラクターが左右方向に移動する様子

スポンサーリンク

方向の定義

ここから作成していくキャラクターの移動を行うメソッドでは、引数として方向を表す値を受け取り、その値に応じた方向にキャラクターを移動させるようにしていきたいと思います。

まずはそのために、方向を表す値をクラス変数として定義したいと思います。

MEMO

今回はクラス変数を利用しますが、enum で値を定義するのでも良いです

具体的には、Character クラスの先頭に下記を追加します。

方向を表すクラス変数
class Character:

	#↓これを追加
	DIRECTION_LEFT = 0
	DIRECTION_RIGHT = 1
	DIRECTION_UP = 2
	#↑これを追加

以降では、方向を表したり指定したりする際には、上記のクラス変数を用いるようにしていきます。

具体的には上記のクラス変数それぞれは次に示す方向を表す変数となります。

  • Character.DIRECTION_LEFT:左方向
  • Character.DIRECTION_RIGHT:右方向
  • Character.DIRECTION_UP:上方向

キャラクターの位置変更

続いて実際にキャラクターの移動を実現していきたいと思います。

キャラクターの移動は、キャラクターの位置を変更することで行います。

下記ページの キャラクターの位置に応じた画像の描画 で解説している通り、キャラクターの位置は Character クラスのデータ属性 xy によって管理されるようになっています。さらに、このデータ属性 xy の位置にキャラクターの画像が描画されるようになっています。

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

したがって、この xy を変化させてやれば画面上のキャラクターの位置が変化することになります。ここでは左右方向の移動のみを行うので、x のみの変更を行なっていきます。

また、キャンバスの座標的に、横方向の正方向は右方向になります。したがって、左方向にキャラクターの位置を変化させるには x を減少させ、右方向にキャラクターの位置を変化させるには x を増加させてやれば良いことになります。

xの増減と移動方向の関係

つまり、キャラクターの位置を変更するメソッドの名前を move とすれば、キャラクターの位置の変更を行うためには Character クラスに下記の move メソッドを追加してやれば良いことになります。

キャラクターの移動(左右)
class Character:

	#↓これを追加
	def move(self, direction):
		if direction == Character.DIRECTION_LEFT:
			self.x = self.x - 1
		elif direction == Character.DIRECTION_RIGHT:
			self.x = self.x + 1
	#↑これを追加

ただ、これだけだと、データ属性 x が際限なく増減してしまうので、ゲームの画面外までキャラクターが移動できることになってしまいます。

そのため、x0 〜 GAME_WIDTH - キャラクターの画像の幅 の間のみ変化するようにしたいと思います。

具体的には、上記の move メソッドを下記のように変更します。

画面内のみのキャラクターの移動
class Character:

	def move(self, direction):
		if direction == Character.DIRECTION_LEFT:
			self.x = max(0, self.x - 1) #←ここを変更
		elif direction == Character.DIRECTION_RIGHT:
			self.x = min(GAME_WIDTH - self.right_image.width(), self.x + 1) #←ここを変更

GAME_WIDTH はゲーム画面の幅を表すグローバル変数となります。また、データ属性 right_image は tkinter 用の画像オブジェクトを参照しており、これに width メソッドを実行させることでその画像の幅を取得することができます。

上記のように変更することで、x は次の図で示す範囲内のみでしか変化しなくなるので、キャラクターが画面外にはみ出てしまうことを防ぐことができます。

xが変化する範囲

キャラクターの位置変更の実行

現状は左右方向のみですが、Character クラスの move メソッドを実行すればキャラクターが移動するようになりました。

また、キーボードのキー入力が実行された際には Game クラスの press メソッドが実行されますので、このメソッドから Character クラスの move メソッドを実行するようにすれば、キー入力に応じてキャラクターが移動するようになります。

具体的には、Game クラスの press メソッドを下記のように変更すれば、キー入力に応じてキャラクターが移動するようになります。move メソッドの引数には、方向の定義 で定義したクラス変数を、押されたキーに応じて指定するようにしています。

キャラクターの移動の実行
class Game:

	def press(self, event):
		#↓これは削除
		#pass
		#↑これは削除

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

今回作成するゲームでキーボードのキー入力により移動を行うのは操作キャラクターのみです(敵キャラクターは定期的に移動する。ゴールは移動しない)。

下記ページの Character クラスのオブジェクトの生成 でも解説したように、Game クラスのデータ属性 player には操作キャラクターのオブジェクトを参照させていますので、この player のみに move メソッドを実行させることで、キーボードのキー入力時に操作キャラクターのみを移動させるようにしています。

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

また、まだ上方向の移動(ジャンプ)の処理は実装していませんが、左右方向の移動と同様に Character クラスの move メソッドの中で処理を実装していくので、上記のように上キーが押された場合(event.keysym == "Up" が成立する場合)も move メソッドを実行するようにしています。

ここまでの変更を行なった後のスクリプトを実行し、キーボードで左右キーを押せば、操作キャラクターが押した方向に移動することが確認できると思います。

キャラクターがキー入力に応じて左右に移動する様子

ただし、実際に試してみると分かると思うのですが、移動速度がめちゃめちゃ遅いです…。流石にこれだと操作が大変なので、次はキャラクターの移動速度の調整を行なっていきたいと思います。

スポンサーリンク

キャラクターの移動速度の設定

キャラクターの移動速度が遅いのは、Character クラスの move メソッドで x の増減量を 1 にしているからです。これだと、左右キー入力時にキャラクターが 1 ピクセルしか移動してくれません。

もちろん、この増減量を直接 1020 などに変更しても良いのですが、今回は Character クラスのデータ属性として増減量を持たせ、そのデータ属性の値に応じて move メソッドでの x の増減を行うようにしていきたいと思います。

MEMO

わざわざデータ属性として用意するのは、サブクラスごとに移動速度を設定できるようにするためです

特に敵キャラクターを作成する際に活躍するデータ属性となります

このために、まず Character クラスの __init__ を下記のように変更します。

移動速度を示すデータ属性
class Character:

	def __init__(self):
		#略

		self.y = self.base_y
		
		#↓これを追加
		self.speed_x = 30
		self.speed_y = 20
		#↑これを追加

speed_x が左右キーを1回押された時の x の増減量となります。

現状だと move メソッドは左右方向にしか対応していないので speed_x があれば十分なのですが、次の章で実装するジャンプ処理も見据えて speed_y の追加も行っています(speed_y はジャンプ時の上昇・下降速度となります)。

さらに、Character クラスの move メソッドを下記のように変更します。

移動速度を考慮した移動
class Character:

	def move(self, direction):
		if direction == Character.DIRECTION_LEFT:
			self.x = max(0, self.x - self.speed_x) #←ここを変更
		elif direction == Character.DIRECTION_RIGHT:
			self.x = min(GAME_WIDTH - self.right_image.width(), self.x + self.speed_x) #←ここを変更

上記のように変更を行なってからスクリプトを実行し、さらに左右キーを入力すれば、先ほどよりも操作キャラクターの移動速度が速くなっていることが確認できると思います。

もっと早く移動させたいのであれば、speed_x をもっと大きな値に、逆にもっと遅く移動させたいのであれば、speed_x をもっと小さな値に設定することで調整することも可能です。

キャラクターのジャンプ

続いて、キャラクターのジャンプを実現していきたいと思います。

キャラクターが上方向にジャンプする様子

ジャンプの実現方法

今回は、上キーが入力された後に、一定時間間隔でキャラクターが上方向に speed_y ずつ移動し、さらに上方向に “一定量” 移動した後は、一定時間間隔でキャラクターが下方向に speed_y ずつ移動するようにジャンプ処理を作成していきたいと思います(speed_y は先ほど Character クラスに追加したデータ属性になります)。

一定間隔ごとにデータ属性yが変化する様子

キャラクターの縦方向の位置は Character クラスのデータ属性 y で管理していますので、要は一定間隔でこの y の値を speed_y 増減させることになります。

以降では、上記で “一定量” と示したジャンプの頂点となる移動量を “ジャンプの高さ” と呼ばせていただきます。

定期的にキャラクターの位置を変化させていくことになるので、tkinter の場合は after メソッドを利用して上記のジャンプ処理を実現していくことになります。

既に Game クラスの update メソッドは、after メソッドにより定期的に実行されるようになっています(この辺りは解説の3ページ目の 定期的なキャラクターの描画 で解説しています)。

ですので、その Game クラスの update メソッドから上記のようなキャラクターの縦方向の移動を行うメソッドを実行するようにすれば、定期的な縦方向の移動を実現することができます。

今後、このキャラクターの縦方向の移動を行うメソッドを Character クラスの update メソッドとして解説していきます。

スポンサーリンク

キャラクターのジャンプ状態の管理

ただし、上記のようなジャンプを行う処理を実現するためには、Character クラスの update メソッドの中でキャラクターが現在どういう状態なのかを判断できるようにしなければなりません。

例えば、上キー入力後にまだジャンプの高さ分上方向に移動していないのであれば、キャラクターを上方向に移動するためにデータ属性 yspeed_y 減少させる必要があります。

逆に、ジャンプの高さ分上方向に移動した後は、キャラクターを下方向に移動させるためにデータ属性 yspeed_y 増加させる必要があります。

また、キャラクターがジャンプ中でないのであれば、データ属性 y の値を変化させないようにする必要があります。

こんな感じで、キャラクターが今どういう状態かを判断しながら、データ属性 y 変化させていく必要があります。

ジャンプ中の状態管理の様子

そのために、まずはキャラクターのジャンプの状態を表す値をクラス変数として定義したいと思います。

さらに、このジャンプ状態を表す値を保持するデータ属性として Character クラスに jump_state を追加したいと思います。ついでにはなりますが、ジャンプの高さを示すデータも属性もあった方が便利なので、Character クラスにデータ属性 jump_height も追加したいと思います。

具体的には、Chracter クラスの先頭部分および __init__ を下記のように変更します。

状態とジャンプの高さの追加
class Character:

	DIRECTION_LEFT = 0
	DIRECTION_RIGHT = 1
	DIRECTION_UP = 2

	#↓これを追加
	JUMP_NO = 0
	JUMP_UP = 1
	JUMP_DOWN = 2
	#↑これを追加

	def __init__(self):

		#略

		self.speed_y = 20

		#↓これを追加
		self.jump_state = Character.JUMP_NO
		
		self.jump_height = 200
		#↑これを追加

追加した各クラス変数は下記の状態を表す値として扱っていきます。

  • Character.JUMP_NO:キャラクターはジャンプ中でない
  • Character.JUMP_UP:キャラクターはジャンプ中かつ上昇中
  • Character.JUMP_DOWN:キャラクターはジャンプ中かつ降下中

Character クラスのオブジェクト生成時は、この状態 jump_stateCharacter.JUMP_NO に設定しています。

また、jump_height200 に設定していますので、この Character クラスのオブジェクトは、上キーが押された時に 200 ピクセル分上に移動してから着地することになります。

キャラクターのジャンプの実装

では、先ほど追加した下記のジャンプの状態を用いてジャンプ処理の実装を行なっていきたいと思います。

  • Character.JUMP_NO:キャラクターはジャンプ中でない
  • Character.JUMP_UP:キャラクターはジャンプ中かつ上昇中
  • Character.JUMP_DOWN:キャラクターはジャンプ中かつ降下中

上キー入力時の処理の実装

まず、上キーが押された時に、キャラクターの状態を Character.JUMP_UP に遷移させるようにします。

上キーが押された時には Character クラスの move メソッドが実行されますので、つまりは Character クラスの move メソッドを下記のように変更します。

上キーが押された時の状態遷移
class Character:

	def move(self, direction):
		if direction == Character.DIRECTION_LEFT:
			self.x = max(0, self.x - self.speed_x)
		elif direction == Character.DIRECTION_RIGHT:
			self.x = min(GAME_WIDTH - self.right_image.width(), self.x + self.speed_x)
		
		#↓これを追加
		elif direction == Character.DIRECTION_UP:
			if self.jump_state == Character.JUMP_NO:
				self.jump_state = Character.JUMP_UP
		#↑これを追加

jump_stateCharacter.JUMP_NO 以外の場合に jump_state を変更しないようにしていますが、これは2段階ジャンプなどが行われないようにするためです。

縦方向の移動処理の実装

続いて Character クラスの update メソッドを作成していきます。

MEMO

念のための復習ですが、まずキャンバスの縦方向に対する正方向は下方向ですので、縦方向の座標値が小さいほど上側の座標を表すことになります

また、Character クラスのデータ属性 base_y は基準位置の縦方向の座標(キャラクターがジャンプしていない時に存在する位置の縦方向の座標)となります

詳細は解説の3ページ目の キャラクターの初期位置の調整 を参照してください

まず、キャラクターの jump_stateCharacter.JUMP_UP の状態で Character クラスの update メソッドが実行された際には、キャラクターの縦方向の位置を示すデータ属性 yspeed_y 減少させるようにします。

これにより、上キーが押された後に Character クラスの update メソッドが実行されればキャラクターが上方向に移動していくようになります。これは、先程の Character クラスの move メソッドの変更により、上キーが押された際にキャクターの jump_state を Character.JUMP_UP に設定するようにしたからです。

ただし、yspeed_y 減少させることで ybase_y - jump_height 以下になった場合は、既にジャンプの高さ分キャラクターが上方向に移動したことになります(ジャンプの頂点に達した)。

したがって、以降ではキャラクターが下方向に移動するよう、この際にはキャラクターの状態を Character.JUMP_DOWN に変化させます。

また、キャラクターの jump_stateCharacter.JUMP_DOWN の状態で Character クラスの update メソッドが実行された際には、キャラクターの縦方向の位置を示すデータ属性 yspeed_y 増加させるようにします。

これにより、ジャンプの高さ分移動した後に Character クラスの update メソッドが実行されればキャラクターが下方向に移動していくようになります(ジャンプの高さ分移動した際に jump_state が Character.JUMP_DOWN に設定されるため)。

ただし、yspeed_y 増加させることで ybase_y 以上になった場合は、キャラクターがジャンプしていない時に存在する位置まで下降したことになります(地面に着地した)。

この際には、キャラクターの jump_stateCharacter.JUMP_NO に変化させ、さらに jump_stateCharacter.JUMP_NO の際に y が変更されないようにしておけば、着地後にキャラクターの縦方向の移動が行われないようになります。

上記の動作を図でまとめたものが下の図になりますので、上記の処理を整理するのに見ていただければと思います(むしろごちゃごちゃしすぎてて複雑かも…)。

jump_stateとyの増減量の関係図

また、上記の処理は、下記の update メソッドを Character クラスに追加することで実現することができます。

キャラクターの縦方向の移動
class Character:

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

上記の解説に加えて、ybase_y - jump_height よりも小さくならないように、さらに ybase_y よりも大きくならないように調整していますのでご注意ください。

キャラクターの状態の定期更新

ここまでの変更により、上キーを押した際にキャラクターの状態が Character.JUMP_UP に変化し、それ以降で上記の Character クラスの update が定期的に実行されれば、その実行間隔でキャラクターの縦方向の位置およびキャラクターの状態が変化してジャンプを実現できる状態になりました。

あとは、Character クラスの update を定期的に実行できればジャンプの実現の完了です。

この定期的な実行を行うために、Game クラスの update メソッドを下記のように変更します。

キャラクターの状態の定期更新
class Game:

	def update(self):
		self.master.after(UPDATE_TIME, self.update)

		#↓これを追加
		for character in self.characters:
			character.update()
		#↑これを追加

		image_infos = []

		#略

これにより、定期的に実行されるキャラクターの画像描画の前にキャラクターの縦方向の位置および状態の更新が行われるようになり、その更新後の位置にキャラクターが描画されるようになります。

また、今後登場する敵キャラクターにもジャンプを行わせるようにしたいため、上記のように update メソッドは、オブジェクトのリスト characters の全要素に対して実行させるようにしています。 

以上の変更を行なった後にスクリプトを起動して上キー入力を行えば、操作キャラクターがジャンプするようになったことが確認できると思います(上キー入力直後に左右キーを押せば、ジャンプ中でも横に移動できるはずです)。

キャラクターがキー入力に応じてジャンプする様子

キャラクターの向きの調整

ここまでの変更によって、操作キャラクターをキーボードのキー入力により移動できるようになりました。

ただ、左右のどちらに移動している時でもキャラクターがずっと同じ方向を向いてしまっているので、このページの最後として、移動方向に合わせて操作キャラクターの向きも切り替えるように改良していきたいと思います。

移動方向に合わせてキャラクターの向きが切り替わる様子

スポンサーリンク

左右反転した画像オブジェクトの生成

移動方向に合わせてキャラクターの向きを変更するために、まずは左右反転したキャラクターの画像を用意し、さらにキャラクターの移動方向に合わせて描画する画像を選択できるようにしていきたいと思います(元画像 or 左右反転後の画像から選択)。

左右反転したキャラクターの画像の生成

まずは左右反転したキャラクターの画像を用意していきたいと思います。

画像の左右反転に関しては、PIL の ImageOpsmirror 関数を実行することで実現することができます。mirror 関数の引数には、左右反転したい PIL 用の画像オブジェクトを指定します(tkinter 用の画像オブジェクトではないことに注意)。

下記ページの キャラクターの画像オブジェクト生成 でも解説している通り、キャラクターの画像オブジェクトの生成は Character クラスの prepareImage メソッドで行なっています。

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

さらに、prepareImage メソッドの中で生成している resized_image が拡大縮小後の PIL 用の画像オブジェクトになります。

したがって、この resized_image に対して mirror 関数を実行し、さらに mirror 関数によって生成された画像オブジェクトを tkinter 用の画像オブジェクトに変換してやれば、キャンバスに描画可能な左右反転後の画像オブジェクトを生成することができます。

具体的には、下記のように Character クラスの prepareImage メソッドを変更することで、キャンバスに描画可能な左右反転後の画像オブジェクトが生成できるようになります。

左右反転後の画像オブジェクトの生成
class Character:

	def prepareImage(self, path, size):

		#略

		resized_image = image.resize(resize_size)

		#↓これを追加
		mirrored_image = ImageOps.mirror(resized_image)
		#↑これを追

		self.right_image = ImageTk.PhotoImage(resized_image)

		#↓これを追加
		self.left_image = ImageTk.PhotoImage(mirrored_image)
		#↑これを追加

上記の変更により、右方向に移動している時に表示する画像オブジェクトがデータ属性 right_image に、左方向に移動している時に表示する画像オブジェクトがデータ属性 left_image に参照されるようになります。

画像の向きに応じた画像オブジェクトの生成

ただし、上記の prepareImage メソッドでは、path で指定された画像のキャラクターが右を向いていることを前提とした作りになってしまっています。

ですので、もし path で指定された画像のキャラクターが左を向いている場合、左を向いた状態の画像オブジェクトが right_image に、さらにそれを左右反転させて右を向くようになった画像オブジェクトが left_image に参照されてしまうことになります。

そうなると、逆方向を向いたままキャラクターが移動することになってしまいます…。

キャラクターが移動方向の逆を向いてしまう例

これを防ぐために、prepareImage メソッドの引数により path で指定する画像のキャラクターが右を向いているかどうかを指定できるようにし、さらにその指定に応じて right_imageleft_image に参照させる画像オブジェクトを切り替えるようにしたいと思います。

具体的には、上記の Character クラスの prepareImage メソッドを次のように変更します(引数も変更しているので注意してください)。

キャラクターの向きを考慮したオブジェクト生成
class Character:

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

		# 略

		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.right_image = ImageTk.PhotoImage(resized_image)
		#self.left_image = ImageTk.PhotoImage(mirrored_image)
		#↑これを削除

引数 is_rightFalse を指定すれば、左右反転後の画像オブジェクトが right_image に、元画像のままの向きの画像オブジェクトが left_image にそれぞれ参照されるようになります。

ですので、ご自身が用意した画像のキャラクターの向きが左向きの場合、prepareImage の第3引数 is_right には False を指定するようにしてください。キャラクターの向きが右向きの場合は、第3引数は省略して問題ありません。

キャラクターの移動方向の管理

右向きと左向きそれぞれの画像オブジェクトが生成できるようになりましたので、次はキャラクターの移動方向に応じて描画に使用する画像を切り替えられるようにしていきたいと思います。

まずは、キャラクターが今現在どちらに移動しようとしているかの情報が管理できるよう、Character クラスに移動方向を示すデータ属性の追加を行います。

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

キャラクターの向きの管理
class Character:

	def __init__(self):

		#略

		self.jump_height = 200

		#↓これを追加
		self.direction = Character.DIRECTION_RIGHT
		#↑これを追加

追加したデータ属性 direction には、方向の定義 で追加した下記のクラス変数のいずれかを指定します。

  • Character.DIRECTION_LEFT:左方向
  • Character.DIRECTION_RIGHT:右方向

上記では directionCharacter.DIRECTION_RIGHT を指定しているので、キャラクターのオブジェクト生成時の移動方向は右方向となります。

さらに、キャラクターが移動する際に、その移動方向に合わせてデータ属性 direction を変更するようにします。

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

キャラクター移動方向の変更
class Character:

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

			#略

キーボードの左キー or 右キーの入力時には上記の Character クラスの move メソッドが実行されますので、上記の変更により、左右キーが押された時に、その方向に合わせて自動的に direction が設定されるようになります。

移動方向を考慮した画像オブジェクトの取得

次は描画時に使用する画像オブジェクトを移動方向(データ属性 direction)に合わせて切り替えられるようにしていきます。

下記ページの キャラクターの画像オブジェクトの取得 でも解説している通り、キャラクターの画像オブジェクトは Character クラスの getImage メソッドにより取得できるようになっており、描画時に使用する画像オブジェクトはこのメソッドを実行して取得されるようになっています(Game クラスの update メソッドから実行される)。

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

ただし、現状の Character クラスの getImage メソッドは何も考えずに right_image を返却するようになっているため、キャラクターの移動方向に合わせて返却する画像オブジェクトを right_image or left_image から選択するようにしていきます。

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

移動方向を考慮した画像オブジェクトの取得
class Character:

	def getImage(self):

		#↓これを追加
		if self.direction == Character.DIRECTION_RIGHT:
			return self.right_image
		elif self.direction == Character.DIRECTION_LEFT:
			return self.left_image
		#↑これを追加

		#↓これを削除
		#return self.right_image
		#↑これを削除

この Character クラスの getImage メソッドは、定期的に実行される画像描画の直前で Gameクラスから実行されるように既になっています。

ですので、上記のように変更してやれば、キャラクターの移動方向、すなわちキー入力された方向(右 or 左)に合わせた向きの画像が自動的に描画されるようになります。

ということで、ここまでの変更を加えたスクリプトを実行し、左右キーの入力を行えば、キャラクターの移動方向に合わせて画像の向きが変化するようになったことを確認できると思います。

キャラクターの画像が入力したキーによって切り替わる様子

ただし、キャラクターを右方向に移動していけば分かると思うのですが、まだ画面が自動でスクロールされないため、キャラクターが右側に行きすぎると画面外に消えてしまいます。

スクロールバーを右方向に移動すればキャラクターがまた画面内に表示されるようになると思いますが、流石にこれだとゲームとして不便なので、次のページではキャラクターの移動に伴って自動的に画面がスクロールされるようにしていきたいと思います。

スポンサーリンク

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

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

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

このページで作成したスクリプト
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"

class Character:

	DIRECTION_LEFT = 0
	DIRECTION_RIGHT = 1
	DIRECTION_UP = 2

	JUMP_NO = 0
	JUMP_UP = 1
	JUMP_DOWN = 2

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

		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

	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)

	def move(self, direction):
		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.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

class Player(Character):

	def __init__(self):
		pass

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

class Screen:

	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)

		xbar = tkinter.Scrollbar(
			self.master,
			orient=tkinter.HORIZONTAL,
		)

		xbar.grid(
			row=1, column=0,
			sticky=tkinter.W + tkinter.E
		)

		xbar.config(
			command=self.canvas.xview
		)

		self.canvas.config(
			xscrollcommand=xbar.set
		)

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

class Game:

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

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

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

		self.update()

	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)

	def update(self):
		self.master.after(UPDATE_TIME, self.update)

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

		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)


def main():
	app = tkinter.Tk()
	game = Game(app)
	app.mainloop()

if __name__ == "__main__":
	main()

まとめ

このページでは、横スクロールアクションゲームにおける「キャラクターの移動」について解説しました。

実際にキーボード入力によってキャラクターが移動するようになり、少しはゲームを作ってる感が出てきたのではないかと思います。

今回はキャラクターの移動は全て等速度で行うようにしていますが、加速度などを考慮して動作するようにするのも面白いと思います!

また、今回はキーボード入力をアプリが受け付けられるよう、イベントの受付設定を行いました。このイベントを利用すれば、キーボード入力だけでなくマウス操作などもアプリが受け付けられるようにすることができます。

こういったイベントの受付を簡単に実現できる点も tkinter の魅力だと思いますので、イベント受付設定を行う手順についてもこの機会に覚えておくと良いと思います!

次のページでは、操作キャラクターの移動に応じて画面を自動で横スクロールするようにしていきます!ちょっと横スクロールゲームを作ってる感が出てくると思いますので、次のページもぜひ読んでみてください!

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