【Python/tkinter】モグラたたきゲームの作り方

Pythonでtkinterを用いがモグラ叩きゲームの作り方の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Python で tkinter を用いた「モグラたたきゲーム」の作り方について解説していきます。

出来上がりは下の動画ようなゲームとなります。

 

この動画で紹介したようなゲームを作ってみたいという方は、是非このページを読み進めていっていただければと思います。

tkinter の使い方についてもたくさん学べますし、ゲームですので楽しくプログラミングしていけると思います!

作成するモグラたたきゲーム

まず、このページで作成する「モグラたたきゲーム」がどのようなものであるのかについて説明しておきます。

このページで作る「モグラたたき」ゲームの動画

まず、このページで作成するモグラたたきゲームは下の動画のようなものになります。

スポンサーリンク

モグラたたきゲームの説明

今回作成する「モグラたたきゲーム」は、まず起動すると下の図のようにモグラの穴が表示されます。

背景と穴が描画された様子

さらに、時間経過と共にランダムな位置の穴からモグラが出現し、穴から出たモグラは上方向に少し移動した後に下方向に移動し、穴まで戻ったら再び穴に隠れてモグラが見えなくなります。

下まで移動したらモグラが穴に隠れる様子

モグラが穴から出てきているときにマウスでクリックすればモグラを叩くことができ、これによりポイントが加算されます。

ポイントの加算と加算ポイントの表示が行われる様子

逆に、モグラを叩くことができずにモグラが穴に隠れてしまった場合、ポイントが減算されることになります。

ポイントの減算と減算ポイントの表示が行われる様子

モグラたたきゲームを作成するのに必要なモジュール

今回、キャンバスにモグラの画像を描画することでモグラの表示を実現しています。この画像を扱う際に、PIL を利用しているので注意してください。

PIL は汎用性の高い画像処理モジュールであり tkinter とも相性の良いモジュールですので、まだインストールしていない方はこの機会にインストールしておくことをオススメします!

モグラたたきゲームのスクリプト

今回作成するモグラたたきゲームのスクリプトの最終形は下記のようになります。

次の モグラたたきゲームの作り方 においては、このスクリプトを作っていく上での過程を説明していきたいと思います。

モグラたたきゲーム
import tkinter
import random
from PIL import Image, ImageTk

MOLE_PATH = "animal_chara_mogura_hakase.png"  # モグラの画像のファイルパス
NUM_H_HOLE = 4  # 横方向の穴の数
NUM_V_HOLE = 3  # 縦方向の穴の数
WIDTH_HOLE = 100  # 穴の幅(長軸の長さ)
HEIGHT_HOLE = 50  # 穴の高さ(短軸の長さ)
WIDTH_SPACE = 20  # 穴と穴のスペースの幅
HEIGHT_SPACE = 80  # 穴と穴のスペースの高さ
POINT_DRAW_TIME = 500  # 加算or減算されたポイントが表示される時間
MOLE_UPDATE_INTERVAL = 100  # モグラの状態や位置を更新する時間間隔
FIGURE_UPDATE_INTERVAL = 100  # モグラの画像を更新する時間間隔
MOLE_CHOICE_INTERVAL = 1000  # 穴から出すモグラを決める時間間隔


class Mole:
	def __init__(self, x, y, width, height, speed, figure):
		'''コンストラクタ'''

		self.x = x  # 中央下の位置(横方向)
		self.y = y  # 中央下の位置(縦方向)
		self.width = width  # 画像の幅
		self.height = height  # 画像の高さ

		self.figure = figure  # 描画画像の図形ID

		self.hole_y = y  # モグラが穴に潜る高さ
		self.top_y = self.hole_y - HEIGHT_HOLE / 3  # 穴から出る高さ
		self.speed = speed  # 移動スピード

		self.point = int(speed * 10)  # 叩いた時にゲットできるポイント

		self.is_up = False  # 上方向に移動中かどうかを管理するフラグ
		self.is_draw = True  # 描画するかどうかを管理するフラグ
		self.is_hitted = False  # 既に叩かれているかどうかを管理するフラグ
		self.is_appearing = False  # 穴から出ているかどうかを管理するフラグ

	def appear(self):
		'''モグラを穴から出す'''

		# 穴から出ているフラグをON
		self.is_appearing = True

		# 上方向に移動を開始
		self.is_up = True

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 上方向に移動
			self.y = max(self.top_y, self.y - self.speed)
			if self.y == self.top_y:
				# 上方向の上限まで移動したので下方向への移動を開始
				self.is_up = False
		else:
			# 下方向への移動中

			# 下方向に移動
			self.y = min(self.hole_y, self.y + self.speed)
			if self.y == self.hole_y:
				# モグラがまた穴に潜ってしまった場合

				# モグラの状態をリセットする
				self.is_appearing = False
				self.is_hitted = False
				self.is_draw = True

		if self.is_hitted:
			# 叩かれたフラグが立っている場合

			# 描画実行フラグのON/OFFを切り替える
			self.is_draw = not self.is_draw

	def hit(self):
		'''叩かれた時の処理を実行'''

		# 下方向への移動を開始
		self.is_up = False

		# 叩かれたフラグをセット
		self.is_hitted = True

	def isHit(self, mouse_x, mouse_y):
		'''叩かれたかどうかを判断する'''

		if not self.is_appearing:
			# 穴から出ていない
			return False

		if self.is_hitted:
			# すでに叩かれている
			return False

		# モグラの画像が表示されている領域の座標を計算
		x1 = self.x - self.width / 2
		y1 = self.y - self.height
		x2 = self.x + self.width / 2
		y2 = self.y

		# (mouse_x,mouse_y)が上記領域内であるかどうかを判定
		if x1 <= mouse_x and x2 >= mouse_x:
			if y1 <= mouse_y and y2 >= mouse_y:

				# 領域内をクリックされたので叩かれた
				return True

		# 叩かれなかった
		return False


class WhackaMole:
	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェット
		self.master = master

		# 幅と高さを穴のサイズ・数、スペースのサイズから計算
		self.width = NUM_H_HOLE * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE
		self.height = NUM_V_HOLE * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()
		self.createMoles()
		self.updateFigures()
		self.choiceMole()
		self.updateMoles()

	def createWidgets(self):
		'''ウィジェットの作成と配置を行う'''

		# キャンバスの作成と配置
		self.canvas = tkinter.Canvas(
			self.master,
			width=self.width,
			height=self.height,
			highlightthickness=0
		)
		self.canvas.pack()

		# ラベルの作成と配置
		self.label = tkinter.Label(
			self.master,
			font=("", 40),
			text="0"
		)
		self.label.pack()

	def drawBackground(self):
		'''背景を描画する'''

		# キャンバス全面に緑色の長方形を描画
		self.bg = self.canvas.create_rectangle(
			0, 0, self.width, self.height,
			fill="green"
		)

	def drawHoles(self):
		'''穴を描画する'''

		# 空の管理リストを作成
		self.hole_coords = []

		for v in range(NUM_V_HOLE):
			for h in range(NUM_H_HOLE):

				# 穴の中心座標を計算
				x = h * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE + WIDTH_HOLE / 2
				y = v * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE + HEIGHT_HOLE / 2

				# 穴の左上と右下座標を計算
				x1 = x - WIDTH_HOLE / 2
				y1 = y - HEIGHT_HOLE / 2
				x2 = x + WIDTH_HOLE / 2
				y2 = y + HEIGHT_HOLE / 2

				# 穴を楕円として描画
				self.canvas.create_oval(
					x1, y1, x2, y2,
					fill="black"
				)

				# 管理リストに追加
				self.hole_coords.append((x, y))

	def createMoles(self):
		'''モグラを作成して表示する'''

		# モグラの画像を読み込む
		image = Image.open(MOLE_PATH)

		# 不要な画像の外側の透明画素を除去
		cropped_image = image.crop(image.getbbox())

		# 穴の幅に合わせて画像を拡大縮小
		ratio = (WIDTH_HOLE - 20) / cropped_image.width
		resize_size = (
			round(ratio * cropped_image.width),
			round(ratio * cropped_image.height)
		)
		resized_image = cropped_image.resize(resize_size)

		# tkinter用の画像オブジェクトに変換
		self.mole_image = ImageTk.PhotoImage(resized_image)

		self.moles = []

		for coord in self.hole_coords:

			# 穴の座標を取得
			x, y = coord

			# 穴の中心にモグラの画像の下が接するように画像を描画
			figure = self.canvas.create_image(
				x, y,
				anchor=tkinter.S,
				image=self.mole_image
			)

			# 描画した画像を背景画像の下側に隠す
			self.canvas.lower(figure, "all")

			# モグラオブジェクトを作成
			width = self.mole_image.width()
			height = self.mole_image.height()
			mole = Mole(x, y, width, height, 1, figure)

			# モグラオブジェクトを管理リストに追加
			self.moles.append(mole)

			# 描画画像がクリックされた時にonClickが実行されるように設定
			self.canvas.tag_bind(figure, "<ButtonPress>", self.onClick)

	def updateFigures(self):
		'''モグラの画像を更新する'''

		for mole in self.moles:

			if mole.is_appearing and mole.is_draw:
				# 出現中&描画フラグONの画像の場合

				# モグラの画像を最前面に移動
				self.canvas.lift(mole.figure, "all")

				# モグラの位置を更新
				self.canvas.coords(mole.figure, mole.x, mole.y)
			else:
				# モグラの画像を最背面に移動
				self.canvas.lower(mole.figure, "all")

		# FIGURE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(FIGURE_UPDATE_INTERVAL, self.updateFigures)

	def choiceMole(self):
		'''選んだモグラを穴から出させる'''

		# 穴に隠れているモグラだけのリストを作成
		hide_moles = []
		for mole in self.moles:
			if not mole.is_appearing:
				hide_moles.append(mole)

		if len(hide_moles) != 0:
			# 穴に隠れているモグラがいる場合

			# ランダムにモグラを1つ選んで穴から出させる
			mole = random.choice(hide_moles)
			mole.appear()

		# MOLE_CHOICE_INTERVAL後に再度別のモグラを穴から出させる
		self.master.after(MOLE_CHOICE_INTERVAL, self.choiceMole)

	def updateMoles(self):
		'''モグラの状態や位置を更新する'''

		for mole in self.moles:

			# 更新前のモグラの状態を退避
			is_hitted = mole.is_hitted
			before_appearing = mole.is_appearing

			# モグラの状態や位置を更新する
			mole.update()

			# 更新後にモグラが隠れたかどうかのフラグを取得
			after_appearing = mole.is_appearing

			if not is_hitted and before_appearing and not after_appearing:
				# moleが叩かれていない&上記のupdateメソッドによりmoleが穴に潜り込んだ場合

				# ポイントの加算と加算ポイントの描画
				self.pointUp(-mole.point)
				self.drawPoint(-mole.point, mole.x, mole.y)

		# MOLE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(MOLE_UPDATE_INTERVAL, self.updateMoles)

	def onClick(self, event):
		'''クリックされた時の処理を実行する'''

		for mole in self.moles:

			# moleの画像がクリックされたかどうかを判断
			if mole.isHit(event.x, event.y):
				# クリックされた場合

				# moleを叩く
				mole.hit()

				# ポイントの加算と加算ポイントの描画
				self.pointUp(mole.point)
				self.drawPoint(mole.point, event.x, event.y)

	def pointUp(self, point):
		'''ポイントを加算する'''

		# ラベルに表示されているポイントを取得
		now_point = int(self.label.cget("text"))

		# 取得したポイントに加算して表示文字列に設定
		now_point += point
		self.label.config(text=str(now_point))

	def drawPoint(self, point, x, y):
		'''加算されたポイントを表示'''

		if point >= 0:
			sign = "+"
			color = "yellow"
		else:
			sign = "-"
			color = "red"

		point_figure = self.canvas.create_text(
			x, y,
			text=sign + str(abs(point)),
			fill=color,
			font=("", 40)
		)

		# DRAW_TIME_POINT後に描画した文字列を削除
		self.master.after(
			POINT_DRAW_TIME, lambda: self.canvas.delete(point_figure))


app = tkinter.Tk()
game = WhackaMole(app)
app.mainloop()

スポンサーリンク

モグラたたきゲームの作り方

では、ここから本題である「モグラたたきゲームの作り方」について解説していきます。

大きく分けると、モグラゲームを作るために下記の9個のことを行なっていきます。結構解説内容の分量も多いですので、休みを入れながらゆっくり読んでいっていただければと思います。

最終的に出来上がるスクリプトは モグラたたきゲームのスクリプト と全く同じになりますので、モグラたたきゲームのスクリプト のスクリプトで何をやっているか分からないところや気になるところだけ読んでも良いですし、もちろん最初から読んでいただいても問題ないです!

解説については、スクリプトを変更しながら、その変更内容について補足していく形で行なっていきたいと思います。スクリプトの変更部分は太字で示していますが、違いが分かりにくい可能性もありますのでご注意ください。

各種パラメータを定義する

では、モグラたたきゲームの作り方の解説を行なっていきます。

まずは、モグラたたきゲームの画面の作成等に必要なパラメータをグローバル変数として設定していきたいと思います。

具体的には、スクリプトを下記のように作成します(必要なモジュールのインポートも行っています)。

各種パラメータの定義
import tkinter
import random
from PIL import Image, ImageTk

MOLE_PATH = "animal_chara_mogura_hakase.png" # モグラの画像のファイルパス
NUM_H_HOLE = 4 # 横方向の穴の数
NUM_V_HOLE = 3 # 縦方向の穴の数
WIDTH_HOLE = 100 # 穴の幅(長軸の長さ)
HEIGHT_HOLE = 50 # 穴の高さ(短軸の長さ)
WIDTH_SPACE = 20 # 穴と穴のスペースの幅
HEIGHT_SPACE = 80 # 穴と穴のスペースの高さ
POINT_DRAW_TIME = 500 # 加算or減算されたポイントが表示される時間
MOLE_UPDATE_INTERVAL = 100 # モグラの状態や位置を更新する時間間隔
FIGURE_UPDATE_INTERVAL = 100 # モグラの画像を更新する時間間隔
MOLE_CHOICE_INTERVAL = 1000 # 穴から出すモグラを決める時間間隔

配置に関するパラメータ

各種パラメータの意味合いはコメントにも書いていますが、特に穴の配置に関するパラメータの意味合いを図で示すと、下の図のようになります。

各種パラメータの意味合いを示す図

時間に関するパラメータ

図で表せなかった時間に関するパラメータについて捕捉しておくと、まず POINT_DRAW_TIME はモグラを叩いて加算されたポイント or モグラを叩けなくて減算されたポイントを画面に描画しておく時間になります。

MOLE_UPDATE_INTERVAL はモグラが穴から出てきた時にモグラを上方向 or 下方向に移動させる時間間隔になります。これが大きいほどモグラが早く移動します。また、モグラの状態の更新もこの時間間隔で行っていきます。

FIGURE_UPDATE_INTERVAL はモグラの位置等に応じてモグラの画像を更新する時間間隔になります。この値は MOLE_UPDATE_INTERVAL 以下の値に設定することをオススメします。MOLE_UPDATE_INTERVAL よりも大きな値に設定すると、モグラを叩けるようにする で導入するモグラの点滅がうまく動作しない場合があるので注意してください。

MOLE_CHOICE_INTERVAL は新しく穴から出すモグラを選択する時間の間隔になります。1匹モグラを穴から出した後、この MOLE_CHOICE_INTERVAL ms 経過した後に新しいモグラを選択して穴から出すことになります。

これらの時間に関するパラメータの単位は全て “ミリ秒” になります。

画像に関するパラメータ

また、MOLE_PATH はモグラを表示する際に使用する画像のファイルパスとなります。ご自身で用意したモグラの画像のファイルパスに応じて MOLE_PATH を設定してください。

ちなみに、このページで紹介するゲーム画面におけるモグラは、全て いらすとや の下記 URL のモグラの画像を使用しています。

https://www.irasutoya.com/2020/11/blog-post_69.html

上記 URL から転載させていただいたものが下の画像となります。

使用するモグラの画像

転載元:いらすとや

特に使用したいモグラの画像がないのであれば、この画像がオススメです。

ただし、前述の通り、ご自身が保存した画像のファイルパスに応じて MOLE_PATH は変更する必要があるのでご注意ください。

ウィジェットを作成する

続いて、モグラたたきゲームに必要になる「ウィジェット」を作成していきます。

今回作成するモグラたたきゲームでは、メインウィンドウに加え、穴やモグラを描画するためのキャンバス及び、ポイントを表示するためのラベルウィジェットを利用します。

これらのウィジェットの作成は、スクリプトを下記のように変更することで実現できます。

ウィジェットの作成
import tkinter
import random
from PIL import Image, ImageTk

MOLE_PATH = "animal_chara_mogura_hakase.png" # モグラの画像のファイルパス
NUM_H_HOLE = 4 # 横方向の穴の数
NUM_V_HOLE = 3 # 縦方向の穴の数
WIDTH_HOLE = 100 # 穴の幅(長軸の長さ)
HEIGHT_HOLE = 50 # 穴の高さ(短軸の長さ)
WIDTH_SPACE = 20 # 穴と穴のスペースの幅
HEIGHT_SPACE = 80 # 穴と穴のスペースの高さ
POINT_DRAW_TIME = 500 # 加算or減算されたポイントが表示される時間
MOLE_UPDATE_INTERVAL = 100 # モグラの状態や位置を更新する時間間隔
FIGURE_UPDATE_INTERVAL = 100 # モグラの画像を更新する時間間隔
MOLE_CHOICE_INTERVAL = 1000 # 穴から出すモグラを決める時間間隔

class WhackaMole:
	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェット
		self.master = master

		# 幅と高さを穴のサイズ・数、スペースのサイズから計算
		self.width = NUM_H_HOLE * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE
		self.height = NUM_V_HOLE * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE

		self.createWidgets()

	def createWidgets(self):
		'''ウィジェットの作成と配置を行う'''

		# キャンバスの作成と配置
		self.canvas = tkinter.Canvas(
			self.master,
			width=self.width,
			height=self.height,
			highlightthickness=0
		)
		self.canvas.pack()

		# ラベルの作成と配置
		self.label = tkinter.Label(
			self.master,
			font=("", 40),
			text="0"
		)
		self.label.pack()


app = tkinter.Tk()
game = WhackaMole(app)
app.mainloop()

今回はモグラたたきゲーム全体を制御するクラスとして WhackaMole を用意しています(モグラたたきは英語で「Whack a Mole」と言うそうです)。

メインウィンドウを tkinter.Tk() により作成し、そのメインウィンドウオブジェクトを WhackaMole クラスに渡し、さらに WhackaMole クラスの createWidgets メソッドで  tkinter.Canvas()tkinter.Label() を実行してメインウィンドウ上へのキャンバス及びラベルの作成を行なっています。

また、上記では WhackaMole クラスのデータ属性を3つ用意しており、それぞれの意味合いは下記のようになります。

  • master:モグラたたきゲームの作成先となる親ウィジェット
  • width:キャンバスの幅
  • height:キャンバスの高さ

master は今回の場合はメインウィンドウとなります。これを tkinter.Canvas() 及び tkinter.Label() の第1引数に指定することで、キャンバスとラベルがメインウィンドウ上に作成されることになります。

また、widthheight に関しては、穴の個数や穴のサイズ、スペースのサイズから計算を行なっており、これらの値を tkinter.Canvas() 実行時にキャンバスのサイズとして指定するようにしています。

今回は、穴の両脇に空きスペースを入れるような配置にしたかったので、下の図のように穴と空きスペースがキャンバス内に配置できるよう、上記のような計算式で幅と高さを計算しています(下の図は幅の例になりますが、高さも同様の考え方で配置を行なっています)。

キャンバスの幅を計算する時の考え方

計算時に用いている各変数の意味合いについては 各種パラメータを定義する を参照していただければと思います。

上記の変更により、スクリプトを実行すればアプリのメインウィンドウが表示され、その中にキャンバスとラベルが表示されるようになり、さらにメインループが実行されアプリが待機状態となります(キャンバスは分かりにくかも…)。

ウィジェットが作成された様子

ウィジェット作成に関する解説は以上となりますが、メインウィンドウに関しては下記ページで、

メインウィンドウ作成解説ページのアイキャッチ Tkinterの使い方:メインウィンドウを作成する

キャンバスに関しては下記ページで、

tkinterのキャンバスの作り方解説ページアイキャッチ Tkinterの使い方:キャンバスウィジェットの作り方

ラベルに関しては下記ページで、

ラベルウィジェット解説ページのアイキャッチ Tkinterの使い方:ラベルウィジェット(Label)の使い方

メインループに関しては下記ページでそれぞれ解説しておりますので、必要に応じてご参照いただければと思います。

tkinterのmainloopの解説ページのアイキャッチ 【Python】tkinterのmainloopについて解説

スポンサーリンク

背景と穴を描画する

続いて「背景」と「穴」を描画していきます。

これらの描画は、WhackaMole クラスに下記の drawBackground メソッドと drawHoles メソッドを追加し、さらに __init__ からこれらのメソッドを実行するように変更することで実現できます(ここからは変更を行うメソッドのみを抜粋して変更箇所を示していきます。変更している箇所はこれまで通り、太字で示しています)。

背景と穴の描画
class WhackaMole:

	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェット
		self.master = master

		# 幅と高さを穴のサイズ・数、スペースのサイズから計算
		self.width = NUM_H_HOLE * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE
		self.height = NUM_V_HOLE * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()

	def drawBackground(self):
		'''背景を描画する'''

		# キャンバス全面に緑色の長方形を描画
		self.bg = self.canvas.create_rectangle(
			0, 0, self.width, self.height,
			fill="green"
		)

	def drawHoles(self):
		'''穴を描画する'''

		# 空の管理リストを作成
		self.hole_coords = []

		for v in range(NUM_V_HOLE):
			for h in range(NUM_H_HOLE):

				# 穴の中心座標を計算
				x = h * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE + WIDTH_HOLE / 2
				y = v * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE + HEIGHT_HOLE / 2

				# 穴の左上と右下座標を計算
				x1 = x - WIDTH_HOLE / 2
				y1 = y - HEIGHT_HOLE / 2
				x2 = x + WIDTH_HOLE / 2
				y2 = y + HEIGHT_HOLE / 2

				# 穴を楕円として描画
				self.canvas.create_oval(
					x1, y1, x2, y2,
					fill="black"
				)

				# 管理リストに追加
				self.hole_coords.append((x, y))

上記のように変更を行なったスクリプトを実行すれば、下の図のような緑色背景の中に黒色の穴が表示されるようになります。

背景と穴が描画された様子

背景の描画

背景の描画を行なっているのが drawBackground メソッドになります。

といっても、drawBackground では単にキャンバスの create_rectangle で色が "green" の長方形をキャンバス全面に描画しているだけです。

実は、キャンバスの背景の色を "green" にするだけであれば、長方形をわざわざ描画しなくても、tkinter.Canvas() 実行時に bg="green" オプションを指定すれば良いだけです。

ただ、今回のモグラたたきゲームにおいては、この長方形がモグラを隠すために非常に重要な役割を果たしますので、今回は長方形を描画することでキャンバスの背景の色の変更を行なっています。

穴の描画

穴の描画を行なっているのが drawHoles メソッドになります。

NUM_V_HOLE x NUM_H_HOLE 個の穴を楕円としてキャンバスの create_oval メソッドで描画しています。

楕円などの図形をキャンバスで描画する際には、その楕円の “左上座標” と “右下座標” をメソッドの引数に指定する必要があります。

楕円描画時に必要になる座標の説明図

そのため drawHoles メソッドでは、左上座標 (x1, y1) と右下座標 (x2, y2) を求めてから、これらの座標を引数に指定して create_oval メソッドを実行するようにしています。

さらに、これらの左上座標と右下座標を求めるにあたって事前に楕円の中心座標 (x, y) を求めています。そして、この中心座標から穴の幅と高さを考慮して左上座標と右下座標を求めています。

難しいのが、この中心座標の求め方かなぁと思います。穴の周りに空きスペースが出来るように中心座標の xy を算出する必要があります。

例えば h = 2v = 1 の場合、drawHoles メソッドでは下の図のような長さを求めながら中心座標を算出する計算を行っています。

穴の中心座標を求める様子

drawHoles にはもう一つポイントがあって、それは各穴の中心座標を hole_coords リストに格納している点です。

以降で作成していくモグラは、穴の中心座標の上側に表示させるようにしていくため、わざわざ計算し直さなくてもいいように、各穴の中心座標をリストで管理できるようにしています。

モグラを描画する

背景と穴が描画できましたので、次はモグラを描画していきたいと思います。

モグラは各穴に1匹ずつ描画していきます。つまり、穴の数だけモグラを描画していきます。

モグラの画像オブジェクトを生成する

前述の通り、モグラは画像を描画することで表現していきます。

そのために、まずはキャンバスに描画可能なモグラの画像オブジェクトを作成していきたいと思います。

ただ、モグラは穴から出てきたり、穴に隠れたりするわけですから、穴のサイズに合わせて画像サイズを拡大縮小してからオブジェクトの生成を行なっていきたいと思います。

画像を穴のサイズに合わせて拡大縮小する様子

また、各種パラメータを定義する で紹介した画像もそうなのですが、モグラの絵の周りに余分な透明な画素が付いている場合があるので、それも除去してから画像オブジェクトを生成したいと思います。

画像の周りの不要な透明画素を除去する様子

これらの処理は、WhackaMole クラスに下記の createMoles メソッドを追加し、さらに __init__ から追加した createMoles メソッドを実行するように変更することで実現することができます。

モグラの画像オブジェクト生成
class WhackaMole:
	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェット
		self.master = master

		# 幅と高さを穴のサイズ・数、スペースのサイズから計算
		self.width = NUM_H_HOLE * (WIDTH_HOLE + WIDTH_SPACE) + WIDTH_SPACE
		self.height = NUM_V_HOLE * (HEIGHT_HOLE + HEIGHT_SPACE) + HEIGHT_SPACE

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()
		self.createMoles()

	def createMoles(self):
		'''モグラを作成して表示する'''

		# モグラの画像を読み込む
		image = Image.open(MOLE_PATH)

		# 不要な画像の外側の透明画素を除去
		cropped_image = image.crop(image.getbbox())

		# 穴の幅に合わせて画像を拡大縮小
		ratio = (WIDTH_HOLE - 20) / cropped_image.width
		resize_size = (
			round(ratio * cropped_image.width),
			round(ratio * cropped_image.height)
		)
		resized_image = cropped_image.resize(resize_size)

		# tkinter用の画像オブジェクトに変換
		self.mole_image = ImageTk.PhotoImage(resized_image)

createMoles メソッドでは、まず PIL の Image.open 関数を実行して MOLE_PATH のモグラの画像ファイルを読み込み、読み込んだ画像の PIL オブジェクト image の生成を行なっています。

各種パラメータを定義する でも解説したように、モグラの画像ファイルは自身で用意していただき、さらに用意したファイルのパスに応じて MOLE_PATH も設定する必要があるので注意してください。

続いて createMoles メソッドでは、imagecrop メソッドを実行させることで、モグラの絵の周りにある不要な透明画素を除去した画像オブジェクト cropped_image を生成しています。

画像の周りの不要な透明画素を除去する様子

さらに、cropped_imageresize メソッド実行させることで、穴の横幅に基づいて拡大縮小を行なった画像オブジェクト resized_image を生成しています。

画像を穴のサイズに合わせて拡大縮小する様子

resize メソッドに指定する引数は拡大縮小後の幅と高さのタプルです。

この拡大縮小後の幅と高さを求めるために、画像の横幅を穴の幅よりも若干小さい WIDTH_HOLE - 20 に拡大縮小するための拡大率 ratio を計算し、さらに求めた拡大率 ratiocropped_image の幅と高さそれぞれに掛けることで、拡大縮小後の幅と高さを求めています。

このように resize メソッドの引数を指定することで、画像の幅を穴の幅よりも若干小さくし、さらに画像の縦横比を保ったまま拡大縮小することを実現しています。

ちなみに拡大率を計算するときに WIDTH_HOLE - 20 を用いていますが、この 20 はてきとうです。画像の幅を穴に対してちょっと小さくしたかったので、とりあえず 20 を選んでみました。

で、生成された resized_image はまさに描画したい画像のオブジェクトとなったのですが、tkinter のキャンバスに描画するためには、tkinter の画像オブジェクトに変換する必要があります。

この変換を、PIL の ImageTk.PhotoImage クラスのコンストラクタ実行により行なっています。そして、このコンストラクタの返却オブジェクトが、キャンバスに描画可能な画像オブジェクトとなります。

この画像オブジェクトの変換については下記ページで解説していますので、詳しく知りたい方は下記ページをご参照ください。

画像オブジェクト変換方法の解説ページアイキャッチ 【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換

モグラを穴の数だけ描画する

キャンバスに描画可能なオブジェクトが準備できましたので、あとはこの画像を穴の数だけ穴の位置に合わせて描画していけばよいだけです。

これは createMoles メソッドを下記のように変更することで実現することができます。

モグラの描画
class WhackaMole:

	def createMoles(self):
		'''モグラを作成して表示する'''

		# モグラの画像を読み込む
		image = Image.open(MOLE_PATH)

		# 不要な画像の外側の透明画素を除去
		cropped_image = image.crop(image.getbbox())

		# 穴の幅に合わせて画像を拡大縮小
		ratio = (WIDTH_HOLE - 20) / cropped_image.width
		resize_size = (
			round(ratio * cropped_image.width),
			round(ratio * cropped_image.height)
		)
		resized_image = cropped_image.resize(resize_size)

		# tkinter用の画像オブジェクトに変換
		self.mole_image = ImageTk.PhotoImage(resized_image)

		for coord in self.hole_coords:

			# 穴の座標を取得
			x, y = coord

			# 穴の中心にモグラの画像の下が接するように画像を描画
			figure = self.canvas.create_image(
				x, y,
				anchor=tkinter.S,
				image=self.mole_image
			)

上記のように変更を加えたスクリプトを実行すれば、全ての穴の上にモグラが表示されるようになったことが確認できると思います。

モグラがキャンバスに描画された様子

WhackaMole クラスの hole_coords背景と穴を描画する で作成した穴を描画するメソッド drawHoles で作成したリストで、各穴の中心座標が管理されています。

createMoles を上記のように変更することで、ループの中でその座標を取得し、取得した座標に先ほど生成した画像オブジェクトの描画を行なっています。画像はキャンバスの create_image メソッドにより描画することができ、さらに描画する位置は第1引数と第2引数で指定することができます。

ポイントは create_image メソッドで指定しているオプション anchor=tkinter.S を指定している点です。この anchor=tkinter.S を指定することで、第1引数と第2引数で指定した座標が画像の中央下に来るように画像を描画することができます。

ですので、上記のように create_image メソッドを指定することで、穴の中心 (x, y) 座標にモグラの画像の中央下が来るように画像が描画され、モグラが穴の上にいる感じで描画することができます。

モグラが穴の中心の上に描画される様子

で、今後はこの位置のモグラを上方向に移動させることで、モグラが穴から出た感じを表現していくことになります。

ちなみにこのオプションを指定しない場合、第1引数と第2引数で指定した座標に画像の中央が来るように画像が描画されることになります。

モグラが穴の中心に描画される様子

こういった、図形を描画する際に使用するメソッドに指定可能なオプションは下記ページで解説していますので、詳しく知りたい方は下記ページをご参照ください。

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

モグラを隠れさせる

モグラは描画できるようになりましたが、モグラたたきゲーム開始時はモグラが穴に隠れている状態にしたいので、次はモグラを隠れさせるようにしていきたいと思います。

隠れさせるためにモグラを非表示にしてもよいのですが、今回はキャンバス全体に背景として長方形が描画されていますので、その長方形の背面にモグラを移動させることで、モグラを隠れさせるようにしたいと思います。

モグラを背面に移動することで見えないようになる様子

これは、WhackaMole クラスの createMoles を下記のように変更するだけで実現することができます。

モグラを隠れさせる
class WhackaMole:

	def createMoles(self):
		# 略

		for coord in self.hole_coords:

			# 穴の座標を取得
			x, y = coord

			# 穴の中心にモグラの画像の下が接するように画像を描画
			figure = self.canvas.create_image(
				x, y,
				anchor=tkinter.S,
				image=self.mole_image
			)

			# 描画した画像を背景画像の下側に隠す
			self.canvas.lower(figure, "all")

上記のように変更を加えれば、モグラが全て隠れた状態になることが確認できると思います。

全てのモグラが隠れた様子

キャンバスの lower メソッドは、第1引数で指定した ID の図形を第2引数で指定した ID の図形の背面に移動させるメソッドになります。

第2引数に "all" を指定した場合、第1引数で指定した ID の図形が最背面に移動することになります(引数にはタグ名を指定することも可能です)。

この図形の ID は、create_image 等の図形を描画するメソッドの返却値として取得することが可能です。上記の createMoles メソッドでは create_image の全ての返却値に対して lower メソッドを実行していますので、全てのモグラの画像が背景の長方形の裏側に隠れることになります。

また、キャンバスには、逆に図形を前面に移動するためのメソッドとして lift が存在します。従って、この lift メソッドを実行すれば、今は隠れて見えないモグラの画像を前面に移動させて見えるようにすることができます。

後述での解説においては、この lift メソッドを利用してモグラの画像を前面に移動させることで、モグラが穴から出たことを表現していきます。

これらの描画した図形を後から操作するメソッドについては下記ページでまとめていますので、詳しく知りたい方は下記ページをご参照いただければと思います。

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

モグラのクラスとモグラのオブジェクトを作成する

ここまでで、モグラたたきゲームの土台が大体できてきたかなぁという感じです。

ここからはモグラに対する処理が多くなってきますので、それらの処理を実装しやすいよう、次はモグラを表現するクラスを作成していきたいと思います。

Mole クラスの作成

今回は、このモグラを表現するクラスを Mole とし、下記のように作成したいと思います。ひとまずコンストラクタの __init__ だけを用意しています。

Moleクラス
# 略

MOLE_CHOICE_INTERVAL = 1000  # 穴から出すモグラを決める時間間隔

class Mole:
	def __init__(self, x, y, width, height, speed, figure):
		'''コンストラクタ'''

		self.x = x  # 中央下の位置(横方向)
		self.y = y  # 中央下の位置(縦方向)
		self.width = width # 画像の幅
		self.height = height # 画像の高さ

		self.figure = figure # 描画画像の図形ID

		self.hole_y = y  # モグラが穴に潜る高さ
		self.top_y = self.hole_y - HEIGHT_HOLE / 3 # 穴から出る高さ
		self.speed = speed # 移動スピード

		self.point = int(speed * 10) # 叩いた時にゲットできるポイント

		self.is_up = False # 上方向に移動中かどうかを管理するフラグ
		self.is_draw = True # 描画するかどうかを管理するフラグ
		self.is_hitted = False # 既に叩かれているかどうかを管理するフラグ
		self.is_appearing = False # 穴から出ているかどうかを管理するフラグ

class WhackaMole:

#略

データ属性が多いですが、それぞれの意味合いはコメントに記述しているので大体わかるかなぁと思います。

一応説明しておくと、まず xy はモグラの現在位置を示すデータ属性です。モグラの画像が描画されている位置と考えてもよいです。

xy は、モグラの中心ではなくモグラの「中央下」の位置を示すデータ属性である点に注意してください。モグラを描画する でモグラの「中央下」の位置を基準に画像の描画を行なったため、それに合わせて「中央下」の位置を示すようにしています。

より具体的に言えば、モグラを描画する で各モグラを穴の中央の位置に合わせて描画を行なったので、この xy にはその穴の中央の位置が指定されることを想定しています。

また、widthheight は描画されているモグラの画像の幅と高さになります。さらに、figure はモグラの画像の図形 ID となります。

Moleクラスのデータ属性x,y,width.height.figureの説明図

今後モグラを表示していく際には、この xy の値に従ってモグラの画像の描画位置を移動していくことになります。

また、モグラを描画する でも使用した lower メソッドのように、キャンバスに描画した図形を操作するためには図形 ID が必要ですので、Mole クラスのオブジェクトからすぐに図形 ID が取得できるよう、データ属性 figure を用意しています。

さらに、モグラの画像がクリックされたかどうかの判定(モグラが叩かれたかどうかの判定)は、xy 及び widthheight から画像が表示されている領域を求め、その領域内がクリックされたかどうかで判定を行なっていきます。

ここまで紹介してきたデータ属性は主に描画されているモグラの画像に関するものでしたが、それに対して hole_ytop_yspeed はモグラの縦方向の移動に関するデータ属性になります。

モグラが穴から出てきた際には、モグラを上方向に移動し、さらに頂点まで達した際にはモグラを穴の中央の位置まで下方向に移動させていくことになります。

この頂点の位置を示すデータ属性が top_y であり、穴の中央の位置を示すデータ属性が hole_y となります。

Moleクラスのデータ属性hole_y,top_yの説明図

さらに、一度の移動で移動する量が、データ属性 speed となります。

また、point はモグラを叩いた時に加算されるポイント、モグラを叩けなかった時に減算されるポイントで、speed が大きい方が叩くのが難しくなるので、その分大きなポイントが加算されるように設定しています。

is が付いているデータ属性は全てモグラの状態を示すフラグです。これらに関しては、後から実際に使用するようになった際に説明していきたいと思います。

Mole オブジェクトの作成

現状はまだ __init__ しかありませんが、一応 Mole クラスの枠組みはできましたので、次は実際に Mole クラスのオブジェクトを作成していきたいと思います。

モグラは穴の数の分だけ穴から出てくる可能性があるため、穴の数の分、Mole クラスのオブジェクトを作成していきます。

このようなオブジェクトの生成は、WhackaMole クラスの createMoles を下記のように変更することで実現することができます。

Moleオブジェクトの生成
class WhackaMole:

	def createMoles(self):

		# 略

		# tkinter用の画像オブジェクトに変換
		self.mole_image = ImageTk.PhotoImage(resized_image)

		# モグラの管理リストを作成
		self.moles = []

		for coord in self.hole_coords:

			# 穴の座標を取得
			x, y = coord

			# 穴の中心にモグラの画像の下が接するように画像を描画
			figure = self.canvas.create_image(
				x, y,
				anchor=tkinter.S,
				image=self.mole_image
			)

			# 描画した画像を背景画像の下側に隠す
			self.canvas.lower(figure, "all")

			# モグラオブジェクトを作成
			width = self.mole_image.width()
			height = self.mole_image.height()
			mole = Mole(x, y, width, height, 1, figure)

			# モグラオブジェクトを管理リストに追加
			self.moles.append(mole)

hole_coords では全ての穴の座標が管理されていますので、hole_coords に対するループの中で Mole() を実行することで、穴の数の分のオブジェクトを生成することができます。

また、Mole() 実行時に下記の引数を指定しています。

  • x:モグラの現在位置(中央下)の横方向の座標
  • y:モグラの現在位置(中央下)の縦方向の座標
  • width:モグラの画像の幅
  • height:モグラの画像の高さ
  • speed:モグラの移動スピード
  • figure:モグラの画像の図形 ID(create_image の返却値)

xy に関しては、create_image の第1引数と第2引数に指定した座標を指定すれば良いですし(create_image の第1引数と第2引数にも画像の中央下の座標を指定している)、figure に関しては create_image の返却値をそのまま指定してやれば良いです。

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

speed1 に設定していますが、各種パラメータを定義する での設定値の場合はこれくらいが丁度いいかなぁと思います。お好みに合わせてこの値は変更しても良いですし、モグラ毎に値を変更したり、時間の経過に伴って後からこの値を変更して徐々にモグラのスピードを上げていくのも面白いかと思います。

上記のように作成した Mole のオブジェクトは全て、データ属性のリスト moles に格納していますので、以降はこの moles からオブジェクトを取得して Mole のオブジェクトに処理を依頼する事ができます。

スポンサーリンク

モグラの画像を定期的に更新する

以降ではモグラを穴から出して上方向に移動するような処理を実現していくのですが、その前に、ここでモグラの画像を定期的に更新するようにしていきたいと思います。

そのために、まず、WhackaMole クラスに下記の updateFigures メソッドを追加し、さらに、この updateFigures を実行するように __init__ を変更します。

モグラの画像の定期更新
class WhackaMole:
	def __init__(self, master):
		# 略

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()
		self.createMoles()
		self.updateFigures()

	def updateFigures(self):
		'''モグラの画像を更新する'''

		for mole in self.moles:

			if mole.is_appearing and mole.is_draw:
				# 出現中&描画フラグONの画像の場合

				# モグラの画像を最前面に移動
				self.canvas.lift(mole.figure, "all")

				# モグラの位置を更新
				self.canvas.coords(mole.figure, mole.x, mole.y)
			else:
				# モグラの画像を最背面に移動
				self.canvas.lower(mole.figure, "all")

		# FIGURE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(FIGURE_UPDATE_INTERVAL, self.updateFigures)

updateFigures メソッドは、モグラのオブジェクト mole の状態や位置に応じて描画している画像を更新するメソッドになります。

mole のデータ属性 is_appearing は、mole が穴から出ている状態であるかを示すフラグとなります。また、mole のデータ属性 is_draw はモグラを描画するかどうかを示すフラグとなります。

より具体的には、データ属性 is_draw は、モグラが叩かれた時にモグラを点滅させるためのフラグになります。これについては後述の モグラを叩けるようにする で詳しく解説しますので、まずは気にせず読み進めていただければと思います。ちなみにデータ属性 is_draw の初期値は True となります。

updateFigures メソッドでは、この2つのデータ属性が True の時に、まず mole の画像をキャンバスの lift メソッドにより最前面に移動します。

モグラを描画する ではモグラが穴に隠れていることを表現するために画像を最背面に移動しましたが、最前面に移動することによりモグラが画面に表示され、モグラが穴から出てきたことを表現する事ができます。

liftメソッドでモグラを穴から出すことを表現する様子

続いて updateFigures メソッドでは、キャンバスの coords メソッドにより mole の画像をモグラの位置(xy)に移動させます。これにより、モグラの位置が移動されていれば、モグラの画像の表示位置が変化することになります。

coordsメソッドでモグラを移動させる様子

逆に is_appearingis_draw のどちらか一方でも False の場合は、lower メソッドによりまた画像を最背面に移動させるような処理を行なっています。

lowerメソッドでモグラを穴に隠すことを表現する様子

さらに、updateFigures メソッドの最後で after メソッドを実行し、FIGURE_UPDATE_INTERVAL ms 後に再度 updateFigures メソッドが実行されるように設定しています。

そのため、この updateFigures メソッドは FIGURE_UPDATE_INTERVAL ms 間隔で定期的に実行されることになります。

ですので、他のメソッドでモグラの状態を変更したり(is_appearing や is_draw を変更)、モグラの位置を変更したりした場合(x や y を変更)、自動的にそれらに応じてモグラの画像が更新されるようになることになります。

と言っても、まだモグラの状態や位置を変更するメソッドを作成していないため、上記のような変更を行なったスクリプトを実行しても今までも見た目に変化はありません。

ただ、お試しで下記のように updateFigures メソッドを変更すれば、モグラの画像が表示され、上側に移動していく様子が確認できると思います(確認が終わったら追加した行は消しておいてください)。

要は、他のメソッドでこのような位置や状態の変更を行なってやることで、モグラを穴から出したり穴に隠したり、穴から出たモグラを上方向に移動したりする様子が、キャンバス上の図形に反映されるようになったことになります。

モグラの画像の定期更新
class WhackaMole:

	def updateFigures(self):
		'''モグラの画像を更新する'''

		for mole in self.moles:

			# お試しで変更
			mole.y -= 1
			mole.is_appearing = True

			if mole.is_appearing and mole.is_draw:
				# 出現中&描画フラグONの画像の場合
				
				# モグラの画像を最前面に移動
				self.canvas.lift(mole.figure, "all")

				# モグラの位置を更新
				self.canvas.coords(mole.figure, mole.x, mole.y)
			else:
				# モグラの画像を最背面に移動
				self.canvas.lower(mole.figure, "all")

		# FIGURE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(FIGURE_UPDATE_INTERVAL, self.updateFigures)

ちなみに、ここで利用した after メソッドについては下記ページで解説していますので、詳しく知りたい方はぜひ下記ページをご参照ください。tkinter を利用するのであれば知っておいて損はないメソッドだと思います。

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

モグラを穴から出す

モグラの状態を変更することでモグラの画像を表示し、さらにモグラの位置を変更することでモグラの画像が移動するようになりましたので、次は「モグラを穴から出す」処理を実現していきたいと思います。

モグラを穴から出す手順

おさらいしておくと、モグラの画像を定期的に更新する で追加した updateFigures メソッドにより、Mole クラスのオブジェクトのデータ属性 is_appearingTrue に設定してやれば、そのモグラの画像が最前面に移動して画面に表示されるようになりました(is_draw に関してはまだ使用しません)。

また、Mole クラスのオブジェクトのデータ属性 xy を変更すれば、モグラの画像の位置が移動するようになりました。

さらに、updateFigures メソッドは定期的に実行されるようにしていますので、上記のデータ属性の変更を行えば、自動的にその変更が画面に反映されることになります。

したがって、モグラを穴から出す処理は、次のようなことを行うことで実現することができます。

まず、穴から出したいモグラのオブジェクトの is_appearingTrue に設定して画面に表示します(穴から出すモグラはランダムに選択するようにしていきます)。

モグラを穴から出させる手順1

その後に y を減少させ、モグラを上側に移動させていきます(キャンバスの縦軸の正方向は下方向のため、y 減少させればキャンバスの上側に移動することになります)。

モグラを穴から出させる手順2

モグラのクラスとモグラのオブジェクトを作成する で解説したように、モグラが上側に移動する頂点の縦軸の座標はデータ属性 top_y で設定されています。ですので、ytop_y 以下になれば上側への移動をやめ、今度は逆に y を増加させることで、モグラを下側に移動させていきます。

モグラを穴から出させる手順3

さらに、y が穴の位置を示すデータ属性 hole_y 以上になった際に is_appearingFalse に設定してやれば、モグラが最背面に移動してモグラが非表示になり、モグラが穴に隠れたように見せる事ができます。

モグラを穴から出させる手順4

上記のような一連の処理を実装してやれば、穴からモグラを出すことを実現する事ができます。

ただし、y をループ処理などで一気に増減させてしまうとモグラが一瞬で移動して隠れることになってしまいます。

そのため、MOLE_UPDATE_INTERVAL ms 毎に定期的に y を増減させるようにすることで、モグラがゆっくり上下方向に移動するようにしていきたいと思います(定期的な処理を実現するので、updateFigures メソッドの時同様に after メソッドを利用します)。

モグラを穴から出させて表示する

では、モグラを穴から出す処理を実装していきたいと思います。

まずは、穴から出た瞬間にモグラを表示するためのメソッドを Mole クラスに追加していきます。

といっても、ここで行うのはモグラを表示させるだけですので、Mole クラスに下記の appear メソッドを追加すれば良いだけです(この appear メソッドは上方向の移動を行うために後から少し変更します)。

appear
class Mole:

	def appear(self):
		'''モグラを穴から出す'''

		# 穴から出ているフラグをON
		self.is_appearing = True

この appear メソッドが実行されれば、実行したモグラの画像が updateFigures メソッドにより最前面に移動し、画面に表示されるようになります。

穴から出すモグラをランダムに選ぶ

次は、穴から出したいモグラのオブジェクトに先ほど追加した appear メソッドを実行させるようにしていきたいと思います。

今回は、穴から出すモグラを「まだ穴から出ていない全モグラ」の中からランダムに選択するようにしていきたいと思います。

より具体的には、WhackaMole クラスのデータ属性 moles には全モグラのオブジェクトが格納されていますので、このオブジェクトの中から is_appearing が False のオブジェクトのみを抽出し、さらにその抽出したオブジェクトの中から穴から出すモグラをランダムに1匹選択するようにします。

このようなランダムな選択は、WhackaMole クラスに下記の choiceMole メソッドを追加し、さらに __init__ からこのchoiceMole メソッドを実行するようにすることで実現することができます。

モグラの選択
class WhackaMole:

	def __init__(self, master):

		# 略

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()
		self.createMoles()
		self.updateFigures()
		self.choiceMole()

	def choiceMole(self):
		'''選んだモグラを穴から出させる'''

		# 穴に隠れているモグラだけのリストを作成
		hide_moles = []
		for mole in self.moles:
			if not mole.is_appearing:
				hide_moles.append(mole)

		if len(hide_moles) != 0:
			# 穴に隠れているモグラがいる場合

			# ランダムにモグラを1つ選んで穴から出させる
			mole = random.choice(hide_moles)
			mole.appear()

		# MOLE_CHOICE_INTERVAL後に再度別のモグラを穴から出させる
		self.master.after(MOLE_CHOICE_INTERVAL, self.choiceMole)

hide_moles には、moles で管理されている Moleクラスの全オブジェクトの中で、is_appearingFalse のオブジェクトのみが格納されます。

さらに、random.choice は引数に指定したリストの中からランダムに1つのオブジェクトを選択して返却する関数ですので、random.choice(hide_moles) を実行すれば、 is_appearingFalse のオブジェクトの中から1つのオブジェクトをランダムに選択することになります。

このような処理により、「まだ穴から出ていない全モグラ」の中からランダムに穴から出すモグラを選択することを実現しています。

さらに、選択したオブジェクトに appear メソッドを実行させることで is_appearingTrue に設定され、そのオブジェクトのモグラの画像が画面に表示されることになります。

モグラを1匹のみ穴から出させるのであれば上記のような処理を1回実行すれば良いだけなのですが、モグラたたきゲームでは定期的にモグラが穴から出てくるようになっているため、今回作成するゲームにおいても定期的に choiceMole メソッドを実行して定期的にモグラを穴から出すようにしています。

そのために、最後の行で  afterr メソッドを実行しています。

上記の変更を加えたスクリプトを実行すれば、MOLE_CHOICE_INTERVAL ms 毎にランダムな穴の位置にモグラの画像が表示されていく様子が確認できると思います(MOLE_CHOICE_INTERVAL各種パラメータを定義する で設定したグローバル変数で、デフォルトは 1000 ms に設定しています)。

ランダムな位置の穴からモグラが現れる様子

ただし、現状ではランダムに選んだモグラを表示しているだけなので、モグラが移動したり消えたりすることはありません。

次は、モグラを上方向に移動させることで、よりモグラが穴から出てきた感を出せるようにしていきたいと思います。

穴から出たモグラを上方向に移動する

前述の通り、モグラを上方向に移動するためには、モグラのオブジェクトのデータ属性 y を減少させることで実現できます。

これを実現するために、まず Mole クラスに下記の update メソッドを追加したいと思います。

モグラの上方向の移動
class Mole:

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 上方向に移動
			self.y = max(self.top_y, self.y - self.speed)

update メソッドは、実行したオブジェクトのデータ属性 is_upTrue の際に、データ属性 yspeed だけ減らすことで上方向に移動させるメソッドになります。

ただし、モグラを上方向に移動する頂点の座標は top_y を下限としていますので、max 関数を利用して top_y よりも y が小さくならないようにしています。

また、データ属性 is_up は、そのオブジェクトが上方向に移動中かどうかを示すフラグです。初期値は モグラのクラスとモグラのオブジェクトを作成する で追加した Mole クラスの __init__ を見ていただければわかるとおり False になっています。

ですので、update メソッドの中でモグラを上方向に移動させるためには、is_up を予め True に設定しておく必要があります。

で、モグラを上方向に移動したいのは、モグラが穴から出てきた時です。そして、モグラを穴から出して表示するためのメソッドは Mole クラスの appear です。

したがって、この appear メソッドの中で is_upTrue に設定するようにすれば、穴から出たモグラが update メソッド実行時に上方向に移動していくようになります。

ということで、Mole クラスの appear メソッドを下記のように変更します。

穴から出た時に上方向に移動
class Mole:

	def appear(self):
		'''モグラを穴から出す'''

		# 穴から出ているフラグをON
		self.is_appearing = True

		# 上方向に移動を開始
		self.is_up = True

あとは、Mole クラスの update メソッドを実行するようにすれば、モグラのオブジェクトの y を減少してモグラの画像が上方向に移動することになります。

ただし、これも前述の通り、ループ処理で y を減少させてしまえばモグラが一気に上方向に移動してしまうことになるため、update メソッドは間隔を空けて定期的に実行する必要があります。

そのため、after メソッドを利用して update メソッドを定期的に実行するようにしていきます。

具体的には、WhackaMole クラスに下記の updateMoles メソッドを追加し、さらに updateMoles メソッドを __init__ から実行するように変更を行います。

モグラの定期的な更新
class WhackaMole:

	def __init__(self, master):

		# 略

		self.createWidgets()
		self.drawBackground()
		self.drawHoles()
		self.createMoles()
		self.updateFigures()
		self.choiceMole()
		self.updateMoles()

	def updateMoles(self):
		'''モグラの状態や位置を更新する'''

		for mole in self.moles:

			# モグラの状態や位置を更新する
			mole.update()

		# MOLE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(MOLE_UPDATE_INTERVAL, self.updateMoles)

updateMoles メソッドでは after を実行して MOLE_UPDATE_INTERVAL ms 後に再度 updateMoles メソッドを実行するように設定します。

そのため、updateMoles メソッドは MOLE_UPDATE_INTERVAL ms 間隔で定期的に実行される関数となります。

また、この updateMoles メソッドの中では、moles で管理されている全ての Mole クラスのオブジェクトに update メソッドを実行させているため、穴から出ているモグラ全てが上方向に移動することになります(ただし、ytop_y になったモグラに関しては、それ以上は上方向に移動しません)。

ここまでの変更を加えたスクリプトを実行すれば、ランダムに選ばれたモグラが表示され、その表示されたモグラが上方向に移動していくことが確認できると思います。

穴から出たモグラが上方向に移動していく様子

ここで、このモグラに対する処理をおさらいしておくと、まず choiceMole メソッドでランダムに穴から出す Mole クラスのオブジェクトが選ばれ、そのオブジェクトが appear メソッドを実行することでデータ属性 is_appearingis_up がともに True に設定されます。

また、updateMoles メソッドの中で全 Mole クラスのオブジェクトが update メソッドを実行し、is_upTrue のオブジェクトに対しては y が減少されます。

さらに updateFigures メソッドの中で is_appearingTrue のオブジェクトの画像が表示され、さらに y の値に従って表示座標が変更されます。

これらの choiceMole メソッド・updateMoles メソッド・updateFigures メソッドはそれぞれ独立して定期的に実行されているように見えますが、実際には上記のように、それぞれモグラのデータ属性の値に応じて連動して動作していることになります。

頂点まで移動したモグラを下方向に移動する

モグラが上方向に移動するようになったので、次はモグラが頂点まで移動した際に、下方向に移動するようにしていきたいと思います。

これは、先ほど追加した Mole クラスの update メソッドを下記のように変更するだけで実現することができます。

モグラの下方向の移動
class Mole:

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 上方向に移動
			self.y = max(self.top_y, self.y - self.speed)
			if self.y == self.top_y:
				# 上方向の上限まで移動したので下方向への移動を開始
				self.is_up = False
		else:
			# 下方向への移動中

			# 下方向に移動
			self.y = min(self.hole_y, self.y + self.speed)

上記の変更により、is_upFalse の時に update メソッドが実行されると y に speed が加算され、モグラが下方向に移動するようになります。ただし、穴の位置までモグラが移動した場合はそれ以上は下方向に移動しないよう、min 関数を使用して制御しています。

さらに is_upTrue の場合は、ytop_y になった際に is_up を False に設定しているように変更してます。

そのため、それ以降に update が実行された際にはモグラが下方向に移動していくようになります。

この変更を加えたスクリプトを実行すれば、モグラが上方向にある程度移動した後、次は下方向に移動し始めることが確認できると思います。また、穴の中心位置まで移動したらモグラが止まることも確認できると思います。

モグラが上方向に移動した後にした方向に移動していく様子

穴まで戻ったモグラを穴に隠れさせる

長い解説でしたが、次がモグラを穴から出させる処理の最後の実装となります。

次は、穴の中心の位置まで降りてきたモグラを画面から見えなくすることで、モグラを穴に隠れさせるようにしていきます。

ただ、これは簡単で、モグラが穴の中心まで降りてきた時、すなわち yhole_y になった時に、モグラを画面から見えなくするように is_appearing を False に設定してやれば良いだけです。

つまり、Mole クラスの update メソッドを下記のように変更すれば良いです。

モグラを隠れさせる
class Mole:

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 上方向に移動
			self.y = max(self.top_y, self.y - self.speed)
			if self.y == self.top_y:
				# 上方向の上限まで移動したので下方向への移動を開始
				self.is_up = False
		else:
			# 下方向への移動中

			# 下方向に移動
			self.y = min(self.hole_y, self.y + self.speed)
			if self.y == self.hole_y:
				# モグラがまた穴に潜ってしまった場合

				# モグラの状態をリセットする
				self.is_appearing = False

変更後のスクリプトを実行すれば、穴から出てきたモグラが上方向および下方向に移動した後に自動的に見えなくなり、モグラが隠れた様子が表現できていることが確認できると思います。

下まで移動したらモグラが穴に隠れる様子

モグラを叩けるようにする

モグラが穴から出るようになりましたので、次はモグラを叩けるようにしていきたいと思います。

今回は、モグラの画像がマウスでクリックされた時、そのモグラが叩かれたと判断するようにしていきたいと思います。

画像をクリックされたモグラが叩かれる様子

そして、叩かれたモグラに関しては、上方向移動中であっても強制的に下方向に移動するようにしていきます。さらに、一度叩かれたモグラは、穴に隠れるまではもう叩けないようにしたいと思います。

叩かれたモグラが強制的に下方向に移動させられる様子

ただ、単に下方向に移動させるだけだと、モグラが叩かれたのかどうかが分かりにくいため、叩かれたモグラを点滅させるようにすることで、どのモグラが叩かれたのかが分かりやすいようにもしていきたいと思います。

モグラが叩かれた時の処理

まずはモグラが叩かれた時の処理を Mole クラスのメソッドとして追加したいと思います。

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

叩かれた時の処理
class Mole:

	def hit(self):
		'''叩かれた時の処理を実行'''

		# 下方向への移動を開始
		self.is_up = False

		# 叩かれたフラグをセット
		self.is_hitted = True

hit メソッドは “叩かれたと判断されたモグラ” のオブジェクトに実行させることを想定したメソッドであり、前述の通り、叩かれたモグラを強制的に下方向に移動させるため、is_upFalse に設定するようにしています。

また、叩かれたモグラが再び叩かれることを防ぐため、モグラが既に叩かれたかどうかが判断できるようにデータ属性 is_hittedTrue にするようにしています。

モグラが叩かれかどうかの判断

次は、モグラが叩かれたかどうかを判断するためのメソッドを Mole クラスに追加します。

モグラを描画するモグラのクラスとモグラのオブジェクトを作成する で解説したように、モグラの画像は Mole クラスのデータ属性 xy の位置が画像の中央下の位置となるように描画されます。

データ属性xとyと描画される画像の位置関係

つまり、モグラの画像は下記の左上座標と右下座標で表される領域内に描画されていることになります。

  • 左上座標
    • x 座標:x - width / 2
    • y 座標:y - height
  • 右下座標
    • x 座標:x + width / 2
    • y 座標:y

従って、マウスでクリックされた位置の座標が上記の領域内であるとすれば、この Mole クラスのオブジェクトは叩かれたと判断することができます。

すなわち、座標 (mouse_x, mouse_y) がクリックされた時に、メソッドを実行した Mole クラスのオブジェクトの画像がクリックされたかどうかは、下記の isHit メソッドにより判断することができます。

叩かれたかどうかの判断
class Mole:

	def isHit(self, mouse_x, mouse_y):
		'''叩かれたかどうかを判断する'''

		if not self.is_appearing:
			# 穴から出ていない
			return False

		if self.is_hitted:
			# すでに叩かれている
			return False

		# モグラの画像が表示されている領域の座標を計算
		x1 = self.x - self.width / 2
		y1 = self.y - self.height
		x2 = self.x + self.width / 2
		y2 = self.y

		# (mouse_x,mouse_y)が上記領域内であるかどうかを判定
		if x1 <= mouse_x and x2 >= mouse_x:
			if y1 <= mouse_y and y2 >= mouse_y:

				# 領域内をクリックされたので叩かれた
				return True

		# 叩かれなかった
		return False

isHit メソッドは、叩かれたと判断した場合に True を、叩かれなかったと判断した場合に False を返却するメソッドになります。

引数 mouse_xmouse_y はマウスでクリックされた座標を受け取ることを想定しています。

基本的に画像が描画されている領域内に座標 (mouse_x, mouse_y) が存在するかどうかで叩かれたかどうかを判断していますが、モグラが穴から出ていない場合(is_appearingFalse の場合)とモグラが既に叩かれている場合(is_hittedTrue の場合)は必ず False を返却するようにしています。

モグラが既に叩かれている場合に False を返却するようにすることで、何回もモグラが叩かれることを防ぐことができます。

ただし、この is_hitted をずっと True にしておくと、一度叩かれたモグラは再度穴から出てきた時にも叩けなくなってしまいます。

そのため、モグラが穴に隠れる際に is_hitted を False に設定するようにしたいと思います。これは、Mole クラスの update メソッドを下記のように変更することで実現することができます。

モグラを叩けるようにする
class Mole:

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 上方向に移動
			self.y = max(self.top_y, self.y - self.speed)
			if self.y == self.top_y:
				# 上方向の上限まで移動したので下方向への移動を開始
				self.is_up = False
		else:
			# 下方向への移動中

			# 下方向に移動
			self.y = min(self.hole_y, self.y + self.speed)
			if self.y == self.hole_y:
				# モグラがまた穴に潜ってしまった場合

				# モグラの状態をリセットする
				self.is_appearing = False
				self.is_hitted = False

画像クリック時のイベントの受付設定

Mole クラスの isHit メソッドでマウスのクリック座標からモグラが叩かれたかどうかが判断できるようになり、さらに Mole クラスの hit メソッドでモグラが叩かれた時の処理を実行できるようになりました。

次は、実際にマウスでクリックされた時に、これらのメソッドを実行するようにしていきたいと思います。

まずは、画像がマウスでクリックされた時のイベント処理の設定を行います。

これは、WhackaMole クラスの createMoles メソッドにおける hole_coords に対するループの最後に、下記のように tag_bind メソッドの実行を追加することで実現できます。

マウスクリックのイベント受付
class WhackaMole:

	def createMoles(self):
		'''モグラを作成して表示する'''

		# 略

		for coord in self.hole_coords:

			# 穴の座標を取得
			x, y = coord

			# 穴の中心にモグラの画像の下が接するように画像を描画
			figure = self.canvas.create_image(
				x, y,
				anchor=tkinter.S,
				image=self.mole_image
			)

			# 描画した画像を背景画像の下側に隠す
			self.canvas.lower(figure, "all")

			# モグラオブジェクトを作成
			width = self.mole_image.width()
			height = self.mole_image.height()
			mole = Mole(x, y, width, height, 1, figure)

			# モグラオブジェクトを管理リストに追加
			self.moles.append(mole)

			# 描画画像がクリックされた時にonClickが実行されるように設定
			self.canvas.tag_bind(figure, "<ButtonPress>", self.onClick)

キャンバスの tag_bind メソッドは、描画した図形に対してイベント受付の設定を行うメソッドになります。第1引数には、イベントを受け付ける図形の ID を指定します。

また、第2引数と第3引数には、通常の bind メソッドの第1引数と第2引数同様に、受け付けたいイベントのイベントシーケンスとイベント発生時に実行したい関数 or メソッドを指定します。

上記のように tag_bind メソッドを実行すれば、描画したモグラの画像のいずれかがマウスでクリックされた際に、WhackaMole クラスの onClick メソッドが実行されるようになります。

さらに、その実行されるメソッド onClickWhackaMole クラスに下記のように追加します。

クリックされた時の処理
class WhackaMole:

	def onClick(self, event):
		'''クリックされた時の処理を実行する'''

		for mole in self.moles:

			# moleの画像がクリックされたかどうかを判断
			if mole.isHit(event.x, event.y):
				# クリックされた場合

				# moleを叩く
				mole.hit()

tag_bind メソッドで設定したような、イベント発生時に実行されるメソッドや関数には、引数としてイベントの詳細情報が渡されます(引数 event)。

さらにマウスでクリックされた時に実行されるメソッドにおいては、この引数のデータ属性 xy にはクリックされた座標が設定されています。

従って、Mole クラスのオブジェクトに isHit(event.x, event.y) を実行させれば、そのオブジェクトの画像がクリックされたかどうかを判断することができます。

そして、クリックされた場合に Mole クラスのオブジェクトに hit メソッドを実行させれば、そのオブジェクトに叩かれた時の処理を実行させることができます。

ここまでの変更を加えたスクリプトを実行してモグラをクリックすれば、モグラが頂点まで移動する前に即座に下方向に移動することが確認できると思います。

ただ、下方向に移動させるだけでは叩かれたかどうかが分かりにくいので、次は叩かれたモグラを点滅して表示させることで、もうちょっと叩かれたモグラが視覚的に分かりやすくなるようにしていきたいと思います。

叩かれたモグラの点滅

モグラの点滅は、叩かれたモグラのオブジェクトのデータ属性 is_draw を定期的に TrueFalse とを交互に設定するようにすることで実現できます。

かなり前に触れた内容なのでここで復習しておくと、モグラの画像を定期的に更新する で作成したWhackaMole クラスの updateFigures メソッドでは、データ属性 is_appearing とデータ属性 is_draw が共に True である Mole クラスのオブジェクトの画像のみが最前面に移動されて画面に表示されるようになっています。

逆に、どちらか一方でも False であれば、そのオブジェクトの画像は最背面に移動されて背景の後ろに隠れて表示されないことになります。

で、現状は is_draw は常に True なので(__init__ で True に初期化される)、データ属性 is_appearing さえ True にしてやれば画面に表示されるようになっています。そしてそれを利用して、モグラを穴から出すことを表現しています。

上記のような仕組みでモグラの画像の表示 or 非表示が切り替わるようになっていますので、叩かれたモグラを点滅させるには、叩かれた後に定期的に is_draw の値を反転(TrueFalse を反転)させてやれば、updateFigures メソッドで画像の表示 or 非表示も定期的に反転していくことになります。

is_drawの反転によりモグラの画像が点滅する様子

さらに、Mole クラスの update は定期的に実行されるメソッドですので、このメソッドの中で叩かれた状態のモグラの is_draw の値を反転させるようにしてやれば、叩かれたモグラの表示 or 非表示が定期的に切り替わり、点滅表示しているように見せることができます。

上記のような処理は、Mole クラスの update メソッドを下記のように変更することで実現できます。

叩かれたモグラの点滅
class Mole:

	def update(self):
		'''定期的な状態更新を行う'''

		if self.is_up:
			# 上方向への移動中

			# 略
		else:
			# 下方向への移動中

			# 下方向に移動
			self.y = min(self.hole_y, self.y + self.speed)
			if self.y == self.hole_y:
				# モグラがまた穴に潜ってしまった場合

				# モグラの状態をリセットする
				self.is_appearing = False
				self.is_hitted = False
				self.is_draw = True

		if self.is_hitted:
			# 叩かれたフラグが立っている場合

			# 描画実行フラグのON/OFFを切り替える
			self.is_draw = not self.is_draw

叩かれたモグラのオブジェクトであるかどうかはデータ属性 is_hitted で判断できますので、is_hittedTrue の場合のみ is_draw の値を反転させるようにしています。

また、モグラが隠れた時、すなわち is_appearingFalse に設定するタイミングで、is_drawTrue に戻すようにしています。これは、穴に隠れたモグラが再び穴から出てきた時に is_drawFalse の状態のままになってしまっていることを防ぐためです。

上記の変更を加えてスクリプトを実行してモグラを叩けば、モグラが点滅するようになったことが確認できると思います。

叩かれたモグラが点滅表示される様子

スポンサーリンク

ポイントの加減算を行う

次に、モグラたたきゲームの作り方の最後として、モグラを叩いた時にポイントを加算し、さらにモグラを叩けなかった時にポイントを減算するようにしていきたいと思います。

ポイント表示用のウィジェットとしてラベルウィジェットを用意していますので、このウィジェットで表示されている値を増減させていくことになります。

ポイントの加減算が行われる様子

また、モグラを叩いてポイントが加算される際には、その加算されるポイントをマウスでクリックされた位置に描画し、

モグラが叩かれた場合にポイントが描画される様子

モグラを叩くことができずにポイントが減算される際には、その叩けなかったモグラが潜り込んだ穴の位置に減算されるポイントを描画するようにしていきます。

モグラが叩かれなかった場合にポイントが描画される様子

ただし、描画したポイントがずっと画面上に残ってしまうと邪魔なので、描画したポイントは一定時間経過後に削除するようにしたいと思います。

ポイントの加減算

まずは、メインウィンドウ下部のラベルウィジェットに表示されているポイントを、モグラを叩いた or モグラを叩けなかった場合に加減算できるよう、下記の pointUp メソッドを WhackaMole クラスに追加します。

ラベルウィジェットのポイントの加減算
class WhackaMole:

	def pointUp(self, point):
		'''ポイントを加算する'''

		# ラベルに表示されているポイントを取得
		now_point = int(self.label.cget("text"))

		# 取得したポイントに加算して表示文字列に設定
		now_point += point
		self.label.config(text=str(now_point))

上記 pointUp は、引数 point の値を現在のラベルウィジェットに表示されている値に加算するメソッドになります。point が負の値の場合は、ラベルウィジェットに表示されている値が減算されることになります。

メインウィンドウ下部のラベルウィジェットには現在のポイントが表示されており、その表示されている文字列は cget("text") をラベルウィジェットに実行させることで取得することができます。

また、ラベルウィジェットに表示されている文字列は、ラベルウィジェットに config メソッドを実行させることで変更可能です(メソッド実行時に text オプションを指定する)。

上記 pointUp メソッドでは、ラベルウィジェットから取得したポイントに point を加算し、ラベルウィジェットの表示文字列を加算後のポイントに変更することで、ポイントの加減算を実現しています。

ラベルウィジェットから取得したポイントに加算を行い、再びラベルウィジェットに加算後のポイントを設定し直す様子

加減算されたポイントの描画

次は、モグラを叩いた or モグラを叩けなかった場合に加減算されるポイントをキャンバスに描画するようにしていきます。

このために、下記の drawPoint メソッドを WhackaMole クラスに追加します。

加減算の描画
class WhackaMole:

	def drawPoint(self, point, x, y):
		'''加算されたポイントを表示'''

		if point >= 0:
			sign = "+"
			color = "yellow"
		else:
			sign = "-"
			color = "red"

		point_figure = self.canvas.create_text(
			x, y,
			text=sign + str(abs(point)),
			fill=color,
			font=("", 40)
		)

		# DRAW_TIME_POINT後に描画した文字列を削除
		self.master.after(
			POINT_DRAW_TIME, lambda: self.canvas.delete(point_figure))

drawPoint は、引数 point で指定されるポイントをキャンバス上の座標 (x, y) の位置に文字列として描画するメソッドになります。文字列の描画はキャンバスの create_text により行うことができます。

point0 を含む正の値の場合は、ポイントの前に + を付加して黄色の文字で描画を行い、point が負の値の場合は、ポイントの前に - を付加して赤色の文字で描画を行います。

ただし、キャンバスに描画した図形は放っておくと永遠にキャンバス上に残り続けてしまいます。

加減算されたポイントがずっと残ってしまうと邪魔なので、after メソッドを利用して POINT_DRAW_TIME ms 後に self.canvas.delete(point_figure) が実行されるようにすることで、時間が経ったら自然に描画した文字列が消えるようにしています(delete メソッドは引数で指定した ID の図形をキャンバスから削除するメソッドになります)。

after メソッドは定期的な処理を実現するだけでなく、処理を遅らせて実行する際にも活躍するメソッドです。

モグラが叩れた時のポイントの加算

ラベルウィジェットのポイントを加減算するメソッド pointUp と加減算されたポイントを描画するメソッド drawPoint が用意できましたので、次は実際にポイントが加算されるタイミング or ポイントが減算されるタイミングでこれらのメソッドを実行するようにしていきたいと思います。

まず、ポイントを加算するタイミングはモグラが叩かれた時です。

モグラが叩かれたかどうかは、WhackaMole クラスの onClick から Mole クラスの isHit メソッドを実行することで判断していますので、isHit メソッドが Trueを返却したときに pointUp メソッドと drawPoint メソッドを実行すれば、適切なタイミングでポイントの加算と加算されたポイントの描画を行うことができます。

このため、WhackaMole クラスの onClick メソッドを下記のように変更します。

ポイントの加算
class WhackaMole:

	def onClick(self, event):
		'''クリックされた時の処理を実行する'''

		for mole in self.moles:

			# moleの画像がクリックされたかどうかを判断
			if mole.isHit(event.x, event.y):
				# クリックされた場合

				# moleを叩く
				mole.hit()

				# ポイントの加算と加算ポイントの描画
				self.pointUp(mole.point)
				self.drawPoint(mole.point, event.x, event.y)

モグラを叩いた時に加算されるポイントは Mole クラスのデータ属性 point で設定されていますので、このデータ属性を pointUp メソッドと drawPoint メソッドの引数に指定するようにしています(データ属性 pointモグラのクラスとモグラのオブジェクトを作成する で追加した Mole クラスの __init__ で設定しています)。

drawPoint メソッドではポイントを描画する位置の x 座標と y 座標を指定する必要があるため、ここにマウスでクリックされた座標である event.xevent.y を指定しています。

ここまでの変更を加えたスクリプトを実行してモグラをクリックすれば、クリックした位置に加算されたポイントが描画され、さらにラベルウィジェットに表示されているポイントも増加していくようになったことが確認できると思います。

ポイントの加算と加算ポイントの表示が行われる様子

モグラが叩れなかった時のポイントの減算

次はモグラが叩かれなかった時のポイントの減算と減算ポイントの描画を行なっていきたいと思います。

「モグラが叩かれなかった」とは、要はモグラが穴から出たにも関わらず「叩かれないまま再びモグラが穴に潜って隠れてしまった」ことを指します。

従って、ポイントの減算と減算ポイントの描画を行うタイミングは、穴から出ている&叩かれていないモグラが穴に潜って隠れてしまった時になります。

ポイントの減算を行うタイミングの説明図

また、モグラが穴から出ているかどうかは  Mole クラスのデータ属性 is_appearing で管理され、さらにモグラが叩かれたかどうかは Mole クラスのデータ属性 is_hitted で管理されています。

さらに、モグラを穴に潜って隠れさせる処理は、Mole クラスの update メソッドの中でのみ行われます(y を増加させて hole_y 以上になった時に is_appearingFalse にセットする)。

ですので、モグラが隠れたタイミングは、update の実行前後で is_appearingTrue から False に変化した時であると考えることができます。

さらに、is_hittedFalse のモグラは叩かれていないモグラですので、この辺りを考慮すれば、下記のように WhackaMole クラスの updateMoles メソッドを変更することで、適切なタイミングでのポイントの減算と減算ポイントの描画を実現することができることになります。

ポイントの減算
class WhackaMole:

	def updateMoles(self):
		'''モグラの状態や位置を更新する'''

		for mole in self.moles:

			# 更新前のモグラの状態を退避
			is_hitted = mole.is_hitted
			before_appearing = mole.is_appearing

			# モグラの状態や位置を更新する
			mole.update()

			# 更新後にモグラが隠れたかどうかのフラグを取得
			after_appearing = mole.is_appearing

			if not is_hitted and before_appearing and not after_appearing:
				# moleが叩かれていない&上記のupdateメソッドによりmoleが穴に潜り込んだ場合

				# ポイントの加算と加算ポイントの描画
				self.pointUp(-mole.point)
				self.drawPoint(-mole.point, mole.x, mole.y)

		# MOLE_UPDATE_INTERVAL後に再度モグラの状態を更新
		self.master.after(MOLE_UPDATE_INTERVAL, self.updateMoles)

今回はポイントの減算を行いますので、Mole クラスのデータ属性 point の符号を - にしたものを pointUp メソッドと drawPoint メソッドに指定するようにしています。

また、ポイントを描画する位置は穴の中心座標になりますが、モグラが穴に隠れたタイミングでは、その Mole クラスのオブジェクトのデータ属性 xy は穴の中心座標と必ず一致するため、この xydrawPoint の引数に指定するようにしています。

このように変更を加えたスクリプトを実行してモグラを叩かずに放置しておけば、モグラが穴に隠れたタイミングで減算ポイントが表示され、さらにラベルウィジェットに表示されているポイントも減っていくことが確認できると思います。

ポイントの減算と減算ポイントの表示が行われる様子

以上で、モグラたたきゲームが完了したことなります!

まとめ

このページでは、Python で tkinter を用いた「モグラたたきゲームの作り方」について解説しました!

単純なゲームではありますが、実際に作ってみると結構大変ですね…。ただ、その分学べることも多かったのではないかと思います!

本当はもうちょっとモグラが穴から出ている感を出したかったのですが、結局今回紹介したような動作とさせていただきました。

今回作成したモグラたたきゲームを動かしてみれば、おそらく皆さんも「ここが気に入らない!」「もっとこうしてみたい!」と感じる点があるのではないかと思います。

もしあるのであれば、是非スクリプトを変更し、その点の改善に挑戦してみてください!プログラミングの力をつける一番の近道は、自身で実現してみたいことをプログラミングしてみることだと思います。

題材がゲームなので、割と楽しくプログラミングもできると思いますので、是非いろんなスクリプトの変更を試し、”より良い” モグラたたきゲームへの発展に挑戦してみてください!

オススメ参考書(PR)

簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!

下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。

  • すごろく
  • おみくじ
  • 迷路ゲーム
  • 落ち物パズル
  • RPG

また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。

  • プログラミング・Python の基礎から解説
  • 絵を用いた解説が豊富
  • ライブラリの使い方から解説(tkitner と Pygame)
  • ソースコードの1行1行に注釈

ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。

プログラミングを学ぶのにゲーム開発は相性抜群だと思います。

Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。

実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!

また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。

この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。

プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!

同じカテゴリのページ一覧を表示