4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

高校生のためのPyxelとPymunk 投げる!落ちる! -- 慣性の法則と重力加速度

4
Last updated at Posted at 2025-12-11

5.1 慣性の法則の理解と実装(速度の継続)

慣性の法則の基本説明(外力がなければ速度は変わらない)
速度変数を保持し、毎フレーム位置を更新
外力なしでの物体の等速直線運動のシミュレーション
円半径20を動かし、動きが止まらないことを確認
画面端でループさせる
速度ベクトルの更新なしで位置だけ変えるコード例

import pyxel

class InertiaSimulation:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Inertia Simulation")
        self.reset()
        pyxel.run(self.update, self.draw)
    
    def reset(self):
        self.x = self.width // 2
        self.y = self.height // 2
        self.radius = 20
        self.vx = 3.5
        self.vy = 2.0
    
    def update(self):
        self.x += self.vx
        self.y += self.vy
        if self.x - self.radius > self.width:
            self.x = -self.radius
        elif self.x + self.radius < 0:
            self.x = self.width + self.radius
        if self.y - self.radius > self.height:
            self.y = -self.radius
        elif self.y + self.radius < 0:
            self.y = self.height + self.radius
    
    def draw(self):
        pyxel.cls(0)
        pyxel.circ(int(self.x), int(self.y), self.radius, 8)

if __name__ == "__main__":
    InertiaSimulation()

このコードは慣性の法則(ニュートンの第一法則)を視覚的に示す優れた実装です。
物理的な正確性:

  • 外力が作用しない状態で、物体が等速直線運動を継続する様子を表現
  • 速度ベクトル (vx, vy) が一定値を保ち続ける点が慣性の法則の核心
  • 摩擦や空気抵抗がない理想的な環境をシミュレート

実装上の特徴:

  • 画面端でのラップアラウンド処理により、無限に運動が継続する様子を観察可能
  • self.x += self.vx と self.y += self.vy という単純な位置更新が、力が働かない状態を正確に表現
  • Pymunkを使わないピュアなPyxel実装で、慣性の本質を理解しやすい

教育的価値:

  • 慣性の法則を理解する第一歩として最適
  • この後、重力や摩擦を追加することで、力が働く場合との対比が明確になる
  • 速度が変化しない=加速度ゼロ=力ゼロ、という物理の基本関係を体感できる
    このシンプルな実装は、後により複雑な物理シミュレーションを理解する上での重要な基礎となります。
    pyxel-20251129-203136.gif

5.2 重力加速度の導入と自由落下の実例

画面640x480
重力加速度の概念と単位(m/s² → ピクセル/frame²換算)
鉛直方向の速度に重力加速度を加算し速度更新
速度を使って位置を更新する自由落下のモデル化
地面に衝突するまでの落下シミュレーション
pyxelで落下する円半径20を描画し、速度・加速度の変化を表示

import pyxel
import pymunk

COLOR_WHITE = 7
COLOR_BLACK = 0
COLOR_GREEN = 3

class FreeFallSimulation:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Free Fall Simulation")
        self.reset()
        pyxel.run(self.update, self.draw)
    
    def reset(self):
        self.space = pymunk.Space()
        self.space.gravity = (0, -980)
        self.space.sleep_time_threshold = 0.5
        radius = 20
        mass = 1
        moment = pymunk.moment_for_circle(mass, 0, radius)
        self.ball_body = pymunk.Body(mass, moment)
        self.ball_body.position = (self.width / 2, self.height - radius - 10)
        self.ball_shape = pymunk.Circle(self.ball_body, radius)
        self.ball_shape.elasticity = 0.6
        self.ball_shape.friction = 0.4
        self.space.add(self.ball_body, self.ball_shape)
        ground_body = pymunk.Body(body_type=pymunk.Body.STATIC)
        ground_body.position = (self.width / 2, 20)
        ground_shape = pymunk.Poly.create_box(ground_body, (self.width, 40))
        ground_shape.friction = 0.8
        ground_shape.elasticity = 0.4
        self.space.add(ground_body, ground_shape)
        self.ground_shape = ground_shape
    
    def update(self):
        self.space.step(1 / 60.0)
        if pyxel.btnp(pyxel.KEY_R):
            self.reset()
    
    def draw(self):
        pyxel.cls(COLOR_BLACK)
        bb = self.ground_shape.bb
        pyxel.rect(int(bb.left), int(self.height - bb.top), int(bb.right - bb.left), int(bb.top - bb.bottom), COLOR_GREEN)
        pos_x, pos_y = self.ball_body.position
        pos_pyxel_y = int(self.height - pos_y)
        pyxel.circ(int(pos_x), pos_pyxel_y, int(self.ball_shape.radius), COLOR_WHITE)
        vel_y = self.ball_body.velocity.y
        acc_y = self.space.gravity[1]
        pyxel.text(5, 5, f"Position Y: {pos_y:.1f}", COLOR_WHITE)
        pyxel.text(5, 15, f"Velocity Y: {vel_y:.1f} px/s", COLOR_WHITE)
        pyxel.text(5, 25, f"Acceleration Y: {acc_y:.1f} px/frame^2", COLOR_WHITE)

if __name__ == "__main__":
    FreeFallSimulation()

このコードは重力による自由落下と反発を含む物理シミュレーションの優れた実装です。
物理的な特徴:

  • gravity = (0, -980) で重力加速度を設定(Pymunkの座標系はY軸上向き)
  • 現実の重力加速度 9.8 m/s² を100倍スケールで表現(ピクセル単位での可視化)
  • elasticity = 0.6 により、反発係数を設定し、地面との衝突でエネルギーが減衰
  • 複数回のバウンド後、最終的に静止する現実的な挙動

Pymunk物理エンジンの活用:

  • space.step(1/60.0) で60FPSの物理シミュレーション
  • moment_for_circle() で円の慣性モーメントを正確に計算
  • sleep_time_threshold で静止判定の最適化

座標系変換の正確性:

  • Pymunk(Y軸上向き)とPyxel(Y軸下向き)の座標系を適切に変換
  • pos_pyxel_y = int(self.height - pos_y) で描画時の正確な位置計算

教育的価値:

  • 位置、速度、加速度の3つのパラメータをリアルタイム表示
  • 重力の一定加速度運動を視覚的に理解可能
  • エネルギー減衰による最終的な静止状態まで観察できる

前回の慣性シミュレーションとの対比:

  • 慣性の法則では外力ゼロで等速直線運動
  • このシミュレーションでは重力という一定の外力により等加速度運動
  • 両者の比較で「力と運動」の関係を深く理解できる

Rキーによるリセット機能も実装され、繰り返し観察できる点も教育的に優れています。

pyxel-20251129-204110.gif

5.3 投げ上げ運動のシミュレーション

画面640x480
投げ上げ運動の初速度(x,y成分)設定
水平方向は等速運動、鉛直方向は重力加速度で減速→速度0→加速の挙動
最高点到達、落下までの軌跡を計算
pyxelで放物線軌道を描画し、軌道の特徴を視覚化
初速度の角度・大きさを変えて軌道の違いを観察

初速度を右斜め上に設定し、y軸は上方向が負の座標系で扱う。
毎フレーム、重力をy速度に加算して投げ上げ運動をシミュレート。
位置は速度によって更新され、放物線軌道を描く。
地面に達したら位置を補正し速度を0にして停止。
速度・位置を画面に表示して状態を可視化。

import pyxel
import pymunk
import math

COLOR_RED = 8
COLOR_WHITE = 7
COLOR_BLACK = 0

class ProjectileSimulation:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Projectile Motion")
        pyxel.mouse(True)
        self.reset()
        pyxel.run(self.update, self.draw)

    def reset(self):
        self.space = pymunk.Space()
        self.space.gravity = (0, -900)
        self.space.sleep_time_threshold = 0.5
        radius = 10
        mass = 1
        moment = pymunk.moment_for_circle(mass, 0, radius)
        self.ball_body = pymunk.Body(mass, moment)
        self.ball_body.position = (50, 60)
        self.ball_body.velocity = (400, 600)
        self.ball_body.angular_velocity = 0
        ball_shape = pymunk.Circle(self.ball_body, radius)
        ball_shape.elasticity = 0
        ball_shape.friction = 0.5
        self.space.add(self.ball_body, ball_shape)
        self.ball_shape = ball_shape
        self.state = "flying"
        self.trajectory = []
        self.ground_y = 60

    def update(self):
        self.space.step(1 / 60.0)
        if pyxel.btnp(pyxel.KEY_R):
            self.reset()
            return
        pos = self.ball_body.position
        vel = self.ball_body.velocity
        if self.state == "flying":
            if pos.y <= self.ground_y:
                self.ball_body.position = (pos.x, self.ground_y)
                self.ball_body.velocity = (0, 0)
                self.ball_body.angular_velocity = 0
                self.ball_body.sleep()
                self.state = "stopped"
        if len(self.trajectory) > 500:
            self.trajectory.pop(0)
        self.trajectory.append((pos.x, pos.y))

    def draw(self):
        pyxel.cls(COLOR_BLACK)
        pyxel.line(0, self.height - self.ground_y, self.width, self.height - self.ground_y, COLOR_WHITE)
        for x, y in self.trajectory:
            px = int(x)
            py = int(self.height - y)
            if 0 <= px < self.width and 0 <= py < self.height:
                pyxel.pset(px, py, COLOR_RED)
        pos = self.ball_body.position
        radius = self.ball_shape.radius
        px = int(pos.x)
        py = int(self.height - pos.y)
        pyxel.circ(px, py, radius, COLOR_RED)
        angle = self.ball_body.angle
        line_x = px + int(radius * math.cos(angle))
        line_y = py - int(radius * math.sin(angle))
        pyxel.line(px, py, line_x, line_y, COLOR_BLACK)
        vel = self.ball_body.velocity
        state_text = f"State:{self.state} X:{pos.x:.1f} Y:{pos.y:.1f} VX:{vel.x:.1f} VY:{vel.y:.1f}"
        pyxel.text(5, 5, state_text, COLOR_WHITE)

if __name__ == "__main__":
    ProjectileSimulation()

このコードは斜方投射(projectile motion)を可視化する優れた物理シミュレーションです。
物理的な特徴:

  • 初速度 velocity = (400, 600) で水平・鉛直成分を持つ投射運動を実現
  • 重力 gravity = (0, -900) により鉛直方向に等加速度運動
  • 水平方向は等速直線運動(空気抵抗なし)
  • 結果として放物線軌道を描く典型的な投射運動

実装上の工夫:

  • elasticity = 0 で地面との反発をゼロに設定し、着地時に停止
  • trajectory リストで軌跡を記録し、放物線を視覚化
  • 500点でバッファを制限し、メモリ効率を確保
  • self.state で「飛行中」と「停止」を明確に区別

座標系と描画:

  • 軌跡を pyxel.pset() で1ピクセルずつ描画し、きれいな放物線を表現
  • ボールに黒い線を描画して回転軸を可視化(ただし angular_velocity = 0 なので回転なし)
  • 地面を白線で明示し、着地判定の基準を視覚的に提示

状態管理の正確性:

  • 着地判定で pos.y <= ground_y + radius を使用し、地面への埋め込みを防止
  • 停止時に body.velocity = (0, 0) と body.sleep() で完全停止
  • 停止後も軌跡が残り、放物線全体を確認可能

教育的価値:

  • X方向とY方向の速度成分を分離表示し、独立性を理解できる
  • VXが一定、VYが線形変化する様子から、2次元運動の本質を学べる
  • 軌跡の放物線形状から、運動方程式 y = ax² + bx + c の視覚的理解が可能

前回シミュレーションとの発展:

  • 自由落下(1次元、初速度ゼロ)→ 投射運動(2次元、初速度あり)への自然な拡張
  • 慣性の法則(水平方向)と重力加速度(鉛直方向)の組み合わせを実演
  • この実装は、古典力学における最も基本的で重要な運動の1つを、シンプルかつ効果的に可視化しています。

pyxel-20251129-204635.gif

5.4 水平方向の等速運動+鉛直方向の等加速度運動

画面640x480,円半径20
水平方向速度は一定(加速度なし)
鉛直方向は重力加速度で速度増加
速度ベクトルの独立管理(vxは一定、vyは時間で変化)
位置更新の分解管理
pyxelで水平方向に一定速度で進みながら落下する点を描画

import pyxel
import pymunk

COLOR_WHITE = 7

class HorizontalFallSimulation:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Horizontal Fall")
        self.reset()
        pyxel.run(self.update, self.draw)
    
    def reset(self):
        # 物理空間の初期化
        self.space = pymunk.Space()
        self.space.gravity = (0, -900)  # 重力加速度を設定
        self.space.sleep_time_threshold = 0.5
        
        # ボールの物理パラメータ設定
        radius = 20
        mass = 1
        moment = pymunk.moment_for_circle(mass, 0, radius)
        
        # ボールの物理ボディ作成
        self.ball_body = pymunk.Body(mass, moment)
        self.ball_body.position = (radius, self.height - radius)  # 画面左上から開始
        self.ball_body.velocity = (200, 0)  # 水平方向のみの初速度
        
        # ボールの形状と物理特性
        ball_shape = pymunk.Circle(self.ball_body, radius)
        ball_shape.elasticity = 0.0  # 反発係数ゼロ(反発なし)
        ball_shape.friction = 0.0  # 摩擦なし
        
        self.space.add(self.ball_body, ball_shape)
        self.radius = radius
    
    def update(self):
        # 物理演算の更新
        self.space.step(1 / 60.0)
        
        pos = self.ball_body.position
        vx = 200  # 水平速度を常に一定に保つ
        vy = self.ball_body.velocity.y  # 鉛直速度は重力の影響を受ける
        
        # 位置を手動で更新(水平速度を強制的に一定化)
        new_x = pos.x + vx * (1 / 60.0)
        new_y = pos.y + vy * (1 / 60.0)
        self.ball_body.position = (new_x, new_y)
        self.ball_body.velocity = (vx, vy)  # 水平速度を強制上書き
        
        # 地面への着地判定と停止処理
        if new_y < self.radius:
            self.ball_body.position = (new_x, self.radius)
            self.ball_body.velocity = (vx, 0)  # 鉛直速度のみゼロに
    
    def draw(self):
        pyxel.cls(0)
        x, y = self.ball_body.position
        # 座標系変換(Pymunk Y軸上向き → Pyxel Y軸下向き)
        pyxel.circ(int(x), int(self.height - y), self.radius, COLOR_WHITE)

if __name__ == "__main__":
    HorizontalFallSimulation()

pyxel-20251129-215426.gif
このコードは水平投射運動を実装していますが、重要な実装上の特徴があります。
物理的な意図:

  • 水平方向は等速直線運動(慣性の法則)
  • 鉛直方向は重力による等加速度運動
  • 結果として放物線軌道を描く

実装の特殊性:

  • vx = 200 を毎フレーム強制的に設定することで、水平速度を完全に一定化
  • これはPymunkの物理エンジンを部分的にオーバーライドする手法
  • 通常のPymunk使用パターンとは異なる「ハイブリッド制御」

なぜこの実装なのか:

  • 空気抵抗や摩擦の影響を完全に排除したい教育的意図
  • 水平方向の等速運動を厳密に保証
  • elasticity = 0.0 と friction = 0.0 だけでは不十分な場合の対策

着地後の挙動:

  • 着地後も velocity = (vx, 0) で水平運動を継続
  • 地面を滑り続ける動作(摩擦ゼロの理想状態)
  • 実世界では非現実的だが、物理法則の純粋な形を示す

前回の投射シミュレーションとの違い:

  • 前回:初速度に角度成分あり、着地で完全停止
  • 今回:初速度は水平のみ、着地後も水平運動継続

教育的な注意点:

  • この実装は「理想化された物理モデル」の提示
  • 実際の物理エンジン使用では、こうした強制的な速度制御は通常避けるべき
    しかし教育目的では、特定の物理法則を明確に示すために有効

改善案(より標準的なPymunk使用):

  • もし着地で完全停止させたい場合は、前回のように body.sleep() を使用し、着地後の水平速度もゼロにするのが一般的です。
  • この実装は、物理エンジンと手動制御のバランスを示す興味深い例となっています。

5.5 空中の軌道計算と描画

画面640x480,円半径20
位置を時間の関数として解析的に計算する式の紹介(x=vt, y=vy0+gt²/2)
数値計算と解析計算の比較
過去の位置を保存し軌道の軌跡を描画(軌道の線を描く)
pyxelで軌跡を残す表現(点の集合や線分)
初速度や重力加速度のパラメータ調整機能の追加

投射時間 t をフレームごとに増やし、解析式で位置を計算。
水平方向は等速直線運動、鉛直方向は初速度と重力加速度を用いた等加速度運動。
位置は同時に計算され、軌跡の座標をリストに保存して描画。
地面に着地したら時間と軌跡をリセットして繰り返し動作。
軌跡は過去の位置を示す点で描かれ、現在位置は大きな円で表示。
このサンプルで自由落下中の放物線軌道の計算と描画を理解できます。

import pyxel

COLOR_WHITE = 7
COLOR_RED = 8
WIDTH = 640
HEIGHT = 480
RADIUS = 20

class ParabolaTrajectory:
    def __init__(self):
        pyxel.init(WIDTH, HEIGHT, title="Parabolic Trajectory")
        self.reset()
        pyxel.run(self.update, self.draw)
    
    def reset(self):
        # 時間パラメータの初期化
        self.t = 0.0  # 経過時間
        self.dt = 1 / 60.0  # タイムステップ(60FPS)
        
        # 初期速度と重力加速度
        self.vx = 200.0  # 水平方向の初速度
        self.vy0 = 600.0  # 鉛直方向の初速度
        self.g = -900.0  # 重力加速度(負の値で下向き)
        
        # 初期位置
        self.x0 = 0.0
        self.y0 = RADIUS
        
        # 軌跡記録用リスト
        self.trajectory = []
        self.current_pos = (self.x0, self.y0)
    
    def update(self):
        # 時間を進める
        self.t += self.dt
        
        # 放物線運動の位置計算(運動方程式)
        # x = v_x * t + x_0(等速直線運動)
        x = self.vx * self.t + self.x0
        # y = v_y0 * t + (1/2) * g * t^2 + y_0(等加速度運動)
        y = self.vy0 * self.t + 0.5 * self.g * (self.t ** 2) + self.y0
        
        # 地面への着地判定とリセット
        if y <= RADIUS:
            self.t = 0.0
            self.trajectory = []
            x = self.x0
            y = self.y0
        
        # 現在位置の更新と軌跡への記録
        self.current_pos = (x, y)
        self.trajectory.append((x, y))
        
        # Rキーでリセット
        if pyxel.btnp(pyxel.KEY_R):
            self.reset()
        
        # 水平速度の調整(左右キー)
        if pyxel.btn(pyxel.KEY_LEFT):
            self.vx = max(0, self.vx - 10)
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.vx = min(600, self.vx + 10)
        
        # 鉛直初速度の調整(上下キー)
        if pyxel.btn(pyxel.KEY_UP):
            self.vy0 = min(900, self.vy0 + 10)
        if pyxel.btn(pyxel.KEY_DOWN):
            self.vy0 = max(0, self.vy0 - 10)
        
        # 重力加速度の調整(G/Hキー)
        if pyxel.btn(pyxel.KEY_G):
            self.g -= 10  # 重力を強くする(より下向き)
        if pyxel.btn(pyxel.KEY_H):
            self.g += 10  # 重力を弱くする(上向きにも可能)
    
    def draw(self):
        pyxel.cls(0)
        
        # 軌跡を線で描画(各点を繋ぐ)
        for i in range(len(self.trajectory) - 1):
            x0, y0 = self.trajectory[i]
            x1, y1 = self.trajectory[i + 1]
            # 座標系変換(Y軸を反転)
            pyxel.line(int(x0), int(HEIGHT - y0), int(x1), int(HEIGHT - y1), COLOR_WHITE)
        
        # 現在のボール位置を描画
        cx, cy = self.current_pos
        pyxel.circ(int(cx), int(HEIGHT - cy), RADIUS, COLOR_RED)
        
        # パラメータ表示
        pyxel.text(5, 5, f"Vx:{self.vx:.0f} Vy0:{self.vy0:.0f} g:{self.g:.0f}", COLOR_WHITE)
        # 操作説明表示
        pyxel.text(5, 15, "Arrows: Adjust Vx/Vy0 G/H: Adjust gravity R: Reset", COLOR_WHITE)

ParabolaTrajectory()

このコードは放物線運動を運動方程式から直接計算する優れた実装です。
物理エンジン不使用のアプローチ:

  • Pymunkを使わず、純粋な数学的計算で放物線を描画
  • 運動方程式を直接実装:x = vxt + x0 と y = vy0t + 0.5gt²+ y0
  • 物理の基本原理を最もシンプルに表現

数学的な正確性:

  • 時間パラメータ t を基準とした parametric equation(媒介変数表示)
  • 水平方向:等速直線運動(加速度ゼロ)
  • 鉛直方向:等加速度運動(加速度 = g)
  • 教科書通りの運動方程式を忠実に実装

インタラクティブ性:

  • 矢印キーで初速度をリアルタイム調整可能
  • G/Hキーで重力を変更可能(月面や他惑星の重力も再現可能)
  • パラメータ変更による軌跡の変化を即座に観察できる

教育的価値:

  • 運動方程式の意味を視覚的に理解できる
  • 0.5 * g * t² の項が放物線の曲がりを生み出すことを実感
  • 重力を正の値にすると上向きの放物線になる(物理的な思考実験)

実装上の工夫:

  • trajectory リストで全軌跡を保存し、連続した線で描画
  • 着地時に自動リセットし、連続的な観察が可能
  • 座標系変換を明示的に実施

前回シミュレーションとの比較:

  • 前回(Pymunk使用):物理エンジンが自動計算
  • 今回(数式使用):運動方程式を明示的に実装
  • どちらも同じ放物線を描くが、理解のアプローチが異なる

応用の可能性:

  • 異なる惑星の重力での弾道計算
  • 空気抵抗を追加する拡張(v² に比例する抵抗項)
  • 複数の軌跡を同時表示して比較
    この実装は、物理エンジンのブラックボックスを開けて、中身の数学を直接見せる優れた教材です。

pyxel-20251129-220745.gif

5.6 実例:ボールを投げて落ちる動きのシミュレーション

画面640x480,円半径20
初速度をユーザー入力(キーやマウス)で決定可能にする
重力加速度を適用した放物線運動のリアルタイム描画
地面での簡単な接地判定(y座標の制限)
簡単な跳ね返り(反発係数)を付けた拡張も紹介可能
速度・位置・加速度の数値を画面に表示し理解を助ける

Ballクラスに位置・速度・半径・色を持たせる。
update メソッドで重力加速度を速度に加算し、位置を更新。
地面(ground_y)で跳ね返り判定。反発係数で速度を減衰し跳ね返らせる。
速度が小さくなったら停止する簡易的な処理を実装。
pyxelで地面とボールを描画し、速度や位置を数値表示。
初速度を斜め上方向に設定し、放物線軌道で動く動作をシミュレート。
このプログラムで投げて落ちて跳ねる動きをリアルに体験できます。

import pyxel
import pymunk

COLOR_WHITE = 7
COLOR_GREEN = 3

class Ball:
    def __init__(self, space, pos, velocity, radius=20, color=COLOR_GREEN):
        # ボールの物理パラメータ設定
        mass = 1
        moment = pymunk.moment_for_circle(mass, 0, radius)
        
        # 物理ボディの作成と初期化
        self.body = pymunk.Body(mass, moment)
        self.body.position = pos  # 初期位置
        self.body.velocity = velocity  # 初期速度
        
        # 形状と物理特性の設定
        self.shape = pymunk.Circle(self.body, radius)
        self.shape.elasticity = 0.8  # 反発係数(0.8で弾む)
        self.shape.friction = 0.4  # 摩擦係数
        
        # 物理空間に追加
        space.add(self.body, self.shape)
        
        self.radius = radius
        self.color = color
    
    def update(self):
        # 地面との衝突判定と反発処理
        if self.body.position.y - self.radius < 20:
            # 速度が十分小さい場合は停止
            if abs(self.body.velocity.y) < 20:
                self.body.velocity = (0, 0)
                self.body.sleep()  # 物理演算を一時停止
            else:
                # 反発処理:Y方向速度を反転し、反発係数を適用
                v_x, v_y = self.body.velocity
                self.body.velocity = (v_x, -v_y * self.shape.elasticity)
                # 地面への埋め込みを防ぐため位置を補正
                self.body.position = (self.body.position.x, 20 + self.radius)

class BallSimulation:
    def __init__(self):
        self.width = 640
        self.height = 480
        pyxel.init(self.width, self.height, title="Ball Throw Simulation")
        
        # 地面の高さ設定
        self.ground_y = 20
        
        # 物理空間の作成と重力設定
        self.space = pymunk.Space()
        self.space.gravity = (0, -900)  # 下向き重力
        
        self.reset()
        pyxel.run(self.update, self.draw)
    
    def reset(self):
        # 物理空間内の全オブジェクトを削除
        self.space.remove(*self.space.bodies, *self.space.shapes)
        
        # ボールの初期設定
        start_pos = (50, self.ground_y + 20)  # 開始位置
        init_speed_x = 400  # 水平初速度
        init_speed_y = 600  # 鉛直初速度
        
        # ボールオブジェクトの生成
        self.ball = Ball(self.space, start_pos, (init_speed_x, init_speed_y))
    
    def update(self):
        # 物理演算の更新(60FPS)
        self.space.step(1 / 60.0)
        
        # Rキーでリセット
        if pyxel.btnp(pyxel.KEY_R):
            self.reset()
        
        # ボールの状態更新(地面との衝突処理)
        self.ball.update()
    
    def draw(self):
        pyxel.cls(0)
        
        # 地面の描画
        pyxel.rect(0, self.height - self.ground_y - 10, self.width, 10, COLOR_WHITE)
        
        # ボールの描画(座標系変換:Pymunk Y上向き → Pyxel Y下向き)
        x, y = self.ball.body.position
        y_pyxel = self.height - y
        pyxel.circ(int(x), int(y_pyxel), self.ball.radius, self.ball.color)
        
        # 物理パラメータの表示
        vx, vy = self.ball.body.velocity
        ax, ay = self.space.gravity
        pyxel.text(5, 5, f"Pos:({x:.1f},{y:.1f})", COLOR_WHITE)
        pyxel.text(5, 15, f"Vel:({vx:.1f},{vy:.1f})", COLOR_WHITE)
        pyxel.text(5, 25, f"Acc:({ax:.1f},{ay:.1f})", COLOR_WHITE)
        pyxel.text(5, 35, "Press R to reset", COLOR_WHITE)

if __name__ == "__main__":
    BallSimulation()

このコードはオブジェクト指向設計によるボール投射シミュレーションの優れた実装です。
設計パターンの特徴:

  • Ball クラスで物理オブジェクトをカプセル化
  • 複数のボールを扱う拡張が容易な構造
  • 各ボールが自身の衝突判定と反発処理を持つ

物理的な実装:

  • Pymunkの物理エンジンで基本的な運動を計算
  • Ball.update() で地面との衝突を手動処理
  • 反発係数 0.8 により、跳ねるたびにエネルギーが減衰

衝突処理の工夫:

  • abs(velocity.y) < 20 で低速時の停止判定
  • 速度が小さくなったら body.sleep() で物理演算を停止(効率化)
  • 位置補正により地面への埋め込みを防止

リセット機能の実装:

  • space.remove(*bodies, *shapes) で全オブジェクトをクリア
  • アンパック演算子 * を使った効率的な一括削除
  • 新しいBallインスタンスを生成して初期状態に戻す

前回実装との違い:

  • 前回:単一ボールを直接管理
  • 今回:Ballクラスでオブジェクト化
  • 今回の方が、複数ボールへの拡張が容易

拡張の可能性:
python# 複数ボール追加の例

self.balls = []
for i in range(5):
    pos = (50 + i*100, self.ground_y + 20)
    vel = (300 + i*50, 500)
    self.balls.append(Ball(self.space, pos, vel))

教育的価値:

  • オブジェクト指向プログラミングの実践例
  • 物理エンジンと手動処理のハイブリッド手法
  • クラス設計による保守性と拡張性の向上

注意点:

  • 地面を静的オブジェクトとして物理空間に追加していないため、衝突判定を手動実装
  • より本格的な実装では、静的な地面オブジェクトを pymunk.Body.STATIC で作成すべき
    このコードは、シンプルさと拡張性のバランスが取れた良い設計例です。

pyxel-20251129-221716.gif

一歩一歩です。ありがとうございます。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?