blog

HPで帯に入れることはできた

  • Claude Code
  • ゲーム開発
  • バランス調整

倉庫を漁って脱出するゲームで、脱出率を30〜90%の帯に収めるのが目標だった。ある時点から、この帯にどうしても入らなくなった。60シード回して60戦60勝。脱出率100%。

まず疑ったのはHPだ。敵の火力が足りないなら、自機の体力を削れば相対的に厳しくなる。125あったHPを下げていった。40まで下げても、脱出率は100%のままだった。

「動かない」を、意味を取り違えて読んだ

ここで「HPをどう動かしても帯には入らない」と結論しかけた。それは間違いだった。

実測すると、1ランで受けるダメージの平均は16.2しかなく、全シードを通した最大でも36だった。HPが40あれば、いちばん殴られた回でも死なない。40で100%なのは調整が足りないからではなく、40が「絶対に死なないHP」だったからだ。動かなくて当たり前の値を掃引していた。

実際、後で下まで振り直したら帯には入った。HP36で85.0%、HP24で60.0%。どちらも30〜90%の中にある。HPで帯に入れることはできた。

ただし敵の攻撃力は12なので、36は3発、24は2発だ。弾2〜3発で落ちる自機ということになる。それは「調整」ではなく別のゲームで、採りたくなかった。正しく言えるのはここまでで、「HPでは動かない」ではない。実際より強い主張で正当化すると、次に読んだ人が再検証しなくなる。

敵が撃っていなかった

そもそも1ランの被ダメージ16.2という数字自体が小さすぎた。

敵が視線を通してプレイヤーを見ている時間は、60シード合計で613.6秒あった。そのうち実際に撃てていたのは199.7秒、32.5%だけだった。残りの3分の2は、銃を持った敵がプレイヤーを見ながら何もしていない時間だった。

ヒットスキャン時代の間合いが、弾になっても残っていた

このゲームは元々、敵の攻撃がヒットスキャン(間合いに入った瞬間に確定で当たる)だった。後で見てから避けられる「弾」に作り変えたとき、弾速も構えのモーションも回避の幾何も作り直した。しかし、いつ撃つかを決める発射条件の距離だけは、ヒットスキャン時代の値(5)がそのまま残っていた。

敵の接近速度は3、プレイヤーの移動速度は6で、敵のほうが遅い。発射条件が「間合い5まで詰め寄ること」のままだと、プレイヤーが走り抜けるだけで敵は永遠にその間合いへ入れない。見えているのに撃てない敵ができあがっていた。

発射条件を「見えているかどうか」(視認距離9+視線)に合わせ直した。詰め寄り終わるのを待たず、見えている限り構えて撃つ。遠くから撃たれるほうがプレイヤーには易しいので、避けられる側の約束も壊れていない。

いまはこうなっている。撃つかどうかを決めているのは上のspottedのほうで、下に残っているfoeApproachRange(値は5のまま)は「詰め寄りをやめる距離」でしかない。

const spotted = dist <= BALANCE.foeSightRange && hasLineOfSight(world, foe.x, foe.z, playerX, playerZ);
if (!spotted) return { ...foe, attackCooldown: cooldown, aiming: 0, justAttacked: false };

let { x, z } = foe;
if (dist > BALANCE.foeApproachRange) {
  const dir = { x: dx / dist, z: dz / dist };
  ({ x, z } = moveWithCollision(world, x, z, dir.x, dir.z, BALANCE.foeApproachSpeed * dt, BALANCE.playerRadius));
}

HPを合わせ直したのはその後

発射条件を直したら、1ランあたりの被ダメージが16.2から59.4に戻った。ここで初めてHPを測り直す。72(=敵の攻撃力12×6発)にしたところ、脱出率は65.0%になった。その後さらに保管庫へ護衛を置いて測り直しているので、いま出荷している値は96(=12×8発)で、脱出率は同じ65.0%に落ち着いている。

順番が逆でも、たぶん帯には入れられた。ただし敵の弾2〜3発で死ぬ自機を選んでいたはずで、そのときは「数字は合ったが、なぜその値なのか説明できない」状態になっていた。

掃引の幅を狭く取ると、この区別が見えなくなる

最初に振った候補域は80から240だった。この幅の中では全部100%で、だから「どこへ振っても入らない」と読めてしまう。帯に入る値は、その下にあった。

いまは候補を敵の攻撃力1発ぶん(12)から振るようにしてある。下まで振ると「入る値は在るが、採りたくない形だ」という区別がちゃんと見える。掃引の幅は結論の形を決めてしまうので、狭く取った時点で見えなくなるものがある。

数字が効かないように見えたとき、疑うべきは数字そのものではなく、その数字が働くはずの構造のほうだった。この一件で、名前が実態と食い違っていた定数(foeAttackRangeという名前なのに、実際は「詰め寄りをやめる距離」でしかなくなっていた)もfoeApproachRangeへ改名した。名前が嘘をつかないようにするための、地味だが必要な後始末だった。

このヒットスキャンあらための弾はProd Runで実際に飛んでくる。

STAGE10/50SCORE0BEST0COMBO×1貯蓄0KILLS0
TAP = FOCUS FIRE
強化ツリー倒した数で買う。1つの強化は1回だけ。枝は左から順に開く。
kill -9 agentKILLS 0
kill -9 agent 停止中