ぐーるらいふ

底辺。

【Unity】ビル群をランダム生成する

お疲れ様です。ぐーるです。
また間が空いてしまった…。

unity1weekがもうすぐあるので、Unity思い出さないとなぁ~と触っています。

ビル群を自動生成する

こんなに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を書き込んでおき、すでにここにはビルがありますよ、
という情報を残しておきます。

f:id:ghoul_life:20190227194818p:plain

これを繰り返して、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);

プロジェクトとして

  1. 空のGameObjectを生成して、StageスクリプトをAdd Component
  2. CubeをInspectorに生成して、BuildingスクリプトをAdd Componentして、Prefab化
  3. 作ったCubeをStageに紐づけ
  4. 実行して、Stage.CreateMap()をコールすればOK

おまけ

なんとなくビルが下からニュッと生える感じにしたかったので、
目標位置と開始位置を変えて、目標位置に到達するまで移動するようにしてます。
上から降ってくる感じとかEasingとか付けるともっと賑やかになります。
(DoTweenとか使うともっと簡単ですね)

f:id:ghoul_life:20190227180855g:plain

最後に

蓋を開けてみたらとんでもなく初歩的なスクリプト
ほんとこんなんでバズって良いんだろうかという気になってます。
実はみんなFracture側の記事を期待してたりして。