このページでは、Python で tkinter を用いた「ブロック崩しゲームの作り方」について解説していきます。
出来上がりは下の動画のようなものになります。
少し設定を変更すれば、下の動画のようなものに変化させることもできます。
ブロック崩しゲームとはまた異なりますが、ブロックが消えていく様子とボールが反射していく様子が気持ちよくて、個人的にはこっちの方が好きです。
上記の動画で紹介したようなゲームを作ってみたいという方は、是非ページを読み進めていっていただければと思います!
Contents
作成するブロック崩しゲームの紹介
まずは、作成する「ブロック崩しゲーム」がどのようなものであるのかについて説明しておきます
このブロック崩しゲームで登場するオブジェクトは下記の3種類となります。
- ボール
- パドル
- ブロック
ゲームを開始する際にはマウスでクリックを行います(どのボタンをクリックするのでも良いです)。ゲームを開始すれば、ボールが移動し、パドルを操作することができるようになります。
パドルに関してはマウスカーソルの位置に応じて横方向に移動します。
また、ボールはある方向に向かって定期的に移動し、パドルや壁(下方向以外の画面の端)に当たった場合は、ボールが反射し、別の方向に移動していくことになります。
ボールとブロックが当たった時もボールが反射することは同じですが、この際には当たったブロックを消すことができます。
全てのブロックを消すことができた場合はゲームクリアとなります。
その一方で、ボールが画面の下まで移動してしまった場合は、そのボールが消えることになります。全てのボールが消えてしまった場合はゲームオーバーとなります。
そうならないように、パドルをうまく操作してボールを反射させていく必要があります。
ちなみに今回は、ボールとブロック及びボールとパドルの当たり判定は「円と矩形」ではなく、「矩形同士」での判定とさせていただいています。ただし、それでもそれなりに上手く動作しているように見えると思います!
ブロック崩しゲームのスクリプト
今回作成するブロック崩しゲームのスクリプトの最終形は下記のようになります。
次の ブロック崩しゲームの作り方 においては、このスクリプトを作っていく上での過程を説明していきたいと思います。
import tkinter
import math
NUM_H_BLOCK = 10 # ブロッックの数(横方向)
NUM_V_BLOCK = 10 # ブロックの数(縦方向)
WIDTH_BLOCK = 40 # ブロックの幅
HEIGHT_BLOCK = 20 # ブロックの高さ
COLOR_BLOCK = "blue" # ブロックの色
HEIGHT_SPACE = 300 # 縦方向の空きスペース
WIDTH_PADDLE = 200 # パドルの幅
HEIGHT_PADDLE = 20 # パドルの高さ
Y_PADDLE = 50 # パドルの下方向からの位置
COLOR_PADDLE = "green" # パドルの色
RADIUS_BALL = 10 # ボールの半径
COLOR_BALL = "red" # ボールの色
NUM_BALL = 2 # ボールの数
UPDATE_TIME = 20 # 更新間隔(ms)
class Ball:
def __init__(self, x, y, radius, x_min, y_min, x_max, y_max):
'''ボール作成'''
# 位置と半径と移動可能範囲を設定
self.x = x
self.y = y
self.r = radius
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
# 一度に移動する距離(px)
self.speed = 10
# 移動方向を設定
self.angle = math.radians(30)
self.dx = self.speed * math.cos(self.angle)
self.dy = self.speed * math.sin(self.angle)
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.r, self.y - self.r, self.x + self.r, self.y + self.r)
def move(self):
'''移動'''
# 移動方向に移動
self.x += self.dx
self.y += self.dy
if self.x < self.x_min:
# 左の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_min
elif self.x > self.x_max:
# 右の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_max
if self.y < self.y_min:
# 上の壁とぶつかった
# 縦方向に反射
self.reflectV()
self.y = self.y_min
# elif self.y > self.y_max:
# 下の壁とぶつかった
# 縦方向に反射
# self.reflectV()
#self.y = self.y_max
def turn(self, angle):
'''移動方向をangleに応じて設定'''
self.angle = angle
self.dx = self.speed * math.cos(self.angle)
self.dy = self.speed * math.sin(self.angle)
def reflectH(self):
'''横方向に対して反射'''
self.turn(math.atan2(self.dy, -self.dx))
def reflectV(self):
'''縦方向に対して反射'''
self.turn(math.atan2(-self.dy, self.dx))
def getCollisionCoords(self, object):
'''objectと当たった領域の座標の取得'''
# 各オブジェクトの座標を取得
ball_x1, ball_y1, ball_x2, ball_y2 = self.getCoords()
object_x1, object_y1, object_x2, object_y2 = object.getCoords()
# 新たな矩形の座標を取得
x1 = max(ball_x1, object_x1)
y1 = max(ball_y1, object_y1)
x2 = min(ball_x2, object_x2)
y2 = min(ball_y2, object_y2)
if x1 < x2 and y1 < y2:
# 始点が終点よりも左上にある
# 当たった領域の左上座標と右上座標を返却
return (x1, y1, x2, y2)
else:
# 当たっていないならNoneを返却
return None
def reflect(self, object):
'''当たった方向に応じて反射'''
# 各オブジェクトの座標を取得
object_x1, object_y1, object_x2, object_y2 = object.getCoords()
# 重なった領域の座標を取得
x1, y1, x2, y2 = self.getCollisionCoords(object)
is_collideV = False
is_collideH = False
# どの方向からボールが当たったかを判断
if self.dx < 0:
# ボールが左方向に移動中
if x2 == object_x2:
# objectの左側と当たった
is_collideH = True
else:
# ボールが右方向に移動中
if x1 == object_x1:
# objectの右側と当たった
is_collideH = True
if self.dy < 0:
# ボールが上方向に移動中
if y2 == object_y2:
# objectの下側と当たった
is_collideV = True
else:
# ボールが下方向に移動中
if y1 == object_y1:
# objectの上側と当たった
is_collideV = True
if is_collideV and is_collideH:
# 横方向と縦方向両方から当たった場合
if x2 - x1 > y2 - y1:
# 横方向の方が重なりが大きいので横方向に反射
self.reflectV()
elif x2 - x1 < y2 - y1:
# 縦方向の方が重なりが大きいので縦方向に反射
self.reflectH()
else:
# 両方同じなので両方向に反射
self.reflectH()
self.reflectV()
elif is_collideV:
# 縦方向のみ当たったので縦方向に反射
self.reflectV()
elif is_collideH:
# 横方向のみ当たったので横方向に反射
self.reflectH()
def exists(self):
'''画面内に残っているかどうかの確認'''
return True if self.y <= self.y_max else False
class Paddle:
def __init__(self, x, y, width, height, x_min, y_min, x_max, y_max):
'''パドル作成'''
# 中心座標と半径と移動可能範囲を設定
self.x = x
self.y = y
self.w = width
self.h = height
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.w / 2, self.y - self.h / 2, self.x + self.w / 2, self.y + self.h / 2)
def move(self, mouse_x, mouse_y):
'''(mouse_x, mouse_y) に移動'''
# 移動可能範囲で移動
self.x = min(max(mouse_x, self.x_min), self.x_max)
self.y = min(max(mouse_y, self.y_min), self.y_max)
class Block:
def __init__(self, x, y, width, height):
'''ブロック作成'''
# 中心座標とサイズを設定
self.x = x
self.y = y
self.w = width
self.h = height
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.w / 2, self.y - self.h / 2, self.x + self.w / 2, self.y + self.h / 2)
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
self.createObjects()
self.drawFigures()
self.setEvents()
def start(self, event):
'''ゲーム開始'''
if len(self.blocks) == 0 or len(self.balls) == 0:
# ゲームクリア or ゲームオーバー時は最初からやり直し
# キャンバスの図形を全て削除
self.canvas.delete("all")
# 全オブジェクトの作り直しと図形描画
self.createObjects()
self.drawFigures()
# ゲーム開始していない場合はゲーム開始
if not self.is_playing:
self.is_playing = True
self.loop()
else:
self.is_playing = False
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
# ゲーム開始していないなら何もしない
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
# 全ボールを移動する
for ball in self.balls:
ball.move()
# ボールが画面外に出たかどうかをチェック
delete_balls = []
for ball in self.balls:
if not ball.exists():
# 外に出たボールは削除対象リストに入れる
delete_balls.append(ball)
for ball in delete_balls:
# 削除対象リストのボールを削除
self.delete(ball)
self.collision()
self.updateFigures()
self.result()
def motion(self, event):
'''パドルの移動'''
self.paddle.move(event.x, event.y)
def delete(self, target):
'''targetのオブジェクトと図形を削除'''
# 図形IDを取得してキャンバスから削除
figure = self.figs.pop(target)
self.canvas.delete(figure)
# targetを管理リストから削除
if isinstance(target, Ball):
self.balls.remove(target)
elif isinstance(target, Block):
self.blocks.remove(target)
def collision(self):
'''当たり判定と当たった時の処理'''
for ball in self.balls:
collided_block = None # 一番大きく当たったブロック
max_area = 0 # 一番大きな当たった領域
for block in self.blocks:
# ballとblockとの当たった領域の座標を取得
collision_rect = ball.getCollisionCoords(block)
if collision_rect is not None:
# 当たった場合
# 当たった領域の面積を計算
x1, y1, x2, y2 = collision_rect
area = (x2 - x1) * (y2 - y1)
# 一番大きく当たっているかどうかを判断
if area > max_area:
# 一番大きく当たった領域の座標を覚えておく
max_area = area
# 一番大きく当たったブロックを覚えておく
collided_block = block
if collided_block is not None:
# 一番大きく当たったブロックに対してボールを反射
ball.reflect(collided_block)
# 一番大きく当たったブロックを削除
self.delete(collided_block)
for another_ball in self.balls:
if another_ball is ball:
# 同じボールの場合はスキップ
continue
# ballとanother_ballとの当たり判定
if ball.getCollisionCoords(another_ball) is not None:
# 当たってたらballを反射
ball.reflect(another_ball)
# ballとself.paddleとの当たり判定
if ball.getCollisionCoords(self.paddle) is not None:
# 当たってたらballを反射
ball.reflect(self.paddle)
def result(self):
'''ゲームの結果を表示する'''
if len(self.blocks) == 0:
self.canvas.create_text(
self.width // 2, self.height // 2,
text="GAME CLEAR",
font=("", 40),
fill="blue"
)
self.is_playing = False
if len(self.balls) == 0:
self.canvas.create_text(
self.width // 2, self.height // 2,
text="GAME OVER",
font=("", 40),
fill="red"
)
self.is_playing = False
def setEvents(self):
'''イベント受付設定'''
self.canvas.bind("<ButtonPress>", self.start)
self.canvas.bind("<Motion>", self.motion)
def createWidgets(self):
'''必要なウィジェットを作成'''
# キャンバスを作成
self.canvas = tkinter.Canvas(
self.master,
width=self.width,
height=self.height,
highlightthickness=0,
bg="gray"
)
self.canvas.pack(padx=10, pady=10)
def createObjects(self):
'''ゲームに登場するオブジェクトを作成'''
# ボールを作成
self.balls = []
for i in range(NUM_BALL):
x = self.width / NUM_BALL * i + self.width / NUM_BALL / 2
ball = Ball(
x, self.height // 2,
RADIUS_BALL,
RADIUS_BALL, RADIUS_BALL,
self.width - RADIUS_BALL, self.height - RADIUS_BALL
)
self.balls.append(ball)
# パドルを作成
self.paddle = Paddle(
self.width // 2, self.height - Y_PADDLE,
WIDTH_PADDLE, HEIGHT_PADDLE,
WIDTH_PADDLE // 2, self.height - Y_PADDLE,
self.width - WIDTH_PADDLE // 2, self.height - Y_PADDLE
)
# ブロックを作成
self.blocks = []
for v in range(NUM_V_BLOCK):
for h in range(NUM_H_BLOCK):
block = Block(
h * WIDTH_BLOCK + WIDTH_BLOCK // 2,
v * HEIGHT_BLOCK + HEIGHT_BLOCK // 2,
WIDTH_BLOCK,
HEIGHT_BLOCK
)
self.blocks.append(block)
def drawFigures(self):
'''各オブジェクト毎に図形を描画'''
# オブジェクト図形を関連づける辞書
self.figs = {}
# ボールを描画
for ball in self.balls:
x1, y1, x2, y2 = ball.getCoords()
figure = self.canvas.create_oval(
x1, y1, x2, y2,
fill=COLOR_BALL
)
self.figs[ball] = figure
# パドルを描画
x1, y1, x2, y2 = self.paddle.getCoords()
figure = self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=COLOR_PADDLE
)
self.figs[self.paddle] = figure
# ブロックを描画
for block in self.blocks:
x1, y1, x2, y2 = block.getCoords()
figure = self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=COLOR_BLOCK
)
self.figs[block] = figure
def updateFigures(self):
'''新しい座標に図形を移動'''
# ボールの座標を変更
for ball in self.balls:
x1, y1, x2, y2 = ball.getCoords()
figure = self.figs[ball]
self.canvas.coords(figure, x1, y1, x2, y2)
# パドルの座標を変更
x1, y1, x2, y2 = self.paddle.getCoords()
figure = self.figs[self.paddle]
self.canvas.coords(figure, x1, y1, x2, y2)
app = tkinter.Tk()
Breakout(app)
app.mainloop()
スポンサーリンク
ブロック崩しゲームの作り方
それでは、ブロック崩しゲームの作り方について解説していきます。
大きく分けると、ブロック崩しゲームを作るために下記の13個のことを行なっていきます。結構解説内容の分量も多いですので、休みを入れながらゆっくり読んでいっていただければと思います。
- 各種パラメータを定義する
- ウィジェットを作成する
- 各種クラスを作成する
- 各種オブジェクトを作成する
- 各種図形を描画する
- 定期的な図形の座標変更を行う
- ボールを移動できるようにする
- パドルを移動できるようにする
- ボールと当たったブロックを消す
- 他のオブジェクトと当たったボールを反射させる
- ゲームクリアを表示する
- ゲームオーバーを表示する
- ゲームのやり直しができるようにする
最終的に出来上がるスクリプトは ブロック崩しゲームのスクリプト と全く同じになりますので、ブロック崩しゲームのスクリプト のスクリプトで何をやっているか分からないところや気になるところだけ読んでも良いですし、もちろん最初から読んでいただいても問題ないです!
解説については、スクリプトを変更しながら、その変更内容について補足していく形で行なっていきたいと思います。スクリプトの変更部分は太字で示していますが、違いが分かりにくい可能性もありますのでご注意ください。
各種パラメータを定義する
では、ブロック崩しゲームの作り方の解説を行なっていきます。
まずはブロック崩しゲームの画面等を作成するために必要になるパラメータをグローバル変数として設定していきたいと思います。
具体的には、スクリプトを下記のように作成します。
NUM_H_BLOCK = 10 # ブロッックの数(横方向)
NUM_V_BLOCK = 10 # ブロックの数(縦方向)
WIDTH_BLOCK = 40 # ブロックの幅
HEIGHT_BLOCK = 20 # ブロックの高さ
COLOR_BLOCK = "blue" # ブロックの色
HEIGHT_SPACE = 300 # 縦方向の空きスペース
WIDTH_PADDLE = 200 # パドルの幅
HEIGHT_PADDLE = 20 # パドルの高さ
Y_PADDLE = 50 # パドルの下方向からの位置
COLOR_PADDLE = "green" # パドルの色
RADIUS_BALL = 10 # ボールの半径
COLOR_BALL = "red" # ボールの色
NUM_BALL = 2 # ボールの数
UPDATE_TIME = 20 # 更新間隔(ms)
各種パラメータの意味合いはコメントにも書いていますが、図で各パラメータの意味合いを示すと下の図のようになります。
UPDATE_TIME
に関しては、ボールの移動や各種オブジェクトの位置に応じて画面を更新する時間間隔となります(単位は ms です)。
現状では、先程のスクリプトにおいて定義したパラメータは単なる数値ですが、上の図のような意味合いになるように、ここからスクリプトを作成していくことになります。
ウィジェットを作成する
続いてウィジェットを作成していきます。
今回使用するウィジェットは「メインウィンドウ」と「キャンバス」のみとなります。
このメインウィンドウとキャンバスの作成およびメインループの実行を行うために、スクリプトを下記のように変更します(太字部分が変更部分になります)。
import tkinter
NUM_H_BLOCK = 10 # ブロッックの数(横方向)
NUM_V_BLOCK = 10 # ブロックの数(縦方向)
WIDTH_BLOCK = 40 # ブロックの幅
HEIGHT_BLOCK = 20 # ブロックの高さ
COLOR_BLOCK = "blue" # ブロックの色
HEIGHT_SPACE = 300 # 縦方向の空きスペース
WIDTH_PADDLE = 200 # パドルの幅
HEIGHT_PADDLE = 20 # パドルの高さ
Y_PADDLE = 50 # パドルの下方向からの位置
COLOR_PADDLE = "green" # パドルの色
RADIUS_BALL = 10 # ボールの半径
COLOR_BALL = "red" # ボールの色
NUM_BALL = 2 # ボールの数
UPDATE_TIME = 20 # 更新間隔(ms)
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
def createWidgets(self):
'''必要なウィジェットを作成'''
# キャンバスを作成
self.canvas = tkinter.Canvas(
self.master,
width=self.width,
height=self.height,
highlightthickness=0,
bg="gray"
)
self.canvas.pack(padx=10, pady=10)
app = tkinter.Tk()
Breakout(app)
app.mainloop()
ここから tkinter
を利用していきますので、import tkinter
を実行するのを忘れないよう注意してください。
今回はブロック崩しゲームの全体を制御するクラスとして Breakout
を用意しています。
メインウィンドウを tkinter.Tk()
により作成し、そのメインウィンドウオブジェクトを Breakout
クラスに渡し、さらに Breakout
クラスの createWidgets
メソッドで tkinter.Canvas()
によりメインウィンドウ上にキャンバスを作成しています。
また、上記では Breakout
クラスのデータ属性を4つ用意しており、それぞれの意味合いは下記のようになります。
master
:ブロック崩しゲームの作成先となる親ウィジェットwidth
:キャンバスの幅height
:キャンバスの高さis_playing
:ゲーム開始しているかどうかを示すフラグ
ウィジェットを作成する上で必須なのは上側の3つのみですが、後から必要になるデータ属性も一緒に追加しています。
master
は今回の場合はメインウィンドウとなります。これを tkinter.Canvas()
の第1引数に指定することで、メインウィンドウ上にキャンバスを作成することができます。
また、width
と height
に関しては、ブロックの個数やブロックのサイズ、さらに空きスペースの高さから計算を行なっており、これらの値を tkinter.Canvas()
実行時にキャンバスのサイズとして指定するようにしています。
計算時に用いている各変数の意味合いについては 各種パラメータを定義する を参照していただければと思います。
さらに、is_playing
はゲーム開始中かどうかを示すフラグになります。
このフラグを利用することで、マウスがクリックされた際にゲームを開始したり、ゲーム開始後に再度クリックされた場合にゲームを停止したりするような処理を実現していきます(この is_playing
に関しては、後述の 定期的な図形の座標変更を行う から使用することになります)。
上記の変更により、スクリプトを実行すればアプリのメインウィンドウが表示され、その中に色が "gray"
のキャンバスが表示されるようになります。さらに、メインループが実行されアプリが待機状態となります。
ウィジェット作成に関する解説は以上となりますが、メインウィンドウに関しては下記ページで、
Tkinterの使い方:メインウィンドウを作成するキャンバスに関しては下記ページで、
Tkinterの使い方:キャンバスウィジェットの作り方メインループに関しては下記ページでそれぞれ解説しておりますので、必要に応じてご参照いただければと思います。
【Python】tkinterのmainloopについて解説スポンサーリンク
各種クラスを作成する
続いて、ボールとパドルとブロックを表現するためのクラスを作成していきたいと思います。
具体的には、下記のようにスクリプトを変更します。変更部分は太字で示しており、要は math
モジュールの import
と Ball
・Paddle
・Block
のクラスの追加のみを行なっています。
これでクラスの大枠は全て揃ったことになりますので、以降でスクリプトを変更する際は、変更を行うメソッド部分のみを示すようにしていきたいと思います(変更部分はこれまで通り、太字で示していきます)。
import tkinter
import math
NUM_H_BLOCK = 10 # ブロッックの数(横方向)
NUM_V_BLOCK = 10 # ブロックの数(縦方向)
WIDTH_BLOCK = 40 # ブロックの幅
HEIGHT_BLOCK = 20 # ブロックの高さ
COLOR_BLOCK = "blue" # ブロックの色
HEIGHT_SPACE = 300 # 縦方向の空きスペース
WIDTH_PADDLE = 200 # パドルの幅
HEIGHT_PADDLE = 20 # パドルの高さ
Y_PADDLE = 50 # パドルの下方向からの位置
COLOR_PADDLE = "green" # パドルの色
RADIUS_BALL = 10 # ボールの半径
COLOR_BALL = "red" # ボールの色
NUM_BALL = 2 # ボールの数
UPDATE_TIME = 20 # 更新間隔(ms)
class Ball:
def __init__(self, x, y, radius, x_min, y_min, x_max, y_max):
'''ボール作成'''
# 位置と半径と移動可能範囲を設定
self.x = x
self.y = y
self.r = radius
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
# 一度に移動する距離(px)
self.speed = 10
# 移動方向を設定
self.angle = math.radians(30)
self.dx = self.speed * math.cos(self.angle)
self.dy = self.speed * math.sin(self.angle)
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.r, self.y - self.r, self.x + self.r, self.y + self.r)
class Paddle:
def __init__(self, x, y, width, height, x_min, y_min, x_max, y_max):
'''パドル作成'''
# 中心座標と半径と移動可能範囲を設定
self.x = x
self.y = y
self.w = width
self.h = height
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.w / 2, self.y - self.h / 2, self.x + self.w / 2, self.y + self.h / 2)
class Block:
def __init__(self, x, y, width, height):
'''ブロック作成'''
# 中心座標とサイズを設定
self.x = x
self.y = y
self.w = width
self.h = height
def getCoords(self):
'''左上の座標と右下の座標の取得'''
return (self.x - self.w / 2, self.y - self.h / 2, self.x + self.w / 2, self.y + self.h / 2)
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
def createWidgets(self):
'''必要なウィジェットを作成'''
# キャンバスを作成
self.canvas = tkinter.Canvas(
self.master,
width=self.width,
height=self.height,
highlightthickness=0,
bg="gray"
)
self.canvas.pack(padx=10, pady=10)
app = tkinter.Tk()
Breakout(app)
app.mainloop()
上記の Ball
・Paddle
・Block
の各クラスでは、メソッドに関してはとりあえず表示に必要なもののみを用意しています。データ属性に関しては移動等を行うために必要なものも用意しています。
__init__
各種クラスの __init__
では下記の引数を受け取り、これらをデータ属性に設定するようにしています(単位は全てピクセルとなります)。
Ball
:x
・y
:ボールの初期位置の中心座標radius
:ボールの半径x_min
・x_max
・y_min
・y_max
:ボールの移動可能範囲
Paddle
:x
・y
:パドルの初期位置の中心座標width
・height
:パドルのサイズx_min
・x_max
・y_min
・y_max
:パドルの移動可能範囲
Block
:x
・y
:ブロックの位置の中心座標(ブロックは移動しない)width
・height
:ブロックのサイズ
特に Ball
に関しては、後述の ボールを移動できるようにする で、x_min
・x_max
・y_min
・y_max
で示す移動可能範囲を「壁の位置」として捉えてボールの反射を実現していきます(移動可能範囲からはみ出たボールを反射させる)。
getCoords
また、getCoords
メソッドは各種オブジェクトの左上座標と右下座標を返却するメソッドになります。
キャンバスで図形を描画する際には、これらの左上座標と右下座標が必要になるため、簡単にこれらの座標が取得できるように、この getCoords
メソッドを用意しています。
また、この getCoords
メソッドは当たり判定を行う際にも利用します。
移動量の設定
さらに、ボールは定期的かつ自動的に移動するので、Ball
クラスでは一度に移動する量をデータ属性 dx
と dy
で保持するようにしています。dx
が横方向の移動量(ピクセル数)、dy
が縦方向の移動量(ピクセル数)となります。
後述の ボールを移動できるようにする でボールを定期的に移動するようにしていきますが、この移動はボールの中心座標を表す x
と y
に、これらの dx
と dy
は足すことで実現していきます。
さらに、これらの dx
と dy
はボールの移動方向(角度)を表す angle
と、移動する際の距離 speed
から算出するようにしています。この算出は下記で行っています。
self.angle = math.radians(30)
self.dx = self.speed * math.cos(self.angle)
self.dy = self.speed * math.sin(self.angle)
ちょっと難しそうですが、angle
と speed
、さらに dx
と dy
の関係を図に表すと下記のようになります(キャンバスでは縦方向の正方向が下方向である点に注意)。
このように、cos
関数や sin
関数を利用することにより、角度と長さ(angle
と speed
)から横軸の長さや縦軸の長さを求めることができます。
この方法で dx
と dy
を求めるメリットは「角度に関わらず移動する距離が一定(その距離は speed
)」になる点です。ですので、どの方向に移動する際にもボールが同じ速度で移動させられることができます。
座標を扱うようなゲームやアプリを作成する際には結構使えるテクニックですので、上記のような式は覚えておいて損はないです!
また、cos
関数や sin
関数の引数には角度を「ラジアン」単位で指定する必要があります。「度」から「ラジアン」への単位変換は math.radians
で行えますので、これも一緒に覚えておくと良いと思います。
で、上記のように dx
と dy
を求めることができますので、ボールが壁や他のオブジェクトにぶつかった際に angle
を変更して再度 dx
と dy
を求め直せば、今度は他の方向にボールを移動させていくことができます。この角度変更については ボールを移動できるようにする で実装していきます。
ちなみにパドルはマウスカーソルの位置に合わせて移動するようにするため、上記のような自動で移動する際の移動量を示すデータ属性は不要になります
各種オブジェクトを作成する
とりあえず表示するためのメソッドは用意しましたので、次はボール・パドル・ブロックの各種オブジェクトを表示するようにしていきたいと思います。
まずは各種オブジェクトの作成を行います。
このオブジェクトの作成を行うために、Breakout
クラスに下記の createObjects
メソッドを追加します。
class Breakout:
def createObjects(self):
'''ゲームに登場するオブジェクトを作成'''
# ボールを作成
self.balls = []
for i in range(NUM_BALL):
x = self.width / NUM_BALL * i + self.width / NUM_BALL / 2
ball = Ball(
x, self.height // 2,
RADIUS_BALL,
RADIUS_BALL, RADIUS_BALL,
self.width - RADIUS_BALL, self.height - RADIUS_BALL
)
self.balls.append(ball)
# パドルを作成
self.paddle = Paddle(
self.width // 2, self.height - Y_PADDLE,
WIDTH_PADDLE, HEIGHT_PADDLE,
WIDTH_PADDLE // 2, self.height - Y_PADDLE,
self.width - WIDTH_PADDLE // 2, self.height - Y_PADDLE
)
# ブロックを作成
self.blocks = []
for v in range(NUM_V_BLOCK):
for h in range(NUM_H_BLOCK):
block = Block(
h * WIDTH_BLOCK + WIDTH_BLOCK // 2,
v * HEIGHT_BLOCK + HEIGHT_BLOCK // 2,
WIDTH_BLOCK,
HEIGHT_BLOCK
)
self.blocks.append(block)
さらに、Breakout
クラスの __init__
を下記のように変更し、アプリ起動時に createObjects
メソッドを実行するようにします。
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
self.createObjects()
createObjects()
メソッドについて簡単に説明しておくと、簡単に言ってしまえば各クラスのコンストラクタ実行によるオブジェクトの作成、および、そのオブジェクトのデータ属性のリストへの格納を行っています。
Ball
クラスと Block
クラスのオブジェクトに関しては複数作成しますので、データ属性にはリストを用意し、そのリストに格納していくことでオブジェクトをデータ属性で管理できるようにしています(Ball
は balls
、Block
は blocks
で管理)。
各クラスのコンストラクタに指定する引数が若干複雑ですが、指定する引数の意味は 各種クラスを作成する で追加した各クラスの __init__
を参照していただければ理解できるのではないかと思います。
まだオブジェクトを生成するだけなので実感はないと思いますが、これらのコンストラクタに指定する引数に応じて各オブジェクトの初期位置や表示する際のサイズが決定されます(さらにはボールやパドルに関しては移動可能な範囲も)。
例えば Ball()
実行時の引数の意味合いを図示すると下の図のようになります。
特に Block
に関しては横方向と縦方向に並べる感じで配置していく必要があるため、第1引数と第2引数はお互いが重ならないように調整しながら指定する必要があります。
また Paddle
に関しては、縦方向には移動しないことを前提としていますので、y_min
と y_max
両方に self.height - Y_PADDLE
を指定しています。逆にこれらに異なる値を設定すれば、パドルも縦方向に移動するようになります。
各種図形を描画する
オブジェクトの生成ができたので、次はそのオブジェクトを図形としてキャンバスに描画して画面に表示していきたいと思います。
ゲーム起動時の図形の描画
まず、ゲーム起動時(スクリプト実行直後)に各種オブジェクトを図形として描画するようにしていきます。
このためには、まず Breakout
クラスに下記の drawFigures
メソッドを追加します。
class Breakout:
def drawFigures(self):
'''各オブジェクト毎に図形を描画'''
# オブジェクト図形を関連づける辞書
self.figs = {}
# ボールを描画
for ball in self.balls:
x1, y1, x2, y2 = ball.getCoords()
figure = self.canvas.create_oval(
x1, y1, x2, y2,
fill=COLOR_BALL
)
self.figs[ball] = figure
# パドルを描画
x1, y1, x2, y2 = self.paddle.getCoords()
figure = self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=COLOR_PADDLE
)
self.figs[self.paddle] = figure
# ブロックを描画
for block in self.blocks:
x1, y1, x2, y2 = block.getCoords()
figure = self.canvas.create_rectangle(
x1, y1, x2, y2,
fill=COLOR_BLOCK
)
self.figs[block] = figure
さらに上記の drawFigures
メソッドがゲーム起動時に実行されるよう、Breakout
クラスの __init__
を下記のように変更します。
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
self.createObjects()
self.drawFigures()
以上の変更を行うことで、createObjects
メソッドで作成したオブジェクトが全て、図形としてキャンバスに描画されるようになります(意図していませんでしたが顔っぽく見えますね…)。
簡単に drawFigures
メソッドで行っている処理について説明しておきます。
まずボールは「円」、さらにパドルとブロックは「長方形」としてキャンバスに描画を行うようにしています。
そのため、ボールを図形として描画する際には、create_oval
メソッドを、パドルとブロックを描画する際には create_rectangle
メソッドを実行するようにしています。
これらのキャンバスに図形を描画するメソッドに関しては下記ページで解説していますので、描画可能な図形の種類や各オプションの意味合い等を知りたい方はこちらを参照していただければと思います。
Tkinterの使い方:Canvasクラスで図形を描画する各種オブジェクトを作成する で作成した createObjects
メソッドでは、各種オブジェクトの生成時に初期位置やサイズの設定を行いました。
さらに各種オブジェクトは Breakout
クラスの下記のデータ属性により管理されるようになっています。
- ボール:
balls
(リスト) - パドル:
paddle
- ブロック:
blocks
(リスト)
また、上記ページでも解説していますが、create_oval
メソッドと create_rectangle
メソッド共に、第1引数と第2引数には描画する図形の左上座標を、第3引数と第4引数には描画する図形の右下座標を指定する必要があります。
これらは Ball
・Paddle
・Block
クラスそれぞれに用意した getCoords
メソッドにより取得することが可能です。
したがって、ボール等のオブジェクトを図形として描画するためには、これらのオブジェクトに getCoords
メソッドを実行させて必要な座標を取得し、さらに create_oval
メソッドや create_rectangle
メソッドの第1引数から第4引数には getCoords
メソッドで取得した座標をそのまま指定して実行してやれば良いことになります。
で、この処理を管理しているオブジェクト全てに対して行なっているのが drawFigures
メソッドとなります。
図形 ID の辞書での記憶
もう一点付け加えておくと、drawFigures
メソッドでは create_oval
メソッドや create_rectangle
メソッドの返却値である図形 ID と描画したオブジェクトとを関連づけて辞書 figs
に保存するようにしています。
図形の削除や移動を行う際には図形 ID が必要になりますので、このように辞書でオブジェクトから図形 ID を取得できるようにしておくと便利です。
例えば、ボールと当たったブロックのオブジェクト block
をキャンバスから削除する場合、辞書 figs
から下記のように図形 ID を取得して図形の削除を行うことが可能です。
# 図形IDを取得してキャンバスから削除
figure = self.figs[block]
self.canvas.delete(figure)
スポンサーリンク
定期的な図形の座標変更を行う
続いて、描画した図形の座標を定期的に更新していくようにしていきたいと思います。
図形の座標変更
先程の drawFigures
の実行により、ゲーム起動時にボールやパドル、ブロックがキャンバスに描画されて画面上に表示されるようになりました。
ただ、現状では図形の描画が起動時に一回実行されるだけなので、ボールやパドルの座標が変化したとしても画面が変わりません。
次は、定期的にボールやパドルの座標を変化させるようにしていきたいと思います。
これを行うために、まず下記の updateFigures
メソッドを Breakout
クラスに追加します。
class Breakout:
def updateFigures(self):
'''新しい座標に図形を移動'''
# ボールの座標を変更
for ball in self.balls:
x1, y1, x2, y2 = ball.getCoords()
figure = self.figs[ball]
self.canvas.coords(figure, x1, y1, x2, y2)
# パドルの座標を変更
x1, y1, x2, y2 = self.paddle.getCoords()
figure = self.figs[self.paddle]
self.canvas.coords(figure, x1, y1, x2, y2)
この updateFigures
メソッドでは、ボールとパドルそれぞれのオブジェクトに対して現在の座標と図形 ID を取得し、さらにそれらの情報を引数としてキャンバスの coords
メソッドを実行しています。
coords
メソッドは、第1引数で指定した図形 ID に対応する図形の座標を第2引数以降で指定した座標に変更するメソッドになります。
したがって、ボールやパドルのオブジェクトの位置(データ属性の x
や y
)が変化した後に上記 updateFigures
が実行されれば、そのオブジェクトの位置に応じて図形の位置も移動することになります。
一点補足しておくと、上の図の通り coords
メソッドは図形のサイズも変更可能ですが、今回は位置のみの変更を行います。位置の変更のみであれば coords
メソッドではなく、move
メソッドや moveto
メソッドでも実現する事が可能です。
これらの描画済みの図形を操作するメソッドについては下記ページでまとめていますので、詳しく知りたい方は下記ページを参照していただければと思います。
Tkinterの使い方:Canvasクラスで描画した図形を操作する定期的な図形の座標変更の実行
続いて、先ほど作成した updateFigures
が定期的に実行されるようにしていきたいと思います。そのために、下記の loop
メソッドを Breakout
クラスに追加します。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
self.updateFigures()
loop
メソッドでは after
メソッドを実行しており、これにより UPDATE_TIME
間隔で loop
が繰り返し実行されることになります。そしてこの loop
メソッドの中で updateFigures
を実行しているため、ほぼ UPDATE_TIME
間隔で各オブジェクトの図形の座標変更が繰り返し行われることになります。
したがって、オブジェクトの座標(データ属性 x
と y
)が変化すれば、その後に実行される updateFigures
メソッドにより自動的に図形の座標(位置)も変化するようになります。
この after
メソッドについては下記ページで解説していますので、詳しく知りたい方はこちらをご参照いただければと思います。
定期的な処理の開始
先ほど追加した loop
メソッドは、前述の通り定期的に実行されるメソッドになります。そのため、ブロック崩しゲームにおいて定期的に必要になる処理はこの loop
メソッド内で行わせるようにしていこうと思います。例えばボールの移動も、この定期的に必要になる処理となります。
したがって、この loop
メソッドが実行された際には、ボールが移動を始め、ブロック崩しゲームが開始されることになります(ボールの移動自体は後から実装します)。
ですので、Breakout
クラスの __init__
からこの loop
メソッドを実行した場合、ゲーム起動時にすぐにゲームが始まることになります。ただそれだと急すぎてユーザーがびっくりするかなぁと思いますので、今回はマウスのボタンがクリックされた時にゲームが始まるようにしたいと思います。
すなわち、マウスのボタンがクリックされた際に loop
メソッドを実行するようにします。
このために、まず下記の setEvents
メソッドを Breakout
クラスに追加します。
class Breakout:
def setEvents(self):
'''イベント受付設定'''
self.canvas.bind("<ButtonPress>", self.start)
これにより、setEvents
メソッド実行以降は、マウスのボタンがクリックされた時に Breakout
クラスの start
メソッドが実行されるようになります。
ここで使用している bind
メソッドは下記ページで解説していますので、詳しく知りたい方は下記ページをご参照ください。
さらに、この setEvents
メソッドを実行するように Breakout
クラスの __init__
を下記のように変更します。
class Breakout:
def __init__(self, master):
'''ブロック崩しゲーム起動'''
self.master = master
# サイズを設定
self.width = NUM_H_BLOCK * WIDTH_BLOCK
self.height = NUM_V_BLOCK * HEIGHT_BLOCK + HEIGHT_SPACE
# ゲーム開始フラグを設定
self.is_playing = False
self.createWidgets()
self.createObjects()
self.drawFigures()
self.setEvents()
これでゲーム起動後にマウスクリックを実行すれば Breakout
クラスの start
メソッドが実行されるようになったことになります。
ただ、まだ肝心の start
メソッドを作成していませんので、ここで下記の start
メソッドを Breakout
クラスに追加します。
class Breakout:
def start(self, event):
'''ゲーム開始'''
# ゲーム開始していない場合はゲーム開始
if not self.is_playing:
self.is_playing = True
self.loop()
else:
self.is_playing = False
この start
メソッドで使用しているデータ属性 is_playing
は ウィジェットを作成する で追加した「ゲーム開始したかどうか」を判断するためのフラグになります。初期値は False
です。
したがって、ゲーム起動後のマウスクリックにより上記の start
メソッドが実行された際には、is_playing
を True
にした後に loop
メソッドが実行され、ゲームが開始されることになります(定期的な図形の座標変更を開始する)。
また上記 start
メソッドでは、is_playing
が True
の場合に False
に設定し直す処理も行っています。これを利用すれば、ゲームを一時停止するようなことも可能です。
具体的には、Breakout
クラスの loop
メソッドを下記のように変更すれば、ゲームを一時停止することができるようになります。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
# ゲーム開始していないなら何もしない
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
self.updateFigures()
上記の変更により、is_playing
が False
の場合には、after
メソッドが実行されないようになります。したがって、それ以降は loop
メソッドの定期的な実行が行われなくなります。
ただし、loop
メソッドの定期的な実行が停止している間でもマウスクリックにより start
メソッドが実行されますので、これにより再びゲームを再開することができます(is_playing
が False
の時に start
メソッドが実行されれば、is_playing
が True
に設定された後に loop
メソッドが実行される)。
要はマウスのクリックでゲーム開始とゲームの停止、ゲームの再開を行えるようになったことになります。
ただ、現状だとボールやパドルが移動しないため、ここまでの変更を行なったスクリプトを実行しても何も動作せず、定期的に図形の描画が行われていることや、ゲームの開始や停止等が行えるようになったことが実感できません。
早くこれらを実感したいという方は、Breakout
クラスの updateFigures
メソッドを下記のように変更してみてください。
class Breakout:
def updateFigures(self):
'''新しい座標に図形を移動'''
# ボールの座標を変更
for ball in self.balls:
ball.x += 1
ball.y += 1
x1, y1, x2, y2 = ball.getCoords()
figure = self.figs[ball]
self.canvas.coords(figure, x1, y1, x2, y2)
# パドルの座標を変更
x1, y1, x2, y2 = self.paddle.getCoords()
figure = self.figs[self.paddle]
self.canvas.coords(figure, x1, y1, x2, y2)
スクリプト実行後にマウスをクリックすれば、ボールが右下方向に移動し始めることが確認できると思います。また、ボールが移動している最中にマウスをクリックすればボールの移動が停止すること、さらに再度クリックすれば、ボールが再び動き出すことが確認できると思います。
これらが確認できれば、ここまでのスクリプトの変更をうまく反映できており、ブロック崩しゲームを作成していく上での基盤が出来上がったことになります。
ただし、上記の変更はあくまでも「お試し」の変更ですので、確認ができれば追加した2行は削除しておいてください。次の ボールを移動できるようにする で正式にボールの移動を実現していきます。
ボールを移動できるようにする
ここまでは単にボール・パドル・ブロックの表示を行うだけでしたので、あまりゲームっぽさがなかったと思います。ですが、ここからの変更によりオブジェクトに動きが出てきてゲーム作成っぽさが出てくると思います!
まずは、ボールを移動するようにしていきます。
ボールの移動
まずは反射を考えずに単に一方向にボールを移動させるようにしていきたいと思います。
これを行うために、まず Ball
クラスに下記の move
メソッドを追加します。
class Ball:
def move(self):
'''移動'''
# 移動方向に移動
self.x += self.dx
self.y += self.dy
さらに、Ball
クラスの全オブジェクトに対して上記の move
メソッドを実行させるよう、Breakout
クラスの loop
メソッドを次のように変更します。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
# 全ボールを移動する
for ball in self.balls:
ball.move()
self.updateFigures()
上記のように変更を行ってスクリプトを実行し、さらにマウスをクリックしてゲームを開始すれば、ボールが移動するようになったことを確認できると思います。ただすぐにボールが画面外に消えてしまうとは思いますが…。
Ball
クラスの move
メソッドではデータ属性 x
と y
の変更を行なっています。この x
と y
はオブジェクトの存在する位置の中心座標を示すデータ属性であり、この座標に基づいてオブジェクトを図形として表示する座標が決まります。
さらに、上記の loop
メソッドは、前述の 定期的な図形の座標変更を行う で追加したメソッドで、after
メソッドにより UPDATE_TIME
間隔で実行されます(UPDATE_TIME
は 各種パラメータを定義する で導入したグローバル変数です)。
この loop
の中で Ball
クラスの move
メソッドと updateFigures
メソッドが実行されるため、ボールの中心座標の変更と、その中心座標に基づいた図形の座標の変更が定期的に行われることになります。そのため、ボールの図形が自動的にどんどん移動してことになります。
壁と当たった時の反射
ただし、まだ反射を行なっていないため、ボールが常に一方向にしか移動せず、すぐに画面外に出てしまって見えなくなってしまいます。
これを防ぐため、次はボールの反射を実現していきたいと思います。
これを実現するため、まずは Ball
クラスに下記の turn
メソッド・reflectH
メソッド・reflectV
メソッドを追加します。
class Ball:
def turn(self, angle):
'''移動方向をangleに応じて設定'''
self.angle = angle
self.dx = self.speed * math.cos(self.angle)
self.dy = self.speed * math.sin(self.angle)
def reflectH(self):
'''横方向に対して反射'''
self.turn(math.atan2(self.dy, -self.dx))
def reflectV(self):
'''縦方向に対して反射'''
self.turn(math.atan2(-self.dy, self.dx))
さらに、前述で追加した Ball
クラスの move
メソッドを下記のように変更します。
class Ball:
def move(self):
'''移動'''
# 移動方向に移動
self.x += self.dx
self.y += self.dy
if self.x < self.x_min:
# 左の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_min
elif self.x > self.x_max:
# 右の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_max
if self.y < self.y_min:
# 上の壁とぶつかった
# 縦方向に反射
self.reflectV()
self.y = self.y_min
elif self.y > self.y_max:
# 下の壁とぶつかった
# 縦方向に反射
self.reflectV()
self.y = self.y_max
上記のように変更を加えた後にスクリプトを実行してマウスをクリックすれば、ボールが移動し、キャンバスの端まで移動した際に反射するようになったことが確認できると思います(ただ、まだ当たり判定や当たった時の処理を実装していないためブロックやパドルの後ろ側をすり抜けてしまいます)。
なぜ上記の変更でボールが反射するようになったかを簡単に説明しておきます。
まず turn
は移動方向を変化させるメソッドになります。
具体的には、引数で指定された angle
に応じてデータ属性 dx
と dy
を設定し直すメソッドになります。dx
と dy
に関しては 各種クラスを作成する で紹介した通り、ボールの横方向と縦方向の移動量を示すデータ属性となります。
キャンバスでは y
軸の正方向が下方向であり、数学等で扱う座標とは逆なので注意してください
y
軸の正方向が下方向であるため、キャンバスでの角度の正方向は時計回り方向になりますので、この点にも注意してください(数学で扱う角度の正方向は “反” 時計回り)
dx
と dy
の求め方についても 各種クラスを作成する で解説していますので、忘れてしまった方は 各種クラスを作成する を参照していただければと思います。
また、reflectH
と reflectV
はそれぞれ、ボールを横方向に反射させるメソッドとボールを縦方向に反射させるメソッドになります。
で、このメソッドで実行しているのが先ほど紹介した turn
メソッドになります。turn
メソッドには math.atan2
関数の返却値を指定しています。
この math.atan2
は引数で指定された座標から角度を求める関数になります。
例えば math.atan2(y, x)
を実行すれば、原点と (x
, y
) 座標を結ぶ直線が横軸と正方向に成す角の角度を求めることができます。
したがって、math.atan2(dy, -dx)
を実行した場合、math.atan2(dy, dx)
に比べて横方向に反射させた場合の角度を求めることができます。
データ属性 dx
と dy
は現在の移動方向に対する移動量になりますので、math.atan2(dy, -dx)
を実行すれば、現在の移動方向から横方向に反射した時の移動方向(角度)を算出することができることになります。
さらに math.atan2(dy, -dx)
の結果に対して turn
メソッドを実行すれば、反射後の移動方向に応じて dx
と dy
が再設定されることになり、それ以降は反射後の移動方向にボールを移動出せていくことができます。
上記の考え方で横方向の反射を実現するために、reflectH
では引数 math.atan2(dy, -dx)
を指定して turn
メソッドを実行しています。
reflectV
も同様で、math.atan2(-dy, dx)
を実行して現在の移動方向から縦方向に反射した時の移動方向(角度)を算出し、
さらに math.atan2(-dy, dx)
の結果を turn
メソッドに指定して実行することで、縦方向の反射を実現しています。
上記で行なっているのは、入射角と反射角が等しくなるような反射になります
この場合、実はわざわざ math.atan2
や turn
メソッドを実行しなくても、横方向に反射させるためには dx = - dx
を、縦方向に反射させるためには dy = - dy
を実行するだけでも実現可能です
ただ、後述の ブロック崩しゲームのカスタマイズ の 反射角度にランダム性を持たせる で紹介するような、反射角度にランダム性を持たせるなどの “移動方向や反射角度に対する拡張” を行うことを考慮すれば、このように角度から dx
や dy
を算出するようにしておいた方が便利です
reflectH
と reflectV
を追加したことで、これらのメソッドを実行するだけでボールの反射を実現することができるようになりました。
あとは、移動したボールが壁とぶつかってしまった場合に(移動可能範囲の外に出てしまった場合に)、どの方向の壁とぶつかったのかを考慮して reflectH
と reflectV
を呼び分けるようにしてやれば、ボールの反射を実現することができます。
で、これを行なっているのが上記の move
メソッドになります。壁の位置は移動可能範囲としてデータ属性 x_min
、x_max
、y_min
、y_max
に設定されていますので、これらの値とデータ属性 x
と y
との大小関係からどの方向の壁とボールがぶつかったを判断し、それに応じて reflectH
と reflectV
を呼び分けるようにしています。
下側の壁とぶつかった場合、普通はブロック崩しゲームではゲームオーバーになるのですが、とりあえず現状は下の壁とぶつかった場合もボールを反射させるようにしています。現状だとパドルに当たってもすり抜けるので、すぐに下の壁とぶつかってしまいますからね…。
パドルを移動できるようにする
ボールが移動できるようになったので、次はパドルを移動させるようにしていきたいと思います。
パドルはボールと異なり、マウスカーソルの位置に応じて移動するようにしていきます。
パドルの位置の変更
まずはパドルの位置を変更できるように Paddle
クラスに下記の move
メソッドを追加します。
class Paddle:
def move(self, mouse_x, mouse_y):
'''(mouse_x, mouse_y) に移動'''
# 移動可能範囲で移動
self.x = min(max(mouse_x, self.x_min), self.x_max)
self.y = min(max(mouse_y, self.y_min), self.y_max)
パドルはボールと違って反射などはしませんので、単純に x_min
・x_max
・y_min
・y_max
から求まる移動範囲内に収まるように mouse_x
と mouse_y
の値を丸め、それを x
と y
に設定するようにしています。
あとは mouse_x
と mouse_y
にマウスカーソルの座標を指定して move
が実行されれば、パドルの位置がマウスカーソルの位置に応じて移動することになります(そして移動後に updateFigures
が実行されることで、移動後の位置にパドルの図形も移動する)
イベントの受付設定
ということで、次は先程作成した Paddle
クラスの move
メソッドがマウスカーソル移動時に実行されるようにしていきたいと思います。
マウスのクリック同様に、こういったマウスカーソルの移動に関してもイベントとして扱うことができ、このイベントが発生した時に実行される関数やメソッドを bind
メソッドで設定することが可能です。
具体的には、下記のように Breakout
クラスの setEvents
メソッドを実行すれば、マウスカーソル移動時に Breakout
クラスの motion
メソッドが実行されるように設定することができます。
class Breakout:
def setEvents(self):
'''イベント受付設定'''
self.canvas.bind("<ButtonPress>", self.start)
self.canvas.bind("<Motion>", self.motion)
さらに、Breakout
クラスに下記の motion
メソッドを追加します。
class Breakout:
def motion(self, event):
'''パドルの移動'''
self.paddle.move(event.x, event.y)
bind
メソッドで実行されるように設定した関数やメソッドに関しては、実行時にイベントの詳細情報 event
が引数として渡されることになります。
この event.x
と event.y
にはマウスカーソルの座標が格納されていますので、これを Paddle
クラスの move
メソッドの引数に指定してやれば、マウスカーソルの位置に応じてパドルを移動させることができることになります。
上記のように変更を加えたスクリプトを実行してマウスをクリックしてからマウスカーソルを移動すれば、パドルがマウスカーソルを追う形で横方向に移動することが確認できると思います。
横方向にしか移動しない理由は Paddle
クラスのコンストラクタ実行時に y_min
と y_max
に同じ値を設定しているからで、これらの値に異なる値を設定してやれば、パドルを縦方向に移動させることも可能になります。
スポンサーリンク
ボールと当たったブロックを消す
ちょっとずつブロック崩しゲームを作っている感じが出てきたのではないでしょうか?
次は、ボール移動後にボールとブロックとの当たり判定を行い、さらに当たったブロックを消すようにしていきます。
もし、ボール移動後に複数のブロックと当たっていた場合は、今回は「一番大きく当たったブロック」のみ当たったと判定し、そのブロックのみを消すようにしていきたいと思います。これはブロックと当たった時のボールの反射角度を求める処理を簡略化するためです。
ボールと他のオブジェクトとが当たった領域を求める
まずは、ボールが他のオブジェクトと当たったかどうかを判定できるようにしていきたいと思います。
今回は簡単のため、ボールも矩形とみなし、各オブジェクトが当たったかどうかは矩形同士が重なったかどうかで判断を行うようにしていきたいと思います。
矩形同士が当たったかどうかを判定するために、まず Ball
クラスに下記の getCollisionCoords
メソッドを追加します。
class Ball:
def getCollisionCoords(self, object):
'''objectと当たった領域の座標の取得'''
# 各オブジェクトの座標を取得
ball_x1, ball_y1, ball_x2, ball_y2 = self.getCoords()
object_x1, object_y1, object_x2, object_y2 = object.getCoords()
# 新たな矩形の座標を取得
x1 = max(ball_x1, object_x1)
y1 = max(ball_y1, object_y1)
x2 = min(ball_x2, object_x2)
y2 = min(ball_y2, object_y2)
if x1 < x2 and y1 < y2:
# 始点が終点よりも左上にある
# 当たった領域の左上座標と右上座標を返却
return (x1, y1, x2, y2)
else:
# 当たっていないならNoneを返却
return None
この getCollisionCoords
メソッドは、実行した Ball
クラスのオブジェクトと引数の object
で指定されるオブジェクトとで当たった領域(重なっている領域)の座標を返却します。もし当たっていない場合は None
を返却します。
ですので、getCollisionCoords
メソッドの返却値が None
以外の場合、2つのオブジェクトは当たったと判定することができます。
また、2つのオブジェクトが当たった場合は「当たった領域の座標」が返却されますので、複数のブロックと当たった時に、どのブロックと一番大きく当たったかを getCollisionCoords
メソッドの返却座標から判断をすることができます。
getCollisionCoords
メソッドで何をやっているかというと、まず前提として、self
と object
が当たっているかどうかは、self
と object
それぞれの左上座標と右下座標から下記によって求まる新たな矩形の始点と終点の位置関係から求めることができます(ここからは、getCollisionCoords
メソッドを実行したオブジェクトを self
として説明させていただきます)。
- 始点の
x
座標:下記の2つの座標の大きい方self
の左上座標のx
座標object
の左上座標のx
座標
- 始点の
y
座標:下記の2つの座標の大きい方self
の左上座標のy
座標object
の左上座標のy
座標
- 終点の
x
座標:下記の2つの座標の小さい方self
の右下座標のx
座標object
の右下座標のx
座標
- 終点の
y
座標:下記の2つの座標の小さい方self
の右下座標のy
座標object
の右下座標のy
座標
上記で求まる矩形の始点と終点において、始点が終点よりも左上側に存在する場合、self
と object
は当たったと判定することができます。そして、この時の始点と終点が、self
と object
とで当たった領域の左上座標と右下座標になります。
逆に始点が終点よりも左上側に存在しない場合、self
と object
は当たっていないと判定することができます。
getCollisionCoords
メソッドは上記の考え方に基づいて処理を行なっており、self
と object
それぞれの左上座標と右下座標を getCoords
メソッドにより取得し、これらの座標から上記で示した方法で新たな矩形の始点(x1
, y1
)と終点(x2
, y2
)を求め、さらに、始点が終点よりも左上に存在するかどうかを if x1 < x2 and y1 < y2
が成立するかどうかで判断しています。
成立する場合は、self
と object
が当たったとみなして (x1, y1, x2, y2)
を返却し、それ以外は None
を返却するようにしています。
オブジェクトを消す
Ball
クラスの getCollisionCoords
メソッドにより、ボールと他のオブジェクトが当たったかどうかを判定するための機能の実装はできましたので、次は当たったブロックを消すための機能を追加していきたいと思います。
このために、Breakout
クラスに下記の delete
メソッドを追加します。
class Breakout:
def delete(self, target):
'''targetのオブジェクトと図形を削除'''
# 図形IDを取得してキャンバスから削除
figure = self.figs.pop(target)
self.canvas.delete(figure)
# targetを管理リストから削除
if isinstance(target, Ball):
self.balls.remove(target)
elif isinstance(target, Block):
self.blocks.remove(target)
この delete
メソッドでは、主に下記の3つのことを行なっています(figs
は 各種図形を描画する で作成した “図形 ID をオブジェクトに関連づけて管理している辞書” です)。
figs
からのtarget
の削除target
に対応する図形の削除- オブジェクト管理リストからの
target
の削除- (オブジェクト管理リスト:
self.blocks
orself.balls
)
- (オブジェクト管理リスト:
要は、この delete
メソッドで target
の図形および、辞書やリストで管理していた target
の情報全てを削除しています。
ですので、当たったと判定されたブロックのオブジェクトを引数に指定して delete
メソッドを実行することで、キャンバスからも削除されますし、以降、そのオブジェクトに対して処理が実行されるようなことは無くなります。
ボールとブロックの当たり判定を実行する
ここまでの変更で、getCollisionCoords
メソッドにより当たったかどうかを判定することができるようになり、delete
メソッドによりオブジェクトを削除することができるようになりました。
あとはこれらのメソッドを実行するようにして、ボールがブロックと当たった時にブロックを消すようにしていきたいと思います。
このために、まずは Breakout
クラスに下記の collision
メソッドを追加します。
class Breakout:
def collision(self):
'''当たり判定と当たった時の処理'''
for ball in self.balls:
collided_block = None # 一番大きく当たったブロック
max_area = 0 # 一番大きな当たった領域
for block in self.blocks:
# ballとblockとの当たった領域の座標を取得
collision_rect = ball.getCollisionCoords(block)
if collision_rect is not None:
# 当たった場合
# 当たった領域の面積を計算
x1, y1, x2, y2 = collision_rect
area = (x2 - x1) * (y2 - y1)
# 一番大きく当たっているかどうかを判断
if area > max_area:
# 一番大きく当たった領域の座標を覚えておく
max_area = area
# 一番大きく当たったブロックを覚えておく
collided_block = block
if collided_block is not None:
# 一番大きく当たったブロックを削除
self.delete(collided_block)
ちょっと長いですが、やってることは、データ属性 balls
で保持しているボール1つ1つに対し、self.blocks
で保持しているブロックの中で一番大きく当たっているブロックを見つけ出し、その一番大きく当たっているブロックを delete
メソッドで削除しているだけです。
で、一番大きく当たっているブロックを見つけ出すためには “ボールとブロックの当たった領域の座標” が必要になるため、そこに前述で作成した Ball
クラスの getCollisionCoords
メソッドを利用しています。そして、得られた座標より当たった領域の面積が一番大きいブロックを見つけ出すようにしています。
後は、上記で用意した Breakout
クラスの collision
メソッドを定期的に実行するよう Breakout
クラスの loop
メソッドを次のように変更すれば、ボールと当たったブロックを消す処理は完成です。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
# ゲーム開始していないなら何もしない
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
# 全ボールを移動する
for ball in self.balls:
ball.move()
self.collision()
self.updateFigures()
これにより、ボールが移動した後に当たり判定が行われ、当たった際にはブロックが消えるようになります。
実際にスクリプトを実行してゲームを開始すれば、ボールと当たったブロックがどんどん消えていくことが確認できると思います。
ただ、まだブロックと当たったボールを反射させるようにしていないため、どんどんブロックを突き抜けていく感じになっています。次は、ブロックなどの他のオブジェクトとボールが当たった際に、ボールを反射させるようにしていきます。
他のオブジェクトと当たったボールを反射させる
では、ボールが他のオブジェクトと当たった時にボールを反射させるようにしていきたいと思います。
ブロックと当たったボールを反射させる
先ほど追加した Breakout
クラスの collision
メソッドにより、既にブロックとボールが当たったかどうかを判定することができるようになっています。ですので、まずはブロックと当たったボールを反射させるようにしていきたいと思います。
このためには、まず Ball
クラスに下記の reflect
を追加します。
class Ball:
def reflect(self, object):
'''当たった方向に応じて反射'''
# 各オブジェクトの座標を取得
object_x1, object_y1, object_x2, object_y2 = object.getCoords()
# 重なった領域の座標を取得
x1, y1, x2, y2 = self.getCollisionCoords(object)
is_collideV = False
is_collideH = False
# どの方向からボールが当たったかを判断
if self.dx < 0:
# ボールが左方向に移動中
if x2 == object_x2:
# objectの左側と当たった
is_collideH = True
else:
# ボールが右方向に移動中
if x1 == object_x1:
# objectの右側と当たった
is_collideH = True
if self.dy < 0:
# ボールが上方向に移動中
if y2 == object_y2:
# objectの下側と当たった
is_collideV = True
else:
# ボールが下方向に移動中
if y1 == object_y1:
# objectの上側と当たった
is_collideV = True
if is_collideV and is_collideH:
# 横方向と縦方向両方から当たった場合
if x2 - x1 > y2 - y1:
# 横方向の方が重なりが大きいので横方向に反射
self.reflectV()
elif x2 - x1 < y2 - y1:
# 縦方向の方が重なりが大きいので縦方向に反射
self.reflectH()
else:
# 両方同じなので両方向に反射
self.reflectH()
self.reflectV()
elif is_collideV:
# 縦方向のみ当たったので縦方向に反射
self.reflectV()
elif is_collideH:
# 横方向のみ当たったので横方向に反射
self.reflectH()
上記の reflect
メソッドは、メソッドを実行したボールを引数 object
で指定されるオブジェクトに対して反射させるメソッドになります(ここからはメソッドを実行したボールのオブジェクトを ball
と呼ばせていただきます)。
また、上記で実行している reflectH
と reflectV
は ボールを移動できるようにする で追加したメソッドで、それぞれ ball
を横方向に反射する、ball
を縦方向に反射するメソッドとなります。
reflect
メソッドでは、ball
が object
のどこと当たったかを確認し、ball
が object
の左もしくは右と当たった場合は ball
に reflectH
を実行させ、ball
が object
の上もしくは下と当たった場合は ball
に reflectV
を実行させることでボールの反射を実現しています。
例えば、ball
の dx
が正の値の場合、ball
は右方向に対して移動していることになります。したがって、ball
が当たるとしたら object
の左側になります。
実際に当たっているかどうかは、object
の左座標である object_x1
と、当たった領域の左座標である x1
が同じであるかどうかで判断することができ、同じである場合、ball
が object
の左と当たったと判定することができます。
他の例で、例えば ball
の dy
が負の値の場合、ball
は上方向に対して移動していることになります。したがって、ball
がぶつかるとしたら object
の下側になります。
実際に当たっているかどうかは、object
の下座標である object_y2
と、当たった領域の下座標である y2
が同じであるかどうかで判断することができ、同じである場合、ball
が object
の下と当たったと判定することができます。
基本はこんな感じで ball
が object
のどこと当たったかを判定しているのですが、ball
が object
の2方向に対して当たっている場合があります。
このような場合は、当たった領域の幅と高さを比較し、幅の方が大きい場合は reflectH
を実行して横方向に反射するようにしています。それ以外は reflectV
を実行して縦方向に反射するようにしています。
この辺りの判定と反射を is_collideV
と is_collideH
を利用しながら処理しているのが、上記の reflect
メソッドとなります。
一点補足しておくと、dx
や dy
が大きい場合、下の図のようにボールがブロックの辺と当たる前にブロックの中に入り込んでしまうことがあります。上記の reflect
メソッドは、ブロックの辺とボールが当たった時の反射を行うものであり、ボールがブロックの中に入り込んでしまうと反射がうまくいきません。
dx
や dy
はデータ属性 speed
によって大きさが変化しますので、この speed
を大きく設定すると上手く動作しない可能性が大きいです(ボールの半径よりも小さくしておくことをおススメします)。そもそも speed
が大きいと当たり判定もすり抜けてしまうので、もうちょっといろいろ考えて当たり判定なども作り直す必要があります。
話を戻しまして、上記の reflect
メソッドが用意できれば、あとはボールが他のオブジェクトに当たった時に reflect
メソッドを実行するようにすれば、他のオブジェクトに当たった際のボールの反射を実現することができます。
このためには、下記のように Breakout
クラスの collision
メソッドを、当たったブロックを消す前に reflect
メソッドを実行するように変更してやれば良いです。
class Breakout:
def collision(self):
'''当たり判定と当たった時の処理'''
for ball in self.balls:
# 略
if collided_block is not None:
# 一番大きく当たったブロックに対してボールを反射
ball.reflect(collided_block)
# 一番大きく当たったブロックを削除
self.delete(collided_block)
ここまでの変更を加えてスクリプトを実行してマウスクリックしてゲームを開始すれば、ボールがブロックに当たった時にブロックが消え、さらにボールが反射していく様子が確認できると思います。
他のボールやパドルと当たったボールを反射させる
先程実装したのはブロックと当たった時にボールを反射させる処理になります。なので、現状ではまだ他のボールやパドルと当たった時にはボールがすり抜けてしまいます。
次は他のボールやパドルと当たった時にもボールが反射するようにしていきたいと思います。
ただ、やり方はブロックと当たった時と同じで、getCollisionCoords
メソッドにより当たったかどうかを判定し、当たった時に reflect
メソッドを実行させてボールを反射させれば良いだけです(ただし、ボールやパドルは当たっても消す必要はありません)。
具体的には、Breakout
クラスの collision
メソッドを下記のように変更すれば良いです。
class Breakout:
def collision(self):
'''当たり判定と当たった時の処理'''
for ball in self.balls:
#略
if collided_block is not None:
# 一番大きく当たったブロックに対してボールを反射
ball.reflect(collided_block)
# 一番大きく当たったブロックを削除
self.delete(collided_block)
for another_ball in self.balls:
if another_ball is ball:
# 同じボールの場合はスキップ
continue
# ballとanother_ballとの当たり判定
if ball.getCollisionCoords(another_ball) is not None:
# 当たってたらballを反射
ball.reflect(another_ball)
# ballとself.paddleとの当たり判定
if ball.getCollisionCoords(self.paddle) is not None:
# 当たってたらballを反射
ball.reflect(self.paddle)
追加した行は全て for ball in self.balls
のループの内側の処理であることに注意してください。
ボールが一度に複数のボールと当たる可能性もあるので、ブロックと同様に一番大きく当たったボールに対してのみ反射を行うようにしたほうが良いかも知れませんが、ボール同士が3つ以上一度に当たる可能性も低いので、単純に当たったボール全てに対して反射するようにしています。
上記のように変更を加えてスクリプトを実行してゲームを開始すれば、ボールがパドルや他のボールと当たった時にも反射するようになったことが確認できると思います。ただボールを同じ色にしているのでボール同士が当たった時の反射はちょっと分かりにくいかも…。
これでほぼブロック崩しゲームが出来上がったことになります。あとは、ゲームクリアの表示・ゲームオーバーの表示・ゲームのやり直しを実現していきます。
ゲームクリアを表示する
まずゲームクリア時に "GAME CLEAR"
と画面に表示されるようにしていきたいと思います。
まず、ブロック崩しゲームの場合、ゲームクリアは「ブロックが残っているかどうか」で判断することができます。
より具体的には、Breakout
クラスのデータ属性 blocks
の要素数が 0
になった場合にゲームクリアと判断することができます(ボールと当たったブロックは、blocks
からも削除されるようになっている)。
さらに、文字列のキャンバスへの描画はキャンバスの create_text
メソッドにより実現できます。
したがって、 Breakout
クラスに下記の result
メソッドを追加することで、ゲームクリア表示機能を Breakout
クラスに持たせることができます。
class Breakout:
def result(self):
'''ゲームの結果を表示する'''
if len(self.blocks) == 0:
self.canvas.create_text(
self.width // 2, self.height // 2,
text="GAME CLEAR",
font=("", 40),
fill="blue"
)
self.is_playing = False
create_text
メソッドについても下記ページで解説していますので、詳しく知りたい方はこちらを参照していただければと思います。
上記の result
メソッドではゲームクリア時に is_playing
に False
を設定していますので、"GAME CLEAR"
が表示されるとともにゲームも停止することになります。
続いて、下記のように Breakout
クラスの loop
メソッドで result
を実行するように変更します。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
# ゲーム開始していないなら何もしない
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
# 全ボールを移動する
for ball in self.balls:
ball.move()
self.collision()
self.updateFigures()
self.result()
ここまでの変更を加えたスクリプトを実行し、ブロックを全て消せば、画面に "GAME CLEAR"
が表示されるようになったことが確認できると思います。
スポンサーリンク
ゲームオーバーを表示する
続いてゲームクリア時同様に、ゲームオーバー時に "GAME OVER"
を表示するようにしていきたいと思います。
ボールが画面下に落ちるようにする
ただし、現状作成中のブロック崩しゲームでは絶対にゲームオーバーになりません。なぜなら、ボールが画面下まで行った時も反射するようになっているからです。
なので、ボールが画面下までいったらボールを反射させるのではなく、ボールを消すようにしていきたいと思います。
そのために、まずボールの反射を行なっている Ball
クラスの move
メソッドを下記のように変更します(差分が分かるようにコメントアウトしていますが、消してしまっても問題ないです)。
class Ball:
def move(self):
'''移動'''
# 移動方向に移動
self.x += self.dx
self.y += self.dy
if self.x < self.x_min:
# 左の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_min
elif self.x > self.x_max:
# 右の壁とぶつかった
# 横方向に反射
self.reflectH()
self.x = self.x_max
if self.y < self.y_min:
# 上の壁とぶつかった
# 縦方向に反射
self.reflectV()
self.y = self.y_min
#elif self.y > self.y_max:
# 下の壁とぶつかった
# 縦方向に反射
#self.reflectV()
#self.y = self.y_max
画面下に落ちたボールを消す
上記の変更によりボールが画面下から消えるようになりますが、実際にはボールは画面には表示されないもののオブジェクトとしては残っており、描画や当たり判定・移動がまだ行われています。
無駄な処理なので、画面下に落ちてしまったボールはキャンバスや Breakout
クラスのデータ属性 balls
から消すようにしたいと思います。
これを実現するために、まずはボールが画面下に落ちていないかどうかを判断するための exists
メソッドを Ball
クラスに追加します。ボールが画面下に落ちていないかどうかは self.y <= self.y_max
により判断することができます。
class Ball:
def exists(self):
'''画面内に残っているかどうかの確認'''
return True if self.y <= self.y_max else False
さらに、ボール移動後にボールが画面下に落ちたかどうかを exists
メソッドで判断し、画面下に落ちたボールに対しては Breakout
クラスの delete
メソッドで削除するようにします。
具体的には、Breakout
クラスの loop
メソッドを下記のように変更します。
class Breakout:
def loop(self):
'''ゲームのメインループ'''
if not self.is_playing:
# ゲーム開始していないなら何もしない
return
# loopをUPDATE_TIME ms後に再度実行
self.master.after(UPDATE_TIME, self.loop)
# 全ボールを移動する
for ball in self.balls:
ball.move()
# ボールが画面外に出たかどうかをチェック
delete_balls = []
for ball in self.balls:
if not ball.exists():
# 外に出たボールは削除対象リストに入れる
delete_balls.append(ball)
for ball in delete_balls:
# 削除対象リストのボールを削除
self.delete(ball)
self.collision()
self.updateFigures()
self.result()
ボールがなくなったときにゲームオーバーを表示する
ここまでの変更により、画面下に落ちたボールは Breakout
クラスのデータ属性 balls
から削除されるようになりました。したがって、全てのボールが画面下に落ちた場合は balls
の要素数が 0
になることになり、この要素数からゲームオーバーかどうかを判断できるようになったことになります。
ですので、あとはゲームクリア表示時と同様に、ゲームオーバー時にキャンバスの create_text
メソッドにより "GAME OVER"
と文字列を描画すれば良いだけです。
具体的には、Breakout
クラスの result
メソッドを下記のように変更します。
class Breakout:
def result(self):
'''ゲームの結果を表示する'''
if len(self.blocks) == 0:
self.canvas.create_text(
self.width // 2, self.height // 2,
text="GAME CLEAR",
font=("", 40),
fill="blue"
)
self.is_playing = False
if len(self.balls) == 0:
self.canvas.create_text(
self.width // 2, self.height // 2,
text="GAME OVER",
font=("", 40),
fill="red"
)
self.is_playing = False
ここまでの変更を加えたスクリプトを実行すれば、ボール全てが画面の下に落ちた際に "GAME OVER"
が表示されるようになったことが確認できると思います。
ゲームのやり直しができるようにする
ほぼここまでの解説でブロック崩しゲームが完成したことになります!
最後に、まぁ正直あってもなくても良い機能ではあるのですが、ゲームクリア or ゲームオーバーになった際にマウスをクリックするだけでゲームを最初からやり直せるようにしたいと思います。
これは、BreakOut
クラスの start
メソッドを下記のように変更するだけで実現することができます。
class Breakout:
def start(self, event):
'''ゲーム開始'''
if len(self.blocks) == 0 or len(self.balls) == 0:
# ゲームクリア or ゲームオーバー時は最初からやり直し
# キャンバスの図形を全て削除
self.canvas.delete("all")
# 全オブジェクトの作り直しと図形描画
self.createObjects()
self.drawFigures()
# ゲーム開始していない場合はゲーム開始
if not self.is_playing:
self.is_playing = True
self.loop()
else:
self.is_playing = False
もう忘れているかも知れないので一応言っておくと、start
メソッドはマウスクリック時に実行されるメソッドになります。
上記の変更では、ゲームクリア or ゲームオーバー時にキャンバスの図形を全て削除し(canvas.delete("all")
により全図形を削除することができる)、さらにオブジェクトを全て作り直してから図形として描画し直すようにしています。
要は、これらの処理を行うことで、ゲーム開始前の状態に戻しています。さらにその後 loop
メソッドが実行され、ゲームが開始することになります。
canvas.delete("all")
を忘れると、図形がそのままキャンバス上に残ることになるため注意してください。
上記の変更を加えれば、ゲームオーバーやゲームクリアになった後にマウスをクリックすれば、ゲームを最初からやり直せるようになったことが確認できると思います。
ブロック崩しゲームのカスタマイズ
以上がブロック崩しゲームの作り方の解説になります。
最後に、作成したブロック崩しゲームのカスタマイズについてちょっとだけ解説しておきます。
スポンサーリンク
ブロックの数や空きスペースのサイズ等を変更する
各種パラメータを定義する で解説したように、ブロックの数やサイズ、パドルのサイズ、ボールのサイズや数、空きスペースのサイズ等はスクリプト先頭付近のグローバル変数を変更することで簡単に変化させることができます。
例えば、このグローバル変数を下記のように変更すれば、
NUM_H_BLOCK = 15 # ブロッックの数(横方向)
NUM_V_BLOCK = 15 # ブロックの数(縦方向)
WIDTH_BLOCK = 40 # ブロックの幅
HEIGHT_BLOCK = 20 # ブロックの高さ
COLOR_BLOCK = "blue" # ブロックの色
HEIGHT_SPACE = 0 # 縦方向の空きスペース
WIDTH_PADDLE = 200 # パドルの幅
HEIGHT_PADDLE = 20 # パドルの高さ
Y_PADDLE = 50 # パドルの下方向からの位置
COLOR_PADDLE = "green" # パドルの色
RADIUS_BALL = 10 # ボールの半径
COLOR_BALL = "red" # ボールの色
NUM_BALL = 3 # ボールの数
UPDATE_TIME = 20 # 更新間隔(ms)
下のアニメのように内側からブロックを消していくような動作にすることができます。ボールの反射やブロックの削除がどんどん行われていくので、見ていてちょっと気持ちいいかなぁと思います!
ただ、ブロックの数やボールの数が増えるとその分当たり判定の処理がたくさん実行されることにな理、処理が重くなるので注意してください(私の環境だとボールが4つあると画面が止まってしまいます…)。
反射角度にランダム性を持たせる
もちろんグローバル変数の変更だけでなく、スクリプトを変更してブロック崩しゲームをカスタマイズしていくこともできます。
例えば Ball
クラスの reflectH
と reflectV
を下記のように変更すれば、反射角度にランダム性を持たせることができ、もうちょっと難易度の高いブロック崩しゲームに仕立てることもできます。
class Ball:
def reflectH(self):
'''横方向に対して反射'''
random_angle = math.radians(random.randrange(-30,30))
self.turn(math.atan2(self.dy, -self.dx) + random_angle)
def reflectV(self):
'''縦方向に対して反射'''
random_angle = math.radians(random.randrange(-30,30))
self.turn(math.atan2(-self.dy, self.dx) + random_angle)
random モジュールを使用しているため、import random
を事前に行っておく必要がありますので、この点に注意してください。
是非カスタマイズに挑戦を!
他にもいろいろカスタマイズできそうなところはあると思います。
私がパッと思いついただけでも下記のようなものがあります。
- ボールが当たったパドルの位置に応じて反射角度を変える
- 複数回ボールを当てないと消えないようなブロックを導入する
- ブロックの配置を変える
- 処理をもっと軽くする
- 現状全てのブロックに当たり判定を行なっているけど内側のブロックは当たり判定は不要
- アイテムを導入する
- パドルとアイテムが当たったら一定時間パドルを長くする
- パドルとアイテムが当たったら一定時間ボールがブロックを突き抜けるようにする
ちょっとでもブロック崩しゲームを作るのを楽しいと感じていただけたのであれば、是非こういったカスタマイズに挑戦してみてください!
もちろん解説を読むだけでも知識は付きますが、自身でプログラミングして何か作ってみたり、元々あるアプリをカスタマイズしてみることで、よりプログラミングの力をつけることができると思います!
スポンサーリンク
まとめ
このページでは、Python で tkinter を用いた「ブロック崩しゲームの作り方」を解説しました!
解説は結構長くなってしまいましたが、ブロックが消えたりボールが反射したりするようになったあたりでは「ゲーム作りの楽しさ」を少しは感じていただけたのではないかと思います。
こういった簡単なゲームを作るだけでも考えることはたくさんありますし、実際に作ってみることで多くのことを学べたと思います。
紹介したスクリプトのカスタマイズや他のゲームの開発に挑戦すれば必ずプログラミングの力は付きますので、是非いろんなゲームやアプリ作りに挑戦してみてください!
オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!