【Unity(C#)】3次元空間にオブジェクトを重なりなく生成(表示)する方法

##デモ

まずはデモです。

特定の範囲内にオブジェクトがランダムかつ、
メッシュ同士が重なって表示されることなく出現します。


![GunShot3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/a6a2b3bb-4754-6e07-4e2e-819c58a7cbdb.gif)

生成位置(表示位置)をランダムにすることは簡単なのですが、
立体的なオブジェクトをメッシュの重なりなく表示するのは
少々入り組んだロジックを考える必要がありましたのでメモしときます。


##コード

まずはコード全文です。
今回は最初からHierarchyにオブジェクトを非表示で配置しておきました。
<img width="109" alt="GenerateRandom.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/57258e8a-8c49-6a28-39f8-277c44a2ddb0.png">


```C#
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Random = UnityEngine.Random;

/// <summary>
/// ブロックをランダムな位置にランダムなタイミングで表示
/// 位置被りナシ
/// </summary>
public class ActivateBlock : MonoBehaviour
{
    [SerializeField] private GameObject _blockParent;
    [SerializeField] private BoxCollider _blockBoxCollider;

    private const float _MIN_INTERVAL_VALUE = 0.5f;
    private const float _MAX_INTERVAL_VALUE = 2.0f;
    private const float _MIN_X_VALUE = -1.0f;
    private const float _MAX_X_VALUE = 1.0f;
    private const float _MIN_Y_VALUE = 0.5f;
    private const float _MAX_Y_VALUE = 2.0f;
    private const float _MIN_Z_VALUE = -1.0f;
    private const float _MAX_Z_VALUE = 1.0f;

    private bool _isGameStart = true;

    private bool _isSetablePositionX;
    private bool _isSetablePositionY;
    private bool _isSetablePositionZ;

    private int _randomNumber;
    private float _randomInterval;
    private float _randomValueX;
    private float _randomValueY;
    private float _randomValueZ;

    private readonly List<Vector3> _usePositionList = new List<Vector3>();

    void Start()
    {
        //空同然だけどリスト作っとく
        foreach (Transform child in _blockParent.transform)
        {
            _usePositionList.Add(child.position);
        }
        
        DelayInitBlock();
    }

    private async UniTask DelayInitBlock()
    {
        while (_isGameStart)
        {
            //ランダムな値
                _randomNumber = Random.Range(0, _blockParent.transform.childCount);
                _randomInterval = Random.Range(_MIN_INTERVAL_VALUE, _MAX_INTERVAL_VALUE);
                _randomValueX = Random.Range(_MIN_X_VALUE, _MAX_X_VALUE);
                _randomValueY = Random.Range(_MIN_Y_VALUE, _MAX_Y_VALUE);
                _randomValueZ = Random.Range(_MIN_Z_VALUE, _MAX_Z_VALUE);

            //選ばれたブロックの位置
            Vector3 selectedBlockPosition =
                    _blockParent.transform.GetChild(_randomNumber).gameObject.transform.position;

            //選ばれたブロックの位置は比較対象から一旦削除
            _usePositionList.Remove(selectedBlockPosition);

            //現在使用中のポジションのリストから今利用検討中のポジションが利用可能か判定
            foreach (Vector3 position in _usePositionList)
            {
                //表示位置被りがないかオブジェクトの大きさでチェック
                _isSetablePositionX =
                    Mathf.Abs(position.x - _randomValueX) > _blockBoxCollider.bounds.size.x ;
                _isSetablePositionY =
                    Mathf.Abs(position.y - _randomValueY) > _blockBoxCollider.bounds.size.y;
                _isSetablePositionZ =
                    Mathf.Abs(position.z - _randomValueZ) > _blockBoxCollider.bounds.size.z ;

                //座標のうち、全ての軸で被っていたら置けないのでやり直し
                if (!_isSetablePositionX && !_isSetablePositionY && !_isSetablePositionZ)
                {
                    _usePositionList.Add(selectedBlockPosition);
                    break;
                }
            }

            //位置被りがどれか1つの軸で無ければ実行する
            if (_isSetablePositionX || _isSetablePositionY || _isSetablePositionZ)
            { 
                Vector3 randomPosition = new Vector3(_randomValueX, _randomValueY, _randomValueZ);

                //ランダムな間隔でDelay
                await UniTask.Delay(TimeSpan.FromSeconds(_randomInterval));
                _blockParent.transform.GetChild(_randomNumber).gameObject.transform.position = randomPosition;

                //新しい使用中のポジションをリストに追加
                _usePositionList.Add(randomPosition);
            }
        }
    }
}
```

流れとしては下記です。
**⓪"オブジェクトの座標を保存しておくリスト"を作成
①ランダムな座標を作成
②ランダムにオブジェクトを選択
③選んだオブジェクトの座標を"オブジェクトの座標を保存しておくリスト"の中から削除
④XYZの3軸それぞれで位置が被っていないか比較
⑤1軸でも座標が被っていないなら重なりなく表示可能なので表示する
⑥"オブジェクトの座標を保存しておくリスト"に今回利用した座標を追加**

##Bounds
オブジェクトの座標同士を比較するだけでは、オブジェクト同士の重なりを防ぐことができません。
ですので、今回は**Bounds**と呼ばれるオブジェクトの領域を指すもの利用することにしました。

**BoxCollider.bounds.size**でオブジェクトの持つコライダーの大きさが取得できます。

このオブジェクトの大きさを利用して、
**新しく配置しようとしている座標**が**既に利用済みの座標**と比べて**Bounds一個分離れているか**
どうかを判定すれば、位置被りをなくすロジックを組むことができます。

```C#
 //表示位置被りがないかオブジェクトの大きさでチェック
 _isSetablePositionX = Mathf.Abs(現在利用中のX座標 - ランダムに生成したX座標) > _blockBoxCollider.bounds.size.x ;
 _isSetablePositionY = Mathf.Abs(現在利用中のY座標 - ランダムに生成したY座標) > _blockBoxCollider.bounds.size.y;
 _isSetablePositionZ = Mathf.Abs(現在利用中のZ座標 - ランダムに生成したZ座標) > _blockBoxCollider.bounds.size.z ;
```

---
2020/07/26　追記
取得したいBoundsを持つオブジェクトが非アクティブな時、
Boundsの取得に失敗することなく、0を返します。(要注意)

##まとめ
結構必要となるロジックだと思いましたが、意外と実装録を見つけられませんでした。
もっとスマートな方法があれば教えてください。