見出し画像

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 #エアホッケー #物理演算 #初心者エンジニア


いいなと思ったら応援しよう!