Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

PyGame-CEによるインタラクティブ表現入門その3:動きと衝突の基礎とゲームへの応用

ボールが跳ね返るアニメーションの作成

ボール(円)がウィンドウ上を移動し,ウィンドウの端で跳ね返るようなアニメーションを作成しよう.将来は物理シミュレーションに近づけられるよう,位置,速度,加速度を考慮したプログラムにしておく.

import pygame

# 初期化
pygame.init()

# 400ピクセル×400ピクセルのウィンドウをつくる
WIDTH, HEIGHT = 400, 400
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("PyGame-CE Program 2: ball motion")

clock = pygame.time.Clock()
running = True
mouseClicked = False

# 円の半径
r = 10
# 初期位置
x = 100
y = 100
# 初速度
vx = 3
vy = 2
# 加速度
ax = 0
ay = 0
# 時間刻み
dt = 1.0

# メインループ
while running:
    # 1. イベント処理
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False  # ウィンドウ右上の×で終了
        elif event.type == pygame.MOUSEBUTTONUP:
            mouseClicked = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            mouseClicked = True
        # Sキーでスクショ保存
        elif event.type == pygame.KEYDOWN and event.key == pygame.K_s:
            pygame.image.save(screen, "screenshot.png")
            print(f"Saved screenshot to screenshot.png.")

    # 2. 位置を更新
    x += vx*dt
    y += vy*dt

    # 3. 速度を更新
    vx += ax*dt
    vy += ay*dt

    # 4. 壁にぶつかったら,速度を反転させる
    if x - r < 0 and vx < 0:
        vx = -vx
    elif x + r > WIDTH and vx > 0:
        vx = -vx
    if y - r < 0 and vy < 0:
        vy = -vy
    elif y + r > HEIGHT and vy > 0:
        vy = -vy

    # 5. 背景を塗る
    screen.fill((220, 220, 220))  # R,G,B [0-255]

    # 6. 図形を描く
    # 赤い円を(x, y)の位置に描く.
    pygame.draw.circle(screen, (255, 0, 0), (x, y), r)

    # 7. 画面を更新
    pygame.display.flip()

    # 8. フレームレート制御 (1秒あたり60フレーム程度)
    clock.tick(60)

# 終了処理
pygame.quit()

このプログラムを実行すると,ボールに見立てた赤い円が画面上を直線的に動き回り,またウィンドウの周囲では跳ね返る(跳ね返っているように運動の向きを変える).どうしてそのように動作するのか,今後の改造に向けてプログラムの中身を理解しておこう.

ボールが跳ね返るプログラムを実行した例.ただしこの例では画面を完全には消さず,ボールの軌道がだんだん薄くなりながら画面に残るように表示している.ボールが直線的に動いていること,壁(ウィンドウの外枠)で跳ね返っていることが分かる.

Figure 72:ボールが跳ね返るプログラムを実行した例.ただしこの例では画面を完全には消さず,ボールの軌道がだんだん薄くなりながら画面に残るように表示している.ボールが直線的に動いていること,壁(ウィンドウの外枠)で跳ね返っていることが分かる.

重力の導入と計算精度向上

重力のような効果を入れるため,yy方向加速度を設定してみよう.

ay = 0.5

PyGame-CEではyy軸の正は下向きなので,ayに正の値を入れると画面上では下向きに加速する.つまり下向きの重力がはたらくことに相当する.(画面はピクセル単位で,物理空間とは単位が異なるため,ayに重力加速度の正しい値を与えることはあまり意味が無く,適切なアニメーションになるような値を設定すれば良い.)

このように変えると,ボールが地面で跳ね返るようなアニメーションが表示されることと思う.

ただしアニメーションをずっと表示し続けると,おそらくだんだんとボールの跳ね返る高さが高くなり,ついには天井にぶつかり,その後もだんだん速くなり続けるだろう.上に述べたように完全弾性衝突に当たる計算をしているから,正しく計算されていればずっと同じ高さまで跳ね返り続けるはずである.どうしてだんだん高くなるのだろうか?

ボールが跳ね返るプログラムを実行した例・重力あり.地面で跳ね返ったボールが放物線を描いて再び落下する様子や,跳ね返る高さがだんだん高くなる様子が分かる.

Figure 73:ボールが跳ね返るプログラムを実行した例・重力あり.地面で跳ね返ったボールが放物線を描いて再び落下する様子や,跳ね返る高さがだんだん高くなる様子が分かる.

位置の計算精度向上

いまのプログラムでは,位置と速度は常微分方程式を数値的に解く最も基本的な方法であるオイラー陽解法で計算されているが,この解法は精度が良くない.速度は時間刻みdtの間にだんだんと変化するはずだが,位置を計算するに際してはそのことを考慮せず,nnステップ目での速度をそのまま用いてn+1n+1ステップ目での位置を求めているからである.特に今回のように常に下向きに加速している場合は,yy方向の速度がdtの間に相対的に下向きに変化することを無視しているので,どんどん高いほうにずれていく.

これを改善する方法はいろいろ提案されているが,今回は加速度が一定であることを利用して位置の計算を改善しよう.

    x += vx*dt + 0.5*ax*dt*dt
    y += vy*dt + 0.5*ay*dt*dt

このようにxxyyの計算に速度だけでなく,加速度も考慮する.この方法は加速度が一定の場合には解析解を与える.(高校物理または大学1年次の物理の授業で学んでいる式だと思われる.忘れていたら復習しよう.)

ボールが跳ね返るプログラムを実行した例・重力あり,制度改善版.跳ね返る高さがほぼ一定になったことが分かる.

Figure 74:ボールが跳ね返るプログラムを実行した例・重力あり,制度改善版.跳ね返る高さがほぼ一定になったことが分かる.

衝突の判定の拡張・長方形との衝突

壁以外のものとの衝突を導入しよう.たとえば長方形を画面上に描き,それと衝突したら跳ね返るようにする.

長方形は画面の端に平行に置き,その領域は左上の座標(xul,yul)(x_{ul}, y_{ul})と右下の座標(xlr,ylr)(x_{lr}, y_{lr})で表すこととする.はじめは固定座標としよう. (ul, lrはそれぞれupper left, lower rightの意味)

# 長方形を配置
x_ul = 150  # 左上のx座標
y_ul = 250  # 左上のy座標
x_lr = 300  # 右下のx座標
y_lr = 270  # 右下のy座標

そしてメインループの中で,長方形ボール(円)が衝突したら跳ね返るようにしよう.

    # 4b. 長方形にぶつかったら,速度を反転させる.
    # 簡易版
    # 長方形の左辺にぶつかった場合
    if vx > 0 and x + r >= x_ul and x - r < x_ul:
        if y + r >= y_ul and y - r <= y_lr:
            vx = -vx
            x = x_ul - r
    
    # 長方形の右辺にぶつかった場合
    if vx < 0 and x - r <= x_lr and x + r > x_lr:
        if y + r >= y_ul and y - r <= y_lr:
            vx = -vx
            x = x_lr + r
    
    # 長方形の上辺にぶつかった場合
    if vy > 0 and y + r >= y_ul and y - r < y_ul:
        if x + r >= x_ul and x - r <= x_lr:
            vy = -vy
            y = y_ul - r
    
    # 長方形の下辺にぶつかった場合
    if vy < 0 and y - r <= y_lr and y + r > y_lr:
        if x + r >= x_ul and x - r <= x_lr:
            vy = -vy
            y = y_lr + r
ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムを実行した例・重力あり

Figure 75:ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムを実行した例・重力あり

マウスの動きに合わせて長方形を動かす

長方形が固定のままだと発展性がないので,マウスの動きに合わせて長方形が動くようにしてみよう.

以前の教材で学んだように,マウスカーソルの位置はmx, my = pygame.mouse.get_pos()で得られるので,長方形の位置をそれに合わせて動かす.今回は長方形の高さは固定し,横方向にのみ動かしてみる.

    # マウス位置を取得
    mx, my = pygame.mouse.get_pos()

    # マウスの位置に合わせて長方形の位置を変える
    x_ul = mx - 50
    x_lr = mx + 50
ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムを実行した例・長方形がマウスの動きに合わせて横移動する場合

Figure 76:ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムを実行した例・長方形がマウスの動きに合わせて横移動する場合

発展:衝突判定の改善

現在の衝突のコードは判定が簡略化されており,特に長方形の角にボールが近づいた場合には実際に衝突する前に跳ね返る.また跳ね返る方向は常に衝突した方向の真逆である.これをより現実的な衝突に変更したい.

ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムで,ボールが長方形の左上隅に「衝突」している例.実際にはボールと長方形とは接触しておらず,また衝突前と速度が反転していることが分かる.

Figure 77:ボールが壁面および配置された長方形と衝突すると跳ね返るプログラムで,ボールが長方形の左上隅に「衝突」している例.実際にはボールと長方形とは接触しておらず,また衝突前と速度が反転していることが分かる.

衝突を改善するにあたり,まずボールの位置によって衝突判定をFigure 78のように8通りに分類する.

ボールと長方形との衝突の場合分け.ボールの中心が長方形まわりのどの位置にあるかによって,衝突判定を8通りに分類する.

Figure 78:ボールと長方形との衝突の場合分け.ボールの中心が長方形まわりのどの位置にあるかによって,衝突判定を8通りに分類する.

ボールの中心が①~④のいずれかにある場合の衝突判定と跳ね返りは,以前に示した衝突と同じように扱う.ボールの中心が⑤~⑧のいずれかにある場合は,長方形の角に衝突するものとして取り扱う.

ここでは長方形の角に当たった場合には,角に向かって心向き直衝突しているものと考える.つまり角からボールの中心に向かうベクトルを考えたとき,角の位置にはこのベクトルと垂直な壁があり,これと衝突し跳ね返ると考える.従って速度の角→ボール中心向きの成分を取りだし,それを反転させる.

角→ボール中心に向かうベクトルは,

n=(xxc,yyc)\boldsymbol{n}=(x-x_{c}, y-y_{c})

と書ける.ここで(xc,yc)(x_c, y_c)は角の座標である.

このベクトルの長さが円の半径rr以下のとき,角と衝突している.以下の式ではnn(太字でないnn)をn\boldsymbol{n}(太字のn\boldsymbol{n},ベクトルを表す)の大きさの意味で使っている.

n2=(xxc)2+(yyc)2r2n^2=(x-x_c)^2+(y-y_c)^2\leq r^2

今後の計算のため,法線ベクトルn\boldsymbol{n}を単位法線ベクトルに変換したものを作る.

n^=nn\hat{\boldsymbol{n}}=\frac{\boldsymbol{n}}{n}

現在の速度を(vx,vy)(v_x, v_y)とすると,法線方向成分はこれとn^\hat{\boldsymbol{n}}との内積で求められる.

vn=vn^=vxn^x+vyn^yv_n = \boldsymbol{v}\cdot\hat{\boldsymbol{n}}=v_x\hat{n}_x+v_y\hat{n}_y

もしvn0v_n\geq 0なら,ボールは角から離れる方向に動いているので,速度は変えなくてよい.vn<0v_n<0ならボールは角に向かって動いているので,その成分だけ反転させる.

v=v2vnn^\boldsymbol{v}'=\boldsymbol{v}-2v_n\hat{\boldsymbol{n}}

これにより角→ボール中心方向成分のみ符号が変わり,接線方向(角→ボール中心方向と垂直な成分)は維持される.

ここで検討した計算方法を,Pythonの関数にしよう.

import math

def collide_circle_corner(x, y, r, vx, vy, x_c, y_c):
    """
    ボール(中心 x, y, 半径 r)と、長方形の角 (x_c, y_c) の衝突を判定し、
    ボールが角に向かっている場合は速度ベクトルを反射させる。

    返り値: (vx_new, vy_new)
    """

    # 角 → ボール中心のベクトル
    dx = x - x_c
    dy = y - y_c

    dist = math.sqrt(dx*dx + dy*dy)

    # 衝突はすでに判定済みとする.(dist <= r のときに呼び出される想定)

    # 単位法線ベクトル(角 → ボール中心)
    nx = dx / dist
    ny = dy / dist

    # 法線方向速度成分 v_n を求める
    v_n = vx * nx + vy * ny  # 法線方向速度成分 v_n

    # v_n < 0 のときだけ「角に向かっている」ので反射させる
    if v_n < 0:
        # 反射: v' = v - 2 (v・n) n
        vx -= 2.0 * v_n * nx
        vy -= 2.0 * v_n * ny

    return vx, vy

そして,⑤~⑧の場合はこの関数を使って衝突後の速度を求める.

    # ①左辺にぶつかった場合
    if x < x_ul and y_ul <= y and y <= y_lr and x + r >= x_ul and vx > 0:
        vx = -vx
    # ②右辺にぶつかった場合
    elif x > x_lr and y_ul <= y and y <= y_lr and x - r <= x_lr and vx < 0:
        vx = -vx
    # ③上辺にぶつかった場合
    elif y < y_ul and x_ul <= x and x <= x_lr and y + r >= y_ul and vy > 0:
        vy = -vy
    # ④下辺にぶつかった場合
    elif y > y_lr and x_ul <= x and x <= x_lr and y - r <= y_lr and vy < 0:
        vy = -vy
    # ⑤左上隅にぶつかった場合
    elif x < x_ul and y < y_ul and (x - x_ul)**2 + (y - y_ul)**2 <= r**2:
        vx, vy = collide_circle_corner(x, y, r, vx, vy, x_ul, y_ul)
    # ⑥右上隅にぶつかった場合
    elif x > x_lr and y < y_ul and (x - x_lr)**2 + (y - y_ul)**2 <= r**2:
        vx, vy = collide_circle_corner(x, y, r, vx, vy, x_lr, y_ul)
    # ⑦左下隅にぶつかった場合
    elif x < x_ul and y > y_lr and (x - x_ul)**2 + (y - y_lr)**2 <= r**2:
        vx, vy = collide_circle_corner(x, y, r, vx, vy, x_ul, y_lr)
    # ⑧右下隅にぶつかった場合
    elif x > x_lr and y > y_lr and (x - x_lr)**2 + (y - y_lr)**2 <= r**2:
        vx, vy = collide_circle_corner(x, y, r, vx, vy, x_lr, y_lr)
ボールと長方形との角の衝突を改善した例.なおボールが角にぶつかりやすいように,ボールの半径を大きくしている.ボールが長方形の右上隅に衝突して速度ベクトルの向きが変わっていることが分かる.

Figure 79:ボールと長方形との角の衝突を改善した例.なおボールが角にぶつかりやすいように,ボールの半径を大きくしている.ボールが長方形の右上隅に衝突して速度ベクトルの向きが変わっていることが分かる.

障害物くぐりゲーム(Flappy Bird風)

Flappy Bird風の障害物くぐりゲームを作ろう.ボールを壁や長方形に衝突しないように操作し,衝突したらゲームオーバーとする.

マウスのクリックに応じて動きを変える

はじめに,ボールがyy軸方向にのみ動くようにし,またマウスをクリックしている間は上向きに,クリックしていないときは下向きに動くようにしよう.長方形はいったん消す.

    if mouseClicked:
        vy = -2
    else:
        vy = 2
    
    # 2. 位置(yのみ)を更新(加速度は使わない)
    y += vy*dt

これで,マウスをクリックしているときは上向き,クリックしていないときは下向きにボールが動くはずである.

衝突判定の拡張:複数の長方形と衝突判定

障害物として複数の長方形が画面に現れ,右から左に向かって動くようにしよう.そのためには複数の長方形を扱う必要があるので,「長方形との衝突」を扱う関数を作ろう.

def collision_test(x, y, r, x_ul, y_ul, x_lr, y_lr):
    """円と矩形の衝突判定(詳細版)"""
    # ①左辺にぶつかった場合
    if x < x_ul and y_ul <= y and y <= y_lr and x + r >= x_ul:
        return True
    # ②右辺にぶつかった場合
    elif x > x_lr and y_ul <= y and y <= y_lr and x - r <= x_lr:
        return True
    # ③上辺にぶつかった場合
    elif y < y_ul and x_ul <= x and x <= x_lr and y + r >= y_ul:
        return True
    # ④下辺にぶつかった場合
    elif y > y_lr and x_ul <= x and x <= x_lr and y - r <= y_lr:
        return True
    # ⑤左上隅にぶつかった場合
    elif x < x_ul and y < y_ul and (x - x_ul)**2 + (y - y_ul)**2 <= r**2:
        return True
    # ⑥右上隅にぶつかった場合
    elif x > x_lr and y < y_ul and (x - x_lr)**2 + (y - y_ul)**2 <= r**2:
        return True
    # ⑦左下隅にぶつかった場合
    elif x < x_ul and y > y_lr and (x - x_ul)**2 + (y - y_lr)**2 <= r**2:
        return True
    # ⑧右下隅にぶつかった場合
    elif x > x_lr and y > y_lr and (x - x_lr)**2 + (y - y_lr)**2 <= r**2:
        return True
    return False

障害物は上下セットで2つ×5段階程度を配置する.最初は画面の右外に配置し,だんだん画面内に入ってくるようにする.

以下のコードでは障害物で通り抜けられる部分の位置をランダムに決め,それより上とそれより下に障害物となる長方形が置かれるように座標を計算する.そしてobstaclesというリストを用意し,これに障害物となる長方形を格納している.

# 障害物群を作る.
import random
obstacles = []
span_x = 200    # 障害物を置く間隔
x_start = WIDTH + 50
for _ in range(5):
    # 障害物で「空いている部分」の高さを決める.
    hole_y = random.randint(30, HEIGHT - 90)
    obstacles.append( [ x_start, 0, x_start + 50, hole_y] )  # 上の矩形
    obstacles.append( [ x_start, hole_y + 80, x_start + 50, HEIGHT] )  # 下の矩形
    x_start += span_x

壁や長方形との衝突を判定する必要がある.衝突したらゲームオーバーにするので,跳ね返りを計算する必要はない.衝突したかどうかを保持する変数collidedを用意する.

collided = False

そしてメインループの中で,壁や障害物(長方形)との衝突を判定する.障害物は複数あるので,forループを使って個々の障害物との衝突を判定する.ここで先ほど作った衝突判定関数collision_testが使われている.

    # 4a. 壁にぶつかったら,ゲームオーバー
    if y - r <= 0 and vy < 0:
        collided = True
    elif y + r >= HEIGHT and vy > 0:
        collided = True
    
    # 4b. 矩形にぶつかったら,ゲームオーバー
    for obs in obstacles:
        x_ul, y_ul, x_lr, y_lr = obs
        if collision_test(x, y, r, x_ul, y_ul, x_lr, y_lr):
            collided = True
            break

障害物は右から左にゆっくりと動かす.つまり障害物のxx座標をだんだん小さくする.障害物が左の壁を通過したら,もういちど右側に持って行く.

        # 一番右の障害物の位置を調べる
        max_x = 0
        for o in obstacles:
            if o[0] > max_x:
                max_x = o[0]
        # 長方形を左に向かって少しずつ移動
        for obs in obstacles:
            obs[0] -= 0.5
            obs[2] -= 0.5
            # 画面外に出た障害物は,一番左にある障害物からx_span空けた位置に再配置
            if obs[2] < 0:
                obs[0] = max_x + span_x
                obs[2] = max_x + span_x + 50

障害物を画面に表示する機能も,忘れずにつけておく.

    # 6b. 障害物を描く
    for obs in obstacles:
        x_ul, y_ul, x_lr, y_lr = obs
        pygame.draw.rect(screen, (0, 0, 255), (x_ul, y_ul, x_lr - x_ul, y_lr - y_ul))

これでFlappy Bird風の障害物くぐりゲームの骨組みができた.

障害物くぐりゲームの画面

Figure 80:障害物くぐりゲームの画面

課題(LMS提出)

以下に課題に取り組み,その成果物のソースコード,および成果物について説明するレポートを提出せよ. 提出方法および提出期限はKU-LMSで確認せよ.

レポートには以下の内容を含めること.

  1. 作品の概要(ゲームのルールや目的)

  2. 操作方法および実行方法(使用ファイル、実行コマンド)

  3. 工夫した点・発展項目(課題2の場合)

  4. 実行中のスクリーンショット

ライセンス