AIゲーム制作ラボ

Unityステルスパズルの作り方:囮の音と敵の巡回をC#で実装

囮で敵を誘う「薄明の回廊」のC#を配布。壁を回り込む床距離で音を判定し、調査・待機・巡回復帰を実装します。予告とUndoの状態コピー、33手の往復で失敗した箇所、6面の検証結果も掲載。

Libra

公開 2026年9月27日

この記事の要点3項目

音を聞く条件は、壁を除いた床の最短距離が6以下。 投げられる範囲と聞こえる範囲を分ける。

残り2つの要点を読む
  • 灯機は調査先まで一手ずつ動き、その場で2手待って巡回へ戻る。 帰り道にも衝突と視界がある。
  • 予告は状態を深くコピーして、本番と同じ行動処理を実行。 Undoも灯機の調査状態まで戻す。
目次9章
検証した環境最終確認 2026年9月27日
  • ゲーム: Unity 6000.6.0f1 / WebGL / Codex制作
  • 配布サンプル: Windows / .NET SDK 10.0.401 / 追加パッケージなし

書いてあるのは、この環境で実際に試した結果です。ツールの仕様や料金は変わるので、重要な判断の前には公式の情報も確認してください。

囮の音を聞くかどうかは、壁を避けて歩ける距離で判定します。 Codexで作った「薄明の回廊」は、灯機の巡回を読み、記録を回収して出口へ戻る手番制ステルスパズルです。投げた囮の音で通路を開きますが、帰りには灯機も元の巡回へ戻ってきます。

この記事では、ゲーム本体のC#を使って床距離による聴覚、調査からの復帰、予告と一手戻す処理を再現します。画面を含めた全制作手順ではありません。Unityを起動せずに実行できるサンプルを用意しました。

薄明の回廊で囮を選択した画面。灯機の次の位置と危険な視界を予告し、確定するまでは手数を進めない
囮を選んだだけでは動かない。次の灯機と視界を確認し、行動を確定する。

C#サンプルを実行する

C#サンプルZIPを展開し、.NET 10 SDKのある環境で次を実行します。追加のNuGetパッケージはありません。

dotnet run --project HakumeiSample.csproj
実行結果から抜粋成功
PASS wall detour: direct distance 4 but floor distance 8
PASS sound reaches by floor path and moves guard
PASS preview does not mutate source
PASS snapshot is a deep copy
PASS round trip needs record before exit
PASS exhaustive no-decoy search cannot pass the occupied choke
ファイル確認する処理
Corridor.cs盤面・視界・床距離・行動・灯機の状態・6面の定義
Program.cs16検査、代表面の33手、囮なし探索、予告の照合
HakumeiSample.csproj.NET 10で動く実行プロジェクト

実行後の verification/core-report.json には検査数、入力列、探索数が残ります。今回の実行は3,000状態・12,220行動で予告と実行が一致しました。これは同じルールを二重に実行した整合性検査です。ルール自体の正しさは、壁越し・視界の向き・衝突などの独立した期待値で別に確かめています。

音の再生と「敵が聞いた」は別に決める

Unityの AudioSource.spatialBlend は、音声を2D/3Dの空間化へどれだけ寄せるかの設定です。これだけで本作の灯機へ調査命令を出す構成にはしていません。Unity公式APIの用途と区別しました。

判定本作の条件理由
囮を投げられるマンハッタン距離4以下、壁を横切らない壁の向こうへ直接投げない
灯機が聞く床の最短距離6以下曲がる通路に音が回り込む
灯機が見る前方90度、マンハッタン距離3以下、壁で遮断背後と壁の陰を使える

投げる距離と、音が届く距離は同じではありません。 効果音は操作への反応として再生し、音を消しても灯機のルールは変えません。これは現実の音響シミュレーションではなく、盤面を読めるように定めたゲームのルールです。

壁を回り込む距離を幅優先探索で求める

囮のマスを0として、上下左右の床へ1ずつ距離を広げます。壁と未到達を除き、一度到達したマスは再登録しません。床の移動費用がすべて1なので、幅優先探索で最短の床距離になります。

public int[] Distances(int destination)
{
    var d = new int[Walls.Length];
    for (int i = 0; i < d.Length; i++) d[i] = -1;
    if (!Floor(destination)) return d;
    var q = new Queue<int>();
    d[destination] = 0;
    q.Enqueue(destination);
    while (q.Count > 0) {
        int p = q.Dequeue();
        for (int k = 0; k < 4; k++) {
            int n = Neighbor(p, k);
            if (Floor(n) && d[n] < 0) {
                d[n] = d[p] + 1;
                q.Enqueue(n);
            }
        }
    }
    return d;
}

第4面の (1,5) と (5,5) は、座標の距離なら4ですが、間に壁があります。通路を回る床距離は8。聴覚の上限6を超えるので音は届きません。サンプルではこの直距離4/床距離8を固定した期待値として検査しました。

見通し判定を聴覚へ流用しない

症状
原因
対処

音にも視界と同じ直線判定を使うと、曲がった通路から誘えなくなります。一方で直距離だけなら壁を挟んだ近い灯機まで反応します。この作品では床距離を選び、壁の厚さ・材質・音量は扱いません。

音を投げた手で、各灯機の現在地の距離を読みます。未到達の -1 を先に除外するのは、負の距離が「6以下」に入るのを防ぐためです。

Decoys--;
var d = Level.Distances(action.Target);
foreach (var g in Guards)
    if (d[g.Position] >= 0 && d[g.Position] <= 6) {
        g.Target = action.Target;
        g.Listen = 2;
        g.Returning = false;
    }

調査・2手の待機・巡回復帰を状態として持つ

灯機は囮へ瞬間移動しません。各行動の確定時に、最短の床経路を一マス進みます。同距離の分岐は北・東・南・西の順で選び、予告と本番の分岐が変わらないようにしています。

状態次の一手
通常巡回定義された次の巡回点へ進む
調査中音のマスへ進む。新しい音を聞けば調査先を更新
調査先に到着その場で2手待つ
復帰中巡回の先頭へ戻り、向きと巡回位置を復元

実装で持つ値は Position、Facing、Cursor、Target、Listen、Returning です。位置だけでは、同じ場所にいる灯機の次の行動を決められません。 調査中なのか、帰る途中なのか、待機が何手残るかで変わります。

代表面の最初の往復経路は、22手目に灯機のいるマスへ進んで失敗しました。音を聞かせた後に安全だと思い込み、帰路の位置を確認し足りなかったためです。

検証で出た失敗エラー
Witness failed at turn 22 Move:24
灯機1に発見。灯機のいるマスへ進みました。

帰りの合流前へ待機を一手入れ、33手で回収・帰還できました。後の探索では囮の位置を変えた28手の経路も見つかりました。33手は修正した経路の証拠で、最短手数ではありません。 灯機の現在地へ移動した場合は、灯機がその後に動くとしても衝突扱いにしています。

予告とUndoには灯機の状態までコピーする

選択時には本体を変更せず、コピーに同じ Apply を実行します。移動、音、灯機の更新、発見、回収、帰還の順序を別実装にしません。

public Run Preview(TurnAction action) {
    var r = Copy();
    r.Apply(action);
    return r;
}
public Run Copy() {
    var r = (Run)MemberwiseClone();
    r.Guards = new GuardState[Guards.Length];
    for (int i = 0; i < Guards.Length; i++)
        r.Guards[i] = Guards[i].Copy();
    return r;
}

MemberwiseClone だけでは参照型の中身を共有します。Microsoft公式の説明を確認し、配列と各灯機を別にしました。GuardState のフィールドは整数と真偽値なので、その段階の浅いコピーで値を独立させられます。変更しない盤面定義は共有しています。

Undoは行動直前のコピーをスタックへ置き、プレイヤーだけでなく、灯機の待機残数・調査先・囮残数・回収状態も戻します。UIで発見後の「一手戻す」を押し、危険な行動を選ぶ前の状態へ戻ることもブラウザーで確認しました。

6面で「同じ操作だけ」を避ける

以前の作品には、ユーザーから「簡単すぎる」「ステージの差がたいしてない」という指摘がありました。今回は囮を増やすだけの展開を避け、最初は囮なしの待機、次は奥へ進む一方通行、後半は往復と複数の灯機を組み合わせました。

面新しく考えること確認したクリア経路安全ランダムの成功
巡回する中庭壁の陰で待って巡回を横切る15手13 / 1,000
音で開く記録室一つの囮で奥へ進む17手3 / 1,000
灯機を誘う往復路帰りのために囮を残す28手0 / 1,000
待ち伏せを迂回する調査先へ追いつかず脇へ隠れる29手0 / 1,000
二つの灯機聞く灯機と巡回のタイミングを分ける32手0 / 1,000
夜明け前の三つの記録三つの囮と二重の巡回を配分する33手0 / 1,000

安全ランダムは、予告で即発見になる行動を除いて選び、80手で打ち切った比較です。各面1,000回、固定した乱数で実行しました。無作為に危険へ飛び込む方式より強い基準ですが、人の難易度、面白さ、最適解の証明ではありません。 全6面には固定の解を置き、PCと375pxのタッチ操作でも帰還まで照合しています。実機スマホは未検証です。

最終面の三つの記録と三体の灯機。壁で仕切られた回廊と、次の視界を赤い網で表示する
最終面は三つの記録を集めて帰還。赤は次の視界。巡回図は手数を使わず確認できる。

Unityへ組み込むときの順序と、この方式の限界

  1. Corridor.cs をUnityのスクリプトへ置き、new Run(Levels.All[0]) で開始する。
  2. タップしたマスから TurnAction を作り、Legal で選択できるか確認する。
  3. Preview の結果で灯機と視界の予告を描く。選択時には本体へ Apply しない。
  4. 確定時に本体をコピーして保存し、Apply を一度だけ呼ぶ。
  5. 結果の Phase と Event を画面へ表示し、Undoで保存した状態を復元する。

本作は最大9×9の固定盤面です。現在の NextToward は呼び出すたびに距離を計算するため、大きな盤面や多数の敵へそのまま拡張する場合には、距離マップの共有や経路更新の条件を見直します。動く壁、音量の減衰、連続時間の追跡はサンプルの対象外です。

音で通路を開いても、帰り道まで安全になるわけではない。

まず代表面の固定経路で調査からの復帰を追い、それから囮の位置を変えて予告を比べると、状態を持つ理由が見えます。画面を作る工程はタイトル画面の作り方、日本語文字の容量はWebGLフォントのサブセットで扱っています。

よくある質問

よくある質問

UnityのAudioSourceを3Dにすれば敵が音を聞きますか?

音声の再生設定と敵の判断は別です。本作では床の最短距離で敵への通知を決め、AudioSourceはプレイヤーへの効果音に使っています。

敵の追跡やNavMeshも含まれますか?

含みません。固定された格子盤面で、音の地点への調査と決まった巡回への復帰を扱います。連続空間や動く障害物には別の設計が必要です。

サンプルだけで完成したゲームになりますか?

盤面とルールの計算を再現できます。タイトル画像、描画、音声、ブラウザ入力、保存は含みません。完成したWebGLゲームは冒頭のリンクから遊べます。

33手が最短の解ですか?

最短とはしていません。記事の33手は衝突を直した往復の検証経路で、同じ代表面には別の囮配置による28手の経路も見つかっています。

出典

出典

  1. 01AudioSource.spatialBlend — Unity 6.0 Scripting APIUnity2026-09-27確認。3D音声の空間化設定と、このゲーム独自の聴覚判定を分けるために参照。実装環境はUnity 6.6。
  2. 02Object.MemberwiseClone — .NET 10Microsoft2026-09-27確認。浅いコピーでは参照先が共有されることを確認。灯機配列と各状態は別にコピーする。

この記事は役に立ちましたか?

関連記事