100日アプリ開発チャレンジ Day2 - エアホッケーで学んだ「物理演算」の沼
Day2、まだ続いてます
100日チャレンジDay2。
昨日「明日はもっとシンプルなものを作ります」って言いましたよね。
嘘つきました(笑)
今日はエアホッケーゲームを作りました。物理演算、AI、衝突判定...。昨日より全然複雑でした。

なぜエアホッケーだったのか
実は、ゲームセンターでエアホッケー台を見かけたんです。
「あ、これならインベーダーより簡単そう」
この判断、今日最大の過ちでした。
見た目はシンプル。パックが滑って、パドルで打って、ゴールに入れるだけ。
でも実際は...
物理演算という名の地獄
1. 「摩擦」って何?(哲学)
最初、パックをただ動かすだけのつもりでした。
でも、現実のエアホッケーってパックが徐々に減速しますよね。あれを再現するために「摩擦」を実装する必要がありました。
self.vx *= PUCK_FRICTION # 0.995をかけ続ける
self.vy *= PUCK_FRICTIONたった2行。
でもこの値を調整するのに30分かかりました。0.99だと止まりすぎ、0.999だと止まらない。0.995でようやく「それっぽく」なりました。
ゲームって、こういう「それっぽさ」の積み重ねなんですね。
2. 衝突判定で頭がパンク
パックとパドルがぶつかったときの処理。
これがもう、数学の授業かと思いました。
パドルとパックの距離を計算
重なってたら押し戻す
反発係数を考慮して跳ね返す
パドルの速度も加味する
def resolve_paddle_hit(puck, paddle):
dx, dy = puck.x - paddle.x, puck.y - paddle.y
dist = math.hypot(dx, dy)
# ... 以下、数学の嵐高校の物理を思い出しました。というか、あの時ちゃんと勉強しておけばよかった(後悔)。
3. AIが強すぎて勝てない問題
最初に作ったAI、めちゃくちゃ強かったんです。
というか、完璧すぎて人間が絶対勝てないレベル。
# 最初のバージョン(強すぎた)
tx, ty = puck.x, puck.y # パックの位置に完璧に追従これだと、AIが全てのパックを完璧に返してきて、ゲームにならない。
だから「わざと完璧じゃない動き」を実装しました。
# 改善版(ちょっとバカになった)
tx, ty = puck.x, (HEIGHT*0.25 if puck.y < HEIGHT/2 else HEIGHT*0.35)パックのY座標じゃなくて、「だいたいこの辺」を目指すように変更。
AIをわざとバカにするって、なんか不思議な気分でした。
一番苦労したところ
壁の跳ね返り判定です。
普通の壁はいいんですよ。でもゴールエリアは壁がないから、そこだけ特別処理が必要で。
gl = WIDTH//2 - GOAL_WIDTH//2 + PUCK_RADIUS
gr = WIDTH//2 + GOAL_WIDTH//2 - PUCK_RADIUS
if self.y < top and not (gl <= self.x <= gr):
self.y = top
self.vy = -self.vy * BOUNCE_DAMPING「ゴールエリア内じゃなかったら跳ね返す」
この条件分岐を思いつくまでに1時間かかりました。
最初はパックがゴールエリアで壁にめり込んで消えたり、逆に壁をすり抜けたり。もうカオスでした(笑)
今日の新しい学び
技術面
Pygameでの物理シミュレーション
ベクトル演算(normalize、内積)
AI実装の基礎(追従アルゴリズム)
FPSを60から120に上げると滑らかになる(発見)
定数の微調整が体験を大きく変える
哲学面(大げさ)
完璧すぎるAIは面白くない
リアルすぎる物理も面白くない
「それっぽさ」が大事
バグが新しいアイデアを生むこともある
コードで気に入っている部分
フェイスオフ(試合再開)の演出です。
def faceoff(puck, who_scored=None):
if who_scored == "player":
ang = random.uniform(math.radians(200), math.radians(340)) # 下向き
elif who_scored == "ai":
ang = random.uniform(math.radians(20), math.radians(160)) # 上向き
else:
ang = None # ランダム得点した側に不利な方向へパックを飛ばす。
これだけで、ゲームのリズムが良くなりました。ずっと一方的な展開にならない。
小さな工夫が、ゲームバランスを変えるんですね。
スタートメニューを作った理由
今日、初めてスタートメニューを実装しました。
別になくてもゲームはできるんですが、やっぱり「ちゃんとしたゲーム感」が欲しくて。
def show_start_menu(screen, clock, bigfont, menufont, font):
# メニュー画面の処理"Start" を選んで Enter を押す。
たったそれだけなんですが、これがあるだけで「作品」っぽくなる気がします。
映画で言う「オープニング」みたいなものですね。
反省点
また身の丈に合わないものを作ってしまった
物理演算の数学、もっと勉強すべき
変数名が `gl` `gr` とか短すぎて後で「何これ?」ってなった
コメントが日本語と英語混在でカオス(統一しろ)
Day2を終えて
正直、今日も疲れました。
物理演算とか、数学とか、苦手なんですよね。
でも、パックがスーッと滑って、パドルに当たって「カーン!」って跳ね返る瞬間。
「お、いい感じ!」ってなるんですよ。
これが楽しい。
昨日は「明日はもっとシンプルなものを」って言いましたけど、たぶん明日もまた何か作っちゃうと思います。
シンプルかどうかは...運次第で(笑)
今日のコード行数: 234行
開発時間: 約9時間(かかりすぎ😢)
AIに負けた回数: 12回(途中から勝てなくなった)
達成感: ★★★★☆
数学への苦手意識: ★★★★★++
それでも楽しい: ★★★★★
100日チャレンジ、Day3に続く。
完全版コード
引き続き、最初の30日間はコードを直接貼り付けていきます。
import math, random, sys, pygame
# ===== Config =====
WIDTH, HEIGHT = 800, 600
FPS = 120
RINK_MARGIN, GOAL_WIDTH, WALL_THICK = 40, 220, 8
PUCK_RADIUS, PADDLE_RADIUS = 14, 26
PUCK_FRICTION, BOUNCE_DAMPING, HIT_IMPULSE = 0.995, 0.98, 1.0
BASE_ACCEL, BASE_MAX_SPEED, PADDLE_FRICTION = 1.1, 12.0, 0.90
SCORE_TO_WIN = 7
STATE_PLAY, STATE_POSTGAME = 1, 2
# DIFFICULTY(将来の拡張用)
DIFFICULTIES = [("EASY", 5.2), ("NORMAL", 6.5), ("HARD", 8.2)]
diff_idx = 1 # NORMAL
diff_name, ai_speed = DIFFICULTIES[diff_idx]
# ===== Init =====
pygame.init()
pygame.display.set_caption("Air Hockey - Start Menu")
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.Font(None, 28)
bigfont = pygame.font.Font(None, 64)
menufont= pygame.font.Font(None, 40)
# ... 以下省略(全234行)
# 完全版が見たい方はコメントでリクエストください!#100日チャレンジ #プログラミング #ゲーム開発 #Python #Pygame #エアホッケー #物理演算 #初心者エンジニア
