動くものの挙動をテストする(時間を引数で進める設計)
敵や弾のように毎フレーム動くものは、遊んでみないと分からない——と思われがちです。時間を引数で進める形にして、エンジンを参照しない場所に置くと、10分ぶん(36,000フレーム)を1秒未満で回せます。4本目で実際に書いたテストと、踏んだ落とし穴を残します。
公開 2026年9月19日
この記事の要点4 points
Tick(float dt)にすると、何万フレームでもテストから回せる(10分ぶん=36,000フレームが一瞬)。- Core は UnityEngine を参照しない設定にして、コンパイラに守らせる(Vector2 も使えない)。
- 乱数は自前の種つきにする。 同じ種なら同じ波が出るので、あとから同じ場面を作れる。
- テスト32件が 0.8秒。 シーンもゲームオブジェクトも作らない。
目次6章
検証した環境最終確認 2026年9月19日
- Windows 11
- Unity 6.6(6000.6.0f1)
- Claude Code(Opus 5 / 思考レベル 高)
書いてあるのは、この環境で実際に試した結果です。ツールの仕様や料金は変わるので、重要な判断の前には公式の情報も確認してください。
弾が飛び、敵が降りてきて、当たれば消える。こういう毎フレーム動くものは「遊んでみないと分からない」と思われがちです。
4本目のゲームでは、遊ばずに確かめられる形にしました。方法は1つだけです。
時間を、外から渡す。
36,000
テストで回すフレーム
10分ぶんを一瞬で
32件
EditMode のテスト
0.8秒
0個
テストで作るシーン
エンジンを使わない
6,000
確保0バイトを見張る回数
フレーム
Tick(float dt) にする
ゲームの中身(自機・弾・敵・当たり判定・点)は、MonoBehaviour ではない普通のクラスに置いています。時間は外から渡します。
public void Tick(float dt, in InputState input)
{
if (State != BattleState.Playing) return;
Time += dt;
MovePlayer(dt, input);
UpdateTimers(dt);
Fire(dt);
MoveShots(dt);
MoveEnemies(dt);
HitEnemies();
HitPlayer();
}
Unity 側(Update)がすることは、これだけになります。
void Update()
{
_director.Tick(Time.deltaTime, Battle);
Battle.Tick(Time.deltaTime, ReadInput());
_view.Render(Battle);
}
テストからは、1/60 秒を好きな回数だけ渡します。
const float Frame = 1f / 60f;
static void Run(Battle battle, float seconds, InputState input)
{
int frames = (int)(seconds / Frame);
for (int i = 0; i < frames; i++) battle.Tick(Frame, input);
}
エンジンを参照しないことを、コンパイラに守らせる
「Unity に依存しないように書く」は、気をつけているだけでは守れません。asmdef の設定で参照そのものを切りました。
{
"name": "Mureuchi.Core",
"noEngineReferences": true,
"references": []
}
こうすると Vector2 も Mathf も使えません。座標は自前の小さな構造体にしています。
public readonly struct Vec2
{
public readonly float X;
public readonly float Y;
/// <summary>長さの2乗。距離の比較はこれで足りる(平方根を取らない)</summary>
public float SqrMagnitude => X * X + Y * Y;
}
不便さが、そのまま速さになることもある
Vector2 が無いので距離の比較を自分で書くことになり、自然と平方根を取らない書き方になりました。当たり判定は「半径の和の2乗」と比べるだけで足ります。
種を決めれば、同じ波が出る
敵の出方は乱数です。そのままだとテストで固定できないので、種を渡せる乱数を自前で持っています(xorshift・十数行)。
public uint NextUInt()
{
uint x = _state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
_state = x;
return x;
}
これで「同じ種なら同じ波」が保証でき、こういうテストが書けます。
[Test]
public void 同じ種なら同じ波が出る()
{
Assert.That(Play(2026, 20f), Is.EqualTo(Play(2026, 20f)));
}
Play は20秒ぶん回して、出てきた敵の種類と位置を文字にして返すだけです。同じ文字になれば同じ波。
実際に書いたテスト
動くものについて見張っていること
最後の2つは、遊びでは気づけない種類のバグを捕まえます。
上限を超えて増える
- 症状
- 長く遊んでいると、だんだん重くなる
- 原因
- 消し忘れ・画面外に出たものを片づけていない
- 対処
- 10分ぶん回して、数が上限以下であることを毎フレーム確かめる
毎フレーム少しずつ確保している
この形にしてから、確保は0バイトのまま- 症状
- ときどきカクつく(掃除が走る)
- 原因
- `Tick` の中で配列やリストを作り直している
- 対処
GC.GetTotalMemoryを前後で比べ、増えていたら落ちるテストにする
run_tests true 32/32 件成功 (EditMode, 0.82s)
dt を固定しないと再現しない
1つ注意があります。同じ種でも、渡す時間(dt)が違えば結果は変わります。 実機の Time.deltaTime は毎フレーム揺れるので、テストと本番でまったく同じにはなりません。
⚠️ テストで確かめられるのは「決まりごと」
テストが守るのは「ルールどおりか」であって、「実機でまったく同じ画になるか」ではありません。実機の揺れまで再現したいなら、固定の時間刻みで進める作り(固定タイムステップ)にする必要があります。4本目ではそこまでは必要ないと判断しました。
まとめ
- 動くものは
Tick(float dt)にする。時間を外から渡せば、テストから何万フレームでも回せる - Core は
noEngineReferences。エンジンの型が混ざらないことをコンパイラに守らせる - 乱数は種つき。同じ種なら同じ波=あとから同じ場面を作れる
- 「長く回す」「確保が増えない」は、遊びでは気づけないバグを捕まえる
- dt が違えば結果は変わる。テストが守るのは決まりごとであって、実機の画そのものではない
FAQ
よくある質問
動きのあるゲームは、どうテストしますか?
時間を引数で受け取る形(Tick(float dt))にして、そのクラスを Unity から切り離します。テストでは 1/60 秒を必要な回数だけ渡せばよく、シーンを開く必要がありません。
MonoBehaviour の Update に書いてはいけませんか?
動かすだけなら書けますが、テストのたびに再生モードへ入ることになります。判定・移動・出現などの決まりごとを普通の C# クラスに移すと、EditMode のテストで一瞬で回せます。
同じ場面を再現するには?
乱数に種を渡せる形にします。System.Random ではなく短い自前の実装(xorshift)にすると、どの環境でも同じ並びになり、テストでも本番でも同じ波を作れます。
エンジンを参照しないことは、どう保証しますか?
asmdef の noEngineReferences を立てます。UnityEngine の型を使った時点でコンパイルが通らなくなるので、うっかり混ざりません。
この記事は役に立ちましたか?
NEXT
あわせて読む
毎フレーム作って壊すのをやめる(オブジェクトプールの効き目を実測)
弾と敵が同時に数百個出るゲームを作りました。表示のたびに GameObject を作って壊す書き方と、作ったものを使い回す書き方を、同じ場面で測って比べます。1フレーム 19.06ms が 0.15ms になり、確保するメモリは 0 バイトになりました。
Unity CLIでAIにテスト・動作確認・ビルドまで回させる手順
Unity CLI の run_tests / editor_play / capture_game_view / build を使って、人が画面を触らずにテストと動作確認とWebGLビルドを回した手順を、実際のコマンドと所要時間つきで残します。
Webゲームの進行を保存する(何を保存しないか・壊れても落ちない形)
ブラウザで遊ぶパズルに「どこまでクリアしたか」と自己ベストを保存しました。盤面は保存しません。保存先がブラウザの領域であること、壊れた保存データで落ちない書き方、記事を書いていて気づいた「版が無い」という穴と、その直し方を残します。