このページでは、Python で tkinter を利用した簡単な「横スクロールアクションゲーム」の作り方を解説していきます。
このページは「横スクロールアクションゲームの作り方の解説」の5ページ目となります。
4ページ目は下記ページとなり、4ページ目では主に操作キャラクターの移動の解説を行なっています。
 【Python/tkinter】横スクロールアクションゲームを作る(キャラクターの移動)
  【Python/tkinter】横スクロールアクションゲームを作る(キャラクターの移動)  
また、このページでは、上記ページの このページで作成したスクリプト で紹介しているスクリプトを変更していきながらゲームを作成していきます。
ですので、事前に上記のページを読んでおくことをオススメします。
この「横スクロールアクションゲームの作り方の解説」の5ページ目では、主に「画面の自動スクロール」について解説をしていきたいと思います。
Contents
画面の自動スクロールの実現方法
では、まずは画面の自動スクロールの実現方法について解説します。
現状作成中のゲームでは、アプリ上のスクロールバーのスライダーを移動させることで画面をスクロールできるようになっています。つまり、スクロールバーのスライダーの位置に伴って画面の表示領域が変化するようになっています。

スライダーの位置を変更する必要があるので、現状では画面のスクロールは手動で行う必要があります。
このページでは、手動ではなく、自動的に画面がスクロールするようにしていきたいと思います。
この画面の自動スクロールは、操作キャラクターが移動した際に、操作キャラクターの横方向の位置に合わせて表示領域を移動させるようにすることで実現していきます。

表示領域の移動
では次は、その表示領域の移動の実現方法について解説していきます。
スポンサーリンク
表示領域の移動方法
この表示領域の移動は、キャンバスの xview_moveto メソッドを利用することで実現することができます。
キャンバスには yview_moveto メソッドも用意されており、このメソッドにより表示領域の縦方向の位置を変化させることが可能です
ですが、今回は横スクロールゲームですので、表示領域の横方向の位置を変化させる xview_moveto メソッドに絞って解説していきます
下記ページの ウィジェットの作成 で、キャンバス作成時に scrollregion オプションによりゲーム全体の画面のサイズを表す矩形の座標を指定しました。
 【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)
  【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)  
xview_moveto は、この scrollregion で指定した矩形の中の「どの位置を表示領域の左端とするか」を指定するメソッドになります。実行すると、引数に応じて表示領域の位置が移動します。

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

要は、表示領域としたい横方向の座標 / scrollregion で指定した矩形の幅 を xview_moveto メソッドの引数に指定する必要があります。
xview_moveto の引数
では、今回のゲームを作る上では、具体的に xview_moveto の引数には何を指定すれば良いでしょうか?これについて考えていきたいと思います。
今回作成するゲームでは、表示領域は下記のように移動を行うようにしたいと思います。
- 操作キャラクターの位置に応じて表示領域を移動する
- 操作キャラクターが表示領域の中央(横方向のみ)に表示されるように表示領域を移動する
まず、表示領域を操作キャラクターの位置に応じて移動するだけであれば、 操作キャラクターの横方向の位置 / scrollregion で指定した矩形の幅 を xview_moveto の引数に指定するだけで実現することができます。
ただし、xview_moveto メソッドでは、引数で指定された値に対応する位置が表示領域の “左端” となるように表示領域の移動が行われます。従って、操作キャラクターの横方向の位置 / scrollregion で指定した矩形の幅 を引数に指定してしまうと、操作キャラクターが表示領域の左端に表示される形で表示領域の移動が行われてしまいます。

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

すなわち、上記で挙げたような表示領域の移動を行うためには、xview_moveto メソッドには下記で計算される値を引数で指定する必要があります。
(操作キャラクターの横方向の位置 - 表示領域の幅 / 2) / scrollregion で指定した矩形の幅上記の計算式における 操作キャラクターの横方向の位置 とは、具体的には操作キャラクターの中央の位置における横方向の座標となります。
現状 Character クラスではキャラクターの左端の位置としてデータ属性 x を保持していますが、これを上記の 操作キャラクターの横方向の位置 にそのまま使用してしまうと、画像の左端が中央に位置するように表示領域が移動することになるので注意が必要です。

以上を踏まえ、表示領域の移動の実装を行なっていきましょう!
表示領域の移動の実装
では実際に、操作キャラクターの位置に応じて表示領域の移動を行うように実装していきたいと思います。
各キャラクターの位置が実際に画面上で変化するのは、キャラクターの描画を行なった時になります。ですので、キャラクターの描画が行われた後に、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_width は scrollregion に指定した矩形の幅(ゲーム画面の幅)となります。意味を忘れてしまった方は、下記ページの ウィジェットの作成 を参照していただければと思います。
 【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)
  【Python/tkinter】横スクロールアクションゲームを作る(ウィジェットの作成および背景の表示)  
また、xview_moveto で min 関数と max 関数を利用しているのは、引数で指定する値を  0 〜 1 に丸めるためです。上記の式からも分かる通り、player_x が view_width / 2 よりも小さい場合は scroll_x が 0 よりも小さくなるので、その場合は max 関数で 0 に丸めるようにしています。
player_x が view_width / 2 よりも小さい場合は xview_moveto の引数は 0 になりますので、この範囲では操作キャラクターを移動しても表示領域は変わらず、キャラクターのみが移動するようになります
ですので、ゲーム画面の左端付近ではキャラクターは表示領域の中央以外に表示されることになります
詳細の説明は省略しますが、ゲーム画面の右端付近でも同様にキャラクターが表示領域の中央以外に表示されます
スポンサーリンク
キャラクターの中央の位置の指定
上記の変更により Screen クラスの update メソッドの引数が変化したため、これに合わせてこのメソッド実行側が指定する引数の変更も行っていきたいと思います。具体的には、引数に操作キャラクターの横方向の座標の指定を追加します。
で、この時に重要になるのが、操作キャラクターの「中央」の位置における横方向の座標を指定する必要がある点になります。
現状 Character クラスでは、キャラクターの横方向の位置を管理するデータ属性として x を保持していますが、この x はキャラクターの左端の位置の座標になります(x を左端とした位置にキャラクターの画像が描画される)。
したがって、キャラクターの中央の位置の座標を指定する際には、この x にキャラクターの画像の幅の半分の値をプラスした値を指定する必要があります。

このキャラクターの画像の幅に関しては以降でも当たり判定等で使用しますので、ここで Character クラスにデータ属性 width を追加したいと思います。また、当たり判定では高さも必要になるため、ついでに高さを示すデータ属性 height もここで追加していきたいと思います。
画像の幅と高さを管理するデータ属性
では、Character クラスにデータ属性 width と height を追加していきます。
画像の幅と高さに関しては、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 でデータ属性の追加を行なっているのは、width と height が画像に関わるデータ属性だからです。今後も画像に関わるデータ属性に関しては、この prepareImage の中で追加を行うようにしたいと思います。
また、Character クラスでは画像を参照するデータ属性として right_image と left_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) #←ここを変更以上の変更により、キャラクターの横方向の位置に応じて表示領域が自動的に変化するようになり、自動スクロールが実現できたことになります。
変更後のスクリプトを実行し、キャラクターを移動させていけば、画面が自動的に横スクロールする様子も確認できると思います。

スクロールバーの削除
ここまでの変更により、操作キャラクターの位置に伴って画面が自動的にスクロールされるようになりました。
今まではスクロールバーを設けて手動でスクロールする必要がありましたが、自動でスクロールできるようになったのでスクロールバーはもはや不要です。
ですので、このスクロールバーはもう削除してしまおうと思います。
もしかしたら 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】横スクロールアクションゲームを作る(ゴールの作成)
  【Python/tkinter】横スクロールアクションゲームを作る(ゴールの作成)  
オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!



