Unity で8番出口風ゲームを作る!#4
Unityでゲームを作る
Unity で8番出口風ゲームを作る!
Unity Version 2022.3.16
無限に続く通路
8番出口の通路は、下図のように2種類の通路が交互に連続して接続されることによって構成されている。そしてプレーヤーは、前方にずっと先に進むことも、後方にずっと戻ることもできる。よって事前に(静的に)通路を生成しておいてそれを表示するということは難しい(メモリ次第では可能だが)。
今回は、下図のように通路Aの特定の位置を通過しことを検出して、その先の通路を先回りして生成して後方の通路は削除するということを繰り返すことで無限の通路を生成することを考える。
初期状態では、通路B + 通路A + 通路B の3つだけが存在するマップを作成する。プレーヤーは、ゲーム開始時に通路A の中央付近にスポーンさせる。例えば、プレーヤーが通路A を前方に進み特定のポイント(ゲート)を通過すると、その先の通路B の先に新しい通路A を作成する。それと同時に一番後方にある通路B は削除する。プレーヤーが逆方向に進む場合も同様の処理を行う。
通路A を生成するか通路B を生成するかの判断は、プレーヤーが通路Aのどの出口(入口)を出るか(入るか)で判定を行う。
生成ルールは以下のとおり
- 通路A の左出口(入口)を
- 出る:前方に通路A を生成する
- 入る:前方に通路B を生成する
- 通路A の右出口(入口)を
- 出る:後方に通路A を生成する
- 入る:後方に通路B を生成する
上記のルールを検知するには、通路A上のどの出口(位置)から出入りしたか(方向)を検知する仕組みが必要になる。このため、通路A にBox Collider をアタッチした透明なオブジェクト(=ゲート)を下図のように4つ配置し、それぞれに別のTag を付けてどのゲートをどの順番で通過したか検知できるようにした。
ゲートは、Box Collider コンポーネントが付いただけの透明なオブジェクトとする。Box Collider には"Is Trigger" のチェックを入れて、衝突検知イベントだけが発生するようにする。こうしておかないと、CharacterController のCollider と衝突したときに通り抜けられなくなるためである。
ゲートオブジェクトには、どの位置のゲートか衝突検知時に判断できるようにそれぞれ別のタグをつけておく。
衝突検知のイベント処理は、プレーヤーオブジェクトにアタッチしたスクリプトで行う。ゲートオブジェクトのBox Collider で"Is Trigger" にチェックを入れているため、衝突イベントは、OnTriggerEnter メソッドで受け取ることができる。
Player.cs の抜粋
OnTriggerEnter メソッドの引数 Collider は、プレーヤーと衝突した相手側のゲートのCollider になっている。Collider クラスには、その所有者であるGameObject への参照が含まれているので、そこからGameObject のTag 情報を引っ張ってくることができる。ここまでくれば後は、Tag 情報からどの位置のゲートと衝突したのかを特定すればよい。さらにプレーヤーが入ったのか出たのか、方向を確認するために前回検知したゲートは覚えておき、前回と今回分の2つを組合わせて方向を特定する。
- void OnTriggerEnter(Collider other)
- {
- if (other.gameObject.CompareTag("GateA2Right"))
- {
- if ( _prevDetectedGate == "GateA2Left")
- {
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateA2Backward));
- }
- _prevDetectedGate = "GateA2Right";
- }
- else if (other.gameObject.CompareTag("GateA2Left"))
- {
- if (_prevDetectedGate == "GateA2Right")
- {
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateA2Forward));
- }
- _prevDetectedGate = "GateA2Left";
- }
- else if (other.gameObject.CompareTag("GateA1Right"))
- {
- if (_prevDetectedGate == "GateA1Left")
- {
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateA1Backward));
- }
- _prevDetectedGate = "GateA1Right";
- }
- else if (other.gameObject.CompareTag("GateA1Left"))
- {
- if (_prevDetectedGate == "GateA1Right")
- {
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateA1Forward));
- }
- _prevDetectedGate = "GateA1Left";
- }
- else if (other.gameObject.CompareTag("GateRobotFWD"))
- {
- other.gameObject.SetActive(false);
- var path = other.gameObject.transform.parent.gameObject;
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateRobotFWD, path));
- }
- else if (other.gameObject.CompareTag("GateRobotBWD"))
- {
- other.gameObject.SetActive(false);
- var path = other.gameObject.transform.parent.gameObject;
- _playerEvent.Invoke(new PlayerEventParams(PlayerEventParams.Type.TouchGateRobotBWD, path));
- }
- }
- 通路オブジェクトの生成/管理
LevelControl.cs の抜粋
通路オブジェクトは、前方と後方の両方に延びていく可能性がある。そこで通常のList ではなくLinkedList を使って管理を行う。LinkedList であれば、List の先頭/末尾にオブジェクトを追加することや、List の先頭/末尾のオブジェクトを取得する/削除するといった操作を簡単に行うことができる。
- private LinkedList<GameObject> _pathList = new LinkedList<GameObject>();
通路A、B のPrefab はリソースから初期化時にロードしておく。
- private void LoadPrefabs()
- {
- _pathAPrefab = Resources.Load<GameObject>("Prefabs/PathA");
- _pathBPrefab = Resources.Load<GameObject>("Prefabs/PathB");
- }
新規に生成する通路オブジェクトの相対位置は、通路組み合わせから事前に決定することができる。
上図の赤と青のベクトルは、"B + A + B" と "A + B + A" の組み合わせでそれぞれが逆向きの関係になっているだけだということがわかる(-1を掛けたもの)。
最終的な新規通路の作成位置は、上記の相対位置に、接続する先頭または末尾の通路オブジェクトの位置を足したものになる。先頭または末尾の通路オブジェクトはLinkedList から取得する(offset)。
- private GameObject CreatePathObject(PathType type, bool isForward)
- {
- GameObject prefab = null;
- Vector3 connectPoint = Vector3.zero;
- if (type == PathType.A)
- {
- prefab = _pathAPrefab;
- connectPoint = (isForward) ? CONNECTION_B2 : CONNECTION_B1;
- }
- else
- {
- prefab = _pathBPrefab;
- connectPoint = (isForward) ? CONNECTION_A2 : CONNECTION_A1;
- }
- var path = Instantiate(prefab, Vector3.zero, Quaternion.identity);
- path.transform.SetParent(this.transform);
- var offset = (isForward) ?
- _pathList.First.Value.transform.localPosition : _pathList.Last.Value.transform.localPosition;
- path.transform.localPosition = offset + connectPoint;
- return path;
- }
作成した通路オブジェクトは、LinkedList(_pathList)の先頭 or 末尾に追加しておく。
- private GameObject CreatePath(PathType type, bool isForward = true, bool isNavRefresh = false)
- {
- var path = CreatePathObject(type, isForward);
- if (type == PathType.B)
- {
- EnableGateRobot(path.transform, isForward);
- }
- if (isForward)
- {
- Debug.Log("Add Forward");
- _pathList.AddFirst(path);
- }
- else
- {
- Debug.Log("Add Backward");
- _pathList.AddLast(path);
- }
- CheckAndDeletePath(isForward);
- if (isNavRefresh)
- {
- _navMeshSurface.BuildNavMesh();
- }
- return path;
- }
最後にLinkedList のサイズが4以上の場合は、進む方向とは逆の末尾/先頭のオブジェクトを抜き出して削除しておく。これで常に生成された通路はプレーヤー付近の3つ以内に制限させることになる。
ここまで試作した結果がこちら。
次の記事