Qiitaを初めたら、意外と自分がLGTMを気にしていることに気づきました。
LGTMされていると執筆のやる気が湧きます。UdonSharpの記事を増やす役に立つと思ってLGTMを押してください。
やったこと
前回は床にUdonBehaviorをつけて、床を踏んだら、UdonBehaviorを持つ床をどんどん生成していき、道の動的生成を行う方法をとりました。しかし、その方法だと現環境では失敗してしまうため、方針を変更(インスタンス化した際にUdonが動作しなくなる場合があるため)。
一つのオブジェクトが床を生成し続ける設計としています。
同期できない方法だけど、道の動的生成に成功したよ。
— はんちょ (@sadimensions) April 28, 2020
今回は十字路だけど、ランダムでT字路混ぜたり、道の形を変えれば、ダンジョンが生成できるね。
ダンジョンのレシピをstring型で同期させたら、疑似的にどうきできるかもしれない。試してみたい pic.twitter.com/V2sdTdHp5Y
そして今回は十字路を生成することとしました。
やり方
オブジェクトの配置
まず動的生成を管理するmanager(Create Empty)と生成されるfloor(Blenderからインポート)を用意します。
floorはmangerからアクセスしやすいように子として追加します。
(floorの座標を確認するために子にしている)

プログラムの設計
決まったサイズの十字路を使用しているため、部屋の移動判定が必要なエリアは等間隔に配置されています。
そしてこのエリアは無限大にあるため、一つ一つ判定するのではなく、数字を加工することで判定します。

絶対値とmod演算(%の演算)を用いることで、部屋の移動判定を少ない条件分岐で表現することが可能です。
今回の場合、X座標とZ座標を部屋の長さ4mで割った余りが比較対象となります。
そうすると下図の緑のエリアだけを判定すれば、部屋の移動が行われたことがわかります。
例1(10, 4) → (2, 0) 部屋移動中
例2(-12, -6) → (0, 2) 部屋移動中
例3(4 , 4) → (0, 0) 部屋の中

部屋の移動がされたことがわかったら、プレイヤーの座標をもとに、上下 / 左右どちらかの部屋を作るか決めるだけです。
設計思想は以上です。他にも座標の調整などがソースコードに反映されていますが、割愛します。
ソースコードのポイント
部屋の移動判定
//部屋移動判定を行う関数
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;
}
空きスペースのチェック
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の生成
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;
}
ソースコード全体
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のデザインやサイズ感がちょっとひどいですが、おいおいはしっかりしたもので作成したいですね。
