UdonSharp日記～道の動的生成2～

Qiitaを初めたら、意外と自分がLGTMを気にしていることに気づきました。
LGTMされていると執筆のやる気が湧きます。UdonSharpの記事を増やす役に立つと思ってLGTMを押してください。

#やったこと
前回は床にUdonBehaviorをつけて、床を踏んだら、UdonBehaviorを持つ床をどんどん生成していき、道の動的生成を行う方法をとりました。しかし、その方法だと現環境では失敗してしまうため、方針を変更(インスタンス化した際にUdonが動作しなくなる場合があるため)。
一つのオブジェクトが床を生成し続ける設計としています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">同期できない方法だけど、道の動的生成に成功したよ。<br>今回は十字路だけど、ランダムでT字路混ぜたり、道の形を変えれば、ダンジョンが生成できるね。<br>ダンジョンのレシピをstring型で同期させたら、疑似的にどうきできるかもしれない。試してみたい <a href="https://t.co/V2sdTdHp5Y">pic.twitter.com/V2sdTdHp5Y</a></p>&mdash; はんちょ (@sadimensions) <a href="https://twitter.com/sadimensions/status/1255157782556274689?ref_src=twsrc%5Etfw">April 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そして今回は十字路を生成することとしました。

#やり方
###オブジェクトの配置
まず動的生成を管理するmanager(Create Empty)と生成されるfloor(Blenderからインポート)を用意します。
floorはmangerからアクセスしやすいように子として追加します。
(floorの座標を確認するために子にしている)
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627563/a21dfa27-f38c-5c9c-cc3d-cc4888aef915.png)

###プログラムの設計
決まったサイズの十字路を使用しているため、部屋の移動判定が必要なエリアは等間隔に配置されています。
そしてこのエリアは無限大にあるため、一つ一つ判定するのではなく、数字を加工することで判定します。
![無題2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627563/0b86ec2b-d4fe-8c92-39ee-776f89ed492d.png)

絶対値とmod演算(%の演算)を用いることで、部屋の移動判定を少ない条件分岐で表現することが可能です。
今回の場合、X座標とZ座標を部屋の長さ4mで割った余りが比較対象となります。
そうすると下図の緑のエリアだけを判定すれば、部屋の移動が行われたことがわかります。
例1(10, 4) →　(2, 0) 　部屋移動中
例2(-12, -6) →　(0, 2) 部屋移動中
例3(4 , 4) →　(0, 0)   部屋の中
![無題3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627563/465ba9fa-d154-4076-ed45-9bff486692b8.png)

部屋の移動がされたことがわかったら、プレイヤーの座標をもとに、上下 / 左右どちらかの部屋を作るか決めるだけです。

![無題4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627563/c145600c-4685-45cc-fd01-b0835a0afaad.png)

設計思想は以上です。他にも座標の調整などがソースコードに反映されていますが、割愛します。

###ソースコードのポイント
####部屋の移動判定

```c#:
    //部屋移動判定を行う関数
    private bool isMoveFloor(Vector3 pos)
    {
        float x = Mathf.Abs(pos.x)+ CORRIDER_WIDTH_HALF;
        float z = Mathf.Abs(pos.z)+ CORRIDER_WIDTH_HALF;

        //廊下はxz座標にそれぞれ等間隔で並んでいるためプレイヤー座標を
        //部屋の幅で割った余りから、下記の判定で求める
        x %= (FLOOR_SIZE_HALF*2);
        z %= (FLOOR_SIZE_HALF*2);
        if(FLOOR_SIZE_HALF - CORRIDER_SIZE_HALF < x && x < FLOOR_SIZE_HALF + CORRIDER_SIZE_HALF && z < CORRIDER_SIZE_HALF) return true;
        if(FLOOR_SIZE_HALF - CORRIDER_SIZE_HALF < z && z < FLOOR_SIZE_HALF + CORRIDER_SIZE_HALF && x < CORRIDER_SIZE_HALF) return true;

        return false;
    }
```

####空きスペースのチェック

```c#:
            bool is_exist = false;//生成座標にすでにFloorがある
            string debug = "";
            foreach (var child in this.GetComponentsInChildren<Transform>())
            {
                //createPos(floor作成座標)とmanagerの子の座標の距離が小さいものがあるか探す
                //距離が小さいということは、もうすでにfloorを生成しているということ
                float distance = Vector3.Distance(createPos, child.transform.position);
                if (distance < FLOOR_SIZE_HALF)
                {
                    is_exist = true;
                    break;
                }
            }
```

####floorの生成

```c#:
            if(is_exist == false)
            {
                //生成座標にFloorがない場合、インスタンスを設定し、managerの子に設定する。
                debug += "----\n";
                print(debug);
                var fl = VRCInstantiate(floor);
                fl.transform.position = createPos;
                fl.transform.rotation = floor.transform.rotation;
                fl.transform.parent = this.transform;
            }
```

###ソースコード全体
```c#:

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UnityEngine.UI;


public class manager : UdonSharpBehaviour
{
    public GameObject floor;

    public Text debugText;
    public Text debugText2;

    private const float FLOOR_SIZE_HALF = 2.0f;//フロア3m 廊下1m
    private const float CORRIDER_SIZE_HALF = 0.5f;//廊下長さ1.0m
    private const float CORRIDER_WIDTH_HALF = 0.25f;//廊下幅0.5m
    private const float FLOOR_HIGHT = 3.0f;//2階の高さ
    private float localPlayerHeight;        //プレイヤーの身長

    void Start()
    {
        //位置の高さ調整のために初回身長を計測する。
        var player = Networking.LocalPlayer;
        localPlayerHeight = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position.y;
    }
    void Update()
    {

        var player = Networking.LocalPlayer;
        if (player != null)//Unityの再生ボタンで実行すると変数がnullになるらしいよ
        {
            Vector3 pos = player.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
            if (isMoveFloor(pos))
            {
                makeFloor(pos);
            }
        }
        dispChild();
    }

    //部屋移動判定を行う関数
    private bool isMoveFloor(Vector3 pos)
    {
        float x = Mathf.Abs(pos.x)+ CORRIDER_WIDTH_HALF;
        float z = Mathf.Abs(pos.z)+ CORRIDER_WIDTH_HALF;

        //廊下はxz座標にそれぞれ等間隔で並んでいるためプレイヤー座標の余りから
        //下記の判定で求める
        x %= (FLOOR_SIZE_HALF*2);
        z %= (FLOOR_SIZE_HALF*2);
        if(FLOOR_SIZE_HALF - CORRIDER_SIZE_HALF < x && x < FLOOR_SIZE_HALF + CORRIDER_SIZE_HALF && z < CORRIDER_SIZE_HALF) return true;
        if(FLOOR_SIZE_HALF - CORRIDER_SIZE_HALF < z && z < FLOOR_SIZE_HALF + CORRIDER_SIZE_HALF && x < CORRIDER_SIZE_HALF) return true;

        return false;
    }

    //Floorを生成する関数
    //1.プレイヤーの位置をもとに、2つの生成候補の座標を作成する
    //2.子オブジェクトの座標を確認し、生成候補の座標が被らなければ、そこにFloorを生成する。
    private void makeFloor(Vector3 pos)
    {
        //プレイヤーの座標をFLOOR_SIZE_HALFの精度にまるめる
        float x = Mathf.Round(pos.x / FLOOR_SIZE_HALF)* FLOOR_SIZE_HALF;
        float z = Mathf.Round(pos.z / FLOOR_SIZE_HALF)* FLOOR_SIZE_HALF;
        float y = Mathf.Round((pos.y - localPlayerHeight) / FLOOR_HIGHT) * FLOOR_HIGHT;
        //上下・左右どちらの廊下を通ったか判定し、結果により生成候補座標を2つ作る。
        Vector3[] spawnPos = new Vector3[2];
        bool is_updown = (Mathf.Abs(z) % (FLOOR_SIZE_HALF*2) > CORRIDER_SIZE_HALF);
        if (is_updown)
        {
            //上下移動の場合
            spawnPos[0] = new Vector3(x, y, z + FLOOR_SIZE_HALF);
            spawnPos[1] = new Vector3(x, y, z - FLOOR_SIZE_HALF);
        }
        else
        {
            //左右移動の場合
            spawnPos[0] = new Vector3(x + FLOOR_SIZE_HALF, y, z);
            spawnPos[1] = new Vector3(x - FLOOR_SIZE_HALF, y, z);
        }

        foreach(Vector3 createPos in spawnPos){
            bool is_exist = false;//生成座標にすでにFloorがある
            string debug = "";
            foreach (var child in this.GetComponentsInChildren<Transform>())
            {
                //child is your child transform
                float distance = Vector3.Distance(createPos, child.transform.position);
                debug += string.Format("create{0}, child{1}, dist {2}, cnt {3}\n", createPos.ToString(), child.transform.position.ToString(), distance);
                if (distance < FLOOR_SIZE_HALF)
                {
                    is_exist = true;
                    break;
                }
            }
            if(is_exist == false)
            {
                //生成座標にFloorがない場合、インスタンスを設定し、managerの子に設定する。
                debug += "----\n";
                print(debug);
                var fl = VRCInstantiate(floor);
                fl.transform.position = createPos;
                fl.transform.rotation = floor.transform.rotation;
                fl.transform.parent = this.transform;
            }
        }
    }

    private void print(string mes)
    {
        if(debugText.text.Length > 500)
        {
            debugText.text = "";
        }
        debugText.text += mes;
    }
    private void dispChild()
    {
        debugText2.text = "";
        foreach (var child in this.GetComponentsInChildren<Transform>())
        {
            debugText2.text += string.Format("{0},{1}\n",child.name, child.transform.position.ToString());
        }
    }
}

```

今回は実験だったので、生成したfloorのデザインやサイズ感がちょっとひどいですが、おいおいはしっかりしたもので作成したいですね。