【Python/tkinter】横スクロールアクションゲームを作る(カスタマイズ例)

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

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

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

8ページ目は下記ページとなり、8ページ目では主に詳細な当たり判定を行う方法の解説を行なっています。

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

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

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

このページでは、今まで作成してきた横スクロールアクションゲームのカスタマイズ例を紹介していきます。

具体的に紹介するカスタマイズ例は下記のようになります。

定義値を変更するだけで行えるカスタマイズもあれば、スクリプトをがっつり変更して機能追加するものもあります。

これらはあくまでもカスタマイズの一例です。

おそらく、ここまで横スクロールアクションゲームの作り方の解説を読んできてくださった方であれば、動作や見た目に不満があったり、もっといろんな機能を追加してみたいと思ってくださっているのではないかと思います。

ぜひ、このページで紹介するカスタマイズ例を参考にして、ご自身が持たれている不満や要望をご自身の手でプログラミングして解決してみてください!

こういったことを繰り返していけば、自然とプログラミング力もみについていくと思います!

敵キャラクターの数やゲーム画面のサイズ変える

まずはグローバル変数の値を変更するだけで行える、一番簡単なカスタマイズ例から紹介していきます。

敵キャラクターの数を変更する

ここまで作成してきたゲームでは、スクリプト先頭部分で設定しているグローバル変数の値を変更することでゲームのカスタマイズが行えるようになっています。

例えば敵キャラクターの数も、スクリプト先頭部分で設定しているグローバル変数の値により変更可能です。

具体的には、下記の NUM_CAT_ENEMY の数を変更すれば猫型敵キャラクターの数を、NUM_DOG_ENEMY の数を変更すれば犬型敵キャラクターの数を変更することが可能です。

敵キャラクターの数の変更
NUM_CAT_ENEMY = 3
NUM_DOG_ENEMY = 3

ただし、敵キャラクターの数が多過ぎると当たり判定や画像の描画の負荷が大きくなりすぎてゲームが起動しなくなったりゲームの動きがガタガタしたりするので注意してください。

スポンサーリンク

ゲーム画面のサイズを変更する

また、下記の GAME_WIDTH では、ゲーム画面の横幅を変更することができますので、ステージの長さを長くしたいのであれば、ここの値を大きくするだけで実現することができます。

ゲーム画面のサイズの変更
GAME_WIDTH = 1500

VIEW_WIDTHVIEW_HEIGHT を変更すれば表示領域のサイズも変更できますし、特に VIEW_HEIGHT を変更することで同時にゲーム画面の高さも変更されますので、縦長のゲームにしたい場合などに変更してみてください。

表示のサイズに関するグローバル変数の説明図

背景画像の縦横比を保つ

先ほど ゲーム画面のサイズを変更する でゲーム画面のサイズを変更可能であることを説明しましたが、ゲーム画面の高さに対してゲーム画面の幅が大きすぎると背景画像が横に伸びてしまい、見栄えが悪くなる可能性があるので注意してください。

画像の縦横比が変化して画像の見栄えが悪くなる例

もし使用している背景画像が反転しても見た目にそこまで影響ないものであれば、Screen クラスの drawBackground メソッドを下記のように変更すれば、この見栄えが悪くなる現象は解決することができると思います。

背景画像の縦横比を保つ
class Screen:

	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)
		#↑これは削除

		#↓これを追加
		ratio = self.game_height / image.height
		resized_image = image.resize((round(ratio * image.width), round(ratio * image.height)))

		import math
		num_image = math.ceil(self.game_width / resized_image.width)
		bg_image = Image.new(resized_image.mode, (num_image * resized_image.width, self.view_height))

		for i in range(num_image):
			if i % 2 == 0:
				bg_image.paste(resized_image, ((i * resized_image.width), 0))
			else:
				bg_image.paste(ImageOps.mirror(resized_image), ((i * resized_image.width), 0))

		self.bg_image = ImageTk.PhotoImage(bg_image)
		#↑これを追加

		self.canvas.create_image(
			0, 0,
			anchor=tkinter.NW,
			image=self.bg_image
		)

上記の drawBackground メソッドで何をやっているかを簡単に説明すると、まず resize メソッド実行部分で背景画像を画像の縦横比を保ったままゲーム画面の高さに合わせて拡大縮小しています。

ゲーム画面の高さに合わせて背景画像を拡大縮小する様子

続いて、その拡大縮小後の画像がゲーム画面を埋め尽くすのに何枚必要であるかをカウントしています(切り上げするための関数 ceil を利用するために import math を行なっています。メソッド内で import していますが、ファイルの先頭で importした方が良いです)。

貼り付けする画像の枚数をカウントする様子

さらに、そのカウントした枚数分の画像が入り切るようなサイズの画像オブジェクトを Image.new により生成しています。

カウントした画像の枚数分入り切るようなサイズの画像オブジェクトを生成する様子

そして、その生成したオブジェクトに拡大縮小後の画像を横に並べるように貼り付けていっています。この画像の貼り付けを行なっているのが pasteメソッド実行部分になります。

画像を並べて貼り付けしていく様子

そのまま単純に横に並べてしまうと、下の図のように画像の端部分がガタガタになってしまう可能性があるので、それを防ぐために左右反転前後の画像を交互に並べるようにして貼り付けるようにしています。

そのままの方向で貼り付けると画像の端部分がガタガタになってしまう様子

画像の左右反転を行なっているので文字などがあるとむしろゲームの背景がおかしくなってしまいますが、今回用意したような背景向けの画像であれば、自然に画像が並べられると思います。

ボタンで「ゲーム開始」「停止」「リセット」を行う

現状では、ゲームはスクリプト実行直後から始まってしまいますし、ゲームが始まったら停止もできません。また、ゲームを最初からやり直そうと思うとスクリプトを終了してから再度実行する必要があります。

ここでは、これらの「ゲーム開始」「停止」「リセット」をボタン操作で行えるようにゲームをカスタマイズしていきたいと思います。

作成する操作ボタンを示す図

ボタンを作成する

まずはこれらを操作するためのボタンを作成したいと思います。

ボタンの作成は Screen クラスの createWidgets メソッドを下記のように変更することで行うことができます。今回は3つの操作を行えるようにしようとしているので、3つのボタンを用意しています。

ボタンの作成
class Screen:

	def createWidgets(self):

		#略

		self.canvas.grid(column=0, row=0)

		#↓これを追加
		self.frame = tkinter.Frame(
			self.master
		)
		self.frame.grid(column=0, row=1)

		self.start_button = tkinter.Button(
			self.frame,
			text="ゲーム開始"
		)
		self.start_button.grid(column=0, row=0)

		self.stop_button = tkinter.Button(
			self.frame,
			text="ゲーム停止"
		)
		self.stop_button.grid(column=1, row=0)

		self.reset_button = tkinter.Button(
			self.frame,
			text="ゲームリセット"
		)
		self.reset_button.grid(column=3, row=0)
		#↑これを追加

一旦フレームウィジェットを作成し、そのフレームウィジェット上にボタンを横に並べるように gridメソッドを利用して配置を行なっています。フレームウィジェット上にボタンを配置するので、tkinter.Button の第1引数はフレームウィジェットの self.frame を指定しています。

フレームウィジェットはアプリの複雑なレイアウトの実現や部品化を行う時に便利です。下記ページで詳細を解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

フレームウィジェットの作り方の解説ページアイキャッチTkinterの使い方:フレームウィジェット(Frame)の使い方

ボタンに関しては、ボタン作成時(tkinter.Button() 実行時)に command オプションでボタンが押された際に実行するメソッドや関数を指定することもできます。ただ今回は、後から Game クラスで bind メソッドを実行し、そこでボタンが押された時に実行するメソッドの設定を行うようにしたいと思います(イベント関連の処理は Game クラスの役割としているため)。

スポンサーリンク

ボタンが押された時のイベント受付設定

続いてボタンが押された時のイベントを受け付けるように設定を行います。

このイベントの受付設定は bind メソッドにより実現することができます。詳しくは下記ページをご参照いただければと思います。

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

このイベントの受付設定を行うために、まずは Game クラスの __init__ を下記のように変更します。

ボタンクリック時のイベント受付設定
class Game:

	def __init__(self, master):

		#略

		for _ in range(NUM_DOG_ENEMY):
			enemy = DogEnemy()
			self.characters.append(enemy)

		#↓これを追加
		self.screen.start_button.bind("<ButtonPress>", self.start)
		self.screen.stop_button.bind("<ButtonPress>", self.stop)
		self.screen.reset_button.bind("<ButtonPress>", self.reset)
		#↑これを追加

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

		self.update()

この変更により、ボタンがマウスでクリックされた時に Game クラスの下記のメソッドが実行されるようになります。

  • ゲーム開始ボタンクリック時:start
  • ゲーム停止ボタンクリック時:stop
  • ゲームリセットボタンクリック時:reset

ただし、これらのメソッドをまだ用意していないので、とりあえず下記のように何もしないメソッドを Game クラスに追加しておきます。

ボタンが押された時に実行されるメソッド
class Game:

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

	def stop(self, event):
		pass

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

ゲーム開始ボタンの実現

次は、「ゲーム開始ボタン」がクリックされた時にゲームを開始するようにしていきたいと思います。

まず、現状なぜゲームを起動した途端(スクリプト実行直後)にゲームが開始されてしまうのかについて整理しておくと、これは下記の2つをゲーム起動時に実行しているからです(より具体的には Game クラスの __init__で下記を実行している)。

  • ゲーム起動時にキーボードのキー入力の受付を行なっている
    • なので、ゲーム起動直後からキー入力でキャラクターの移動が可能になる
  • ゲーム起動時に定期的な処理を開始している
    • なので、ゲーム起動直後から敵キャラクターの移動やキャラクターの画像描画等が定期的に行われる

したがって、ゲーム起動時には上記が行われないようにし、さらに「ゲーム開始ボタン」がクリックされた時に初めて上記が行われるようにしてやれば、「ゲーム開始ボタン」がクリックされた時にゲームを開始させるようにすることができます。

前者に関しては、ゲーム起動時にはキーボードのキー入力に対するイベント受付設定(bind メソッドの実行)を行わないようにし、さらに「ゲーム開始ボタン」をクリックされた時にこの設定を行うように変更すれば良いです。

後者に関しては、まず定期的に処理が行われる仕組みを復習しておくと、これは Game クラスの update メソッドが実行された際に after メソッドで再度 update メソッドを実行するようになっているからです。これにより一度 Game クラスの update メソッドが実行されれば、以降定期的に繰り返し Game クラスの update メソッドが実行されることになっています。

ですので、Game クラスの update メソッドの実行をゲーム起動時に行わないようにし、さらに「ゲーム開始ボタン」がクリックされた時に初めて update メソッドを実行するようにしてやれば、「ゲーム開始ボタン」がクリックされない限り定期的な処理が始まらないようにすることができます。

では、上記のように処理を変更していきたいと思います。

まずは Game クラスの __init__ を下記のように変更します。これにより、ゲーム起動時にキーボードのキー入力のイベント受付設定と定期的処理の開始が行われなくなります。

ゲーム起動時の処理の削除
class Game:

	def __init__(self, master):

		#略

		self.screen.start_button.bind("<ButtonPress>", self.start)
		self.screen.stop_button.bind("<ButtonPress>", self.stop)
		self.screen.reset_button.bind("<ButtonPress>", self.reset)

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

		#self.update()
		#↑これを削除

さらに、先ほど追加した Game クラスの start メソッドに上記で削除した処理を追加します。具体的には、Game クラスの start メソッドを下記のように変更します。

ゲーム開始ボタンクリック時の処理の追加
class Game:

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

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

		self.update()
		#↑これを追加

一応これらの変更で、ゲーム起動直後にはゲームが開始されず、「ゲーム開始ボタン」がクリックされた時にゲームが開始されるようにすることはできます。

ただ、これだけだと「ゲーム開始ボタン」がクリックされるたびに何回も Game クラスの update メソッドが実行されてしまって動きがおかしくなってしまいます…(二重に after メソッドが実行されるので敵キャラクターの移動や画像描画が倍速で行われるようになる)。

ですので、ゲーム開始後は「ゲーム開始ボタン」がクリックされても何も行わないようにしていきたいと思います。

そのため、Game クラスにゲームを開始しているかどうかを判断するためのデータ属性 is_playing を追加し、Game クラスの __init__ でデータ属性 is_playingFalse に設定するようにします。

ゲーム開始の状態を管理するデータ属性
class Game:

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

		#↓これを追加
		self.is_playing = False
		#↑これを追加
		
		self.screen = Screen(self.master)

		#略

さらに、Game クラスの start メソッドでは is_playingFalse の場合のみ処理を行うよう、下記のように変更を行います。start メソッドが実行されたということは、ゲーム開始されたということなので、is_playingTrue に設定するようにもしています。

ゲーム開始中の処理のスキップ
class Game:

	def start(self, event):
		#↓これを追加
		if self.is_playing:
			return

		self.is_playing = True
		#↑これを追加

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

		self.update()

これでゲーム開始後に「ゲーム開始ボタン」がクリックされても何も実行されないようにすることができます。

ゲーム停止ボタンの実現

次は「ゲーム停止ボタン」が押された時にゲームを停止するようにしていきます。

これは、先ほどの「ゲーム開始ボタン」の時とは逆に、stop メソッドで下記の3つのことを行えば良いです。

  • is_playingFalse に設定する
  • キーボードのキー入力のイベントを受け付けないように設定する
  • Game クラスの update メソッドの定期的実行を止める

まず、イベントの受付を取り消すためには unbind メソッドを実行してやれば良いです。したがって、上記の前者2つを実現するためには、Game クラスの stop メソッドを下記のように変更すれば良いです。

ゲームの停止処理1
class Game:

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

		#↓これを追加
		if not self.is_playing:
			return

		self.is_playing = False

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

念の為ゲーム開始していない状態で押された場合には何も処理を行わないように最初の if 文を入れています。

また、Game の update メソッドの定期的実行を止めるためには、まず after メソッドの実行を after_cancel メソッドによりキャンセルする方法が考えられます。

もしくは、stop メソッドの中で is_playing False に設定するのですから、Game クラスの update メソッドの中で is_playing False の場合に after メソッドを実行しないようにすることでも定期的実行を止めることもできます。

今回は後者の方法を選択したいと思います。具体的には、下記のように Game クラスの update メソッドの先頭部分を変更し、is_playing False の場合は何も処理を行わないようにします(is_playingstop メソッドで False に設定される)。

ゲームの停止処理2
class Game:

	def update(self):
		#↓これを追加
		if not self.is_playing:
			return
		#↑これを追加

		if self.player.state == Character.STATE_NORMAL:
			self.master.after(UPDATE_TIME, self.update)

		#略

このように変更を行えば、「ゲーム停止ボタン」がクリックされた時にゲームが停止することが確認できると思います。さらに、ゲーム停止した状態で「ゲーム開始ボタン」がクリックされればゲームが再開できることも確認できると思います。

ちなみに、今回は使用しませんでしたが、after_cancel メソッドも after メソッドによる定期的処理を停止させる時に便利です。下記ページの途中で after_cancel についても解説していますので、興味のある方はぜひ読んでみてください。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

スポンサーリンク

ゲームリセットボタンの実現

この章の最後として、「ゲームリセットボタン」が押された際にゲームをリセット、つまり最初からやり直せるようにしていきたいと思います。

ゲームのリセットに関しては、Screen クラスや Character クラスのオブジェクトを初期化してやることで実現することができます。これらの初期化はオブジェクト生成時に行なっていますので、要は各オブジェクトを生成し直してやればゲームのリセットが実現できます。

これらのオブジェクトの生成は Game クラスの __init__ から行なっていますので、「ゲームリセットボタン」が押された際にこれを実行してやれば良いのですが、Game クラスから自身の __init__ を実行するのはちょっと違和感があります。

そのため、オブジェクト生成を行う部分を __init__ から新たなメソッド prepare に移動させ、「ゲームリセットボタン」がクリックされた際に、その prepare をメソッドを実行するようにしたいと思います(つまり reset メソッドから prepare メソッドを実行する)。

このために、まずは下記のように Game クラスの __init__ の変更と prepare メソッドの追加を行います。__init__ には self.master = masterself.is_playing = Falseself.prepare() のみが残ることになります。

オブジェクト生成処理の移動
class Game:

	def __init__(self, master):
		self.master = master
		self.is_playing = False

		#↓これは削除してprepareに移動
		#self.screen = Screen(self.master)

		#略

		#self.screen.reset_button.bind("<ButtonPress>", self.reset)
		#↑これは削除してprepareに移動

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

	#↓これを追加
	def prepare(self):
		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.screen.start_button.bind("<ButtonPress>", self.start)
		self.screen.stop_button.bind("<ButtonPress>", self.stop)
		self.screen.reset_button.bind("<ButtonPress>", self.reset)
	#↑これを追加

ボタンに対する bind 実行部分も prepare 側に移動させたのは、この bind を実行するオブジェクトであるボタンウィジェットが prepare 側の Screen() の中で行われるからです(Screen() が実行されるたびに bind も実行しなくてはならない)。

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

ゲームのリセット
class Game:

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

		#↓これを追加
		self.stop(None)

		self.screen.start_button.unbind("<ButtonPress>")
		self.screen.stop_button.unbind("<ButtonPress>")
		self.screen.reset_button.unbind("<ButtonPress>")

		self.prepare()
		#↑これを追加

stop メソッドでゲームを停止してから、prepare メソッドでのオブジェクト生成のやり直しを行うようにしています。

また、一応ボタンに対するイベント設定も unbind で解除してから prepare メソッドを実行するようにしています。prepare メソッドの中でガベージコレクションが動作して古いボタンのオブジェクトも削除されるはずなので、この unbind はもしかしたら不要かもしれません。ちょっと確証が持てなかったため、念の為実行するようにしています。

これらの変更により、「ゲームリセットボタン」をクリックしてから「ゲーム開始ボタン」をクリックすれば、ゲームを最初からやり直すことができるようになります。

例えばゲームクリアやゲームオーバーになってからでもゲームのやり直しが可能です。ただ、敵キャラクターの位置は現状ランダムに決めているため、敵キャラクターの初期位置まで元に戻るわけではないので気をつけてください。

新たなキャラクターを作成する

次は新たなキャラクターを作る例を紹介していきます。

新たな敵キャラクターを作成する

まずは新たな敵キャラクターを作成する例を紹介します。

敵キャラクターを新たに作成するためには、基本下記の3つを行えば良いです。

  • 新たな敵キャラクター用の Enemy クラスのサブクラスを作成する
  • 作成したサブクラスのオブジェクトを生成し、Game クラスのデータ属性 characters に追加する
  • 必要に応じて collided メソッドで新たな敵キャラクター用の衝突時の処理を追記する

ここでは無敵な敵キャラクターを TurtleEnemy クラスとして作成していく例で説明していきます。

ちなみにですが、私が動作確認時に使用した画像は いらすとや の下記 URL の亀の画像になります。

https://www.irasutoya.com/2017/03/blog-post_31.html

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

新たな敵キャラクターの亀の画像

転載元:いらすとや

サブクラスの作成

まずは TurtleEnemy クラスを作成していきます。といっても作り方は他の敵キャラクターの時と同じです。

この辺りの敵キャラクターの作成方法は下記ページで解説していますので、詳しく知りたい方はこちらを読んでみてください。

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

今回は下記のような TurtleEnemy クラスを作成したいと思います。"画像のファイルパス" 部分は亀の画像のファイルパスが指定されることを想定しています。

亀型敵キャラクターの作成
class TurtleEnemy(Enemy):

	def __init__(self):
		super().__init__("画像のファイルパス", (80, 80), False)

		self.jump_height = 0
		self.speed_x = 10

jump_height0 にしているのでジャンプできないようになっています。また speed_x10 に設定しているので他のキャラクターよりも動作が遅くなっています。この辺りのサブクラスごとのカスタマイズは、作成したいキャラクターに応じて適当に変えてやれば良いです。

オブジェクトの生成

次は先程作成したオブジェクトを生成し、Game クラスのデータ属性 characters に追加します。この characters にオブジェクトを格納しておけば、自動的に定期的に移動したり画像が描画されたりするようになります。

オブジェクトの生成
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(NUM_CAT_ENEMY):
			enemy = CatEnemy()
			self.characters.append(enemy)

		for _ in range(NUM_DOG_ENEMY):
			enemy = DogEnemy()
			self.characters.append(enemy)

		#↓これを追加
		enemy = TurtleEnemy()
		self.characters.append(enemy)
		#↑これを追加

上記では __init__ を変更していますが、ボタンで「ゲーム開始」「停止」「リセット」を行う のカスタマイズをされている場合は prepare メソッドを変更する必要があるので注意してください。

以上の変更で新たに作成した敵キャラクターが画面に表示され、定期的な移動や当たり判定が他の敵キャラクター同様に行われるようになります。

当たった時の処理

もし当たった時の処理を他の敵キャラクターと変更したい場合は Game クラスの collide メソッドを変更する必要があります。

このメソッドでクラスごとに当たった時の処理を定義していますので、作成したクラスに対して新たな処理を追加してやれば、当たった時にその処理が実行されるようになります。

例えば下記のように collide メソッドを変更すれば、TurtleEnemy のオブジェクトと Player のオブジェクトが当たった際には、必ず Player が倒されてゲームオーバーになるようになります。これで無敵な敵キャラクターの完成です。

当たった時の処理の追加
class Game:

	def collide(self, character, opponent):

		#略

		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)

		#↓これを追加(Enemyに対する処理よりも上に追加)
		elif isinstance(character, Player) and isinstance(opponent, TurtleEnemy):
			character.defeated()
		elif isinstance(character, TurtleEnemy) and isinstance(opponent, Player):
			opponent.defeated()
		#↑これを追加(Enemyに対する処理よりも上に追加)
		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()

変更時のポイントは、スーパークラスである Enemy に対する elif 文よりも上側にサブクラスの当たった時の処理を追加する必要がある点です。

isinstance は第1引数のオブジェクトが第2引数のクラスのインスタンスであるかどうかを判断する関数ですが、第2引数にスーパークラスのものを指定した場合、そのサブクラスのインスタンスであっても True が返却されます。

ですので、スーパークラスである Enemy に対する elif 文よりも下側に追加してしまうと、characteropponent が追加したサブクラスのオブジェクトであっても isinstance(character, Enemy)  or  isinstance(opponent, Enemy)True を返却し、Enemy 側に対する処理が実行されてしまいます。

こういう処理の順序も意識する必要があるので、collide メソッドは Character の各サブクラス側で実装した方が良かったかもなぁとちょっと思ってます…。

とりあえず、新たな敵キャラクターの追加の仕方のイメージは伝わったのではないでしょうか?

スポンサーリンク

アイテムを作成する

ここまで解説してきた枠組みに捉われず、他の種類のキャラクターを追加するのも良いカスタマイズだと思います。

ということで、次はアイテムを作成する例を紹介します(アイテムがキャラクターなのかは置いといて…)。

アイテムも基本的に敵キャラクターを作成する時と同様で、まず下記の3つが必要になります(スーパークラスは Character にしていますが、追加したいアイテムが多いのであれば Item クラスのようなスーパークラスを作成しても良いと思います)。

  • 新たなアイテム用の Character クラスのサブクラスを作成する
  • 作成したサブクラスのオブジェクトを生成し、Game クラスのデータ属性 characters に追加する
  • collided メソッドで新たなアイテム用の衝突時の処理を追記する

今回は取得すれば(当たったら)無敵状態になれるアイテム用のクラス Star を作っていきたいと思います。

無敵状態なのですから、通常状態とは異なる動作を実現する必要があります。今回は無敵状態の間は敵に踏みつけられても倒されないようにしていきたいと思います。

そのため下記の処理も追加で実現していく必要があります

  • 当たった時に操作キャラクターを無敵状態にする
  • 無敵状態になったら敵に踏みつけられても倒されないようにする
  • 時間が経ったら無敵状態から通常状態に戻るようにする
  • 無敵状態であることが分かるように、無敵状態の時は見た目を変更する

ここからは変更箇所だけサラッと紹介していきたいと思います。

サブクラスの作成

今回は下記のような Star クラスを作成しました。"画像のファイルパス" 部分は星の画像のファイルパスが指定されることを想定しています。

アイテムのサブクラスの作成
class Star(Character):

	def __init__(self):
		super().__init__("画像のファイルパス", (50, 50))

		self.x = random.randrange(300, GAME_WIDTH - 300)
		self.direction = Character.DIRECTION_LEFT

		self.speed_x = 30
		self.speed_y = 30
		self.jump_height = 50
		

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

ちなみにですが、私が動作確認時に使用した画像は いらすとや の下記 URL の星の画像になります。

https://www.irasutoya.com/2013/08/blog-post_5160.html

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

新たなアイテムの星の画像

転載元:いらすとや

オブジェクトの生成

オブジェクトの生成はコンストラクタの実行により行い、いつも通り生成したオブジェクトは Game クラスの characters に追加します。

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

	def __init__(self, master):

		#略

		enemy = TurtleEnemy()
		self.characters.append(enemy)

		#↓これを追加
		star = Star()
		self.characters.append(star)
		#↑これを追加

		#略

上記では __init__ を変更していますが、ボタンで「ゲーム開始」「停止」「リセット」を行う のカスタマイズをされている場合は prepare メソッドを変更する必要があるので注意してください。

当たった時の処理

当たった時の処理も下記のように collide メソッドの最後に追加しています。

当たった時の処理
class Game:

	def collide(self, character, opponent):

		#略

		#↓これを追加
		elif isinstance(character, Star) and isinstance(opponent, Player):
			opponent.muteki()
			character.defeated()
		elif isinstance(character, Player) and isinstance(opponent, Star):
			character.muteki()
			opponent.defeated()
		#↑これを追加

muteki は後から追加するキャラクターを無敵状態にするメソッドになります。

当たった時にアイテム側は defeated メソッドを実行していますが、このメソッドを実行している理由は当たったアイテムを非表示にするためです。

今回は簡単のためこのメソッドを使用していますが、別にアイテムは倒されるわけではないので、本当は別途別のメソッドを用意した方が良いです。

無敵状態への遷移

次は先程追加した当たった時の処理で実行される muteki メソッドを作成します。他の状態と見分けがつくように、クラス変数の追加も行っています。

無敵状態への遷移
class Character:

	STATE_NORMAL = 0
	STATE_CLEAR = 1
	STATE_DEFEATED = 2

	#↓これを追加
	STATE_MUTEKI = 3
	#↑これを追加

class Player(Character):

	#↓これを追加
	def muteki(self):
		self.state = Character.STATE_MUTEKI
		self.muteki_time = 5000
	#↑これを追加

muteki_time は無敵状態でいる間の時間です。単位は ms になります。後でこのデータ属性を使用し、ここで指定した時間が経過した後に通常状態に戻るようにしていきます。

ただ、上記で state Character.STATE_NORMAL から変化してしまうと、Game クラスの update メソッドが定期実行されなくなってしまうため、この update メソッドの下記の変更も必要になります。

定期実行の継続条件の変更
class Game:

	def update(self):

		#略

		if self.player.state == Character.STATE_NORMAL or self.player.state == Character.STATE_MUTEKI: #←ここを変更
			self.master.after(UPDATE_TIME, self.update)

		#略

倒されないようにするための処理

続いて、Player クラスでスーパークラスの defeated メソッドをオーバーライドし、無敵状態の場合は倒されないようにします。

倒されないようにする
class Player(Character):

	#↓これを追加
	def defeated(self):
		if self.state != Character.STATE_MUTEKI:
			super().defeated()
	#↑これを追加

時間経過で通常状態に戻す処理

以上の変更でスタートと操作キャラクターが当たった後は、操作キャラクターが敵キャラクターに当たっても倒されないようになりました。

ただ、ずっと無敵状態というのもゲームとして面白くないので、時間経過によって通常状態に戻るようにするため、Player クラスでスーパークラスの update メソッドを下記のようにオーバーライドしたいと思います。

時間経過で通常状態に戻す
class Player(Character):

	#↓これを追加
	def update(self):
		if self.state == Character.STATE_MUTEKI:
			self.muteki_time -= UPDATE_TIME
			if self.muteki_time <= 0:
				self.state = Character.STATE_NORMAL
	
		super().update()
	#↑これを追加

この変更で、muteki メソッドで設定された muteki_time0 以下になったら通常状態に戻るようになります。

この update メソッドは after メソッドにより UPDATE_TIME ごとに実行されるので、実行されるたびに muteki_timeUPDATE_TIME 毎に減らしていけば、大体 muteki メソッドで設定された muteki_time の時間が経過した後に通常状態に戻るようにすることができます。

ただしこの方法だと誤差が出るので、通常状態に戻るまでの時間を正確にしたいのであれば、時間を計測し、その計測時間から通常状態に戻るかどうかを判断するようにしたほうが良いです。

無敵状態用の画像を表示する

最後に無敵状態であることが視覚的に分かるように、無敵状態の時は通常状態の時から操作キャラクターの見た目を変化させるようにしていきたいと思います。

まずは無敵状態用の画像を作成するために、下記のように変更を行います。

無敵状態用の画像の作成
class Character:

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

		#略

		if len(mirrored_channels) == 4:
			mirrored_alpha = mirrored_channels[3]
		else:
			mirrored_alpha = Image.new("L", (mirrored_image.width, mirrored_image.height), 255)

		#↓これを追加
		muteki_image = ImageOps.invert(resized_image.convert("RGB"))
		muteki_image.putalpha(resized_alpha)
		mirrored_muteki_image = ImageOps.mirror(muteki_image)
		#↑これを追加

		if is_right:
			self.right_image = ImageTk.PhotoImage(resized_image)
			self.left_image = ImageTk.PhotoImage(mirrored_image)

			#↓これを追加
			self.muteki_right_image = ImageTk.PhotoImage(muteki_image)
			self.muteki_left_image = ImageTk.PhotoImage(mirrored_muteki_image)
			#↑これを追加

			self.right_alpha = resized_alpha.getdata()
			self.left_alpha = mirrored_alpha.getdata()
		else:
			self.left_image = ImageTk.PhotoImage(resized_image)
			self.right_image = ImageTk.PhotoImage(mirrored_image)

			#↓これを追加
			self.muteki_left_image = ImageTk.PhotoImage(muteki_image)
			self.muteki_right_image = ImageTk.PhotoImage(mirrored_muteki_image)
			#↑これを追加

			self.left_alpha = resized_alpha.getdata()
			self.right_alpha = mirrored_alpha.getdata()

		#略

無敵状態用の画像として、通常状態用の画像の色を反転したものを作成しています。とりあえず今回は通常状態から見た目を変化させることだけを目的とし、簡単に実行できる色の反転で済まさせていただいてます。

色の反転は、PIL の ImageOps.invert により行うことができます。

ただアルファチャンネルが設定されていると上手く反転できないようなので、一旦 convert メソッドでアルファチャンネル無しの画像に変換してから ImageOps.invert を実行し、さらにその後に putalpha で通常状態の時と同様にアルファチャンネルの設定を行うようにしています。

最後に、画像を取得するメソッドで、キャラクターの状態によって返却する画像オブジェクトを変更するようにすれば完成です。

無敵状態用の画像の取得
class Character:

	def getImage(self):
		#↓これを追加
		if self.state == Character.STATE_MUTEKI:
			if self.direction == Character.DIRECTION_RIGHT:
				return self.muteki_right_image
			elif self.direction == Character.DIRECTION_LEFT:
				return self.muteki_left_image
		else:
			if self.direction == Character.DIRECTION_RIGHT:
				return self.right_image
			elif self.direction == Character.DIRECTION_LEFT:
				return self.left_image
		#↑これを追加

		#↓これは削除
		#if self.direction == Character.DIRECTION_RIGHT:
		#	return self.right_image
		#elif self.direction == Character.DIRECTION_LEFT:
		#	return self.left_image
		#↑これは削除

以上の変更を行なったスクリプトを実行すれば、ゲーム画面上にアイテムが登場し、そのアイテムに当たった後には操作キャラクターの見た目が変化し、無敵状態(他の敵キャラクターに当たっても倒されない)になることが確認できると思います(約5秒後に通常状態に戻る)。

キャラクターが無敵状態に変化する様子

また、getImage メソッドを下記のように変更した場合、無敵状態の際に通常状態用の画像と無敵状態用の画像が交互に描画されるようになります(ちょっとマリオでスターを取った時の見た目にも近づく)。

ただし、下記では muteki_time の値が UPDATE_TIME で割り切れることを前提とした作りになっている点に注意してください。 

画像を交互に切り替える
class Character:

	def getImage(self):
		if self.state == Character.STATE_MUTEKI:
			if self.muteki_time % (UPDATE_TIME * 2) == 0:
				if self.direction == Character.DIRECTION_RIGHT:
					return self.muteki_right_image
				elif self.direction == Character.DIRECTION_LEFT:
					return self.muteki_left_image
			else:
				if self.direction == Character.DIRECTION_RIGHT:
					return self.right_image
				elif self.direction == Character.DIRECTION_LEFT:
					return self.left_image
		else:
			if self.direction == Character.DIRECTION_RIGHT:
				return self.right_image
			elif self.direction == Character.DIRECTION_LEFT:
				return self.left_image

こんな感じで、キャラクターの状態を追加することで、いろんなバリエーションのゲームにカスタマイズすることができます。

応用すれば、踏みつけられた敵キャラクターを一定時間点滅させた後に非表示にするようなことも可能です。

ただキャラクターの状態が多くなるとどんどんスクリプトが複雑になっていくので、メソッドの分割はもちろんのこと、キャラクターの状態毎にクラスを用意することを検討した方が良いと思います。

まとめ

このページでは、ここまで作成してきた横スクロールアクションゲームの「カスタマイズ例」を紹介しました。

「横スクロールアクションゲームの作り方」の解説ページとしては、このページが最後となります。ここまで読んでくださった方、本当にありがとうございます。

少しでも新しく学べたことがあったのであれば幸いですし、プログラミングの楽しさが少しでも伝わったのであれば嬉しいです。

今回ゲームを作成していく上で解説した内容については、他のゲームやアプリを開発する上でも役立つことが多いと思いますので、是非このゲームの作り方を応用して、いろんなアプリやゲーム作りに挑戦してみてください!