コンテンツにスキップ

跳弾(ちょうだん)のしくみ

あなたの担当 この章は、bot の中で使う計算の説明です。使うだけなら → はね返りで撃つ

弾は鉄ブロック(STEEL)に当たると 1 回だけはね返ります。 この性質を使うと、壁の陰にいて直接は狙えない敵にも当てられます。

ここでは、はね返りがどう計算されているかを見ます。 規則そのものは短いのですが、「角にちょうど当たったとき」だけは特別扱いが要ります。 その理由まで含めて説明します。

地形grid の値弾が当たると
空き地(EMPTY)0何も起きない(通過)
木箱(BRICK)1はね返らない。 耐久が 1 減り、弾は消える
鉄(STEEL)2はね返る。 2 回目の接触で消える
水(WATER)3通過する(戦車は入れないが、弾は飛び越える)

はね返るのは鉄だけです。木箱を撃つと壊れていき、耐久 2 から 0 で消えます。

壁に当たった弾は、鏡に映したように向きを変えます。 このとき使うのが、壁の面に垂直な向きを表す法線ベクトル n\mathbf{n} です。

入ってきた速度を v\mathbf{v}、はね返ったあとを v\mathbf{v}' とすると

v=v2(vn)n(n=1)\mathbf{v}' = \mathbf{v} - 2\,(\mathbf{v} \cdot \mathbf{n})\,\mathbf{n} \qquad (\Vert{}\mathbf{n}\Vert{} = 1)

vn\mathbf{v} \cdot \mathbf{n}内積で、vxnx+vynyv_x n_x + v_y n_y のことです。 これは「v\mathbf{v} のうち、壁に向かって突っ込んでいる成分の大きさ」を表します。 その成分だけを 2 倍引くので、壁に垂直な向きだけが逆転し、壁に沿う向きはそのままになります。

鉄の壁に入射した弾が、法線ベクトルnを軸にして鏡のように反射し、入射角と反射角が等しくなる図

この式が、軸に沿った壁だと一気に簡単になる

Section titled “この式が、軸に沿った壁だと一気に簡単になる”

CodeTank Arena のフィールドはマス目です。鉄ブロックの面は必ず縦か横を向いていて、 法線は 4 通りしかありません。

当たった面法線 n\mathbf{n}v=v2(vn)n\mathbf{v}' = \mathbf{v} - 2(\mathbf{v}\cdot\mathbf{n})\mathbf{n} の結果
左面・右面(X 面)(±1,0)(\pm 1,\, 0)(vx,  vy)(-v_x,\; v_y)vxv_x の符号を反転
上面・下面(Y 面)(0,±1)(0,\, \pm 1)(vx,  vy)(v_x,\; -v_y)vyv_y の符号を反転

法線の向き(+1+11-1)は結果に影響しません。n\mathbf{n} が 2 回掛かるので符号が打ち消し合うためです。 つまり実装は「どちらの軸の面に当たったかを決めて、その軸の速度の符号を反転する」だけで済みます。 三角関数も平方根も要りません。

弾の半径は 0.08 マス、1 フレームの移動量は約 0.146 マスで、どちらもマスの幅 1.0 マスよりずっと小さい。 なので壁をすり抜けること(トンネリング)は起きません。判定は次の手順です。

  1. 動かしたあとの弾の中心が、どのマスに入ったかを調べる
  2. そのマスが STEEL なら、動かす前と後の「マス座標」を見比べる
    • cellX も cellY も変わった → 角に当たった(後述のタイブレーク)
    • cellX だけ変わった → X 面
    • それ以外 → Y 面
  3. まだはね返っていなければ、速度を反転し、位置を接触点まで戻す。すでに 1 回はね返っていれば、弾を消す

「動かす前と後で、どちらのマス座標がまたいだか」を見るだけです。 横にまたいだなら左右の面、縦にまたいだなら上下の面に当たったということです。

角に当たったとき:X 軸の反転を優先する

Section titled “角に当たったとき:X 軸の反転を優先する”

問題は、cellX と cellY が同時に変わったときです。 弾がブロックの角をかすめると、1 フレームのあいだに縦にも横にもマスをまたぎます。

鉄ブロックの角に斜めから当たった弾。X 面に当たったとも Y 面に当たったとも読めるが、エンジンは X 面として扱うと決めている

このとき「X 面に当たった」と読むか「Y 面に当たった」と読むかで、 はね返る向きが 90 度変わります。そしてどちらの解釈も、幾何学的には等しく正しいのです。

そこで、エンジンは規則で決め打ちします。

「どちらでもよい」場面に選択の余地を残さないためです。

CodeTank Arena は決定論を守ります。 同じ試合は、どの端末で再生しても、1 ビットたりとも違わない結果にならなければいけません。 試合は映像ではなく短い数字の列(リプレイ)として保存され、再生のたびに計算し直されるからです。

もしここで「実装の都合で先に見つかったほう」を採ると、 配列を回す順番や、コンパイラの最適化や、端末の違いで答えが変わり得ます。 曖昧さは、いつか必ず食い違いになります。

だから「X を優先する」と先に決めてあります。 X が Y より優れているからではありません。決まっていること自体に意味があります。

これは跳弾に限った話ではなく、同じ考え方がエンジン全体を貫いています。

同時に起きたとき決めてあること
弾が角に当たったX 軸の反転を優先
戦車と壁が重なったX → Y の順で押し戻す
戦車の中心が完全に一致したA を x-x、B を +x+x へ押し出す
複数の弾が同時に相殺した生成順 ID の昇順で処理する

はね返った弾は、自分にも当たる

Section titled “はね返った弾は、自分にも当たる”

跳弾は撃った本人にも命中します。自爆があります。

狭い場所で壁に向かって撃つと、はね返ってきた弾が自分に刺さります。 state にはその情報が入っているので、読んで避けられます。

自分が撃った弾が、はね返ってきていないか見る
def update(state):
danger = False
i = 0
while i < len(state.my_missiles):
m = state.my_missiles[i]
if m.bounced: # 1 回はね返ったあとの自分の弾
d = distance(state.me.x, state.me.y, m.x, m.y)
if d < 2.0:
danger = True
i = i + 1
# 危なければ撃たずに下がる
if danger:
return {"drive": "backward", "steer": state.me.angle, "fire": False, "barrier": False}
return {"drive": "forward", "steer": state.me.angle, "fire": True, "barrier": False}

はね返った自分の弾は incoming_missiles(飛んでくる弾)にも載ります。 そちらは危ない順(eta の昇順)に並んでいるので、incoming_missiles[0] が 「いちばん先に当たる弾」です。この順番は仕様なので、bot が頼ってかまいません。

壁を使って回り込む(鏡像法)

Section titled “壁を使って回り込む(鏡像法)”

はね返りを攻めに使うときの考え方も、鏡の話に落ちます。

縦の鉄の壁が x=wx = w にあるとき、その壁を鏡だと思って敵を鏡の向こうに映します

pmirror=(2wxenemy, yenemy)\mathbf{p}_{\text{mirror}} = (\,2w - x_{\text{enemy}},\ y_{\text{enemy}}\,)

そして鏡像をまっすぐ狙って撃ちます。 弾は壁で折れ曲がり、ちょうど本物の敵に届きます。 折れ線で考えると難しい経路が、直線 1 本の問題に変わるのがこの方法の利点です。

縦の壁を鏡だと見立てて敵を壁の向こうに映し、その鏡像をまっすぐ狙うと弾が壁で折れ曲がってちょうど本物の敵に届く鏡像法の図

横の壁なら yy を折り返します。pmirror=(xenemy, 2wyenemy)\mathbf{p}_{\text{mirror}} = (x_{\text{enemy}},\ 2w - y_{\text{enemy}}) です。

実際に撃つ前には、2 本の線がどちらも通っているかline_of_sight で確かめてください。

  • 自分 → 壁の反射点まで、さえぎるものがないか
  • 反射点 → 敵まで、さえぎるものがないか

どちらかが別のブロックでふさがれていれば、その跳弾は届きません。 また、反射は 1 回きりなので、反射点の手前に別の鉄があると、そこではね返って終わりです。