動きのあるゲーム
Pythonで描画機能を扱えるライブラリは色々あるが、ゲーム作成に適しているライブラリとしてPyGameを紹介する。
ウィンドウを表示する
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
図形を動かす
等速直線運動
まず、円を指定した座標の位置に描いてみる。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす pygame.draw.circle(surface, (0, 0, 0), (0, 240), 50) #円を描く for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
while True:以下は繰り返して実行されているので、繰り返しのたびに座標が変わるようにすればよい。円の中心座標をxとして、毎回値を増やしてみる。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_x = 0 #円の中心のx座標 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす pygame.draw.circle(surface, (0, 0, 0), (circle_x, 240), 50) #円を描く circle_x += 1 #中心のx座標を1増やす for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
イベントループは、clock.tick(30)の作用で1/30秒ごとに繰り返されている(PCの性能が低かったり、処理内容が多すぎて間に合わない場合はより時間がかかることがある)。なので、円は秒速30ピクセルで右に等速直線運動することになる。
円の速度を3倍にするにはどうすればよいか?
円を左上から、x方向の速度を60ピクセル/秒、y方向の速度を30ピクセル/秒で動くようにしてみよう。PyGameでは座標の原点はウィンドウの左上で、y軸方向は下向きになっていることに注意。
放物線運動
放物線運動は2次方程式で表せる。x方向に等速運動として、x座標からy座標を計算すればよい。放物線の頂点座標や係数を調整するのが少々ややこしい。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_x = 0 #円の中心のx座標 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす circle_y = (circle_x - 320) * (circle_x - 320) * (480.0 / (320.0 * 320.0)) #y座標を計算する pygame.draw.circle(surface, (0, 0, 0), (circle_x, circle_y), 50) #円を描く circle_x += 1 #中心のx座標を1増やす for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
x方向とy方向を別々に考えることもできる。x方向には等速運動、y方向には等加速度運動とし、y方向の速度をv = v0 + atとして計算する考え方である。イベントループが一定の時間Δtごとに繰り返される場合、vの値は前回の値からaΔtだけ増えることになる。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_x = 0 #円の中心のx座標 circle_y = 480 #円の中心のy座標 v_y = -3.2 #円のy方向の速度、値は適当に設定 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす pygame.draw.circle(surface, (0, 0, 0), (circle_x, circle_y), 50) #円を描く circle_x += 1 #中心のx座標を1増やす circle_y += v_y #y座標を計算する v_y += 0.01 #y方向の速度を加速度分変更する、値は適当に調整 for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
ジャンプするゲームでは、放物線運動のままでは思ったとおりに着地しにくく、落ちるときの方が素早く落ちるような軌道の方が調整しやすいそうである。
等速円運動
回転の中心座標と円の中心座標の角度θを一定速度で変化させ、θと回転半径からx, y座標を計算すればよい。
import sys #sys.exit()を呼ぶため import math #sin, cosを使うため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): theta = 0.0 #角度、ラジアン単位 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす circle_x = math.cos(theta) * 200.0 + 320 circle_y = math.sin(theta) * 200.0 + 240 pygame.draw.circle(surface, (0, 0, 0), (circle_x, circle_y), 50) #円を描く theta += 0.01 for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
ユーザの操作に反応する
コンピュータで一般に使われる入力装置(入力デバイス)としてはマウスとキーボードがある。ゲームの場合はコントローラ(ジョイスティック)も使われるだろう。これらのデバイスに対応するには、大きく2つの方法がある。
- デバイスの状態を定期的にチェックし、状態に応じて処理する。(ポーリング)
- デバイスの入力を一旦イベントキューに貯め、定期的に取り出して処理する。(イベントドリブン)
前者の方法はリアルタイム性に優れているが、チェックするタイミングによってはとりこぼすおそれがある。後者の方法は取りこぼす心配はないが、イベントが多発すると処理に時間がかかりリアルタイム性が損なわれることもある。
応用の広い、イベントキューを使う方法を知っておけばよいだろう。イベントキューを扱う環境では、デバイスの入力イベントを含め、様々なイベントは自動的にイベントキューに追加されていく。自分の書くプログラムでは、イベントキューに入っているイベントを1つずつ取り出して、イベントの種類を判別し、対応する処理を実行する。これまでのプログラムの例では、ウィンドウが閉じられたとき(pygame.QUIT)のイベントのみに対応している。
マウス
PyGameでマウスボタンを押すイベントはpygame.MOUSEBUTTONDOWNである。これを使って、マウスボタンを押したところに円を描くプログラムは次のようになる。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす def main(): while True: for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する elif event.type == pygame.MOUSEBUTTONDOWN: pygame.draw.circle(surface, (0, 0, 0), event.pos, 20) #円を描く pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
マウスに関するイベントは他に、ボタンを離した時、マウスが動いたときを使うことができる。また、イベントのデータとしてマウスの座標、押した/離したボタンを使うことができる。
キーボード
PyGameでキーを押すイベントはpygame.KEYDOWNである。これを使って、キー操作で円を動かすプログラムは次のようになる。
import sys #sys.exit()を呼ぶため import pygame from pygame.locals import * #いちいちpygame.と書くのが面倒なので省略できるようにする pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_pos = [320, 240] #円の中心座標 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす for event in pygame.event.get(): #イベントをチェックする if event.type == QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する elif event.type == KEYDOWN: if event.key == K_a: circle_pos[0] -= 5 elif event.key == K_d: circle_pos[0] += 5 elif event.key == K_w: circle_pos[1] -= 5 elif event.key == K_s: circle_pos[1] += 5 pygame.draw.circle(surface, (0, 0, 0), circle_pos, 20) #円を描く pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
このプログラムでは「キーを押すたびに」しか動かない。キーを押しっぱなしにしていれば動くようにするには、例えば次のようにする。他に、キーの状態を都度チェックする方法もある。
import sys #sys.exit()を呼ぶため import pygame from pygame.locals import * #いちいちpygame.と書くのが面倒なので省略できるようにする pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_pos = [320, 240] #円の中心座標 key_status = [False, False, False, False] #キーを押しているかどうか。左、右、上、下の順 while True: surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす for event in pygame.event.get(): #イベントをチェックする if event.type == QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する elif event.type == KEYDOWN: if event.key == K_a: key_status[0] = True elif event.key == K_d: key_status[1] = True elif event.key == K_w: key_status[2] = True elif event.key == K_s: key_status[3] = True elif event.type == KEYUP: if event.key == K_a: key_status[0] = False elif event.key == K_d: key_status[1] = False elif event.key == K_w: key_status[2] = False elif event.key == K_s: key_status[3] = False #キーの状態に応じて動かす if key_status[0]: circle_pos[0] -= 5 if key_status[1]: circle_pos[0] += 5 if key_status[2]: circle_pos[1] -= 5 if key_status[3]: circle_pos[1] += 5 pygame.draw.circle(surface, (0, 0, 0), circle_pos, 20) #円を描く pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
当たり判定を行う
アクションゲームでは障害物に当たった、弾が当たった、などの当たり判定を使うことが多い。PyGameのSpriteには当たり判定を行う機能があるが、ここでは簡単な考え方を紹介する。
壁で跳ね返るようにする
このページの最初に紹介した等速直線運動のプログラムはウィンドウの外に出てしまっても動き続けている。外に出ないように、ウィンドウの端で反対向きに移動するようにするために、円の中心座標とウィンドウの端の座標を比較して、外に出そうになったら移動速度を反転させる。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_pos = [50, 50] speed = [5, 3] while True: #イベントループ surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす pygame.draw.circle(surface, (0, 0, 0), circle_pos, 50) #円を描く for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する if circle_pos[0] < 50 or circle_pos[0] > 640 - 50: speed[0] = -speed[0] if circle_pos[1] < 50 or circle_pos[1] > 480 - 50: speed[1] = -speed[1] circle_pos[0] += speed[0] circle_pos[1] += speed[1] pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
範囲チェックする
上のプログラムで、動いている円をクリックしたら色が変わるようにしてみよう。クリックした座標は点で、円は大きさを持っている。
厳密にクリックした点が円の中に入っているかをチェックするには、円の中心とクリックした点の距離が、円の半径以下かどうかを判定すればよい。
import sys #sys.exit()を呼ぶため import pygame pygame.init() #PyGameの初期化 surface = pygame.display.set_mode((640, 480)) #ウィンドウを開く。引数はウィンドウの大きさ pygame.display.set_caption("sample window") #ウィンドウのタイトルを設定する clock = pygame.time.Clock() #フレームレートを設定するためのオブジェクト def main(): circle_pos = [50, 50] speed = [5, 3] color = False #色を表すフラグ while True: #イベントループ surface.fill((255, 255, 255)) #ウィンドウ全体を白で塗りつぶす if color: pygame.draw.circle(surface, (255, 0, 0), circle_pos, 50) #赤色 else: pygame.draw.circle(surface, (0, 0, 255), circle_pos, 50) #青色 for event in pygame.event.get(): #イベントをチェックする if event.type == pygame.QUIT: #ウィンドウが閉じられたら pygame.quit() #PyGameを終了する sys.exit() #プログラムを終了する elif event.type == pygame.MOUSEBUTTONDOWN: dist = (event.pos[0] - circle_pos[0]) * (event.pos[0] - circle_pos[0]) \ + (event.pos[1] - circle_pos[1]) * (event.pos[1] - circle_pos[1]) if dist <= 50 * 50: color = not color if circle_pos[0] < 50 or circle_pos[0] > 640 - 50: speed[0] = -speed[0] if circle_pos[1] < 50 or circle_pos[1] > 480 - 50: speed[1] = -speed[1] circle_pos[0] += speed[0] circle_pos[1] += speed[1] pygame.display.update() #画面を更新する clock.tick(30) #30fpsになるように調整する if __name__ == '__main__': #複数のソースファイルがある場合のためにmainを実行するかチェック main()
ターゲットが円でない場合はどうするか?見た目が円に近ければ円に近似して判定してしまえばよい。矩形に近ければ、座標の範囲で判定することもできる。
円や矩形に近似する場合、自分の操作するオブジェクトは小さめに、敵は大きめにしておくなど、見た目どおりの大きさではなく多少調整したほうが自然に感じられる。