UDONメモ
UdonGraphはクソ
UdonSharp
最高。C#と同じ構文でUdonを記述できる。
https://github.com/Merlin-san/UdonSharp
インストール
上記サイトからUnitypackageをダウンロードし、VRChatSDK3をインポートしてからインポート。
スクリプトの作成
UdonBehaviour
コンポーネントを付けて、プルダウンから Udon C# Program Asset
を選択する。
そして New Program
でProgram Sourceを作成。Create Script
でスクリプトファイルを作成する。
あとはコーディング!
用語定義
ちょくちょく混同するのでここで定義します。
Master(マスター)
VRChat技術メモ帳ではInstance内にいる中で、ローディングが終了した順番が一番早い人。Instanceを出ると次に早い人に移行するとのこと。 Udon環境ではPlayerIDが取れるようになったため、ワールド内でPlayerIDが一番若い人がMasterという認識で多分間違っていない。
InstanceOwner(インスタンスオーナー)
Instanceを立てた人のこと(VRChat技術メモ帳では単にオーナーという)。FriendなどのInstanceの公開範囲に関わる。
OwnerShip(オーナーシップ)
オブジェクト一つ一つに設定されている、オブジェクトの同期を行う権限のこと。
ObjectOwner(オブジェクトオーナー)
OwnerShipを持っているプレイヤーについての用語がInstanceを立てた人のことであるOwnerと混同しやすいため、このサイトでは「そのオブジェクトのOwnerShipを持っているプレイヤー」のことをObjectOwnerと呼ぶことにする。(筆者の造語なので注意。 正しい言い方があったらTwitterで教えて・・・ (2020/04/09追記 Object Ownerと名付けました。))
同期変数
UdonSharpでは以下のように、UdonSynced属性がついた変数のことをいう。
1 2 | [UdonSynced(UdonSyncMode.None)] public float syncFloatValue = 0.0f; |
インタラクト
IntaractはどうやらUdonBehaviourがついている物自体にコライダーがついてないと入っていない?
インタラクト時に呼ばれるコールバック関数はこのように定義する。
1 2 3 | public override void Interact() { } |
インタラクト時のヒント表示を変更する
InspectorをDebug表示にするとUdonBehaviourでInteract Textから変更できる。
(2020/04/11追記)Normal表示のUdonBehaviourで変更できるようになりました。
SetOwner
インタラクトした人にOwnerShipを渡す関数。 ただし反映されるのは次のインタラクション時。(近日中に修正されることが予定されている。)
1 2 3 4 | public override void Interact() { Networking.SetOwner(Networking.LocalPlayer, gameObject); } |
Networking
GetOwner
そのオブジェクトのObjectOwnerの名前が取れる。
1 | Networking.GetOwner(gameObject).displayName |
VRCPlayerApi
GetPlayerById
IDからプレイヤーを取得する。そのIDのプレイヤーがいない場合nullを返す。
1 | var p = VRCPlayerApi.GetPlayerById(i); |
VRCPlayerAPI同士の比較
Networking.GetOwner
などで取れるVRCPlayerAPIは比較演算子を使ってNetworking.LocalPlayer
と比較は出来なかった。
こっちはダメ
1 | Networking.LocalPlayer == Networking.GetOwner(playerList.gameObject); |
こっちはOK
1 | Networking.IsOwner(Networking.LocalPlayer, gameObject); |
OnPlayerJoin
やOnPlayerLeft
の引数のplayer
はNetworking.LocalPlayer
と比較演算子(==)を使用しても正しく比較できるが、きちんと比較したい場合はplayerIDで比較するのがよいかもしれない。
PlayerIDの決定規則
プレイヤーIDは最初に入った人を1として、どんどん数字が増えていく
1 : user1
2 : user2
3 : user3
ここでuser2が抜けて、そのあとuser4が入ってきた場合は2が欠番となり、user4のplayerIDは4になる。
1 : user1
3 : user3
4 : user4
また、ここでuser2が戻ってきた場合はplayerIDは5になる
1 : user1
3 : user3
4 : user4
5 : user2
同期周りで陥りがちな問題・バグ
OnPlayerLeft時に同期変数が更新されない
OnPlayerLeftのタイミングで以下のように同期変数を修正する場合、このオブジェクトのObjectOwnerがワールドから移動する場合は値が更新されない。
1 2 3 4 5 6 7 8 9 10 | [UdonSynced(UdonSyncMode.None)] private int syncValue = 0; public override void OnPlayerLeft(VRCPlayerApi player) { if (Networking.IsOwner(Networking.LocalPlayer, gameObject)) { syncValue = 1; } } |
ObjectOwnerが落ちるとそのワールドのMasterにOwnerShipが移動するが、Networking.SetOwner
と同様にOwnerShipの移動にラグがある。
憶測だが、OwnerShip移行時にOpPlayerLeftでIf分内が新しいObjectOwnerで動作するが、全体ではまだ新しいObjectOwnerがObjectOwnerではないため、値の同期は走らないのではないかと思われる。
少なくとも、OwnerShip移動と同期変数の更新は同タイミングで行わないほうがいい。
Synchronize Position がオンだと同期変数が同期しない
同期変数があるプログラムを Synchronize Position
をオンにしているUdonBehaviourにセットした場合、ObjectOwner以外のプレイヤーでは同期変数が同期されなくなる。
あと、同期ではまりそうなの: https://t.co/ByrtYT8pKy variable を synced にしているプログラムを Synchronize Position を on にしている UdonBehaviour で動かすと、owner ではない人の所で variable の同期が止まる現象があります。
— naqtn(なくとん) (@naqtn) April 6, 2020
コンパイルが走らないとき
Udon C# Program AssetでSource Script
がセットされていない物があると以下のようなエラーが出てすべてのファイルのコンパイルが走らなくなる。
シーンで使っていなくても走らなくなるので削除する。
1 2 | ArgumentException: Asset 'Assets/Scenes/SampleScene_UdonProgramSources/Cube (5) Udon C# Program Asset 1.asset' does not have a valid program source to compile from UdonSharp.CompilationModule.Compile (System.Collections.Generic.List`1[T] classDefinitions) (at Assets/UdonSharp/Editor/UdonSharpCompilationModule.cs:46) |
ちなみに、このエラーが出ている状況でも、Force Compile Script
を押せば単体でコンパイルできる。
Stringを使った同期リスト
現在(2020/04/09)同期変数で配列を使用することが出来ない。また、Listのような可変長の配列データも扱うことが出来ない。
しかし、string型の同期変数を使うことでこれらを疑似的に再現することは可能。
仕組みとしては、カンマ区切りで数列を持ちstring.Split
でそれぞれを分割して処理する。
1 2 | // カンマ区切りの文字列を配列として使う string test = "0,1,2,3,4,5,6"; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | [UdonSynced(UdonSyncMode.None)] private string ListString = ""; // リストの中身を列挙する public ListUp() { if(ListString.Equals("")) return; string[] strs = ListString.Split(','); for(int i = 0; i < strs.Length; ++i) { Debug.Log(strs[i]); } } // リストに追加する public void Add(string str) { if(!ListString.Equals("")) ListString += ","; ListString += str; } // リストから削除する public bool Remove(string str) { if(ListString.Equals("")) return false; string[] strs = ListString.Split(','); string newList = ""; bool exists = false; for(int i = 0; i < strs.Length; ++i) { if(str.Equals(strs[i])) { exists = true; } else { if(!newList.Equals("")) newList += ","; newList += strs[i]; } } ListString = newList; return exists; } // リストサイズを取得 public int GetListSize() { return ListString.Split(',').Length; } |
string
から整数や浮動小数点数に変換したい場合は以下の関数を使う。
1 2 3 4 5 | string intValue = "5" int num = int.Parse(intValue); string floatValue = "3.4"; float num = float.Parse(floatValue); |
「Disable Station Exit」のついたVRC_Stationからプレイヤーをおろす方法
1 2 3 | public VRCStation station; // VRCStationの変数を用意 station.ExitStation(Networking.LocalPlayer); |
実はテレポートでもおろせる。(最初の方法に気づかず最近までこちらの方法を取っていたのは内緒)
1 | Networking.LocalPlayer.TeleportTo(exitPoint.transform.position, exitPoint.transform.rotation); |
(作例)同期スイッチ
オブジェクトのアクティブ、非アクティブを同期的に切り替えるUdon。 OwnerShipの移行は行わないため、常にインスタンスのMasterがOwnerShipを持つ。 切り替えの変数はMasterが保持するため、Master以外が高速で切り替えると値の更新が遅れて切り替わりがラグくなる時がある。 ただし、状態は間違いなく同期される。 後からワールドに入ってきた場合も同じ状態を保持する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | using UnityEngine; using UdonSharp; using VRC.Udon; using VRC.SDKBase; public class NewBehaviourScript : UdonSharpBehaviour { public GameObject mirror; [UdonSynced(UdonSyncMode.None)] private bool mirrorEnable = false; // Start is called before the first frame update void Start() { mirror.SetActive(mirrorEnable); } public override void OnPlayerJoined(VRCPlayerApi player) { // Joinしたプレイヤーの同期 // 2番目以降に入ってきたプレイヤーはStart()が動かないのでこれで値を同期する。 if(Networking.LocalPlayer == player) { mirror.SetActive(mirrorEnable); } } // Update is called once per frame void Update() { } public override void Interact() { if (!mirrorEnable) { // ユーザー全体にイベントを通知 SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "SetMirrorActive"); } else { // ユーザー全体にイベントを通知 SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "SetMirrorInactive"); } } public void SetMirrorActive() { mirror.SetActive(true); mirrorEnable = true; // ココはオーナーのみ動く } public void SetMirrorInactive() { mirror.SetActive(false); mirrorEnable = false; // ココはオーナーのみ動く } } |
変数の同期
UdonSharpで変数の同期設定はアトリビュートでできる。
1 2 | [UdonSynced(UdonSyncMode.None)] public float syncFloatValue = 0.0f; |
SyncモードはUdonSyncMode
で変更できる
モード | |
---|---|
UdonSyncMode.NotSynced | 同期しない |
UdonSyncMode.None | 補間なし |
UdonSyncMode.Linear | 線形補間 |
UdonSyncMode.Smooth | スムース補間 |
SendCustomNetworkEvent
ネットワーク越しでイベントを発行するのに使えるメソッド、オーナーシップとの兼ね合いはまだよくわからない。
ここに指定するメソッドはPublicじゃないといけない。
以下のコードでインタラクト時に、ワールドにいる全員に対してイベントを発火し、ExampleMethodを実行する。
1 2 3 4 5 6 7 8 9 | public override void Interact() { SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "ExampleMethod"); } public void ExampleMethod() { Debug.Log("test"); } |
VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner
にすることで、ObjectOwnerのみに発火させることができる。
この時、インタラクトした人は誰であっても問題ない。AllならObjectOwnerやObjectOwner以外がインタラクトした場合、全員にイベントが発火し、OwnerならObjectOwnerとObjectOwner以外がインタラクトするとObjectOwnerのみExampleMethodが実行される。
SendCustomEvent()
名前が似ているけれど、こちらはだた単純に名前で関数を実行できる関数だった。
(作例)PlayerModに代わるUdon
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using UdonSharp; using UnityEngine; using VRC.SDKBase; using VRC.Udon; public class WorldSettings : UdonSharpBehaviour { public float walkSpeed = 2; // 歩く速度 public float runSpeed = 5; // 走る速度 public float gravity = 1; // 重力 public float jump = 3; // ジャンプの強さ void Start() { VRCPlayerApi localPlayer = Networking.LocalPlayer; // エディターで動かすとプレイヤーがいないためnullチェックしておく。 if (localPlayer == null) return; localPlayer.SetJumpImpulse(jump); localPlayer.SetWalkSpeed(walkSpeed); localPlayer.SetRunSpeed(runSpeed); localPlayer.SetGravityStrength(gravity); } } |
UdonSharpBehaviourのトリガー関数
Interact()
VRC_Triggerでいうところの、OnInteract。 オブジェクトに対してアクションしたときに呼ばれる。
OnDrop()
Pickupオブジェクトを放したときに呼ばれる関数。
OnPickupUseDown()
Pickupオブジェクトを持った状態でUseするためにトリガーを押し込んだ瞬間1回だけ呼ばれる関数。
OnOwnershipTransferred()
OwnerShipを失う時に、失ったプレイヤーで実行される。 実行された段階でプレイヤーにはOwnerShipがないため、ここで同期変数を更新することはできない。
OnPickupUseUp()
Pickupオブジェクトを持った状態でトリガーの押し込みを解除した瞬間に1回だけ呼ばれる関数。
OnPlayerJoined(VRC.SDKBase.VRCPlayerApi player)
プレイヤーがインタンスに入った時に呼ばれる関数。引数のplayerにはそのプレイヤー情報が入る。
OnPlayerLeft(VRC.SDKBase.VRCPlayerApi player)
プレイヤーがインタンスから離れた時に呼ばれる関数。引数のplayerにはそのプレイヤー情報が入る。
OnSpawn()
公式ドキュメント
このオブジェクトがローカルプレイヤーのためにスポーンしたときに発生します。バッファリングされていないので、遅れて入場したプレイヤーはこのイベントを取得しません。ネットワークインスタンス化でオブジェクトがスポーンされた場合にのみ発生します。オブジェクトがベースシーンに存在する場合は発生しません。
現在(2020/04/10)、 ネットワークインスタンス化を行う方法がないため発動条件を満たせない?
OnStationEnterd()
UseAttachedStation()
などで、VRC_Station
に座った時に実行される関数。
ただし、 現在(2020/04/10)は動作しない。
OnStationExited()
VRC_Station
から離れた時に実行される関数。
ただし、 現在(2020/04/10)は動作しない。
OnVideoEnd(),OnVideoPouse(),OnVideoPlay(),OnVideoState()
おそらくVRC_SyncVideoPlayer
用のトリガー関数。現在(2020/04/10)はVRC_SyncVideoPlayer
がSDK3に実装されていないため使用できない。Unity純正のVideoPlayer
というコンポーネントがあるが、こちらはUdonに対応していない。
OnPreSerialization(),OnDeserialization()
同期変数を送受信したときに呼ばれる関数。同期変数の値が変更されたときに呼ばれそうだが、 変更されていなくても毎度実行されている。 (無駄では・・・?)
つまり同期変数は変更されていようがなかろうが常に通信されている?
OnPreSerialization()
ObjectOwnerが同期変数を送信するときにObjectOwner側で呼ばれる関数。
OnDesrialization()
ObjectOwnerから同期変数を受信した時にObjectOwner以外で呼ばれる関数。
同期回りを突っついている人が多そうなので:
— naqtn(なくとん) (@naqtn) April 6, 2020
OnDeserialization はマスターから synced variable の更新値が届いた時に発生します。ただし、どの変数が変わったのかは分かりません。対となる、マスター側で送り出す前に発生するのが OnPreSerialization です。
バグ
WheelCollider.GetWorldPose()は使用できない(2020/04/09現在)
WheelCollider.GetWorldPoseは関数としても、Udonのノードとしても存在するが動かない。
WheelCollider.GetWorldPose returns 0
VRCInstantiateのあれこれ(2020/04/11現在)
- 現在同期的にインスタンス化を行う方法がない?
- インスタンス化したオブジェクトではUdonBehaviourが死ぬ。
- SendCustomNetworkEventで同期的にインスタンス化出来ているようにみえるが、PickupとUdonBehaviourが死ぬ。
- プレハブの座標とは違う場所からインスタンス化される。
リンク
Udonの同期のVRChat公式の提言
How to Sync with Udon
はつぇさんのU#に関する記事
真時代傾向璋
やぎりさんのメモ
UdonSharp走り書きメモ.cs(執筆中、順次更新)
naqtnさんのツイート