Rect型
#
参考書1 p.140(Chapter 1.4)
PyGame の画像やスクリーンは、\(x\)座標、\(y\)座標、幅、高さをひとまとめにしたデータとして、Rectというデータ型を持っています。
このデータ型を用いることで、物体を移動させたり、物体が壁や障害物と衝突したかを判定したりが簡単にできるようになります。
# 位置と大きさをまとめて扱う。
# 変数 = pg.Rect(x, y, width, height)
box = pg.Rect(10, 20, 30, 40)
Rect型のそれぞれの値には、box.xやbox.heightのようにアクセスします。
ipmort pygame as pg
box = pg.Rect(10, 20, 30, 40)
print(f"(x, y)=({box.x}, {box.y}), (width, height)=({box.width}, {box.height})")
box.x = box.x + 10
print(f"(x, y)=({box.x}, {box.y}), (width, height)=({box.width}, {box.height})")
ここで、box.x = box.x + 10 は、box変数内の\(x\)座標の値(初期値は10)に10を足しています。
その結果、box.xの値は20になっています。
Rectを使って図形を動かそう
#
Rect を使って、図形を動かすプログラムを作成しましょう。
図形の位置を更新して、pg.display.update() 関数を呼び出せば図形の位置が更新されます。
位置を更新し、pg.display.update()を呼び出すという処理を何度も繰り返すことで、図形を移動させることができます。
位置を更新するオブジェクトを図形ではなく、画像にすれば画像が移動します。
以下のサンプルプログラム(ex04.py)を作成し、実行してみましょう。
import pygame as pg # PyGame をインポートし、pg という名前で使えるようにする。
import sys # sys モジュールをインポートする(終了処理で使う)。
# 1. ゲームの準備をする。
pg.init()
screen = pg.display.set_mode((800, 600))
box = pg.Rect(20, 50, 100, 150) # 幅100、高さ150の四角形を(20, 50)の位置に置く。
clock = pg.time.Clock()
# 2. メインループ
while True:
# 2-1 画面を初期化する。
screen.fill(pg.Color("WHITE")) # 画面を白色で塗りつぶす。
# 2-3. 絵を描いたり、判定したりする。
box.x = box.x + 1 # box の x 座標を +1 する。
pg.draw.rect(screen, pg.Color("RED"), box) # draw.rect関数は box を四角形として表示できる。
pg.draw.ellipse(screen, pg.Color("GREEN"), box, 5) # draw.ellipse 関数は、boxを円として表示できる。
# 2-4. 画面を表示する。
pg.display.update()
clock.tick(60) # 画面の更新を1秒間に60回以下のスピードに設定する。
# 2-5. 閉じるボタンが押されたら終了する。
for event in pg.event.get(): # イベント一覧を取得して、各イベントを調べる。
if event.type == pg.QUIT: # もし、閉じるボタンが押されたら。
pg.quit() # PyGame を終了する。これだけではウィンドウは閉じない。
sys.exit() # ウィンドウを閉じて、プログラムを終了する。
このプログラムを実行すると、以下のように赤い長方形に内接する緑の楕円が右側に移動していく様子が見られます。
pg.display.update() で絵の位置などを更新させています。
この1回の画面更新を1フレームと呼びます。
ただし、PC の性能や処理の重さによって1フレームの描画に要する時間が異なります。
アニメーションの場合は、どのような性能のPCであっても同じような時間で更新されることが求められます。
そこで、描画間隔を制御するため、pg.time.Clock().tick(60) という1行が書かれています。
この命令は、1フレームの処理が終わった後、次のフレームの描画までの待機時間を調整し、ループの実行速度を最大60回/秒に抑える命令です。
描画処理が軽ければ、tick(60)は適切な待機時間を設けて、FPS(Frame Per Seconds)を 60 に近づけます。
clock.get_fps() で実測の FPS が得られます。
フレーム数が多いほど動きが滑らかになりますが、処理が重いとフレームレートが落ちて、動作がカクカクするようになります。
例題11-4. 画像の移動によるアニメーション #
以下のトラックの画像を左右に移動させてみましょう(animation.py)。
ただし、トラックの大きさは 50x50 とし、画面の端に到達すれば、逆方向に移動させてください。
その際、トラックの向きと移動方向を合わせてください。
これを実現するためには、以下の問題を解決する必要があります。
- 画像を描画する。
- 画像の大きさを変更する(例題11-2 画像を表示するプログラムにて解決済)。
- 画像の向きを反転する。
- 画面の端に到達したことを確認する。
画像を描画する #
画像を描画するには、画像を表示する で述べたように、screen.blit関数を用います。
先の例では、blit関数の第2引数でどこに描画するかをタプルで指定していましたが、Rect オブジェクトでも指定できます(Rectオブジェクトを指定できますが、width、heightは参照されず、x、yのみが有効です)。
screen = # screen を初期化する。
box = pg.Rect(0, 100, 50, 50)
img = # 画像をロードする。
img = pg.transform.scale(img, (50, 50)) # box の大きさに画像の大きさを合わせる。
...
screen.blit(img, box) # box の位置へ img を描画する。
画像の向きを反転する #
画像の向きを反転するには、pg.transform.flip 関数を利用します。
pg.transform.flip関数は引数を3つ受け取ります。
引数は最初から順に、画像を表す変数、左右反転フラグ、上下反転フラグです。
反転フラグが True であれば、その方向に反転させ、Falseであれば当該方向は反転されません。
そのため、左右のみを反転させた画像は、以下のようなプログラムで取得できます。
imgL = # 画像読み込み。
imgR = pg.transform.flip(imgL, True, False)
画面の端に到達したことを確認する #
基本的には、(x)座標と幅を確認し、screen の範囲から出ると端に到達したと考えれば良いでしょう。
ただし、座標を取り出し、自分で計算するのではなく、オブジェクトに依頼するのがオブジェクト指向の考え方です。
座標計算はややこしいので、自前で計算しようとするとバグの温床になるので、この意味からも避けましょう。
つまり、まず screen が表す Rect を取り出し、sbox とします。
次に、画像を表す box と sbox の重なり具合を確認し、sboxの中から box が飛び出せば端に到達したことになります。
Rectには、contains というメソッドがあり、親.contains(子) という呼び出しで、親の領域内に子が収まっているかを確認できます。
box.xを移動させ、sbox.contains(box) が False を返すときに、移動方向を反対にし、画像の向きを逆にすれば良いです。
...
if not sbox.contains(box): # box が sbox 内に含まれていれば True。
dx = dx * -1 # 移動方向を逆方向に変更する。
if dx < 0: # 左向きの画像に変更する。
img = truckL
else:
img = truckR
box.x = box.x + dx
screen.blit(img, box)
...
プログラム #
これまでの説明をもとに、トラック画像を最初は右向きに走らせ、端に到達すれば、逆方向に走らせるようにしましょう。
import pygame as pg # PyGame をインポートし、pg という名前で使えるようにする。
import sys # sys モジュールをインポートする(終了処理で使う)。
# 1. ゲームの準備をする。
pg.init()
screen = pg.display.set_mode((800, 600))
orig_img = # トラック画像を読み込む。
truckL = # orig_img を 50x50 の大きさにリサイズする。
truckR = # 画像の向きを左右反転させる。
clock = pg.time.Clock()
box = pg.Rect(0, 100, 50, 50) # 幅50、高さ50の四角形を(0, 100)の位置に置く(画像の表示位置を表す)。
dx = 3 # 移動量を設定する。
sbox = screen.get_rect() # screenからRectオブジェクトを取り出す。
img = # 描画する画像を選択する(truckL, truckRの適切な方が代入される)。最初は右向き。
# 2. メインループ
while True:
# 2-1 画面を初期化する。
screen.fill(pg.Color("WHITE")) # 画面を白色で塗りつぶす。
# 2-3. 絵を描いたり、判定したりする。
# 端に到達したときの処理をかく。
# box.x を更新し、screen に img を box の位置に描画する。
# 2-4. 画面を表示する。
pg.display.update()
clock.tick(60) # 画面の更新を1秒間に60回以下のスピードに設定する。
# 2-5. 閉じるボタンが押されたら終了する。
for event in pg.event.get(): # イベント一覧を取得して、各イベントを調べる。
if event.type == pg.QUIT: # もし、閉じるボタンが押されたら。
pg.quit() # PyGame を終了する。これだけではウィンドウは閉じない。
sys.exit() # ウィンドウを閉じて、プログラムを終了する。
🏁 解答例
import pygame as pg # PyGame をインポートし、pg という名前で使えるようにする。
import sys # sys モジュールをインポートする(終了処理で使う)。
# 1. ゲームの準備をする。
pg.init()
screen = pg.display.set_mode((800, 600))
orig_img = pg.image.load("truck.png") # トラック画像を読み込む。
truckL = pg.transform.scale(orig_img, (50, 50)) # orig_img を 50x50 の大きさにリサイズする。
truckR = pg.transform.flip(truckL, True, False) # 画像の向きを左右反転させる。
clock = pg.time.Clock()
box = pg.Rect(0, 100, 50, 50) # 幅50、高さ50の四角形を(0, 100)の位置に置く(画像の表示位置を表す)。
dx = 3 # 移動量を設定する。
sbox = screen.get_rect() # screenからRectオブジェクトを取り出す。
img = truckR # 描画する画像を選択する(truckL, truckRの適切な方が代入される)。
# 2. メインループ
while True:
# 2-1 画面を初期化する。
screen.fill(pg.Color("WHITE")) # 画面を白色で塗りつぶす。
# 2-3. 絵を描いたり、判定したりする。
if not sbox.contains(box): # box が sbox 内に含まれていれば True。
dx = dx * -1 # box が sbox の範囲から越えれば移動の向きを逆にする。
if dx < 0: # 左向きなら truckL
img = truckL
else: # 右向きなら truckR
img = truckR
box.x = box.x + dx # box の x 座標を変更する。
screen.blit(img, box) # img を box の位置に描画する。
# 2-4. 画面を表示する。
pg.display.update()
clock.tick(60) # 画面の更新を1秒間に60回以下のスピードに設定する。
# 2-5. 閉じるボタンが押されたら終了する。
for event in pg.event.get(): # イベント一覧を取得して、各イベントを調べる。
if event.type == pg.QUIT: # もし、閉じるボタンが押されたら。
pg.quit() # PyGame を終了する。これだけではウィンドウは閉じない。
sys.exit() # ウィンドウを閉じて、プログラムを終了する。