コンテンツにスキップ

先読み射撃(偏差射撃)

あなたの担当 この章は、bot の中で使う計算の説明です。使うだけなら → 動く敵に当てる

止まっている敵には、まっすぐ狙えば当たります。 動いている敵には当たりません。 理由は単純で、弾が飛んでいるあいだに敵が移動するからです。

敵が「これから居るところ」を狙うのが偏差射撃です。ゲームでも実際の射撃でも同じ言葉を使います。

なぜ、まっすぐ撃つと当たらないのか

Section titled “なぜ、まっすぐ撃つと当たらないのか”

CodeTank Arena の弾は 8.0 マス/秒で飛びます。かなり速いのですが、無限ではありません。

たとえば敵が 8 マス先にいるとき、弾が届くまでにかかる時間は

8 マス8.0 マス/秒=1.0 秒\frac{8\ \text{マス}}{8.0\ \text{マス/秒}} = 1.0\ \text{秒}

戦車の前進速度は 3.0 マス/秒なので、その 1.0 秒のあいだに敵は 3 マス動きます。 戦車の当たり判定は半径 0.40 マス、つまり直径 0.8 マスですから、3 マスのずれは「かすりもしない」量です。

撃った瞬間には敵の正面にいた弾が、1.0秒後には敵がいた場所に届くだけで、敵自身は3マス先へ移動してしまっている図

当たらないのは腕の問題ではなく、狙う場所が間違っているのです。

まず、弾が届くまでの時間 tt を求めます。 自分の位置を pme\mathbf{p}_{\text{me}}、敵の位置を penemy\mathbf{p}_{\text{enemy}}、弾速を vbulletv_{\text{bullet}} とすると

t=penemypmevbullett = \frac{\Vert{}\mathbf{p}_{\text{enemy}} - \mathbf{p}_{\text{me}}\Vert{}}{v_{\text{bullet}}}

記号の読み方です。

  • p\mathbf{p}位置。太字なのは「xxyy の 2 つ組」だからです。pme=(xme,yme)\mathbf{p}_{\text{me}} = (x_{\text{me}},\, y_{\text{me}})
  • \Vert{}\cdots\Vert{}長さab\Vert{}\mathbf{a} - \mathbf{b}\Vert{} で「a\mathbf{a}b\mathbf{b} のあいだの距離」です。 中身は三平方の定理そのもので、(xaxb)2+(yayb)2\sqrt{(x_a - x_b)^2 + (y_a - y_b)^2} を短く書いたものにすぎません。
  • vbullet=8.0v_{\text{bullet}} = 8.0(マス/秒)。

つまりこの式は「距離 ÷ 速さ = 時間」と言っているだけです。 プログラムでは distance() がこの \Vert{}\cdots\Vert{} をそのままやってくれます。

t = distance(state.me.x, state.me.y, state.enemy.x, state.enemy.y) / 8.0

時間 tt が分かったので、その時間だけ敵が進んだ先を狙います。 敵の速度を venemy\mathbf{v}_{\text{enemy}} とすると、狙う点 paim\mathbf{p}_{\text{aim}}

paim=penemy+venemyt\mathbf{p}_{\text{aim}} = \mathbf{p}_{\text{enemy}} + \mathbf{v}_{\text{enemy}}\, t

xxyy に分けて書けば、見慣れた形になります。

xaim=xenemy+vxtyaim=yenemy+vyt\begin{aligned} x_{\text{aim}} &= x_{\text{enemy}} + v_{x}\, t \\ y_{\text{aim}} &= y_{\text{enemy}} + v_{y}\, t \end{aligned}

「いまの位置 + 速さ × 時間」。小学校で習う「道のり = 速さ × 時間」を、xx 方向と yy 方向で 1 回ずつやっているだけです。

予測はずれる、だから繰り返す

Section titled “予測はずれる、だから繰り返す”

ここまでの計算には、実はごまかしがあります。

tt を出すときに使ったのは「いまの敵の位置までの距離」でした。 しかし本当に狙うのは未来の位置 paim\mathbf{p}_{\text{aim}} で、そこまでの距離は違います。 敵が遠ざかっていれば、弾はもっと長く飛ばねばならず、tt はもっと大きいはずです。

対処は簡単で、出た答えでもう一度計算し直すだけです。

tk+1=(penemy+venemytk)pmevbullett_{k+1} = \frac{\Vert{}(\mathbf{p}_{\text{enemy}} + \mathbf{v}_{\text{enemy}}\, t_{k}) - \mathbf{p}_{\text{me}}\Vert{}}{v_{\text{bullet}}}

t0t_0 に最初の答えを入れて 2 回ほど回すと、値はほとんど動かなくなります。 弾速 8.0 マス/秒に対して戦車は 3.0 マス/秒と遅いので、収束が速いのです。

そのまま bot に貼って動きます。使っているのは distanceangle_to の 2 つだけです。

偏差射撃で撃つ bot
BULLET_SPEED = 8.0 # マス/秒
FPS = 60.0 # 1 秒あたりのフレーム数
def aim_point(state):
"""敵の未来の位置(狙点)を返す。"""
ex = state.enemy.x
ey = state.enemy.y
# vx / vy は マス/フレーム なので、マス/秒 にそろえる
evx = state.enemy.vx * FPS
evy = state.enemy.vy * FPS
tx = ex
ty = ey
i = 0
while i < 2: # 2 回で十分収束する
t = distance(state.me.x, state.me.y, tx, ty) / BULLET_SPEED
tx = ex + evx * t
ty = ey + evy * t
i = i + 1
return (tx, ty)
def update(state):
tx, ty = aim_point(state)
# angle_to は「ここから、あそこ」を向く角度(度)を返す
want = angle_to(state.me.x, state.me.y, tx, ty)
# いまの向きと狙いの差。-180〜180 に収まる
diff = angle_diff(want, state.me.angle)
# 砲身が向ききる前に撃っても当たらない。4 度以内になってから撃つ
fire = state.me.can_fire and abs(diff) < 4.0
return {"drive": "stop", "steer": want, "fire": fire, "barrier": False}

steer には向きたい角度そのものを入れます。差分ではありません。 戦車は毎秒およそ 180 度(546 angle-units/frame)まで回れて、その角度へ最短回りで向きます。

組み込みの predict_lead は何をしているか

Section titled “組み込みの predict_lead は何をしているか”

同じことをする組み込み関数があります。

tx, ty = predict_lead(state.enemy, 8.0, state.me.x, state.me.y)

こちらは繰り返しではなく、方程式を解いて一発で答えを出しています。

「弾が tt 秒で進む距離」と「tt 秒後の敵までの距離」が等しい、と書くと

r+venemyt2=(vbullett)2(r=penemypme)\Vert{}\mathbf{r} + \mathbf{v}_{\text{enemy}}\, t\Vert{}^2 = (v_{\text{bullet}}\, t)^2 \qquad (\mathbf{r} = \mathbf{p}_{\text{enemy}} - \mathbf{p}_{\text{me}})

左右を展開して tt について整理すると、中学校で習う 2 次方程式になります。

(venemy2vbullet2)at2+2(rvenemy)bt+r2c=0\underbrace{\left(\Vert{}\mathbf{v}_{\text{enemy}}\Vert{}^2 - v_{\text{bullet}}^2\right)}_{a}\, t^2 + \underbrace{2\,(\mathbf{r} \cdot \mathbf{v}_{\text{enemy}})}_{b}\, t + \underbrace{\Vert{}\mathbf{r}\Vert{}^2}_{c} = 0

ここで rv\mathbf{r} \cdot \mathbf{v}内積で、rxvx+ryvyr_x v_x + r_y v_y のことです。

解の公式 t=b±b24ac2at = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a} から出た 2 つの解のうち、 predict_lead正で小さいほう(=いちばん早く当たる時刻)を採ります。 正の解がない、または 10 秒を超える場合は「当てられない」と判断し、敵の現在位置を返します。