現場レベルのゲーム制作が、すべてここで学べます。
この記事は、Unity ホラー風脱出ゲームの作り方をわかりやすく解説していくシリーズの第6回です。
前回は、異変のアルゴリズムを実装し、実際に異変を発生させるところまで進めました。
前回の記事:

まずは Project ウィンドウから、
「script」フォルダ内にある前回作成した異変システムの基盤スクリプト 「EventManager」 を探し、
ダブルクリックで開きましょう。
ここに、今回の「正解/不正解判定」を行うための処理を追加していきます。

開けたら、下記のマーカー箇所を追加しましょう。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
using System; using System.Collections.Generic; using Unity.Collections; using UnityEngine; public class EventManager : MonoBehaviour { [Header("現在発生している異変(Anomaly)の番号")] public int currentAnomalyIndex; [Header("デバック用、次の異変(Anomaly)の番号を設定")] public bool nextAnomalyForced = false; public int number; [Header("最初の異変(Anomaly)の番号を設定")] public int FirstNumber; [Header("異変(Anomaly)の設定")] public List<MapAnomalyList> MapAnomalyList = new List<MapAnomalyList>(); /*構造として MapAnomalyList[0] → 異変なし(noAnomaly)として MapAnomalyList[1]以降 → 異変あり(Anomaly)とします */ [Header("スタートA,B")] public GameObject startA; public GameObject startB; [Header("エンドA,B")] public GameObject endA; public GameObject endB; //どちら側方向が異変なし、または異変ありなのか判定を収納 public int entrySide; // 異変なし(currentAnomalyIndex = 0)の確率を連続出現で減衰させる設定 [SerializeField, Range(0f, 1f)] float baseNoAnomalyProb = 0.5f; // “異変なし”の基本確率 [SerializeField, Range(0f, 1f)] float minNoAnomalyProb = 0.05f; // どれだけ異変なし(currentAnomalyIndex = 0)連続しても下回らない下限 [SerializeField, Range(0f, 1f)] float decayPerStreak = 0.5f; // 継続時の減衰係数 int noAnomalyStreak = 0; // “異変なし”が続いた回数 void Start() { //最初の異変を設定 currentAnomalyIndex = FirstNumber; ApplyAnomaly();//最初の異変適用 //判定リセット endA.SetActive(false); endB.SetActive(false); } public void roomStart(int a)//スタート(次のターンの)フラグ { RandomMap();//ランダムに異変あり、なしまたありの場合ランダムに異変を呼び出す startA.SetActive(false);//入室フラグとなるオブジェクトを非アクティブオブジェクトに startB.SetActive(false); endA.SetActive(true);//退室フラグとなるオブジェクトをアクティブオブジェクトに endB.SetActive(true); entrySide = a == 0 ? 1 : 0;//どちら方向から部屋に侵入したか判定して保持する } public void roomEnd(int a)//終了(このターン終わりの)フラグ { RecoverMap();//異変により変わったところをもとにもどす startA.SetActive(true);//入室フラグとなるオブジェクトをアクティブオブジェクトに startB.SetActive(true); endA.SetActive(false); ;//退室フラグとなるオブジェクトを非アクティブオブジェクトに endB.SetActive(false); int Answer = a == entrySide ? 0 : currentAnomalyIndex;//終わりトリガーオブジェクトの引数でentrySideから正解不正解を判定 if (currentAnomalyIndex == Answer) { Debug.Log("正解(True)"); SaveValue.usedMaps.Add(currentAnomalyIndex);//正解の場合使用済みの異変に加える } else { Debug.Log("不正解(False)"); } } public void RandomMap()//異変をランダムに制御する { if (nextAnomalyForced)//インスペクターから次の異変を指定する場合はこちらから強制的に実行される { currentAnomalyIndex = number; ApplyAnomaly(); return; } //0を出す確率(連続出現で減衰) float currentZeroProb = Mathf.Max(minNoAnomalyProb, baseNoAnomalyProb * Mathf.Pow(decayPerStreak, noAnomalyStreak)); if (UnityEngine.Random.value < currentZeroProb)//異変ありかなしかをランダムに決める { currentAnomalyIndex = 0; noAnomalyStreak++;//異変なし連続カウントを増やす ApplyAnomaly();//適用 return; } else { noAnomalyStreak = 0; // 0以外が出たら連続カウントをリセット } //未使用マップを取得 List<int> candidates = new List<int>(); for (int i = 0; i < MapAnomalyList.Count; i++) { if (!SaveValue.usedMaps.Contains(i)) { candidates.Add(i); } } //全部使い切ったら if (candidates.Count == 0) { Debug.Log("すべてのマップを出し終えました"); currentAnomalyIndex = 0; return; } //未使用マップからランダム選択 currentAnomalyIndex = candidates[UnityEngine.Random.Range(0, candidates.Count)]; ApplyAnomaly();//適用 } void ApplyAnomaly()//異変を適用する { //消すオブジェクト foreach (GameObject obj in MapAnomalyList[currentAnomalyIndex].DelObj) { obj.SetActive(false); } //表示するオブジェクト foreach (GameObject obj in MapAnomalyList[currentAnomalyIndex].AddObj) { obj.SetActive(true); } } void RecoverMap()//変更した異変を修復 { Debug.Log("usedMaps: " + string.Join(", ", SaveValue.usedMaps)); //消したオブジェクトを表示 foreach (GameObject obj in MapAnomalyList[currentAnomalyIndex].DelObj) { obj.SetActive(true); } //表示したオブジェクト消す foreach (GameObject obj in MapAnomalyList[currentAnomalyIndex].AddObj) { obj.SetActive(false); } } } |
スクリプトの解説
マーカー部分について解説をします。
入口側の保持(前処理)
|
1 |
entrySide = a == 0 ? 1 : 0; |
-
これは 入室時 に「どちら側から入ってきたか」を保持するための値です。
-
a == 0 ? 1 : 0により、-
startAから入ったら
entrySide = 1(=“反対側はB”) -
startBから入ったら
entrySide = 0(=“反対側はA”)
を記録します。
→ “引き返したか/まっすぐ進んだか” を後で照合するための基準 になります。
-
正解・不正解の心臓部
|
1 2 3 4 5 6 7 8 9 10 |
int Answer = a == entrySide ? 0 : currentAnomalyIndex; if (currentAnomalyIndex == Answer) { Debug.Log("正解(True)"); SaveValue.usedMaps.Add(currentAnomalyIndex); } else { Debug.Log("不正解(False)"); } |
-
aは 退室時 に踏んだ終点(EndA=0/EndB=1)の値。 -
a == entrySideで、入ってきた側に引き返したか を判定しています。-
true(= 引き返した)ならAnswer = 0 -
false(= まっすぐ進んだ)ならAnswer = currentAnomalyIndex
-
ここで重要な前提:
currentAnomalyIndex == 0 が「異変なし」、currentAnomalyIndex != 0 が「異変あり」 を表す設計になっています。
-
引き返した場合(
a == entrySide)はAnswer = 0(=“正解条件は異変なし”)。-
異変があるターンなら「引き返しが正解」→
currentAnomalyIndexは0以外なので 不一致、正解にはならない?…ではなく、次のケースで帳尻が合うようになっています。
-
-
まっすぐ進んだ場合(
a != entrySide)はAnswer =currentAnomalyIndex。-
異変なしターンなら
currentAnomalyIndex == 0→ “前進が正解” として一致。 -
異変ありターンなら
currentAnomalyIndex != 0→ “前進は不正解” で不一致。
-
つまり最終行の
|
1 |
if (currentAnomalyIndex == Answer) |
-
異変なし なら「前進」で一致→正解
- 異変あり なら「引き返し」で一致→正解
…という 8番出口のルール を一行で満たす比較になっています。
また、正解時のみ SaveValue.usedMaps.Add(currentAnomalyIndex) が走り、
そのターンで使った異変IDを「使用済み」に登録します。
三項演算子の補足
書式:
条件 ? 真のときの値 : 偽のときの値
今回のコード :
|
1 |
entrySide = a == 0 ? 1 : 0; |
は次と同じ意味です:
|
1 2 3 4 |
if (a == 0) entrySide = 1; else entrySide = 0 |
ゲームを再生して確かめる
補足:Lightingの警告が出る場合
もしゲーム再生中に次のようなログが頻繁に表示される場合

これはライトの影(シャドウマップ)が多すぎるために出ている警告です。
次の手順で設定を変更しましょう。
Projectウィンドウから Prefab フォルダを開き、「Room_Start」プレハブをダブルクリックして開きます。
Hierarchy から Lights を探し、その中にある Light コンポーネント すべての Shadow TypeをNo Shadowsにしておきましょう。今後の開発でも、影を生成させるライトは必要最低限にしましょう。

UnityのCanvas UIで3D空間に看板を作って進行方向をわかりやすくする
異変がなければ前へ進み、異変があれば引き返す。これが8番出口の基本ルールです。
しかし現状のままでは、部屋の中を見回しているうちに「どちらから来たのか」がわからなくなってしまうことがあります。
そこで、前方(=異変なし側)にExit看板を配置してあげることで、プレイヤーは進行方向を直感的に把握できるようになります。
この工夫により、探索中の混乱を防ぎ、よりスムーズにゲームを進行させることができます。
Canvas UIで3D空間内にExit看板を作成する
まずは、「Room_Start」プレハブに入りましょう。

まず、Hierarchyウィンドウで右クリック → UI > Image を選択します。
すると、自動的に Canvas と Image オブジェクトが作成されます。
作成されたImageの名前を 「ExitA」 に変更しておきましょう。

次に、Hierarchyウィンドウから「Canvas」 を選択し、
Inspectorウィンドウで以下のように設定しましょう。

Rect Transformは通常では操作できないですが、CanvasのRender ModeをWorld Spaceにすると値を編集できるようになります。
World Spaceは、3D空間にUIを配置できるモードで、今回のように「Exit看板」を
シーン内に実際に立てたい場合に最適です。
Canvas UIのRender Mode 3種類とその特徴
| モード名 | 概要 | 主な用途 |
|---|---|---|
| Screen Space – Overlay | 画面全体にUIを最前面で表示します。カメラの位置や角度に影響されません。 | メニュー画面、HPバー、スコア表示など常に見えるUI向け。 |
| Screen Space – Camera | 特定のカメラを指定してUIを表示します。UIがカメラの遠近感の影響を受けます。 | ミニマップや3D演出を含むHUD(例:照準マークやレーダー)などに最適。 |
| World Space | UIを3D空間内に実際のオブジェクトとして配置します。 |
看板・モニター・インゲームの立体UIなど、今回のような用途に最適。 |
補足説明(参考として)
「HUD」は “Head-Up Display” の略で、
プレイヤーが視点を動かさずに見る情報表示を指します。
たとえば:
-
照準(クロスヘア)
-
弾数や体力ゲージ
-
レーダー(敵位置の簡易マップ)
つまり「3Dの中にあるけど、プレイヤー視点のUI」
というのが Screen Space - Camera の特徴と合致します。
これで、ゲーム空間内に実際のオブジェクトとしてUIが表示されるようになります。
続いて、Hierarchyから「ExitA」を選択し、Inspectorの「RectTransform」を以下のように設定しましょう。

次に、ExitAを選択したまま、Inspector内の Image コンポーネント を確認しましょう。
Color項目をクリックして、Green(緑色) に設定します。

次に、Hierarchyから「ExitA」をクリックし、右クリックメニューから
UI > Text – TextMeshPro を選択します。
すると、「Text (TMP)」オブジェクトが ExitAの子オブジェクト として追加されます。

続いて、先ほど作成した Text (TMP) オブジェクトを選択し、
Inspector から以下のように設定しましょう。
Text(テキスト内容):
|
1 2 |
← Exit |
Font Size:1
Alignment(整列):中央揃え
これで、看板の中央に「← Exit」というテキストが表示され、
プレイヤーにとって視覚的にわかりやすい目印になります。

次に、反対方向用のExit看板(ExitB) を作成します。
まず、Hierarchy で「ExitA」を選択し、
キーボードの Ctrl + Dを押して複製しましょう。
複製されたオブジェクトの名前を 「ExitB」 に変更します。

続いて、Hierarchy から先ほど複製した ExitB を選択し、
Inspector の RectTransform を以下のように設定しましょう。

これで、「ExitA」、「ExitB」で部屋の両方に「Exit」看板が配置されました。
スクリプトでExit看板の表示を制御する
ここまでで、ExitAとExitBの看板を配置しました。
ここからは、これらの看板がプレイヤーの進行方向に応じて自動で切り替わるように制御していきます。
まず、Projectウィンドウから
scriptフォルダ内にある 「EventManager」スクリプト を探しましょう。
見つけたら、そのスクリプトをダブルクリックして開きます。
Visual Studio(または設定しているコードエディタ)が起動し、
コードの編集画面が表示されるはずです。
Exit看板をプレイヤーの進行方向に応じて切り替えるための制御を追加していきます。

以下のマーカー部分を「EventManager」に追加しましょう
まとめと次回予告
今回は、プレイヤーの行動をもとに正解・不正解を判定し、
その結果に応じて「今何番出口」カウンターを制御しました。
また、プレイヤーが方向感覚を見失わないように、
EXIT看板を設置して進行方向をわかりやすくする工夫も行いました。
ここまでで、ゲームとしての基本的なループ構造が完成し、
プレイ感もかなり本家『8番出口』に近づいてきたと思います。
次回は、これまでとは一味違う動的な異変を2種類ほど作成していきます。
見た目だけでなく、プレイヤーの行動に反応するタイプの異変を導入し、
さらに臨場感のある世界へ進化させましょう。
次回の記事↓

現場レベルのゲーム制作が、すべてここで学べます。







コメント