【Unity】ビル群をランダム生成する
お疲れ様です。ぐーるです。
また間が空いてしまった…。
unity1weekがもうすぐあるので、Unity思い出さないとなぁ~と触っています。
ビル群を自動生成する
Unityでビル街を自動生成するツールを作った。
— ぐーるさん (@uuha_goul) February 26, 2019
単純なランダムじゃなくて、必ず道が作られるようにした。 pic.twitter.com/dZHQf0aFvE
こんなにRT&いいねされたのは初。
正直大したことはしていなくて、とっても恐縮。
ランダムでscale決めて、空いている所に配置しているだけです。
早速コード
BuildingMapCreater
using System.Collections; using System.Collections.Generic; using UnityEngine; // ビル一棟ごとのデータ(位置と大きさだけ) public class BuildingMapObjectData { public int id; public Vector3Int position; public Vector3Int scale; } public class BuildingMapCreater { // もっとマップを広くしたい場合は大きくすればOK private int MAP_SIZE_W = 100; private int MAP_SIZE_H = 100; private int[][] _maps; private static BuildingMapCreater _instance; private BuildingMapCreater() { } public static BuildingMapCreater Instance { get { if (_instance == null) { _instance = new BuildingMapCreater(); } return _instance; } } // ビル群のマップを自動で生成する public List<BuildingMapObjectData> CreateMap() { var result = new List<BuildingMapObjectData>(); var i = 0; var j = 0; var id = 1; BuildingMapObjectData mo = null; // 初期化(-1)埋め _maps= new int[MAP_SIZE_H][]; for (i = 0; i < MAP_SIZE_H; i++) { _maps[i] = new int[MAP_SIZE_W]; for (j = 0; j < MAP_SIZE_W; j++) { _maps[i][j] = -1; } } i = 0; j = 0; while (true) { mo = new BuildingMapObjectData(); mo.id = id++; mo.position = Vector3Int.zero; // ビルの大きさをランダムで適当に mo.scale.x = Random.Range(1, 20); mo.scale.y = Random.Range(1, 20); mo.scale.z = Random.Range(1, 20); if (!ExecBuild(mo)) { break; } result.Add(mo); } return result; } // 範囲内に他のビルが重なっていないかチェックする private bool ExecBuild(BuildingMapObjectData mo) { while (true) { if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2)) { PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2); PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z); return true; } else { if (mo.position.x < MAP_SIZE_W) { mo.position.x += 1; } else if (mo.position.z < MAP_SIZE_H) { mo.position.z += 1; mo.position.x = 0; } else { return false; } } } } // 指定範囲を塗りつぶす private void PaintMapRect(int id, int x, int y, int w, int h) { for (var yy = y; yy < y + h; yy++) { for (var xx = x; xx < x + w; xx++) { PaintMap(id, xx, yy); } } } // 指定範囲が空いているか調べる private bool IsEmptyMapRect(int x, int y, int w, int h) { for (var yy = y; yy < y + h; yy++) { for (var xx = x; xx < x + w; xx++) { if (!IsEmptyMap(xx, yy)) { return false; } } } return true; } // 指定位置をIDで塗る private void PaintMap(int id, int xx, int yy) { if (yy >= 0 && yy < _maps.Length && xx >= 0 && xx < _maps[yy].Length) { _maps[yy][xx] = id; } } // 指定位置が空いているかチェック private bool IsEmptyMap(int xx, int yy) { if (yy >= 0 && yy < _maps.Length && xx >= 0 && xx < _maps[yy].Length) { if (_maps[yy][xx] > 0) { return false; } return true; } return false; } // デバッグ用出力 private void DebugOutput() { for (var i = 0; i < MAP_SIZE_H; i++) { string output = ""; for (var j = 0; j < MAP_SIZE_W; j++) { output += "[" + _maps[i][j] + "]"; } Debug.Log(output); } } }
Stage
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Stage : MonoBehaviour { [SerializeField] Building _buildingPrefab; private List<Building> _mapObjects; public void CreateMap() { StartCoroutine(_CreateMap()); } private IEnumerator _CreateMap() { if (_mapObjects != null) { foreach (var m in _mapObjects) { Destroy(m.gameObject); } _mapObjects.Clear(); _mapObjects = null; } _mapObjects = new List<Building>(); var mapObjectDatas = BuildingMapCreater.Instance.CreateMap(); foreach (var mod in mapObjectDatas) { var fs = Instantiate(_buildingPrefab, this.transform); fs.SettingBuildingMapObjectData(mod); _mapObjects.Add(fs); yield return new WaitForEndOfFrame(); } } }
Building
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Building : MonoBehaviour { [SerializeField] int _id; private bool _isInitialize = false; private Vector3 _targetPosition = Vector3.zero; private float INIT_TIME = 10.0f; // Update is called once per frame void Update() { if (_isInitialize) { if (Vector3.Distance(this.transform.position, _targetPosition) > 0.01f) { this.transform.position = Vector3.MoveTowards(this.transform.position, _targetPosition, INIT_TIME * Time.deltaTime); } else { this.transform.position = _targetPosition; _isInitialize = false; } } } public void SettingBuildingMapObjectData(BuildingMapObjectData mapObjectData) { // dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整 var x = mapObjectData.position.x + (mapObjectData.scale.x / 2); var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる var z = mapObjectData.position.z + (mapObjectData.scale.z / 2); _id= mapObjectData.id; _targetPosition = new Vector3(x, y, z); // 最初沈ませておいて、下から迫り上がるような演出をする this.transform.position = new Vector3(_targetPosition.x, _targetPosition.y - mapObjectData.scale.y, _targetPosition.z); this.transform.localScale = mapObjectData.scale; _isInitialize = true; } }
ビル群を生成するイメージ
まず、ランダムでビルという名のただのcubeの大きさを適当に決めます。
その後配置するのですが、この時
- ビル同士が被らない
- ビルとビルの間に1マス以上の隙間を空ける
この2つを実現させます。
高さは被らないため、除外して考えるので、単純に2次元配列で管理します。
0,0の位置から大きさ分の四角形が入る位置を探します。
この時、-1 , + 2をして道路分を空けるようにします。
(この辺の値を大きくするとさらに道路を広く出来ます。
また、この列や行はビル配置禁止!といった値を決めておくと大通りを作ることが出来ます)
if (IsEmptyMapRect(mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2)) { PaintMapRect(0, mo.position.x - 1, mo.position.z - 1, mo.scale.x + 2, mo.scale.z + 2); PaintMapRect(mo.id, mo.position.x, mo.position.z, mo.scale.x, mo.scale.z); return true; }
入らなければ一マス動かす、で入る位置を探します。
入ったらその位置にIDを書き込んでおき、すでにここにはビルがありますよ、
という情報を残しておきます。
これを繰り返して、MAP内に入り切らなくなったら終了です。
余談
記事書いてて思ったのですが、HITしたらIDからビル情報取ってきて、その大きさ足したほうが良かった
生成したビル情報使う
ビル情報のスケールはそのまま入れますが、positionはpivotの関係上、計算が必要です。
// dataの位置ではpivot(0,0)で計算されているため、pivot(0.5,0.5)で位置調整 var x = mapObjectData.position.x + (mapObjectData.scale.x / 2); var y = (mapObjectData.scale.y + 1) / 2; // 高さはスケールに合わせる var z = mapObjectData.position.z + (mapObjectData.scale.z / 2);
プロジェクトとして
おまけ
なんとなくビルが下からニュッと生える感じにしたかったので、
目標位置と開始位置を変えて、目標位置に到達するまで移動するようにしてます。
上から降ってくる感じとかEasingとか付けるともっと賑やかになります。
(DoTweenとか使うともっと簡単ですね)
最後に
蓋を開けてみたらとんでもなく初歩的なスクリプトで
ほんとこんなんでバズって良いんだろうかという気になってます。
実はみんなFracture側の記事を期待してたりして。