大手ゲーム開発者は、ゲームの世界のために多くのスペースを作る必要があるときに何をしますか? たくさんのアーティストを雇います。 怠け者/貧乏人/孤独なゲーム開発者は同じ状況で何をしますか? 彼はすべての汚い仕事をする手続き型ジェネレーターを書いています。
フロアプランの手順生成に関する 多くの 記事があります。 記事 へのリンク がさらに 5つ あり ます 。 それらのどれにもソースコードはありません。
この記事では、Unity3dに1つの単純な生成方法を実装した方法について説明します。これは、良好な結果につながり、簡単に変更できます。 写真とソースコード付き。
必要に応じて、 元の記事を読むことができますが、本質は次のとおりです。
- すべてのスペースは、建築計画のグリッドのようなセルに分割されます。
- 外壁はすでに存在し、プログラムの入り口に供給されます。
- 計画に配置する必要がある部屋のリストがあります。
- 部屋にはパラメーター「望ましいサイズ」と互いに引き合う係数があります
- 部屋の誕生地点は、おそらくおおよその地図の助けを借りて、計画に従ってランダムに散らばっています。
- その後、各ポイントは、定義された寸法に達するまで長方形に成長し始めます。
- すべての長方形が大きくなると、部屋は可能な限り長く文字Lの形で大きくなり始めます。
- 残りの空きスペースは隣人に固定されます。
部屋は、壁と部屋が互いの上でrawうのを避けるために、一度に1つの壁を成長させます。 壁は部屋の最大の壁から選択されます。同じ壁がある場合は、ランダムに選択されます。 部屋の面積の成長が最大になるように最大の壁が取られ、それらは「ふくらんでいる」。
今、私の実装について。 これまでのところ、部屋の現実的なレイアウト、係数、サイズに煩わされていませんでした-これらはすべて装飾であり、基本的なアルゴリズムに取り組んでおり、他のすべてをすばやく追加できます。 また、この記事では、U字型の部屋の出現を防ぐ特別なチェックについて説明していますが、私はそうしたことはしませんでした。 また、廊下、ドア、窓は配置しませんでした。これは別のトピックであり、それらの配置は建物の残りの部分の生成に依存する場合があります。
最も困難なことは、部屋の拡大の原則と、部屋と建物に関するデータを保存する方法を選択することでした。 セルオートマトンを使用しても、アレイを単純に実行しても同じ結果が得られます。 私はいくつかのオプションを試しましたが、部屋の寸法と壁の正確な位置について常に考えたいと思っていたので、部屋の角だけが保存され、隣接する2つの角をリンクすることで壁が自動的に作成されるクラスを作成しました。
壁のリンクは、基本的に一連のポイントからポリゴンシェルを検索することです。 このタスクは簡単ではなく、いくつかのトリックがあります 。興味がある場合は、 こちらをご覧ください 。 幸いなことに、私は自分自身を隣接する壁の間の正しい角度に制限することに決め、簡単なアルゴリズムを作成しました。
- ポイントのセットでX座標とY座標の最小値と最大値を見つけます。
- ポイント(minX、minY)から、イグルカを上に移動し、そのような座標を持つポイントを探します。それがない場合は、右、下、左を探します。 見つかったら、古いリストからそれを取り出し、新しいリストに転送します。
- 古いリストのこのポイントの座標から、ゲーム内で次の上位のポイントを探し、見つかった場合は新しいリストに転送し、古いリストから削除します。Yに平行な壁が見つかったので、次の壁はXに平行でなければなりません。
- 右と左でXでポイントを検索し、新しいリストに転送し、見つけた壁の向きを覚えて、類推してさらに調べます。
- 古いリストの最後のポイントは、新しいリストに単純に転送されます。
部屋のコーナーを並べ替える
public GridVector SortCorners() { // var minX = corners[0].x; var maxX = corners[0].x; var minY = corners[0].y; var maxY = corners[0].y; foreach (var corner in corners) { if (corner.x < minX) minX = corner.x; if (corner.x > maxX) maxX = corner.x; if (corner.y < minY) minY = corner.y; if (corner.y > maxY) maxY = corner.y; } // var oldC = new List<GridVector>(corners); var newC = new List<GridVector>(); bool parallelX = false; while (oldC.Count > 1) { // if (newC.Count == 0) { if (ScanUp(ref oldC, ref newC, minX, minY, maxY)) continue; if (ScanRight(ref oldC, ref newC, minX, minY, maxX)) continue; if (ScanDown(ref oldC, ref newC, minX, minY, minY)) continue; if (!ScanLeft(ref oldC, ref newC, minX, minY, minX)) { Debug.Log("Error on start"); return null; } } // else { var last = newC[newC.Count - 1]; if (parallelX) { if (ScanRight(ref oldC, ref newC, last.x, last.y, maxX)) { parallelX = false; continue; } if (ScanLeft(ref oldC, ref newC, last.x, last.y, minX)) { parallelX = false; continue; } } else { if (ScanUp(ref oldC, ref newC, last.x, last.y, maxY)) { parallelX = true; continue; } if (ScanDown(ref oldC, ref newC, last.x, last.y, minY)) { parallelX = true; continue; } } Debug.Log("Error -------------------------------------------------"); Debug.Log("Corners: " + corners.Count); Debug.Log("OldC: " + oldC.Count); Debug.Log("NewC: " + newC.Count); Debug.Log(last); color = Color.red; return last; } } // newC.Add(oldC[0]); corners = newC; return null; } bool ScanLeft(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int minX) { for (var x = startX; x >= minX; x--) { var index = oldC.FindIndex(gv => gv.x == x && gv.y == startY); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanUp(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int maxY) { for (var y = startY; y <= maxY; y++) { var index = oldC.FindIndex(gv => gv.x == startX && gv.y == y); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanRight(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int maxX) { for (var x = startX; x <= maxX; x++) { var index = oldC.FindIndex(gv => gv.x == x && gv.y == startY); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; } bool ScanDown(ref List<GridVector> oldC, ref List<GridVector> newC, int startX, int startY, int minY) { for (var y = startY; y >= minY; y--) { var index = oldC.FindIndex(gv => gv.x == startX && gv.y == y); if (index > -1) { newC.Add(oldC[index]); oldC.RemoveAt(index); return true; } } return false; }
最後に、時計回りにソートされた角度のリストを取得し、そこから壁を簡単に表示できます。 並べ替えのおかげで、別の重要なことを学ぶことが容易になります-部屋の壁から外側への方向。 壁の端を外すには、XとYを交換し、最初の座標を反転して次を取得する必要があります:(-y、x)。
部屋はポイントの形式で保存され、ポイントは並べ替えられ、壁は自動的に構築され、外向きがわかります。これは、フロアプランの空きスペースを確認し、部屋を拡大するのに十分です。 古い壁が何かにかかって成長できなくなったときに、新しい壁を追加しなければならないこともあります。
セルの可用性を確認し、新しい壁の可能性を検索するために、1つの関数を使用します。この関数は、入り口に供給される壁の成長セクションで使用可能なリストを収集します。 次に、部屋全体から見つかったセグメントをカテゴリに分類します:固体の壁、壁の端の部分、中央のセグメント。 利用可能な壁から最大の壁を選択し、それを部屋の成長機能に送信すると、彼女はすでにそのような壁があるか、新しい壁を作成する必要があるかを理解します。 壁がある場合、その端の座標は外側にシフトされ、隣接する壁が自動的に延長されます。
壁セグメントを検索する
List<RoomWall> FindSegments(RoomWall wall, Color freeColor, Color roomColor) { var moved = wall + wall.outwards.minimized; BresenhamLine(moved, new Color(Random.value * 0.7f + 0.1f, Random.value * 0.7f + 0.1f, Random.value * 0.7f + 0.1f), segmentsTexture); var x0 = moved.start.x; var y0 = moved.start.y; var x1 = moved.end.x; var y1 = moved.end.y; var segments = new List<RoomWall>(); GridVector start = null; GridVector end = null; bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0); if (steep) { Swap(ref x0, ref y0); Swap(ref x1, ref y1); } if (x0 > x1) { Swap(ref x0, ref x1); Swap(ref y0, ref y1); } for (int x = x0; x <= x1; x++) { for (int y = y0; y <= y1; y++) { int coordX = steep ? y : x; int coordY = steep ? x : y; Color color = texture.GetPixel(coordX, coordY); if (color != freeColor && color != roomColor) { if (end != null && start != null) { var segment = new RoomWall(start, end); segment -= wall.outwards.minimized; segments.Add(segment); start = null; end = null; } scanTexture.SetPixel(coordX, coordY, Color.red); } else { if (start == null) { start = new GridVector(coordX, coordY); } end = new GridVector(coordX, coordY); scanTexture.SetPixel(coordX, coordY, Color.green); } } } if (end != null && start != null) { var segment = new RoomWall(start, end); segment -= wall.outwards.minimized; segments.Add(segment); } return segments; }
すべての操作を部屋で表示するには、壁の位置にピクセルを入力するテクスチャを使用します。 古い壁を消去しないと、記事の冒頭のgifのように塗りつぶされた領域が得られます。 ここで書いたブレゼンハムラインを使って壁を描きます 。 壁の接着に問題がある場合、すべてがすぐに見えるようになります。 テクスチャの代わりに、2次元配列を使用するか、すぐに3次元モデルを操作できます。
外壁は非常に簡単に生成できます。 任意のサイズの黒い長方形をいくつか描画し、その上に同じものだけを描画し、両側に1ピクセルずつ小さくします。 多くの異なる家が判明しました。 また、3次元で作成し、屋根で覆うこともできます。 Voice of Kanevsky:ただし、これはまったく別の話です。
以下のリンクで、完成したプロジェクトのバイナリとソースコードを確認できます。
Unity Web Player | Windows | Linux | Mac | GitHubソース
Shift-ランダムな部屋でランダムな外壁を作成します
Ctrl-ランダムルームでテストテクスチャをロードする
Enter-すべての部屋を削除し、テストテクスチャをロードします
スペースバー-部屋の成長を止める
アルファベットキーボードのユニット-フリーセルの検索の視覚化を有効にします
空き領域でマウスの左ボタン-部屋を追加
Esc-終了
Linuxユーザーの場合:「chmod + x ProceduralExperiments.x86」を使用してProceduralExperiments.x86ファイルを実行可能にし、実行します。