WASDで逃げるキャラと、自動追跡する鬼+壁+慣性+捕獲カウントまで実装しました。
はじめに
強化学習で鬼ごっこAIを作ってみたいと思い、シリーズ形式で記録していきます。
始めた理由はシンプルで、
- 試験勉強中に大きなアプリ開発を始めると止まらなくなりそうだった
- 勉強しながらでも進捗が「見える」ものを作りたかった
- 小さいものが成長していくのを見るのが好き
だからまずは「学習を入れられる環境」から作ることにしました。
今回はまだ強化学習は出てきません。
学習を面白くするための土台作りです。
実装環境
- OS: Windows
- エディタ: VSCode
- 言語: Python 3.13
- ライブラリ: pygame
⚠ Pythonバージョンについて
Python 3.14 では pip install pygame がうまく動作しませんでした。
そのため Python 3.13 を使用しています。
仮想環境を作成して実行しています。
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install pygame
今日作るもの
ただの追跡では学習が単純になりすぎるので、
環境側に少し“物理っぽさ”を入れました。
- 壁(障害物)
- 慣性っぽい動き(速度が急に変わらない)
- 斜め移動の正規化
- 捕まえた回数のカウント表示
① 斜め移動が速くなる問題
WASD入力をそのまま足すと、斜め移動は √2 倍速くなってしまいます。
そこでベクトルを正規化します。
move = Vector2(0, 0)
if keys[pygame.K_d]: move.x += 1
if keys[pygame.K_a]: move.x -= 1
if keys[pygame.K_s]: move.y += 1
if keys[pygame.K_w]: move.y -= 1
if move.length_squared() > 0:
move = move.normalize()
これで移動方向は保ちつつ、移動速度を一定にできます。
② 慣性っぽい動き
いきなり速度を変更するのではなく、
目標速度に“徐々に追従”させています。
runner_vel += (desired_vel - runner_vel) * (1.0 - pow(2.718281828, -runner_response * dt))
runner += runner_vel * dt
runner_response が大きいほど、入力に対して素早く反応します。
小さいと、重い動きになります。
強化学習にしたときに、単純すぎない挙動になることを期待しています。
③ 壁との衝突処理(円 vs 矩形)
キャラクターは円、壁は矩形で扱っています。
矩形の中で最も近い点(最近点)を求め、
そこから押し出す方式で衝突を解決しています。
def resolve_circle_rect(pos, vel, r, rect):
nx = max(rect.left, min(pos.x, rect.right))
ny = max(rect.top, min(pos.y, rect.bottom))
nearest = Vector2(nx, ny)
delta = pos - nearest
d2 = delta.length_squared()
if d2 >= r * r:
return pos, vel
dist = delta.length()
if dist == 0:
n = Vector2(0, -1)
dist = 1e-6
else:
n = delta / dist
pos += n * (r - dist)
vdot = vel.dot(n)
if vdot < 0:
vel -= n * vdot
return pos, vel
雑に位置を戻すよりも自然な動きになります。
④ 捕まえた回数のカウント
if (runner - chaser).length() < CATCH_DIST:
caught += 1
runner, chaser, runner_vel, chaser_vel = reset()
画面左上に表示しています。
最終コード
クリックで展開
import pygame
from pygame.math import Vector2
pygame.init()
WIDTH = 800
HEIGHT = 600
screen = pygame.display.set_mode((WIDTH,HEIGHT))
font = pygame.font.SysFont(None, 28)
clock = pygame.time.Clock()
runner = Vector2(400,300)
runner_R = 10
runner_vel = Vector2(0,0)
runner_max_speed = 460.0
runner_response = 12.0
chaser = Vector2(150,150)
chaser_R = 12
chaser_vel = Vector2(0,0)
chaser_max_speed = 460.0
chaser_response = 10.0
CATCH_DIST = runner_R + chaser_R
caught = 0
walls = [
pygame.Rect(200, 150, 300, 20),
pygame.Rect(100, 350, 20, 180),
pygame.Rect(500, 320, 200, 20),
]
running = True
def resolve_circle_rect(pos, vel, r, rect):
nx = max(rect.left, min(pos.x, rect.right))
ny = max(rect.top, min(pos.y, rect.bottom))
nearest = Vector2(nx, ny)
delta = pos - nearest
d2 = delta.length_squared()
if d2 >= r * r:
return pos, vel
dist = delta.length()
if dist == 0:
n = Vector2(0, -1)
dist = 1e-6
else:
n = delta / dist
pos += n * (r - dist)
vdot = vel.dot(n)
if vdot < 0:
vel -= n * vdot
return pos, vel
def reset():
runner = Vector2(400,300)
chaser = Vector2(150,150)
runner_vel = Vector2(0,0)
chaser_vel = Vector2(0,0)
return runner, chaser, runner_vel, chaser_vel
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill((20,25,25))
keys = pygame.key.get_pressed()
move = Vector2(0,0)
if keys[pygame.K_d]: move.x += 1
if keys[pygame.K_a]: move.x -= 1
if keys[pygame.K_s]: move.y += 1
if keys[pygame.K_w]: move.y -= 1
if move.length_squared() > 0:
move = move.normalize()
desired_vel = move * runner_max_speed
else:
desired_vel = Vector2(0,0)
runner_vel += (desired_vel - runner_vel) * (1.0 - pow(2.718281828, -runner_response * dt))
runner += runner_vel * dt
to_runner = runner - chaser
if to_runner.length_squared() > 0:
desired_chaser_vel = to_runner.normalize() * chaser_max_speed
else:
desired_chaser_vel = Vector2(0,0)
chaser_vel += (desired_chaser_vel - chaser_vel) * (1.0 - pow(2.718281828, -chaser_response * dt))
chaser += chaser_vel * dt
runner.x = max(0,min(runner.x,WIDTH))
runner.y = max(0,min(runner.y,HEIGHT))
chaser.x = max(0,min(chaser.x,WIDTH))
chaser.y = max(0,min(chaser.y,HEIGHT))
for w in walls:
runner, runner_vel = resolve_circle_rect(runner, runner_vel, runner_R, w)
chaser, chaser_vel = resolve_circle_rect(chaser, chaser_vel, chaser_R, w)
if (runner - chaser).length() < CATCH_DIST:
caught += 1
runner, chaser, runner_vel, chaser_vel = reset()
pygame.draw.circle(screen, (80,140,255), (int(runner.x),int(runner.y)), runner_R)
pygame.draw.circle(screen, (220,60,60), (int(chaser.x),int(chaser.y)), chaser_R)
for w in walls:
pygame.draw.rect(screen, (90,90,90), w)
text = font.render(f"Caught: {caught}", True, (230,230,230))
screen.blit(text,(10,10))
pygame.display.flip()
pygame.quit()
次回
ここまでで「学習させる環境」は完成しました。
ですが、この時点で私は
強化学習について一ミリも知りません。
次回はまず、
- 強化学習とは何なのか?
- 教師あり学習と何が違うのか?
- なぜ鬼ごっこに向いているのか?
- 「状態」「行動」「報酬」とは何か?
といった基礎から勉強していこうと思います。
