Imaginantia

思ったことを書きます

Udon/U#についての諸記録

追記 (2023/01): この記事は 2020 年に書いたもので、現在の Udon の話ではないので、いろいろと差異があることを前提として読んでください。

参考資料

基本

C#を書けば良い。但し全てのC#コードが動くわけではない。

  • ファイル名と一致する名前でないクラスを宣言できない
    • Only one class declaration per file is currently supported by UdonSharp
  • 多次元配列はダメ (1次元にして)
    • UdonSharp does not supporrt multidimensional or jagged array accesses yet
    • (v0.15.5) もともとjagged array (int[][]) は動いたらしいのでコレは使えます (int[,] はダメ、まぁ大丈夫そう)
  • Genericのある型もダメ (配列のみ)
    • Cannot resolve generic arguments on non-method expression
      • 宣言時の型がここでの expression にあたりそう
      • 逆に言えばメソッドのGenericコールはできる (GetComponent<> は動く)
    • 確保とかは自由にできるので自前実装すれば良い
  • 自作の out 変数を受け取る関数も動かない (が、コンパイルは通るので怖い)
  • uintlong の剰余演算など、幾らか存在しないメソッドがある
    • Method not exposed to Udon / Field accessor ... is not exposed in Udon
      • その関数がUdon自体の制約によって使えない (例えUnity C#で使えていたとしても)
      • これはヤバい機能 (VRCPlayerApi.gameObject) の隠蔽などに使っているんだと思います
    • 使えません
  • 伴ってTexture系やComputeShaderも使えない

VRC系の機能

  • まずPickupやStationは今までと変わらず対応コンポーネントが存在する
  • VRC.SDK3.Components 名前空間
    • PickupやStationなどが定義されてるところ
    • Pickupは(従来どおり) PlayHaptics で振動させたりできる
  • Networking 名前空間
    • LocalPlayer 自分のこと (Unity上では存在しない)
    • SetOwner 所有権を与えるのに使える
    • IsOwner 所有権を確認するのに使える
    • IsMaster Masterか判定するのに使える
    • 他はほとんど使えないと思います
  • VRCPlayerApi (Networking.LocalPlayer の型)
    • GetPosition / GetRotation / GetVelocity はい
    • GetJumpImpulse / SetJumpImpulse 系もそのまま
    • GetTrackingData 頭と手の位置が取れますが、後述
    • GetBonePosition / GetBoneRotation Humanoidボーンの位置が取れますが、これも後述
    • 他もC#から見えるけどUdonには無い関数がほとんど
  • UdonSharpBehaviour
    • 諸々をoverrideすることで今までのTriggerの代替を実装することができる
    • SendCustomNetworkEvent
      • publicメソッドの名前を指定するとそれをネットワーク越しに呼んでくれる (同期用)
      • 引数は与えられない
    • VRCInstantiate
      • Instantiate の代替、同期は (今のところ) 特にしない
      • positionとかは無いので後で代入する感じで
        • instantiateしたGameObjectに取り付いている自作U#スクリプトは普通に GetComponent<> で取れる
        • が、取れるのはちょっと経ってから (canny)
          • GetComponent が成功することを確認してから処理をすれば大丈夫

同期

基本的に従来どおり大変です。まず分散処理は大変なのでしょうがないのです。

  • [UdonSynced] 属性
    • 全プレイヤーに同期される変数
    • Primitive型とか Vector / Quaternion くらいしかやってくれない
    • 代入できるのはそのGameObjectのOwnerのみ
      • 所有権を持ってない人が使う場合は Networking.SetOwner してからちょっと待たないとダメ
        • どれくらい待つべきかは未検証な気がする (分散処理的にちゃんと合意が取れてからじゃないと怖そう)
    • 代入後即時は反映されない
      • late-joinerはおそらくこれを受け取ってから実行を開始する?(未検証) ので最初から代入されてるかも (知らん)
      • ちょっと待つと同期されるけどその間隔は未規定だと思います
  • SendCustomNetworkEvent メソッド
    • 全プレイヤーに指定したpublicメソッドを呼ばせることでトリガー的同期を行う
    • Eventの消失は起きないが順序は規定されない
      • 1F間隔で呼ぶとほぼ確実に順序通り届く (検証)
  • UdonBehaviourのSynchronize Position
    • 位置と回転を同期します (Scaleはされないと思う(Photon的に))
    • おそらく UdonSynced と同じくらいの粒度なので即時の反映は期待できない

トリガーをひたすら叩くタイプの仕組みは基本的にうまくいくと思いますが、複数人がほぼ同時に SendCustomNetworkEvent を発火したときに大丈夫にするようにする必要はありそう (Ownerしか呼ばないとか)

デバッグ

  • ログファイルは C:/Users/.../AppData/LocalLow/VRChat/VRChat/output_log_....txt にある
    • 現在のログは最新の時刻が記録されているもの (過去のはほっとくと消えていく)
    • Udonがクラッシュした場合の詳細はここを見るのが早い
  • VRChatの起動オプションで内部で色々見れる
    • --enable-debug-gui コンソールが見られるようになる
    • --enable-sdk-log-levels SDKが吐くログを全部出すんですが今のところ必要になったことがない
    • --enable-udon-debug-logging Udon関連のエラーを表示するらしいが表示されたことがない
  • VRCのデバッグ情報は「右シフト + @ + 数字キー」で表示切り替えができる (つまりバッククォート + 数字)
    • 3でコンソール、7でトリガー、8で所有権やオブジェクト名などの情報
    • 7,8での表示はワールド作者 (か運営) のみ (ワールドの設定で許可した場合は全員見られる)
  • Debug.Log は動く (VRCのコンソールにも出る)
    • ちょっと面倒なので自分でCanvas/Text使ったLoggerを作ったりもしたけど

注意点

  • そこそこの頻度でUdonまたはU#がクラッシュする
    • System とかその辺のエラーログが出てきたらヤバい
      • System.Reflection.ReflectionTypeLoadException とか Did not capture a valid namespace とか
      • Recursive type definition detected とか
    • エラーログをクリア → 適当なC#ファイルを編集、UnityにコンパイルさせてUdon本体っぽいエラーが出てきたらおしまい (再起動)
    • エラーが出てこないが既存のC#ファイルで上書き保存をしてUdonSharpがコンパイルして正常っぽくないエラーが出てきたらおしまい (再起動)
    • 一応治ることもそこそこある
  • UdonBehaviourを編集してもシーンに変更があったことにならない (SetDirtyされない)
    • つまりこの状態でいくらCtrl+Sしても保存されない
    • 新規GameObjectを作成して消去したりUdonBehaviour以外のチェックボックスをダブルクリックしてから保存したりしています
  • Start メソッドの実行順は未規定らしい (詳細は未検証)
  • 乱数の同期について
    • 基本的に Random.InitState で同じ値を与えられれば以降の乱数は同じものが吐かれる
      • が、これは「誰か」が乱数にアクセスしただけでズレるので怖い
      • または複数のUdonBehaviourが触りに行くと順序未規定でズレるかもしれない
      • ちなみに Random.state を取得できないのでUnityの機構を流用することができない
    • 自前で乱数生成が安定感ある (私のコード)
      • ちなみにseedをUdonSyncedさせたりするとヤバい (Owner以外が新たな乱数を引けない)
        • 「あるタイミングで現行の乱数の種を全員統一する」のが正しい
  • SendCustomNetworkEvent について
    • VRChatじゃないと動かない
      • デバッグ用に #if debug とかでイベントを直接呼ぶコードを書いたりした
    • publicじゃないと動かない (仕組み的にそうっぽい)
  • OnTriggerEnter はプレイヤーを取得できない
    • セキュリティ的な問題だと思う (参考)

VRCPlayerApiのBone取得について

  • GetTrackingData で取れる位置は見かけの位置と一致する
    • つまりマジのトラッキングデータではなくアバター上での位置 (まぁうれしい)
    • ただこれをベースに頭の向きを計算したときに下記と同様の意図外挙動になったケースがあるらしい (自環境では発現せず)
  • GetBonePosition / GetBoneRotation本当にボーンの (ワールド空間上の) 位置を拾ってくる
    • つまりアバター毎のボーンの向きの差異が直接反映されている ()
      • 大体のアバターは+Y軸が先端向きなんだけど幽狐さんは-X軸方向
      • あと幽狐さんはArmatureにZ-90°のRotationが入ってる
    • 恐らくこのボーン位置を直接他のアバターに突っ込むことは (Humanoidでさえ) 汎用的には出来ない
    • 指の向きすら簡単には取れない
      • まぁ推定はできる (ThumbIntermediate から ThumbDistal への向きがどの軸に一番近いかを調べた)
Vector3[] tipDirCandidate = new Vector3[6] {
    new Vector3(+1,0,0),
    new Vector3(0,+1,0),
    new Vector3(0,0,+1),
    new Vector3(-1,0,0),
    new Vector3(0,-1,0),
    new Vector3(0,0,-1)
};
Vector3 estimatedTipDir = Vector3.zero;
Vector3 dir = (fingerPos[3] - fingerPos[2]).normalized;
Quaternion rot = fingerRot[2];
float matchAmount = 0;
for(int i=0;i<6;i++) {
    Vector3 cand = rot * tipDirCandidate[i];
    float d = Vector3.Dot(dir, cand);
    if(matchAmount < d) {
        estimatedTipDir = tipDirCandidate[i];
        matchAmount = d;
    }
}

しょうもない話

  • 宣言されてない変数で a; みたいなことをやってもUdonSharpは文句を言わない (C#コンパイルエラーにはなる)
  • 宣言されてる変数で a; みたいなことをやると文句を言う (Only assignment, call, increment, decrement, await, and new object expressions can be used as a statement) (これはC#の仕様だった (まぁそれはそう))
  • int b = a; で宣言されてない変数 a を参照するとエラーメッセージがおかしい (The name '' does not exist in the current context) (v0.15.5で修正)
    • 普通にバグだと思うけどソースをちらっとみた感じ直すのがだるそうだったので何も報告していない (よくないけどどうせ困らん気はする)
  • while(f()); が書けない (UdonSharp does not currently support node type EmptyStatement) (v0.15.5で追加)
    • まぁ書き直せばいい

直接の報告は投げてないのに対応してくださってありがとうございます :pray: