Unityでの手続き型ラビリンス生成

画像

泚このチュヌトリアルはUnity 2017.1.0向けに曞かれおおり、䞊玚ナヌザヌを察象ずしおいたす。 Unityでのゲヌムのプログラミングにすでに慣れおいるこずは理解できたす。

Unity開発者は、おそらく手動でレベルを䜜成する十分な経隓を持っおいたす。 しかし、その堎でレベルを生成したいず思ったこずはありたすか 以前に䜜成されたモデルの単玔な配眮ずは察照的に、床ず壁のメッシュの手順生成は、ゲヌムのはるかに高い柔軟性ず再珟性を提䟛したす。

このチュヌトリアルでは、次のこずを孊びたす。


仕事を始める


ほずんどのアルゎリズム このアルゎリズムやこのアルゎリズムなどは、「理想的な」密な迷路、぀たり1぀の正しいパスのみを持ち、ルヌプを持たないものを䜜成したす。 パズルの新聞セクションに掲茉されおいる迷宮のように芋えたす。


ただし、迷路が䞍完党でルヌプがある堎合、ほずんどのゲヌムの方がプレむしやすいです。 それらは広倧で、狭い曲がりくねった廊䞋ではなく、オヌプンスペヌスで構成されおいる必芁がありたす。 これは、手続きレベルがそれほど「迷宮」ではなく、ダンゞョンであるならず者のようなゞャンルに特に圓おはたりたす。


このチュヌトリアルでは、 ここで説明する最も単玔な迷路アルゎリズムの1぀を実装したす 。 最小限の劎力でゲヌム内の迷路を実珟するために遞択したした。 この単玔なアプロヌチは、ここにリストされおいる叀兞的なゲヌムでうたく機胜するため、 Speedy Treasure Thiefず呌ばれるゲヌムで迷路を䜜成するために䜿甚できたす。

このゲヌムでは、各レベルは宝箱が隠された新しい迷路です。 しかし、譊備員が戻る前に圌を捜玢しお逃げる時間はあたりありたせん 各レベルには時間制限があり、捕たるたでプレむできたす。 獲埗するポむントは、盗んだ宝物の量によっお異なりたす。


たず、Unityで新しい空のプロゞェクトを䜜成したす。

空癜のプロゞェクトをダりンロヌドしお解凍し、新しいプロゞェクト** proc-mazes-starter.unitypackage **にむンポヌトしたす。 ドラフトコンテンツには次のコンテンツが含たれたす。

  1. グラフィックフォルダヌ。ゲヌムに必芁なすべおのグラフィックが含たれおいたす。
  2. シヌンシヌンは、プレヌダヌずUIを含むこのチュヌトリアルの゜ヌスシヌンです。
  3. 2぀のヘルパヌスクリプトを含むScriptsフォルダヌ。 チュヌトリアルの実行䞭に残りのスクリプトを䜜成したす。

仕事を始めるにはこれで十分です。 これらの各ポむントに぀いおは、埌で詳しく怜蚎したす。

コヌドアヌキテクチャの定矩


空のプロゞェクトをシヌンに远加するこずから始めたしょう。 GameObject▾Create Emptyを遞択し、 Controllerずいう名前を付けおX0、Y0、Z0に配眮したす。 このオブゞェクトは、ゲヌムを制埡するスクリプトの接続点にすぎたせん。

プロゞェクトのScriptsフォルダヌで、 GameControllerずいうCスクリプトを䜜成し、別のスクリプトを䜜成しおMazeConstructorずいう名前を付けたす。 最初のスクリプトはゲヌム党䜓を制埡し、2番目のスクリプトは迷路を生成したす。

GameControllerのすべおの行を次のコヌドに眮き換えたす。

using System; using UnityEngine; [RequireComponent(typeof(MazeConstructor))] // 1 public class GameController : MonoBehaviour { private MazeConstructor generator; void Start() { generator = GetComponent<MazeConstructor>(); // 2 } } 

䜜成したものを簡単に説明したす。

  1. RequireComponent属性は、このスクリプトをGameObjectに远加するずきにMazeConstructorコンポヌネントを远加したす。
  2. プラむベヌト倉数は、 GetComponent()によっお返されるリンクを保持したす。

このスクリプトをシヌンに远加したす。ProjectりィンドりからGameControllerスクリプトをHierarchyりィンドりのGameObject Controllerにドラッグしたす。

MazeConstructorもControllerに远加されおいるこずに泚意しおください。 これは、 RequireComponent属性のおかげで自動的にRequireComponentたす。

次に、 MazeConstructorのすべおを次のコヌドに眮き換えたす。

 using UnityEngine; public class MazeConstructor : MonoBehaviour { //1 public bool showDebug; [SerializeField] private Material mazeMat1; [SerializeField] private Material mazeMat2; [SerializeField] private Material startMat; [SerializeField] private Material treasureMat; //2 public int[,] data { get; private set; } //3 void Awake() { // default to walls surrounding a single empty cell data = new int[,] { {1, 1, 1}, {1, 0, 1}, {1, 1, 1} }; } public void GenerateNewMaze(int sizeRows, int sizeCols) { // stub to fill in } } 

ここで䜕が起こるかです

  1. これらすべおのフィヌルドは、 むンスペクタヌで利甚できたす。 showDebugはデバッグ衚瀺を切り替え、さたざたなマテリアルリンクは生成されたモデルのマテリアルです。 ずころで、倉数がprivateであっおも、 SerializeField属性はInspectorにフィヌルドを衚瀺したす。
  2. 次はdataプロパティです。 アクセス宣蚀たずえば、プロパティをpublicずしお宣蚀した埌、 private set割り圓おるは、クラス倖で読み取り専甚にしたす。 したがっお、迷路のデヌタを倖郚から倉曎するこずはできたせん。
  3. 興味深いコヌドの最埌の郚分はAwake()たす。 この関数は、れロを囲む3 x 3のナニット配列でdataを初期化したす。 1は壁を意味し、0は空きスペヌスを意味したす。぀たり、グリッドはデフォルトでは壁のある郚屋のように芋えたす。

これはすでにコヌドの優れた基盀ですが、今のずころは䜕も衚瀺したせん

迷路デヌタを衚瀺しおその倖芳を確認するには、次のメ゜ッドをMazeConstructorに远加したす。

 void OnGUI() { //1 if (!showDebug) { return; } //2 int[,] maze = data; int rMax = maze.GetUpperBound(0); int cMax = maze.GetUpperBound(1); string msg = ""; //3 for (int i = rMax; i >= 0; i--) { for (int j = 0; j <= cMax; j++) { if (maze[i, j] == 0) { msg += "...."; } else { msg += "=="; } } msg += "\n"; } //4 GUI.Label(new Rect(20, 20, 500, 500), msg); } 

コメントされた各セクションを怜蚎しおください。

  1. このコヌドは、デバッグ衚瀺がオンになっおいるかどうかを確認したす。
  2. いく぀かのロヌカル倉数の初期化保存された迷路のロヌカルコピヌ、最倧の行ず列、および行。
  3. 2぀のネストされたルヌプは、2次元配列の行ず列を通過したす。 配列の各行/列に察しお、コヌドは保存された倀をチェックし、倀がれロかどうかに応じお「....」たたは「==」を远加したす。 たた、行のすべおの列を調べた埌、コヌドは新しい行を远加しお、配列の各行が新しい行で始たるようにしたす。
  4. 最埌に、 GUI.Label()は䜜成される文字列を衚瀺したす。 このプロゞェクトは新しいプレヌダヌ出力GUIシステムを䜿甚したすが、叀いシステムは高速デバッグメッセヌゞを䜜成するのが簡単です。

MazeConstructorコンポヌネントのShow Debugを必ず有効にしおください。 [ 再生]をクリックするず、保存された迷路デヌタが画面に衚瀺されたす珟圚はデフォルトの迷路です。


いいスタヌトです ただし、コヌドはただ迷路自䜓を生成したせん。 次のセクションでは、この問題を解決する方法を説明したす。

迷路デヌタ生成


これたでのずころ、 MazeConstructor.GenerateNewMaze()は空です。 これは空癜で、埌で入力したす。 GameControllerスクリプトのStart()メ゜ッドの最埌に、次の行を远加したす。 圌女はこのスタブメ゜ッドを呌び出したす。

  generator.GenerateNewMaze(13, 15); 

「マゞック」番号13ず15は、迷路のサむズを決定するメ゜ッドのパラメヌタヌです。 ただ䜿甚しおいたせんが、これらのサむズオプションは、グリッドの行ず列の数を指定したす。

この時点で、迷路のデヌタの生成を開始できたす。 新しいMazeDataGeneratorスクリプトを䜜成したす。 このクラスはデヌタ生成ロゞックをカプセル化し、 MazeConstructorで䜿甚されたす。 新しいスクリプトを開き、すべおを次のコヌドに眮き換えたす。

 using System.Collections.Generic; using UnityEngine; public class MazeDataGenerator { public float placementThreshold; // chance of empty space public MazeDataGenerator() { placementThreshold = .1f; // 1 } public int[,] FromDimensions(int sizeRows, int sizeCols) // 2 { int[,] maze = new int[sizeRows, sizeCols]; // stub to fill in return maze; } } 

このクラスはMonoBehaviourを継承しないこずに泚意しおください。 コンポヌネントずしお盎接䜿甚されるのではなく、 MazeConstructor内でのみ䜿甚されるため、 MonoBehaviourの機胜を持぀必芁はありたせん。

  1. スペヌスが空であるかどうかを決定するために、 placementThresholdはデヌタ生成アルゎリズムによっお䜿甚されたす。 デフォルトのコンストラクタヌは、クラスコンストラクタヌでこの倉数に割り圓おられたすが、他のコヌドが生成された迷路の蚭定を制埡できるようにpublicされたす。
  2. メ゜ッドの1぀この堎合はFromDimensions() が再び空になり、空癜が残りたす。これは埌で入力したす。

次に、 MazeConstructorにコヌドのいく぀かのセクションを远加しお、 スタブメ゜ッドを呌び出せるようにしたす。 たず、デヌタゞェネレヌタヌを栌玍するプラむベヌト倉数を远加したす。

 private MazeDataGenerator dataGenerator; 

次に、 Awake()でむンスタンスを䜜成し、 Awake()メ゜ッドの先頭に次の行を远加しお、ゞェネレヌタヌを新しい倉数に保存したす。

  dataGenerator = new MazeDataGenerator(); 

最埌に、 GenerateNewMaze() FromDimensions()でFromDimensions()を呌び出し、グリッドサむズを枡し、結果のデヌタを保存したす。 GenerateNewMaze() // stub to fill inず蚀う行を芋぀けお、次の行に眮き換えたす。

  if (sizeRows % 2 == 0 && sizeCols % 2 == 0) { Debug.LogError("Odd numbers work better for dungeon size."); } data = dataGenerator.FromDimensions(sizeRows, sizeCols); 

生成された迷路は壁に囲たれるため、サむズに奇数を䜿甚する方がよいずいう譊告がここに远加されたした。

ゲヌムを実行しお、空の迷路デヌタを衚瀺したすが、正しい寞法で


いいね すべおが迷路デヌタを保存しお衚瀺する準備ができたした FromDimensions()から迷路生成アルゎリズムを実装する時が来FromDimensions() 。


䞊蚘のアルゎリズムは、壁を配眮し、ブロックする隣接スペヌスを遞択するこずにより、グリッド内のすべおのセルをバむパスしたす぀たり、すべおのセルではありたせん。 ここでプログラムされたアルゎリズムはそれずは少し異なりたす。たた、スペヌスをスキップするかどうかを決定したす。これにより、迷路に空きスペヌスが出珟する可胜性がありたす。 アルゎリズムは倚くの情報を保存したり、迷路の残りの郚分、たずえば通過する必芁のある分岐点に぀いお倚くを知る必芁がないため、コヌドは非垞に単玔になりたす。

この迷路生成アルゎリズムを実装するには、 FromDimensions()からFromDimensions()次のコヌドを远加し、行を// stub to fill in眮き換えお入力// stub to fill inたす。

  int rMax = maze.GetUpperBound(0); int cMax = maze.GetUpperBound(1); for (int i = 0; i <= rMax; i++) { for (int j = 0; j <= cMax; j++) { //1 if (i == 0 || j == 0 || i == rMax || j == cMax) { maze[i, j] = 1; } //2 else if (i % 2 == 0 && j % 2 == 0) { if (Random.value > placementThreshold) { //3 maze[i, j] = 1; int a = Random.value < .5 ? 0 : (Random.value < .5 ? -1 : 1); int b = a != 0 ? 0 : (Random.value < .5 ? -1 : 1); maze[i+a, j+b] = 1; } } } } 

ご芧のずおり、コヌドは2D配列の境界を取埗し、それをバむパスしたす。

  1. 各グリッドセルに぀いお、珟圚のセルがグリッドを超えおいるかどうか぀たり、むンデックスのいずれかが配列の境界䞊にあるかどうかを最初に確認したす。 もしそうなら、圌は1を割り圓おる壁を蚭定したす。
  2. 次に、コヌドは、 2番目のセルごずにアクションを実行するために、座暙が完党に2で陀算されるかどうかを確認したす。 たた、このセルをランダムにスキップしおアレむを走査し続けるために、䞊蚘のplacementThreshold倀の远加チェックがありたす。
  3. 最埌に、コヌドは珟圚のセルずランダムに遞択された隣接セルに倀1を割り圓おたす。 このコヌドは、いく぀かの3項挔算を䜿甚しお0、1、たたは-1を配列むンデックスに远加し、隣接セルのむンデックスを取埗したす。

迷路デヌタを再床衚瀺しお、生成された迷路がどのように芋えるかを確認したす。


ゲヌムを再起動しお、迷路デヌタが毎回新しいこずを確認したす。 いいね

次の倧きな課題は、2D迷路デヌタから3Dメッシュを生成するこずです。

ラビリンスメッシュ生成


迷路のすべおのデヌタを生成した埌、これらのデヌタに基づいおメッシュを構築できたす。

別の新しいMazeMeshGeneratorスクリプトを䜜成したす。 MazeDataGeneratorが迷路生成ロゞックをカプセル化したように、 MazeMeshGeneratorはメッシュ生成ロゞックを含み、この迷路生成ステップを完了するためにMazeConstructorによっお䜿甚されたす。

より正確には、 埌でメッシュ生成ロゞックが含たれたす。 最初に、デモ甚のテクスチャ付き四角圢を䜜成し、次にこのコヌドを倉曎しお迷路党䜓を生成したす。 これを行うには、Unity゚ディタヌに小さな倉曎を加えおから、コヌドを掘り䞋げる必芁がありたす。

たず、生成されたメッシュに適甚されるマテリアルをバむンドする必芁がありたす。

[ プロゞェクト]りィンドりで[ グラフィックス ]フォルダヌを遞択し、りィンドりで[ 階局コントロヌラヌ]を遞択しお、 むンスペクタヌにそのMaze Constructorコンポヌネントを衚瀺したす。

マテリアルをGraphicsフォルダからMaze Constructorマテリアルスロットにドラッグしたす。 マテリアル1にフロア マットを 、マテリアル2にりォヌル マットを䜿甚し、 開始ず宝を適切なスロットにドラッグしたす。

すでにむンスペクタヌで䜜業しおいるため、 生成されたタグも远加したす。 むンスペクタヌの䞊郚にある[タグ]メニュヌをクリックし、[タグを远加]を遞択したす。 メッシュを生成するずき、それらを芋぀けるためにこのタグを割り圓おたす。

Unity゚ディタヌで必芁な倉曎をすべお行った埌、新しいスクリプトを開き、すべおをこのコヌドで眮き換えたす。

 using System.Collections.Generic; using UnityEngine; public class MazeMeshGenerator { // generator params public float width; // how wide are hallways public float height; // how tall are hallways public MazeMeshGenerator() { width = 3.75f; height = 3.5f; } public Mesh FromData(int[,] data) { Mesh maze = new Mesh(); //1 List<Vector3> newVertices = new List<Vector3>(); List<Vector2> newUVs = new List<Vector2>(); List<int> newTriangles = new List<int>(); // corners of quad Vector3 vert1 = new Vector3(-.5f, -.5f, 0); Vector3 vert2 = new Vector3(-.5f, .5f, 0); Vector3 vert3 = new Vector3(.5f, .5f, 0); Vector3 vert4 = new Vector3(.5f, -.5f, 0); //2 newVertices.Add(vert1); newVertices.Add(vert2); newVertices.Add(vert3); newVertices.Add(vert4); //3 newUVs.Add(new Vector2(1, 0)); newUVs.Add(new Vector2(1, 1)); newUVs.Add(new Vector2(0, 1)); newUVs.Add(new Vector2(0, 0)); //4 newTriangles.Add(2); newTriangles.Add(1); newTriangles.Add(0); //5 newTriangles.Add(3); newTriangles.Add(2); newTriangles.Add(0); maze.vertices = newVertices.ToArray(); maze.uv = newUVs.ToArray(); maze.triangles = newTriangles.ToArray(); return maze; } } 

クラスの最䞊郚にある2぀のフィヌルドwidthずheight 、 MazeDataGeneratorのplacementThreshold䌌おいplacementThreshold 。これらは、コンストラクタヌでデフォルトで蚭定され、メッシュ生成コヌドで䜿甚される倀です。

興味深いコヌドの倧郚分はFromData()たす。 これは、 MazeConstructorがメッシュを生成するために呌び出すメ゜ッドです。 珟時点では、このコヌドは単玔に四角圢を䜜成しおその動䜜を瀺しおいたす。 すぐに党䜓レベルに拡匵したす。

この図は、四角圢の構成芁玠を瀺しおいたす。


コヌドは長くなりたすが、わずかに倉化しお非垞に匷く繰り返されたす。

  1. メッシュは、頂点、UV座暙、および䞉角圢の3぀のリストで構成されたす。
  2. 頂点のリストには、各頂点の䜍眮が栌玍されおいたす...
  3. リストされたUV座暙は、このリストの頂点に察応しおいたす...
  4. そしお、䞉角圢は頂点のリスト内のむンデックスです぀たり、「この䞉角圢は頂点0、1、2で構成されおいたす」。
  5. 2぀の䞉角圢が䜜成されおいるこずに泚意しおください。 四角圢は2぀の䞉角圢で構成されたす。 たた、 Listデヌタ型がリストに参加するために䜿甚されたすが、最終的にはMeshはArraysが必芁です。

MazeConstructorはMazeMeshGeneratorのむンスタンスを䜜成し、メッシュ生成メ゜ッドを呌び出す必芁がありたす。 たた、メッシュも衚瀺されるはずなので、次のコヌドフラグメントを远加したす。

たず、メッシュゞェネレヌタヌを栌玍するプラむベヌトフィヌルドを远加したす。

 private MazeMeshGenerator meshGenerator; 

Awakeでそのむンスタンスを䜜成し、 Awakeメ゜ッドの先頭に次の行を远加しお、メッシュゞェネレヌタヌを新しいフィヌルドに保存したす。

  meshGenerator = new MazeMeshGenerator(); 

次に、 DisplayMazeメ゜ッドを远加したす。

 private void DisplayMaze() { GameObject go = new GameObject(); go.transform.position = Vector3.zero; go.name = "Procedural Maze"; go.tag = "Generated"; MeshFilter mf = go.AddComponent<MeshFilter>(); mf.mesh = meshGenerator.FromData(data); MeshCollider mc = go.AddComponent<MeshCollider>(); mc.sharedMesh = mf.mesh; MeshRenderer mr = go.AddComponent<MeshRenderer>(); mr.materials = new Material[2] {mazeMat1, mazeMat2}; } 

最埌に、 DisplayMazeを呌び出すには、 GenerateNewMazeの最埌に次の行を远加したす。

  DisplayMaze(); 

Mesh自䜓は単なるデヌタです。 シヌン内のオブゞェクトより具䜓的にMeshFilterオブゞェクトのMeshFilter に割り圓おられるたで衚瀺されMeshFilter 。 したがっお、 DisplayMaze()はMazeMeshGenerator.FromData()呌び出すだけでなく、この呌び出しを新しいMeshFilterむンスタンスの䜜成䞭に挿入し、 生成タグを蚭定し、 MeshFilterず生成メッシュを远加し、メッシュずの衝突のためのMeshColliderを远加し、最埌にMeshRendererずマテリアルを远加したす。

MazeMeshGeneratorクラスを䜜成し、 MazeConstructorでむンスタンス化したので、[ 再生 ]をクリックしたす。


テクスチャ付き四角圢を完党にコヌドで構築したした これは興味深く重芁な出発点なので、この段階で䌑憩を取っお䜜業を分析し、コヌドがどのように機胜するかを理解しおください。

次に、 FromData()リファクタリングし、そのようなコヌドで完党に眮き換えたす

 public Mesh FromData(int[,] data) { Mesh maze = new Mesh(); //3 List<Vector3> newVertices = new List<Vector3>(); List<Vector2> newUVs = new List<Vector2>(); maze.subMeshCount = 2; List<int> floorTriangles = new List<int>(); List<int> wallTriangles = new List<int>(); int rMax = data.GetUpperBound(0); int cMax = data.GetUpperBound(1); float halfH = height * .5f; //4 for (int i = 0; i <= rMax; i++) { for (int j = 0; j <= cMax; j++) { if (data[i, j] != 1) { // floor AddQuad(Matrix4x4.TRS( new Vector3(j * width, 0, i * width), Quaternion.LookRotation(Vector3.up), new Vector3(width, width, 1) ), ref newVertices, ref newUVs, ref floorTriangles); // ceiling AddQuad(Matrix4x4.TRS( new Vector3(j * width, height, i * width), Quaternion.LookRotation(Vector3.down), new Vector3(width, width, 1) ), ref newVertices, ref newUVs, ref floorTriangles); // walls on sides next to blocked grid cells if (i - 1 < 0 || data[i-1, j] == 1) { AddQuad(Matrix4x4.TRS( new Vector3(j * width, halfH, (i-.5f) * width), Quaternion.LookRotation(Vector3.forward), new Vector3(width, height, 1) ), ref newVertices, ref newUVs, ref wallTriangles); } if (j + 1 > cMax || data[i, j+1] == 1) { AddQuad(Matrix4x4.TRS( new Vector3((j+.5f) * width, halfH, i * width), Quaternion.LookRotation(Vector3.left), new Vector3(width, height, 1) ), ref newVertices, ref newUVs, ref wallTriangles); } if (j - 1 < 0 || data[i, j-1] == 1) { AddQuad(Matrix4x4.TRS( new Vector3((j-.5f) * width, halfH, i * width), Quaternion.LookRotation(Vector3.right), new Vector3(width, height, 1) ), ref newVertices, ref newUVs, ref wallTriangles); } if (i + 1 > rMax || data[i+1, j] == 1) { AddQuad(Matrix4x4.TRS( new Vector3(j * width, halfH, (i+.5f) * width), Quaternion.LookRotation(Vector3.back), new Vector3(width, height, 1) ), ref newVertices, ref newUVs, ref wallTriangles); } } } } maze.vertices = newVertices.ToArray(); maze.uv = newUVs.ToArray(); maze.SetTriangles(floorTriangles.ToArray(), 0); maze.SetTriangles(wallTriangles.ToArray(), 1); //5 maze.RecalculateNormals(); return maze; } //1, 2 private void AddQuad(Matrix4x4 matrix, ref List<Vector3> newVertices, ref List<Vector2> newUVs, ref List<int> newTriangles) { int index = newVertices.Count; // corners before transforming Vector3 vert1 = new Vector3(-.5f, -.5f, 0); Vector3 vert2 = new Vector3(-.5f, .5f, 0); Vector3 vert3 = new Vector3(.5f, .5f, 0); Vector3 vert4 = new Vector3(.5f, -.5f, 0); newVertices.Add(matrix.MultiplyPoint3x4(vert1)); newVertices.Add(matrix.MultiplyPoint3x4(vert2)); newVertices.Add(matrix.MultiplyPoint3x4(vert3)); newVertices.Add(matrix.MultiplyPoint3x4(vert4)); newUVs.Add(new Vector2(1, 0)); newUVs.Add(new Vector2(1, 1)); newUVs.Add(new Vector2(0, 1)); newUVs.Add(new Vector2(0, 0)); newTriangles.Add(index+2); newTriangles.Add(index+1); newTriangles.Add(index); newTriangles.Add(index+3); newTriangles.Add(index+2); newTriangles.Add(index); } 

うわヌ、なんお長いコヌドでしょう しかし、ここでもほずんど同じこずが繰り返されたす。䞀郚の数倀のみが倉曎されたす。 特に、クアッド生成コヌドは別のAddQuad()メ゜ッドに移動され、各グリッドセルの床、倩井、壁に察しお再床呌び出されたす。

  1. AddQuad()の最埌の3぀のパラメヌタヌは、頂点、UV、および䞉角圢の同じリストです。 メ゜ッドの最初の行は、開始するむンデックスを取埗したす。 新しい四角圢を远加するず、むンデックスが増加したす。
  2. ただし、 AddQuad()最初のパラメヌタヌは倉換行列であり、この郚分を理解するのは難しい堎合がありたす。 実際、䜍眮/回転/スケヌルは行列ずしお保存し、頂点に適甚できたす。 これは、たさにMultiplyPoint3x4()コヌルが行うこずです。 したがっお、四角圢生成コヌドは、床、倩井、壁などに䜿甚できたす。 䜿甚する倉換行列を倉曎するだけです
  3. FromData()にFromData()たす。 UV頂点ず䞉角圢のリストが䞊郚に䜜成されたす。 今回は、䞉角圢の2぀のリストがありたす。 Mesh Unityオブゞェクトには、それぞれに異なるマテリアルを持぀倚くのサブミックスを含めるこずができたす。぀たり、䞉角圢の各リストは個別のサブミックスです。 フロアず壁に異なるマテリアルを割り圓おるこずができるように、2぀のミックスを発衚したす。
  4. その埌、2D配列を調べお、各グリッドセルの床、倩井、壁の四角圢を䜜成したす。 各セルには床ず倩井が必芁です。さらに、壁の必芁性に぀いお隣接セルのチェックが行われたす。 AddQuad()は耇数回呌び出されたすが、そのたびに異なる倉換マトリックスず、床ず壁に䜿甚される䞉角圢の異なるリストが呌び出されるこずに泚意しおください。 たた、四角圢の䜍眮ずサむズを決定するためにwidthずheightが䜿甚されるこずに泚意しおください。
  5. ああ、もう1぀小さな远加 RecalculateNormals()は、メッシュをラむティング甚に準備したす。

[再生]をクリックしお、メッシュ党䜓がどのように生成されるかを確認したす。


おめでずう、ここで迷路ずSpeedy Treasure Thiefに必芁なプログラミングの䞻芁郚分が生成されたした 次のセクションでは、ゲヌムの残りの郚分を芋おいきたす。

ゲヌムを終了する


コヌドに他の远加や倉曎を加える必芁がありたすが、たず、ドラフトプロゞェクトにあったものを䜿甚したしょう。 導入郚で述べたように、ドラフトプロゞェクトには2぀のスクリプトがありたす。プレヌダヌずUIのあるシヌンず、迷路で遊ぶためのすべおのグラフィックです。 FpsMovementスクリプトは、 私の本のキャラクタヌコントロヌラヌのシングルスクリプトバヌゞョンであり、 TriggerEventRouterは、ゲヌムトリガヌの操䜜に䟿利な補助コヌドです。

プレヌダヌは、 FpsMovementコンポヌネントず
指向性光源がカメラに取り付けられおいたす。 さらに、スカむボックスずアンビ゚ント照明は、 照明蚭定りィンドりで無効になっおいたす。 最埌に、シヌンにはポむントず時間のマヌクが付いたUIキャンバスがありたす。

そしお、それがドラフトプロゞェクトにありたす。 次に、ゲヌムの残りのコヌドを蚘述したす。

MazeConstructorから始めたしょう。 たず、次のプロパティを远加しお、寞法ず座暙を保存したす。

 public float hallWidth { get; private set; } public float hallHeight { get; private set; } public int startRow { get; private set; } public int startCol { get; private set; } public int goalRow { get; private set; } public int goalCol { get; private set; } 

次に、新しいメ゜ッドを远加する必芁がありたす。 最初はDisposeOldMaze()です。 名前が瀺すように、既存の迷路を削陀したす。 このコヌドは、 生成されたタグを持぀すべおのオブゞェクトを芋぀けお砎棄したす。

 public void DisposeOldMaze() { GameObject[] objects = GameObject.FindGameObjectsWithTag("Generated"); foreach (GameObject go in objects) { Destroy(go); } } 

次に、 FindStartPosition()メ゜ッドを远加したす。 このコヌドは0,0から始たり、空きスペヌスが芋぀かるたで迷路内のすべおのデヌタを調べたす。 次に、これらの座暙が迷路の初期䜍眮ずしお保存されたす。

 private void FindStartPosition() { int[,] maze = data; int rMax = maze.GetUpperBound(0); int cMax = maze.GetUpperBound(1); for (int i = 0; i <= rMax; i++) { for (int j = 0; j <= cMax; j++) { if (maze[i, j] == 0) { startRow = i; startCol = j; return; } } } } 

同様に、 FindGoalPosition()基本的に同じこずを行い、最倧倀から開始しおカりントダりンしたす。 このメ゜ッドも远加したす。

 private void FindGoalPosition() { int[,] maze = data; int rMax = maze.GetUpperBound(0); int cMax = maze.GetUpperBound(1); // loop top to bottom, right to left for (int i = rMax; i >= 0; i--) { for (int j = cMax; j >= 0; j--) { if (maze[i, j] == 0) { goalRow = i; goalCol = j; return; } } } } 

PlaceStartTrigger()およびPlaceGoalTrigger()は、オブゞェクトをシヌン内の開始䜍眮ずタヌゲット䜍眮に配眮したす。 それらのコラむダヌはトリガヌであり、察応するマテリアルが適甚されおから、 TriggerEventRouterが远加されたすプロゞェクトブランクから。このコンポヌネントは、トリガヌのスコヌプに入ったずきに呌び出されるむベント凊理関数を受け取りたす。これら2぀のメ゜ッドを远加したす。

 private void PlaceStartTrigger(TriggerEventHandler callback) { GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.transform.position = new Vector3(startCol * hallWidth, .5f, startRow * hallWidth); go.name = "Start Trigger"; go.tag = "Generated"; go.GetComponent<BoxCollider>().isTrigger = true; go.GetComponent<MeshRenderer>().sharedMaterial = startMat; TriggerEventRouter tc = go.AddComponent<TriggerEventRouter>(); tc.callback = callback; } private void PlaceGoalTrigger(TriggerEventHandler callback) { GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.transform.position = new Vector3(goalCol * hallWidth, .5f, goalRow * hallWidth); go.name = "Treasure"; go.tag = "Generated"; go.GetComponent<BoxCollider>().isTrigger = true; go.GetComponent<MeshRenderer>().sharedMaterial = treasureMat; TriggerEventRouter tc = go.AddComponent<TriggerEventRouter>(); tc.callback = callback; } 

最埌に、メ゜ッド党䜓をGenerateNewMaze()次のコヌドに眮き換えたす。

 public void GenerateNewMaze(int sizeRows, int sizeCols, TriggerEventHandler startCallback=null, TriggerEventHandler goalCallback=null) { if (sizeRows % 2 == 0 && sizeCols % 2 == 0) { Debug.LogError("Odd numbers work better for dungeon size."); } DisposeOldMaze(); data = dataGenerator.FromDimensions(sizeRows, sizeCols); FindStartPosition(); FindGoalPosition(); // store values used to generate this mesh hallWidth = meshGenerator.width; hallHeight = meshGenerator.height; DisplayMaze(); PlaceStartTrigger(startCallback); PlaceGoalTrigger(goalCallback); } 

曞き換えられたGenerateNewMaze()メ゜ッドは、叀いメッシュの削陀やトリガヌの配眮などの操䜜のために远加したばかりの新しいメ゜ッドを呌び出したす。MazeConstructorに

はすでに倚くの機胜が远加されおいたす。幞い、このクラスはこれで完了です。もう1぀コヌドが残っおいたす。次に、新しいコヌドをGameControllerに远加したす。ファむルの内容党䜓を次のものに眮き換えたす。



 using System; using UnityEngine; using UnityEngine.UI; [RequireComponent(typeof(MazeConstructor))] public class GameController : MonoBehaviour { //1 [SerializeField] private FpsMovement player; [SerializeField] private Text timeLabel; [SerializeField] private Text scoreLabel; private MazeConstructor generator; //2 private DateTime startTime; private int timeLimit; private int reduceLimitBy; private int score; private bool goalReached; //3 void Start() { generator = GetComponent<MazeConstructor>(); StartNewGame(); } //4 private void StartNewGame() { timeLimit = 80; reduceLimitBy = 5; startTime = DateTime.Now; score = 0; scoreLabel.text = score.ToString(); StartNewMaze(); } //5 private void StartNewMaze() { generator.GenerateNewMaze(13, 15, OnStartTrigger, OnGoalTrigger); float x = generator.startCol * generator.hallWidth; float y = 1; float z = generator.startRow * generator.hallWidth; player.transform.position = new Vector3(x, y, z); goalReached = false; player.enabled = true; // restart timer timeLimit -= reduceLimitBy; startTime = DateTime.Now; } //6 void Update() { if (!player.enabled) { return; } int timeUsed = (int)(DateTime.Now - startTime).TotalSeconds; int timeLeft = timeLimit - timeUsed; if (timeLeft > 0) { timeLabel.text = timeLeft.ToString(); } else { timeLabel.text = "TIME UP"; player.enabled = false; Invoke("StartNewGame", 4); } } //7 private void OnGoalTrigger(GameObject trigger, GameObject other) { Debug.Log("Goal!"); goalReached = true; score += 1; scoreLabel.text = score.ToString(); Destroy(trigger); } private void OnStartTrigger(GameObject trigger, GameObject other) { if (goalReached) { Debug.Log("Finish!"); player.enabled = false; Invoke("StartNewMaze", 4); } } } 

  1. 最初に远加したのは、シヌン内のオブゞェクトのフィヌルドをシリアル化するこずでした。
  2. タむマヌずゲヌムポむントを远跡するためのいく぀かのプラむベヌト倉数ず、タヌゲットが迷路で芋぀かったかどうかを远加したした。
  3. MazeConstructor , , Start() , GenerateNewMaze() .
  4. StartNewGame() , . , , .
  5. StartNewMaze() , . , , .
  6. Update() , , , . .
  7. OnGoalTrigger() OnStartTrigger() — , TriggerEventRouter MazeConstructor . OnGoalTrigger() , , . OnStartTrigger() , , , .

そしお、これがすべおのコヌドです。Unityのシヌンに戻りたしょう。開始するには、[ 階局]りィンドりで[ キャンバス]を遞択し、むンスペクタヌで有効にしたす。迷路コヌドの䜜成時にデバッグの衚瀺を劚げないように、Canvasは無効になっおいたす。シリアル化されたフィヌルドが远加されおいるこずを忘れないでください。そのため、シヌンオブゞェクトPlayer、CanvasのTimeラベル、およびScoreラベルをむンスペクタヌのスロットにドラッグしたす。[ デバッグの衚瀺 ]をオフにしお、[再生]をクリックするこずもできたす。


玠晎らしい仕事です手続き的に迷路を生成するのは倧倉な䜜業ですが、その結果、刺激的でダむナミックなゲヌムプレむが埗られたす。

次はどこぞ行きたすか


私の埌に繰り返した堎合、あなたはすでに完成したゲヌムを䜜成しおいたす。必芁に応じお、完成したUnityプロゞェクトをここからダりンロヌドできたす。

次に、のコヌドを眮き換えるこずにより、他の迷路生成アルゎリズムを調べるこずができたすFromDimensions()。他の環境を生成するこずもできたす。セルオヌトマトンで掞窟の生成を探玢するこずから始めたす。

マップ䞊のオブゞェクトず敵のランダムな生成は、非垞に興味深いアクティビティです。

Source: https://habr.com/ru/post/J353104/


All Articles