【Python/tkinter】横スクロールアクションゲームを作る(画面の自動横スクロール)

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

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

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

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

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

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

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

この「横スクロールアクションゲームの作り方の解説」の5ページ目では、主に「画面の自動スクロール」について解説をしていきたいと思います。

画面の自動スクロールの実現方法

では、まずは画面の自動スクロールの実現方法について解説します。

現状作成中のゲームでは、アプリ上のスクロールバーのスライダーを移動させることで画面をスクロールできるようになっています。つまり、スクロールバーのスライダーの位置に伴って画面の表示領域が変化するようになっています。

スライダーの位置の変更に応じて表示領域が変化する様子

スライダーの位置を変更する必要があるので、現状では画面のスクロールは手動で行う必要があります。

このページでは、手動ではなく、自動的に画面がスクロールするようにしていきたいと思います。

この画面の自動スクロールは、操作キャラクターが移動した際に、操作キャラクターの横方向の位置に合わせて表示領域を移動させるようにすることで実現していきます。

キャラクターの異動に伴って表示領域も移動する様子

表示領域の移動

では次は、その表示領域の移動の実現方法について解説していきます。

スポンサーリンク

表示領域の移動方法

この表示領域の移動は、キャンバスの xview_moveto メソッドを利用することで実現することができます。

MEMO

キャンバスには yview_moveto メソッドも用意されており、このメソッドにより表示領域の縦方向の位置を変化させることが可能です

ですが、今回は横スクロールゲームですので、表示領域の横方向の位置を変化させる xview_moveto メソッドに絞って解説していきます

下記ページの ウィジェットの作成 で、キャンバス作成時に scrollregion オプションによりゲーム全体の画面のサイズを表す矩形の座標を指定しました。

横スクロールアクションゲームを作る(ウィジェットの作成と背景表示)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)

xview_moveto は、この scrollregion で指定した矩形の中の「どの位置を表示領域の左端とするか」を指定するメソッドになります。実行すると、引数に応じて表示領域の位置が移動します。

xview_movetoメソッドの意味合いに説明図

xview_moveto メソッドの引数には 01 の実数値を指定します。すなわち、scrollregion で指定した矩形の左端が 0、右端が 1 であると考えて、xview_moveto メソッドの引数に値を指定する必要があります。

xview_movetoの引数についての説明図

要は、表示領域としたい横方向の座標 / scrollregion で指定した矩形の幅xview_moveto メソッドの引数に指定する必要があります。

xview_moveto の引数

では、今回のゲームを作る上では、具体的に xview_moveto の引数には何を指定すれば良いでしょうか?これについて考えていきたいと思います。

今回作成するゲームでは、表示領域は下記のように移動を行うようにしたいと思います。

  • 操作キャラクターの位置に応じて表示領域を移動する
  • 操作キャラクターが表示領域の中央(横方向のみ)に表示されるように表示領域を移動する

まず、表示領域を操作キャラクターの位置に応じて移動するだけであれば、 操作キャラクターの横方向の位置 / scrollregion で指定した矩形の幅xview_moveto の引数に指定するだけで実現することができます。

ただし、xview_moveto メソッドでは、引数で指定された値に対応する位置が表示領域の “左端” となるように表示領域の移動が行われます。従って、操作キャラクターの横方向の位置 / scrollregion で指定した矩形の幅 を引数に指定してしまうと、操作キャラクターが表示領域の左端に表示される形で表示領域の移動が行われてしまいます。

単純にキャラクターの位置に応じて表示領域を移動させるとキャラクターが左端に表示されてしまう様子

操作キャラクターを表示領域の中央に表示したいのであれば、表示領域の幅 / 2 前側(左側)に表示領域を移動する必要があります(その分小さな値を引数に指定する)。

表示領域の位置を表示領域の幅の半分だけ前方にずらすことで操作キャラクターが中央に表示されるようになる様子

すなわち、上記で挙げたような表示領域の移動を行うためには、xview_moveto メソッドには下記で計算される値を引数で指定する必要があります。

xview_movetoの引数の計算
(操作キャラクターの横方向の位置 - 表示領域の幅 / 2) / scrollregion で指定した矩形の幅

上記の計算式における 操作キャラクターの横方向の位置 とは、具体的には操作キャラクターの中央の位置における横方向の座標となります。

現状 Character クラスではキャラクターの左端の位置としてデータ属性 x を保持していますが、これを上記の 操作キャラクターの横方向の位置 にそのまま使用してしまうと、画像の左端が中央に位置するように表示領域が移動することになるので注意が必要です。

データ属性xからxview_movetoの引数を計算して画像の左端が表示領域の中央に表示されてしまう例

以上を踏まえ、表示領域の移動の実装を行なっていきましょう!

表示領域の移動の実装

では実際に、操作キャラクターの位置に応じて表示領域の移動を行うように実装していきたいと思います。

各キャラクターの位置が実際に画面上で変化するのは、キャラクターの描画を行なった時になります。ですので、キャラクターの描画が行われた後に、xview_moveto メソッドを実行して表示領域を移動させるようにしていきたいと思います。

具体的には、キャラクターの描画を行なっているのは Screen クラスの update メソッドですので、Screen クラスの update メソッドを下記のように変更します。引数 player_x を追加しており、この player_x により操作キャラクターの中央の座標(横方向)を指定できるようにしています。

画面の自動スクロール
class Screen:

	def update(self, image_infos, player_x): #←ここを変更

		#略

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

データ属性 view_width は表示領域の幅、game_widthscrollregion に指定した矩形の幅(ゲーム画面の幅)となります。意味を忘れてしまった方は、下記ページの ウィジェットの作成 を参照していただければと思います。

横スクロールアクションゲームを作る(ウィジェットの作成と背景表示)の解説ページアイキャッチ【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)

また、xview_movetomin 関数と max 関数を利用しているのは、引数で指定する値を  0 〜 1 に丸めるためです。上記の式からも分かる通り、player_xview_width / 2 よりも小さい場合は scroll_x0 よりも小さくなるので、その場合は max 関数で 0 に丸めるようにしています。

MEMO

player_xview_width / 2 よりも小さい場合は xview_moveto の引数は 0 になりますので、この範囲では操作キャラクターを移動しても表示領域は変わらず、キャラクターのみが移動するようになります

ですので、ゲーム画面の左端付近ではキャラクターは表示領域の中央以外に表示されることになります

詳細の説明は省略しますが、ゲーム画面の右端付近でも同様にキャラクターが表示領域の中央以外に表示されます

スポンサーリンク

キャラクターの中央の位置の指定

上記の変更により Screen クラスの update メソッドの引数が変化したため、これに合わせてこのメソッド実行側が指定する引数の変更も行っていきたいと思います。具体的には、引数に操作キャラクターの横方向の座標の指定を追加します。

で、この時に重要になるのが、操作キャラクターの「中央」の位置における横方向の座標を指定する必要がある点になります。

現状 Character クラスでは、キャラクターの横方向の位置を管理するデータ属性として x を保持していますが、この x はキャラクターの左端の位置の座標になります(x を左端とした位置にキャラクターの画像が描画される)。

したがって、キャラクターの中央の位置の座標を指定する際には、この x にキャラクターの画像の幅の半分の値をプラスした値を指定する必要があります。

キャラクターの中央の位置の計算

このキャラクターの画像の幅に関しては以降でも当たり判定等で使用しますので、ここで Character クラスにデータ属性 width を追加したいと思います。また、当たり判定では高さも必要になるため、ついでに高さを示すデータ属性 height もここで追加していきたいと思います。

画像の幅と高さを管理するデータ属性

では、Character クラスにデータ属性 widthheight を追加していきます。

画像の幅と高さに関しては、tkinter の画像オブジェクトに width メソッドと height メソッドを実行させることで取得することができます。

ですので、Character クラスの prepareImage を下記のように変更することで、Character クラスのデータ属性として、そのキャラクターの画像の幅を示す width と高さを示す height をそれぞれ追加することができます。

画像のサイズのデータ属性の追加
class Character:
	def prepareImage(self, path, size, is_right=True):
		# 略

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

今までのように __init__ ではなく prepareImage でデータ属性の追加を行なっているのは、widthheight が画像に関わるデータ属性だからです。今後も画像に関わるデータ属性に関しては、この prepareImage の中で追加を行うようにしたいと思います。

また、Character クラスでは画像を参照するデータ属性として right_imageleft_image の2つを持っていますが、これらは左右反転しているかどうかの違いしかないため、幅と高さに変わりはありません。ですので、どちらから幅と高さを取得しても問題ないです(上記では right_image から幅と高さを取得しています)。

中央の位置の指定

続いて、本題である Screen クラスの update メソッドに指定する引数の追加を行いたいと思います。

具体的には、Game クラスの update メソッドを下記のように変更し、引数で操作キャラクターの中央の位置(横方向)を指定するようにします。

キャラクターの中央の位置の指定
class Game:

	def update(self):

		#略

		image_infos = []
		for character in self.characters:
			#略

		self.screen.update(image_infos, self.player.x + self.player.width / 2) #←ここを変更

以上の変更により、キャラクターの横方向の位置に応じて表示領域が自動的に変化するようになり、自動スクロールが実現できたことになります。

変更後のスクリプトを実行し、キャラクターを移動させていけば、画面が自動的に横スクロールする様子も確認できると思います。

キャラクターの移動に伴い表示領域も移動する様子

スクロールバーの削除

ここまでの変更により、操作キャラクターの位置に伴って画面が自動的にスクロールされるようになりました。

今まではスクロールバーを設けて手動でスクロールする必要がありましたが、自動でスクロールできるようになったのでスクロールバーはもはや不要です。

ですので、このスクロールバーはもう削除してしまおうと思います。

MEMO

もしかしたら tkinter のバージョン等により、スクロールバーを削除すると上手くスクロールできなくなる可能性があるので注意してください

その場合は、スクロールバーは消さずに残しておいてください

このスクロールバーの削除は、Screen クラスの createWidgets メソッドにおけるスクロールバーに関する処理を全て削除することで実現できます。

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

スクロールバーの削除
class Screen:

	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)

		# 以降の処理は削除

変更後にスクリプトを実行すれば、スクロールバーが消えていること、さらにスクロールバーがなくても画面が自動的にスクロールしてくれることを確認できると思います。

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

短めではありますが、キリも良いので以上でこのページの解説は終了させていただきます。

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

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

		self.width = self.right_image.width()
		self.height = self.right_image.height()

	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)
		

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

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

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

if __name__ == "__main__":
	main()

スポンサーリンク

まとめ

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

キャラクターの移動に合わせて画面が自動的にスクロールするようになり、ちょっとずつ横スクロールアクションゲームが出来てきたことが実感できるのではないかと思います。

ここまでゲームに登場するキャラクターは操作キャラクターのみでしたが、次のページではゴールを作成し、ゴールとの当たり判定およびゴールと当たった時にゲームクリアを表示するようにしていきたいと思います!

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