はじめに
生成AIメニューを使って「PyxelとPymunk」のサンプルプログラムを作成するプロンプトをこれまで、試してきました。動かしてみて、自然なふるまいができるかどうか、検証、確認が、大切であるし、Pymunkで物理空間を生成し、描画はPyxelで行うというデジタルな実験装置をつくるのは、ものづくりの面白さだと思うところです。
高等学校では、物理基礎を履修する学校もあれば、ないところもあります。CGの世界では、物理的なふるまいは大切なところでもあります。とにかく動くというところから、ハマってもらえるきっかけがあれば、楽しくなると思うところです。
さて、デジタル実験装置のサンプルプログラム、コピペ道場のはじまりです。動かしてパラメータ変更していろいろ試してみてください。
3.1 位置と速度の基本概念
1つの半径20の円が初期位置から一定速度で動くシンプルな例。
速度は1次元(x方向)で固定。
画面に半径20の円を描画し、位置が毎フレーム更新される。
import pyxel
import pymunk
COLOR_WHITE = 7
class SimpleMovingCircle:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# Pyxelの初期化(マウスカーソルは非表示)
pyxel.init(self.width, self.height, title="Simple Moving Circle")
pyxel.mouse(False)
# 物理シミュレーションの初期化
self.reset()
# メインループ開始
pyxel.run(self.update, self.draw)
def reset(self):
# Pymunkの物理空間を作成
self.space = pymunk.Space()
# 重力なし(等速直線運動を観察するため)
self.space.gravity = (0, 0)
# オブジェクトがスリープ状態に入るまでの時間閾値
self.space.sleep_time_threshold = 0.5
# 円の物理ボディを作成
radius = 20
mass = 1
# 円の慣性モーメントを計算
moment = pymunk.moment_for_circle(mass, 0, radius)
self.body = pymunk.Body(mass, moment)
# 初期位置:左端、画面中央の高さ
self.body.position = (radius + 10, self.height // 2)
# 初期速度:右方向に400px/s
self.body.velocity = (400, 0)
# 円の形状(コライダー)を作成
shape = pymunk.Circle(self.body, radius)
# 弾性係数0.0(完全非弾性、反発なし)
shape.elasticity = 0.0
# 摩擦係数0.0(摩擦なし)
shape.friction = 0.0
# ボディと形状を物理空間に追加
self.space.add(self.body, shape)
self.shape = shape
self.radius = radius
def update(self):
# 物理シミュレーションを1フレーム分進める(必ず最初に実行)
self.space.step(1 / 60.0)
# Rキーでリセット
if pyxel.btnp(pyxel.KEY_R):
self.reset()
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 円の現在位置を取得
x, y = self.body.position
# 円を描画(Pyxelの座標系はそのまま使用)
pyxel.circ(int(x), int(y), self.radius, COLOR_WHITE)
if __name__ == "__main__":
SimpleMovingCircle()
- 等速直線運動:重力も摩擦もないため、円は初速度400px/sで右方向へ等速直線運動を続けます
- Pyxelの座標系:Pyxelは左上が原点(0,0)で下方向がY正のため、Pymunkの座標をそのまま使用(座標変換なし)
- シンプルな構成:地面や障害物がなく、円の等速運動のみを観察できる最小構成
3.2 1次元運動のプログラム(速度一定の直線運動)
画面640x480、半径20の円。
pymunkを使わずに、速度を変数化し、時間経過で位置が変化。
速度の意味、座標の更新式(位置 += 速度)を示す。
画面外に出たら左端に戻す。
import pyxel
class LinearMotion:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# 円の半径
self.radius = 20
# 初期位置:左端、画面中央の高さ
self.x = self.radius
self.y = self.height // 2
# 速度:右方向に8px/frame
self.vx = 8
# Pyxelの初期化とメインループ開始
pyxel.init(self.width, self.height, title="1D Linear Motion")
pyxel.run(self.update, self.draw)
def update(self):
# 位置を更新(等速直線運動)
self.x += self.vx
# 円が画面右端を完全に通過したら左端にリセット
if self.x - self.radius > self.width:
self.x = self.radius
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 円を描画(色8=赤)
pyxel.circ(int(self.x), int(self.y), self.radius, 8)
if __name__ == "__main__":
LinearMotion()
- 物理エンジン不使用:Pymunkを使わず、Pyxelのみで実装された等速直線運動
- シンプルな運動学:x += vx による最も基本的な位置更新
- ループ処理:画面右端を通過したら左端に戻る無限ループ
- フレームレート依存:速度8px/frameは、Pyxelのデフォルト60FPSで約480px/s(前のPymunk版の400px/sに近い速度)
前のPymunkコードとの比較:
- Pymunk版:物理エンジンによる厳密なシミュレーション(質量、慣性モーメント、物理空間)
- この版:直接的な座標計算による簡易実装(学習の初期段階に適している)
3.3 2次元ベクトルでの速度表現と加算
速度を(x, y)ベクトルで表現。
位置ベクトルに速度ベクトルを足す形で更新。
斜め方向にも動かせる。
画面640x480、半径20の円。
画面端でループさせる
import pyxel
class Vector2D:
"""2次元ベクトルクラス(位置や速度を表現)"""
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""ベクトルの加算演算子をオーバーロード"""
return Vector2D(self.x + other.x, self.y + other.y)
def copy(self):
"""ベクトルのコピーを作成"""
return Vector2D(self.x, self.y)
class MovingCircle:
"""2次元空間を移動する円オブジェクト"""
def __init__(self, pos, vel, radius, screen_width, screen_height):
self.pos = pos # 位置ベクトル
self.vel = vel # 速度ベクトル
self.radius = radius # 円の半径
self.screen_width = screen_width
self.screen_height = screen_height
def update(self):
# 位置を速度分だけ更新(ベクトル加算)
self.pos = self.pos + self.vel
# X方向の画面端処理(トーラス状のループ)
if self.pos.x < 0:
self.pos.x += self.screen_width
elif self.pos.x > self.screen_width:
self.pos.x -= self.screen_width
# Y方向の画面端処理(トーラス状のループ)
if self.pos.y < 0:
self.pos.y += self.screen_height
elif self.pos.y > self.screen_height:
self.pos.y -= self.screen_height
def draw(self):
"""円を描画(色8=赤)"""
pyxel.circ(int(self.pos.x), int(self.pos.y), self.radius, 8)
class App:
"""メインアプリケーションクラス"""
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
pyxel.init(self.width, self.height, title="2D Vector Velocity")
# 円の初期化:画面中央から斜め右下方向に移動
pos = Vector2D(self.width / 2, self.height / 2)
vel = Vector2D(4, 3) # 速度ベクトル(vx=4, vy=3)
self.circle = MovingCircle(pos, vel, 20, self.width, self.height)
# メインループ開始
pyxel.run(self.update, self.draw)
def update(self):
# 円の位置を更新
self.circle.update()
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 円を描画
self.circle.draw()
App()
- ベクトル演算の導入:Vector2Dクラスで位置と速度をベクトルとして扱う
- 2次元等速直線運動:速度ベクトル(4, 3)により斜め方向に移動
- トーラス状ループ:画面の上下左右の端から出ると反対側から現れる(パックマン方式)
- オブジェクト指向設計:MovingCircleクラスで円の振る舞いをカプセル化
前のコードからの進化:
- 1D → 2D:X方向のみ → X, Y両方向の運動
- スカラー → ベクトル:個別の変数 → ベクトルクラスによる統一的な扱い
- クラス設計:機能を適切に分割(Vector2D, MovingCircle, App)
3.4 ベクトルの大きさ(速度の大きさ)計算
画面640x480、半径20の円。
ベクトルの大きさ(速度の大きさ)計算
速度ベクトルの大きさ(速さ)を計算。
速度の大きさを画面に表示したり、速度の正規化(単位ベクトル化)を紹介。
import pyxel
import pymunk
import math
COLOR_WHITE = 7
COLOR_RED = 8
COLOR_GREEN = 3
class CircleSpeedSimulation:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# Pyxelの初期化
pyxel.init(self.width, self.height, title="Circle Speed Simulation")
# 物理シミュレーションの初期化
self.reset()
# メインループ開始
pyxel.run(self.update, self.draw)
def reset(self):
# Pymunkの物理空間を作成
self.space = pymunk.Space()
# 重力なし(等速直線運動を観察)
self.space.gravity = (0, 0)
# スリープ閾値の設定
self.space.sleep_time_threshold = 0.5
# 円の物理ボディを作成
mass = 5
radius = 20
# 円の慣性モーメントを計算
moment = pymunk.moment_for_circle(mass, 0, radius)
self.body = pymunk.Body(mass, moment)
# 初期位置:画面中央
self.body.position = (self.width / 2, self.height / 2)
# 円の形状を作成
self.shape = pymunk.Circle(self.body, radius)
# 弾性係数0.8(80%の反発)
self.shape.elasticity = 0.8
# 摩擦係数0.5(ただし空間内に他の物体がないため効果なし)
self.shape.friction = 0.5
# ボディと形状を物理空間に追加
self.space.add(self.body, self.shape)
# 初期速度:右上方向(vx=250, vy=-200)
# ※Pymunkは上方向が正のY軸
self.body.velocity = (250, -200)
def update(self):
# 物理シミュレーションを1フレーム分進める
self.space.step(1 / 60.0)
# Rキーでリセット
if pyxel.btnp(pyxel.KEY_R):
self.reset()
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 円の現在位置を取得
pos_x, pos_y = self.body.position
radius = self.shape.radius
# 速度ベクトルを取得
speed_x, speed_y = self.body.velocity
# 速度の大きさ(スカラー)を計算
speed = math.sqrt(speed_x ** 2 + speed_y ** 2)
# 速度ベクトルを正規化(単位ベクトル化)
if speed != 0:
norm_x = speed_x / speed
norm_y = speed_y / speed
else:
# ゼロ除算を防ぐ
norm_x = 0
norm_y = 0
# 円を描画(緑色)
pyxel.circ(int(pos_x), int(pos_y), int(radius), COLOR_GREEN)
# 正規化された速度ベクトルを赤い線で表示(円の中心から半径分)
pyxel.line(
int(pos_x), int(pos_y),
int(pos_x + norm_x * radius), int(pos_y + norm_y * radius),
COLOR_RED
)
# 速度の大きさを表示
pyxel.text(5, 5, f"Speed: {speed:.2f}", COLOR_WHITE)
# 正規化された速度ベクトルを表示
pyxel.text(5, 15, f"Normalized Velocity: ({norm_x:.2f}, {norm_y:.2f})", COLOR_WHITE)
if __name__ == "__main__":
CircleSpeedSimulation()
- 速度の可視化:速度ベクトルの方向を赤い線で表示
- ベクトルの正規化:速度ベクトルを単位ベクトルに変換(方向のみを表現)
- 速度の大きさ計算:speed = √(vx² + vy²) でスカラー速度を算出
- Pymunkの座標系:Y軸上向きが正(velocity = (250, -200)は右上方向)
学習ポイント:
- ベクトルの大きさ:ピタゴラスの定理による計算
- 正規化:各成分を大きさで割ることで単位ベクトル化
- 可視化の重要性:抽象的なベクトル概念を視覚的に理解
3.5 簡単な方向転換:ベクトルの向きを変える
画面640x480、半径20の円。
簡単な方向転換:ベクトルの向きを変える
左右矢印キー入力で速度ベクトルの向きを変える。
三角関数を使ってベクトルの向きを回転させる例。
画面端でループ処理
角度(度)を表示、速度ベクトル表示
import pyxel
import pymunk
import math
COLOR_WHITE = 7
COLOR_RED = 8
class CircleVectorRotate:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# Pyxelの初期化(マウスカーソル表示)
pyxel.init(self.width, self.height, title="Circle Vector Rotate")
pyxel.mouse(True)
# 物理シミュレーションの初期化
self.reset()
# メインループ開始
pyxel.run(self.update, self.draw)
def reset(self):
# Pymunkの物理空間を作成
self.space = pymunk.Space()
# 重力なし(等速直線運動)
self.space.gravity = (0, 0)
# スリープ閾値の設定
self.space.sleep_time_threshold = 0.5
# 円の物理ボディを作成
mass = 1
radius = 20
# 円の慣性モーメントを計算
moment = pymunk.moment_for_circle(mass, 0, radius)
self.body = pymunk.Body(mass, moment)
# 初期位置:画面中央
self.body.position = (self.width / 2, self.height / 2)
# 円の形状を作成
shape = pymunk.Circle(self.body, radius)
# 弾性係数0.9(90%反発)
shape.elasticity = 0.9
# 摩擦係数0.5
shape.friction = 0.5
# ボディと形状を物理空間に追加
self.space.add(self.body, shape)
self.radius = radius
# 初期角度:0度(右方向)
self.angle_deg = 0
# 初期速度:右方向に200px/s
speed = 200
self.vx = speed
self.vy = 0
self.body.velocity = (self.vx, self.vy)
def update(self):
# 物理シミュレーションを1フレーム分進める
self.space.step(1 / 60.0)
# 現在の位置と速度を取得
pos_x = self.body.position.x
pos_y = self.body.position.y
vel_x = self.body.velocity.x
vel_y = self.body.velocity.y
# Rキーでリセット
if pyxel.btnp(pyxel.KEY_R):
self.reset()
return
# 左矢印キー:反時計回りに10度回転
if pyxel.btnp(pyxel.KEY_LEFT):
self.angle_deg = (self.angle_deg + 10) % 360
# 右矢印キー:時計回りに10度回転
if pyxel.btnp(pyxel.KEY_RIGHT):
self.angle_deg = (self.angle_deg - 10) % 360
# 現在の速度の大きさを計算(math.hypot = √(x²+y²))
speed = math.hypot(vel_x, vel_y)
# 角度をラジアンに変換
rad = math.radians(self.angle_deg)
# 極座標から直交座標への変換(速度ベクトルの回転)
self.vx = speed * math.cos(rad)
self.vy = speed * math.sin(rad)
# 新しい速度を適用
self.body.velocity = (self.vx, self.vy)
# 画面端のループ処理(トーラス状)
x = self.body.position.x
y = self.body.position.y
# X方向の画面端処理
if x < -self.radius:
self.body.position = (self.width + self.radius, y)
elif x > self.width + self.radius:
self.body.position = (-self.radius, y)
# Y方向の画面端処理
if y < -self.radius:
self.body.position = (x, self.height + self.radius)
elif y > self.height + self.radius:
self.body.position = (x, -self.radius)
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# Pymunk座標系(Y上向き)からPyxel座標系(Y下向き)への変換
x = int(self.body.position.x)
y = int(self.height - self.body.position.y)
# 円を描画(赤色)
pyxel.circ(x, y, self.radius, COLOR_RED)
# 速度ベクトルを取得
vel_x = self.body.velocity.x
vel_y = self.body.velocity.y
# 速度ベクトルを白い線で表示(0.2倍にスケーリング)
vx_end_x = int(x + self.vx * 0.2)
vx_end_y = int(y - self.vy * 0.2) # Y軸反転
pyxel.line(x, y, vx_end_x, vx_end_y, COLOR_WHITE)
# 角度と速度の情報を表示
text_angle = f"Angle: {self.angle_deg} deg"
text_vel = f"Velocity: ({vel_x:.1f}, {vel_y:.1f})"
pyxel.text(5, 5, text_angle, COLOR_WHITE)
pyxel.text(5, 15, text_vel, COLOR_WHITE)
if __name__ == "__main__":
CircleVectorRotate()
- 速度ベクトルの回転:矢印キーで速度の方向を10度ずつ回転
- 極座標変換:角度と速度の大きさから直交座標(vx, vy)を計算
- 速度の大きさ保持:回転しても速度の大きさ(200px/s)は一定
- 座標系の変換:Pymunk(Y上向き) ↔ Pyxel(Y下向き)の変換処理
学習ポイント:
- 極座標と直交座標:vx = speed * cos(θ), vy = speed * sin(θ)
- math.hypot():√(x²+y²)を計算する関数(速度の大きさ)
- 剰余演算:(angle + 10) % 360 で角度を0-359度に正規化
- 座標系の注意:描画時のY軸反転(self.height - y, y - vy * 0.2)
3.6 複数物体の速度管理と更新
画面640x480、半径20の円。
複数のカラフルな円がそれぞれ異なる速度ベクトルで動く。
画面端でループさせる
リストや配列で物体を管理し、各物体の位置・速度を更新。
import pyxel
# Pyxelで使用可能な色のリスト(色番号1-15)
COLOR_LIST = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
class MultipleCircles:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# 円の半径
self.radius = 20
# Pyxelの初期化
pyxel.init(self.width, self.height, title="Multiple Moving Circles")
# 円の初期配置
self.reset()
# メインループ開始
pyxel.run(self.update, self.draw)
def reset(self):
# 円のリストを初期化
self.circles = []
# 40個の円を生成
for i in range(40):
# ランダムな初期位置(画面内に収まるように)
x = pyxel.rndi(self.radius, self.width - self.radius)
y = pyxel.rndi(self.radius, self.height - self.radius)
# ランダムな速度(-10 ~ 10 px/frame)
vx = pyxel.rndf(-10, 10)
vy = pyxel.rndf(-10, 10)
# 色をリストから順番に割り当て(15色を循環)
color = COLOR_LIST[i % len(COLOR_LIST)]
# 円の情報を辞書で保存
self.circles.append({"x": x, "y": y, "vx": vx, "vy": vy, "color": color})
def update(self):
# Rキーでリセット
if pyxel.btnp(pyxel.KEY_R):
self.reset()
# 全ての円の位置を更新
for c in self.circles:
# 位置を速度分だけ更新
c["x"] += c["vx"]
c["y"] += c["vy"]
# X方向の画面端処理(トーラス状ループ)
if c["x"] < -self.radius:
c["x"] = self.width + self.radius
elif c["x"] > self.width + self.radius:
c["x"] = -self.radius
# Y方向の画面端処理(トーラス状ループ)
if c["y"] < -self.radius:
c["y"] = self.height + self.radius
elif c["y"] > self.height + self.radius:
c["y"] = -self.radius
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 全ての円を描画
for c in self.circles:
pyxel.circ(int(c["x"]), int(c["y"]), self.radius, c["color"])
if __name__ == "__main__":
MultipleCircles()
- 複数オブジェクトの管理:リストで40個の円を一括管理
- ランダム生成:pyxel.rndi()(整数)とpyxel.rndf()(浮動小数点)でランダムな初期値
- 辞書によるデータ構造:各円の情報(x, y, vx, vy, color)を辞書で保存
- 色の循環割り当て:i % len(COLOR_LIST) で15色を繰り返し使用
前のコードからの進化:
- 単一オブジェクト → 複数オブジェクト(40個)
- 固定値 → ランダム生成(位置、速度)
- 単色 → 多色表示(15色の循環)
- リストとループによる効率的な管理
学習ポイント:
- リスト内包表記の代替:forループで辞書をリストに追加
- 剰余演算の活用:i % len(COLOR_LIST) で循環参照
- 辞書のキーアクセス:c["x"], c["vx"] などでデータを取得・更新
3.6.2 複数の円の衝突
import pyxel
import pymunk
# Pyxelで使用可能な色のリスト(色番号1-15)
COLOR_LIST = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
class MultipleCirclesCollision:
def __init__(self):
# 画面サイズの設定
self.width = 640
self.height = 480
# 円の半径
self.radius = 20
# Pyxelの初期化
pyxel.init(self.width, self.height, title="Multiple Circles with Collision")
# 物理シミュレーションの初期化
self.reset()
# メインループ開始
pyxel.run(self.update, self.draw)
def reset(self):
# Pymunkの物理空間を作成
self.space = pymunk.Space()
# 重力なし(等速運動を観察)
self.space.gravity = (0, 0)
# スリープ閾値の設定
self.space.sleep_time_threshold = 0.5
# 円のリストを初期化
self.circles = []
# 40個の円を生成
for i in range(40):
# ランダムな初期位置(画面内に収まるように)
x = pyxel.rndi(self.radius + 10, self.width - self.radius - 10)
y = pyxel.rndi(self.radius + 10, self.height - self.radius - 10)
# ランダムな速度(-200 ~ 200 px/s)
vx = pyxel.rndf(-200, 200)
vy = pyxel.rndf(-200, 200)
# 色をリストから順番に割り当て(15色を循環)
color = COLOR_LIST[i % len(COLOR_LIST)]
# 円の物理ボディを作成
mass = 1
# 円の慣性モーメントを計算
moment = pymunk.moment_for_circle(mass, 0, self.radius)
body = pymunk.Body(mass, moment)
body.position = (x, y)
body.velocity = (vx, vy)
# 円の形状を作成
shape = pymunk.Circle(body, self.radius)
# 弾性係数1.0(完全弾性衝突)
shape.elasticity = 1.0
# 摩擦係数0.0(摩擦なし)
shape.friction = 0.0
# ボディと形状を物理空間に追加
self.space.add(body, shape)
# 円の情報を保存
self.circles.append({
"body": body,
"shape": shape,
"color": color
})
def update(self):
# 物理シミュレーションを1フレーム分進める
self.space.step(1 / 60.0)
# Rキーでリセット
if pyxel.btnp(pyxel.KEY_R):
self.reset()
return
# 全ての円に対して画面端処理(トーラス状ループ)
for c in self.circles:
x = c["body"].position.x
y = c["body"].position.y
vx, vy = c["body"].velocity
# X方向の画面端処理
if x < -self.radius:
c["body"].position = (self.width + self.radius, y)
elif x > self.width + self.radius:
c["body"].position = (-self.radius, y)
# Y方向の画面端処理
if y < -self.radius:
c["body"].position = (x, self.height + self.radius)
elif y > self.height + self.radius:
c["body"].position = (x, -self.radius)
def draw(self):
# 画面を黒でクリア
pyxel.cls(0)
# 全ての円を描画
for c in self.circles:
# Pymunk座標系(Y上向き)からPyxel座標系(Y下向き)への変換
x = int(c["body"].position.x)
y = int(self.height - c["body"].position.y)
pyxel.circ(x, y, self.radius, c["color"])
# 情報表示
pyxel.text(5, 5, f"Circles: {len(self.circles)}", 7)
pyxel.text(5, 15, "Press R to reset", 7)
if __name__ == "__main__":
MultipleCirclesCollision()
1 Pymunkの導入:
- pymunk.Space() で物理空間を作成
- 各円に Body と Circle 形状を設定
2 衝突の実装:
- shape.elasticity = 1.0:完全弾性衝突(エネルギー保存)
- Pymunkが自動的に円同士の衝突を検出・処理
3 座標系の変換:
- 描画時に y = int(self.height - c["body"].position.y) でY軸を反転
- Pymunkは上向きが正、Pyxelは下向きが正
4 データ構造の変更:
- 辞書に body, shape, color を保存
- 位置・速度は body オブジェクトが管理
このコードの特徴:
- 物理エンジンによる衝突:Pymunkが運動量保存則に従って衝突を計算
- 完全弾性衝突:エネルギー損失なし(円は永遠に動き続ける)
- 40個の同時衝突処理:複数オブジェクト間の衝突を自動的に処理
■「AIに書かせる」のではなく、「AIに下書きさせて、人間が仕上げる」というスタンスが大切です。
■「わかったつもり」になる(The Illusion of Competence)「自分はすごいものを作れる」と錯覚します。「なぜそのコードで動くのか」を必ず検証しながら、探求していきましょう。
いろいろ、試したくなります。ありがとうございます。

