現場レベルのゲーム制作が、すべてここで学べます。
この記事は、Unityを使った「8番出口」ライクなホラー風脱出ゲームの作り方講座の第5回です。
前回は、エリアごとに音の響きを変えるリバーブを設定し、シーン全体の臨場感をアップさせました。その結果、“ホラーらしい雰囲気”が一気に引き立ちました。
前回の記事:

今回は、ついに“異変”が発生する仕組みを実装していきます!
“異変”の発生を制御するアルゴリズムを作成
まず、”異変”とは簡単に言うと入室するたびに生じる部屋の変化のことです。入室するたびに部屋が変化してるかどうかをチェックし、部屋が変化していれば来た道を引き返す。変化していなければ先に進む。
これを繰り返すのが8番ライクと呼ばれる8番出口風ゲームの特徴です。
アルゴリズムとしては、大まかに以下のような流れになっています。
① 入室時のトリガー
プレイヤーが部屋に入る少し手前で「スタートフラグ」が呼び出され、異変の処理がスタートします。
② ランダム判定
その部屋で“異変が起こるかどうか”、さらに“どの異変が発生するのか”をランダムに決定します。
③ 異変の適用
選ばれた異変の内容を実際にゲーム内へ反映し、空間の雰囲気やオブジェクトが変化します。
④ 退室時のトリガー
プレイヤーが部屋を出るタイミングで「エンドフラグ」が呼び出され、異変の処理を終了します。
⑤ 状態のリセット
適用されていた異変の効果を元に戻し、次の部屋でも同様の判定が行えるようにリセットします。
このようなアルゴリズムを、実際に C# で実装 していきましょう。
異変を管理するスクリプトを作成する
まずは Projectウィンドウ を開き、以前作成した「script」フォルダを開きましょう。
フォルダ内には、これまでに作成した 「PlayerCTRL」 と 「ReverbByFloorRay」 のスクリプトがあるはずです。
次に、空いているスペースで 右クリック → 「Create」>「MonoBehaviour Script」 を選択します。
新しく作成されるスクリプトの名前を 「EventManager」 に変更しましょう。

作成した 「EventManager」 スクリプトを ダブルクリック で開きましょう。
これで、スクリプトエディタ(通常は Visual Studio や Rider など)が起動します。
ここから実際に、異変の発生や制御を行うための処理を書いていきます。
前回までに行った「リバーブ判定」や「プレイヤーの操作処理」と同様に、
このスクリプトでもゲーム内の動きを支える重要な部分を担当します。
下記のスクリプトを記述してください。このコードが、異変を管理する基本的な仕組みとなります。
|
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 |
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; // 異変なし(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); } public void roomEnd(int a)//終了(このターン終わりの)フラグ { RecoverMap();//異変により変わったところをもとにもどす startA.SetActive(true);//入室フラグとなるオブジェクトをアクティブオブジェクトに startB.SetActive(true); endA.SetActive(false); ;//退室フラグとなるオブジェクトを非アクティブオブジェクトに endB.SetActive(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); } } } |
スクリプトの解説
先に EventManager の考え方を理解しておきましょう。
いまは MapAnomalyList と SaveValue を用意していないため、エラーになりますが、ここでは流れをつかむことを目的に解説します。
何をするスクリプト?
-
部屋に入る/出るをトリガーにして、
-
異変(=マップ差し替え)を適用し、
-
退室時に元に戻す
までを一括管理する「イベント(異変)マネージャ」です。
-
-
異変の出現はランダム。異変なしが連続したときは、次第に“異変あり”が出やすくなる確率調整を行うことで単調さを回避します。
主なフィールド(Inspectorで見るところ)
-
currentAnomalyIndex:現在の異変(何番の異変が適用中か) -
nextAnomalyForced/number:デバッグ用。次の異変を強制指定したいときに使う(nextAnomalyForcedがONならnumberを採用。Unity内インスペクターから制御可能) -
FirstNumber:ゲーム開始時に最初に適用する異変番号(異変なしの基本状態を0とする) -
MapAnomalyList:異変のリスト。-
各異変は消すオブジェクト(
DelObj)と表示するオブジェクト(AddObj)のセットで表現
-
-
startA,startB/endA,endB:-
入室/退室のトリガー用オブジェクト(アクティブ切替でフラグ管理)
-
ランダム制御のパラメータ
-
baseNoAnomalyProb:異変なし(0)の基本確率(初期の出やすさ) -
minNoAnomalyProb:どれだけ連続しても下回らない下限 -
decayPerStreak:0が続くたびに確率へ掛ける係数(例:0.5なら半減) -
noAnomalyStreak:直近で0が何連続かのカウンタ
実行の流れ
-
Start()
-
currentAnomalyIndex = FirstNumberを適用して最初の異変を反映(ApplyAnomaly()) -
退室フラグ(
endA/endB)をOFF、入室待ちの状態に
-
-
roomStart(int a)(入室トリガーで呼ぶ想定)
-
RandomMap():次の異変を決定 -
入室トリガー
startA/BをOFF、退室トリガーendA/BをON
-
-
roomEnd(int a)(退室トリガーで呼ぶ想定)
-
RecoverMap():適用した異変を元に戻す -
入室トリガー
startA/BをON、退室トリガーendA/BをOFF
-
重要メソッドの中身
RandomMap()(次の異変を決める)
-
デバッグ強制
-
nextAnomalyForcedが true ならnumberをそのまま採用 →ApplyAnomaly()で即適用
-
-
異変なし(0)の確率を調整
-
現在の0確率 =
max(minNoAnomalyProb, baseNoAnomalyProb * pow(decayPerStreak, noAnomalyStreak)) -
抽選で0が出たら
-
currentAnomalyIndex = 0 -
noAnomalyStreak++(0連続カウント増やす) -
ApplyAnomaly()で適用して return
-
-
0以外が出た場合は
noAnomalyStreak = 0にリセット
-
-
未使用マップからランダム選択
-
SaveValue.usedMapsに入っていないインデックスだけcandidatesに集める -
使い切っていたら「すべてのマップを出し終えました」として
currentAnomalyIndex = 0(異変なし) -
候補からランダムに1つ選んで
currentAnomalyIndexに設定 →ApplyAnomaly()で適用
-
ApplyAnomaly()(異変の適用)
-
MapAnomalyList[currentAnomalyIndex]の-
DelObjを 非アクティブ -
AddObjを アクティブ
に切り替える
-
RecoverMap()(元に戻す)
-
直前に適用した
DelObjを 再表示(SetActive(true)) -
直前に適用した
AddObjを 非表示(SetActive(false))
補足:foreachとは?
スクリプト内では、
|
1 2 3 4 |
foreach (GameObject obj in MapAnomalyList[currentAnomalyIndex].DelObj) { obj.SetActive(false); } |
のように何度も foreach が登場します。
この構文は、リストや配列などに入っているすべての要素を順番に取り出して処理するための書き方です。
上の例では、MapAnomalyList[currentAnomalyIndex].DelObj に含まれるすべてのオブジェクトを1つずつ obj として受け取り、SetActive(false) を実行しています。つまり「リストに入っているオブジェクトを全部非表示にする」という動きになります。
通常の for 文よりもコードが短く、意図が分かりやすいのが特徴です。
リストの中身を順番に処理したいときは、foreach を使うのが基本的で安全な方法です。
減衰つきランダムの意図(ゲーム体験のための工夫)
-
単純なランダムだと「いつまでも何も起こらない」偏りが発生することがある
-
このスクリプトは、“異変なし(
MapAnomalyList[0])”が続くほどMapAnomalyList[0]の確率を下げる
⇒ 一定間隔で異変が起きやすくなり、体験のテンポが保たれる -
baseNoAnomalyProb / minNoAnomalyProb / decayPerStreakを調整すれば、
緊張と緩和のリズムを好みに合わせて設計可能
異変を管理するリストを作成
「EventManager」を作成したときと同じように、まず「script」フォルダ を開きましょう。
フォルダ内の空いているスペースで 右クリック → 「Create」>「MonoBehaviour Script」 を選択します。
作成されたスクリプトの名前を 「MapAnomalyList」 に変更してください。

次に、作成した 「MapAnomalyList」 スクリプトを開き、
下記のコードを記述してください。
まとめと次回予告

今回は、異変のアルゴリズムを実装し、実際に異変を発生させるところまで進めました。
次回はまず、正解/不正解を判定する仕組みを導入して、プレイヤーが異変を見つけたかどうかを判定します。
また、「今、何番出口か」を表示するカウンターを実装し、進行度を常に把握できるようにします。
さらに、現状は前後の向きが分かりにくいため、進行方向側に「Exit」看板を表示して方向を明確化していきます。
次回の記事↓

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






コメント