この記事について
UdonSharpを使ってVRChat上で何ができるか確認していたことの一つ。PrefabやGenerateがうまくいくのかを確認したところでストップ。
VRChat上でテトリスを作りには以下の2つの壁を超える必要がありそう。
- VRCInstantiateで生み出したものは同期がとれない
- 操作が難しい
とりあえず成果物だけ共有。テトリスのブロックが出現して回転しているだけ。
やったこと
大まかには下記の3点
- L字のブロックを作る
- 回転させる
- 新たに出現させる
L字のブロックを作る
後々のことを考えるとブロックが横一列にそろったときに消えてくれないと困るため、立方体ブロックを4つつなげる方法をとるべきだが、正直面倒だったので1x3x1のブロックと1x1x1のブロックを親子関係にした。親子の関係はHierarchyでドラッグするだけでできる。最初はComponentのFixed Jointとか必要かと思ったが不要だった。最終的にはPrefabにしたため、下図の構成になった。
回転させる
ここが一番手間取った。まず回転させること自体は親子関係を作る前に確認で作っており、そのときは問題なく動いていた。以下がコード(後から復元したから動作確認していない)。
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;
public class tetris_4block : UdonSharpBehaviour
{
float delta = 0;
//子オブジェクトの設定
GameObject Cube_2;
void Start()
{
//子オブジェクトの探索
this.Cube_2 = GameObject.Find("Child");
}
void Update()
{
//1秒ごとに処理
this.delta += Time.deltaTime;
if (this.delta > 1.0f)
{
this.delta = 0;
//L字の長い棒部分にU#をつけているため下記で回転処理
Quaternion move_q = Quaternion.Euler(0f, 00f, 90f);
transform.rotation = transform.rotation * move_q;
//子オブジェクトの回転処理
this.Cube_2.transform.RotateAround(transform.position, new Vector3(0, 0, 1), 90f);
}
}
}
注意点として、子オブジェクトと中で表記しているし、実際にChildというオブジェクトを探しているが、このときはHierarchyとしては同じレベルに並列表記されている状態で動作させていた。
ここまではよかったがこれを本当の親子関係にしてPrefab化し、新しいインスタンスを発生させた結果が以下の動画。
今見返してもよくわからないが、おそらく問題は3点。
- 子オブジェクトは親子関係になった時点で勝手に回転するので自分で回転させる必要がない(スクリプトで回転させることで2回回転させてしまった)
- GameObject.Find()で同じ”Child”という名前のオブジェクトを探索している
- 親子関係になった子オブジェクトのScaleは親オブジェクトに影響される
まず1点目は簡単に解決する。上のコードの”子オブジェクトの回転処理”部分をコメントアウトすれば解決する。親子にすると勝手に回転してくれるのは今後も便利に使えそう。当たり前なのかもだけど。
2点目はPrefabを使ったジェネレータで常に起こりうる課題な気がする(ジェネレータは後述)。新しく作られたインスタンスも常にChildという子要素を持っている。このときインスタンスにはUdonSharpで書かれた回転コードもそれぞれにある。このそれぞれが探すChildは誰の子なのかが問題で、現象を見る限り一番最初に作られたChildをすべての親が自分の子供だと思い込んでいる気がする(言葉にして想像するとすごい不思議な現象な気がする)。
これを解決するには自分の独自の子要素を探索するGetChild()メソッドを使えばいい。今回は一つしか子要素がないので0を引数に入れるとちゃんと自分の子供を認識してくれるようになる。
3点目が少し厄介だった。親子一緒に回転してくれるのはいいが、親子で形状まで一緒に代わってしまう現象が起きた。原因はScaleの計算方法にあることが後々わかった。
親は1x3x1のブロックで子は1x1x1のブロックだが、実は親子になった瞬間にLocalScaleで1x0.33333x1のブロックになっている。つまり(1,3,1)と(1,0.33333,1)の内積で(1,1,1)を作り出しているみたい。これが厄介なことに回転に対応してくれていない。例えば親がZ軸で回転して3x1x1のブロックになったとすると(3,1,1)と(1,0.33333,1)の内積で(3,0.33333,1)の謎ブロックを作り出す(調べても他に困っている人が少なかったがもしかしたらCubeなどのUnity提供のObjectじゃないとおきないからなのか?)。
これも解決法がありグローバルのサイズであるLossyScaleを使うことで解決できる。やりたいことは上記内積で作り出した(3,0.33333,1)を(1,1,1)に戻すこと。それであれば(3,0.33333,1)で割ってやればもとに戻る。この現在の最終的な形状を保持しているのがLossyScaleなのでこれを使う(ちなみにLocalScaleは(1,0.33333,1)でInspectorのScaleの値)。
これらを反映したコードは下記のようになった。
using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;
public class tetris_4block : UdonSharpBehaviour
{
float delta = 0;
//子オブジェクトの設定
GameObject Cube_2;
void Start()
{
//子オブジェクトの探索
this.Cube_2 = transform.GetChild(0).gameObject;
}
void Update()
{
//1秒ごとに処理
this.delta += Time.deltaTime;
if (this.delta > 1.0f)
{
this.delta = 0;
//L字の長い棒部分にU#をつけているため下記で回転処理
Quaternion move_q = Quaternion.Euler(0f, 00f, 90f);
transform.rotation = transform.rotation * move_q;
//子オブジェクトのサイズ補正
var localScale = this.Cube_2.transform.localScale;
var lossyScale = this.Cube_2.transform.lossyScale;
this.Cube_2.transform.localScale = new Vector3(
localScale.x / lossyScale.x,
localScale.y / lossyScale.y,
localScale.z / lossyScale.z);
//this.Cube_2.transform.RotateAround(transform.position, new Vector3(0, 0, 1), 90f);
}
}
}
新たに出現させる
これはL字をPrefab化しておき、ジェネレータをUdonSharpで作って空のGameObjectに貼っただけ。ただし注意点としてはInstantiateではなくVRCInstantiateを使わないとダメということ。一応コードも貼っておく。
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
public class L_Generator : UdonSharpBehaviour
{
public GameObject L_Prefab;
//新規ブロック出現間隔
float span = 10f;
float delta = 0;
//出現ブロックカウント
int cnt = 0;
void Start()
{
}
void Update()
{
//span秒ごとに処理
this.delta += Time.deltaTime;
if (this.delta > this.span)
{
this.delta = 0;
//とりあえず最大3個まで
if (cnt < 3)
{
GameObject Go = VRCInstantiate(L_Prefab);
Go.transform.position = new Vector3(0, 20 - 4 * cnt, 30);
cnt++;
}
}
}
}
今後の課題(挫折した理由)
最初にも書いたがここで挫折。課題を再掲。
- VRCInstantiateで生み出したものは同期がとれない
- 操作が難しい
一点目は昔からあるバグ(というか仕様?)のようで新たに生み出したインスタンスは同期がとれないらしい。実際にどんな現象が起きるかは確認していないが、ネットを見ている感じは何かしらUdonSharpのコードによって改変された状態が他の人からは見えない、というもののよう。これはもうどうしようもない。
今回はよくよく考えるとUdonSharpでなくてもいいかもしれないと思いC#で書いてみたりしたが動かず。そもそもVRChat上では通常のC#は動かないのかも。
これを回避するためには最初からブロックをActiveじゃない状態で空間のどこかに配置しておき、登場する瞬間にActiveにして瞬間移動してくる方法しかない気がする。ただ、この方法はテトリス盤面をそれぞれ7種類のブロックで埋まった可能性を考えて(盤面サイズ21 x 10 / ブロックサイズ 4) x ブロック種類 7でおよそ360くらいのブロックを最初からどこかに置いておく必要がある。加えてブロックが消えることなどを実装しようとするとより多くのブロックが必要になり現実的でない気がする。早くインスタンスの同期を実装してほしい。
二点目も将来的な課題。VRChatではBボタンのメニューなどのようにコントローラのボタン系に役割が割り当てられており、またスティックも移動や視点変更などに使っているため使えない。残されたGrabとUseだけで操作しようとするとテトリスはなかなか難しい気がする。
これらの課題からとりあえずテトリス実装は中断。メモだけ残しておく。
参考サイト
https://nekojara.city/unity-circular-motion
https://framesynthesis.jp/tech/vrchat/
https://qiita.com/masamin/items/f452c8f85a69b10ac2b9
https://zenn.dev/daichi_gamedev/articles/b901ca3a1b4391