入門 ②の処理をOnValueChangedを用いて書いてみる
こんばんは。ハツェです。
お久しぶりです。
今回は、最近Udonに実装された変数の値が変わった時に発生するイベントOnValueChangedについて取り上げます。
今回も、この記事はコードを基礎的に書ける方を対象としています。予めご了承ください。
動作環境は、Unity : 2019.4.29f1、VRCSDK : 2021.08.11.15.16、U# : v0.20.2です。
目次
対象読者
以下を理解している方を前提に書いています。あらかじめご了承ください。
もし、Udonの同期処理の仕組みを知らないという方は、以下の記事を一度ご参照の上、当記事をお読みいただければと思います。
hatuxes.hatenablog.jp
今回の概要
今回は、Udon入門②にて取り上げた同期変数の例をOnValueChangedで書き換えるという話題について取り上げます。
OnValueChangedとは
OnValueChangedとは、指定した変数の値が代入もしくはSetProgramVariableで変更された時に呼び出されるイベントです。
OnValueChanged自体は、同期されているかどうかに関わらず、全ての変数に適用出来ます。すいません。OnValueChangedは同期変数にのみ対応していました。
OnValueChangedは、変数ごとに個別の処理を記述出来ること、OnDeserializationよりも更新速度が速いことがメリットになっています。
docs.vrchat.com
OnValueChangedをU#で実装する
OnValueChangedの実装方法は、UdonGraphとU#で異なります。UdonGraphの実装方法は割愛します。
U#では、C#のプロパティのような機能を用いて実装します。
おおよそ以下の通りです。
[UdonSynced, FieldChangeCallback(nameof(SyncedVariable))] private int _syncedVariable; public int SyncedVariable { get => _syncedToggle; set { _syncedVariable = value; // 以下、任意の処理 } }
必須な実装は以下の通りです。
- OnValueChangedイベントを適用する変数にFieldChangeCallback属性を付ける
- 適用する変数に対応させるプロパティを記述する
- 適用する変数のFieldChangeCallback属性の引数に、2番目で作ったプロパティの名前を設定する
これで、OnValueChangedイベントが適用した変数にて発生するようになります。
そのため、もし変数が変わった時に動作させたい処理があるなら、setterの部分に記述することで動作するようになります。
感覚としては、OnDeserializationのようにsetterを扱うイメージです。
公式の例については、以下を参照してください。
github.com
実際に改変してみる
ここからは、実際に改変した例を紹介します。
Ownerに処理を渡す例の改変例
元のスクリプトは以下のものです。
hatuxes.hatenablog.jp
これをOnValueChangedを用いた処理に改変してみました。少し変数名とかを変えて見やすくしています。
using UdonSharp; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class ViaOwner_Countup_System_WithOnValueChanged : UdonSharpBehaviour { [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(CountData))] private int _countData; // データ本体 public Text DisplayDataText; // データを表示するText public Text OptionText; // 誰がOwnerかを表示するText // OnValueChanged用のプロパティ public int CountData { get => _countData; set { _countData = value; DisplayCountData(); // Owner以外のデータ表示処理 } } private void Start() { // Onwerかどうかを表示 SetOptionalText(Networking.LocalPlayer); } // Cubeをインタラクトしたときに呼ばれる public override void Interact() { if (Networking.LocalPlayer.IsOwner(this.gameObject)) { // Ownerが押したら、純粋にカウントアップ CountUp(); } else { // Owner以外が押したら、Ownerにカウントアップさせるように命令する SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, nameof(CountUp)); } } // Ownerが値を+1する処理 public void CountUp() { CountData++; // データ更新 RequestSerialization(); // 同期更新 } // 同期変数の値をUIに表示する処理 public void DisplayCountData() { DisplayDataText.text = _countData.ToString(); // データ表示更新 } // プレイヤーがOwnerかどうかをテキストで表示させる処理 public void SetOptionalText(VRCPlayerApi player) { if (player.IsOwner(this.gameObject)) { // Owner側の処理 OptionText.text = $"<color=red>{player.displayName} is Owner!</color>"; } else { // Owner以外の処理 OptionText.text = $"{player.displayName} isn't Owner"; } } }
改変としては、OnDeserializationをOnValueChangedに書き換えただけです。
この方がコードとしても見やすくていいですね。ゴチャゴチャしてない感じ。
Ownerを移行して処理する例の改変例
元のスクリプトは以下のものです。
hatuxes.hatenablog.jp
これを同様に改変してみました。以下の通りです。
using UdonSharp; using UnityEngine.UI; using VRC.SDKBase; [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class OwnerTransfer_Countup_System_WithOnValueChanged : UdonSharpBehaviour { [UdonSynced(UdonSyncMode.None), FieldChangeCallback(nameof(CountData))] private int _countData; // データ本体 public Text DisplayDataText; // データを表示するText public Text OptionText; // 誰がOwnerかを表示するText // OnValueChanged用のプロパティ public int CountData { get => _countData; set { _countData = value; DisplayCountData(); // Owner以外のデータ表示処理 } } private void Start() { // Onwerかどうかを表示 SetOptionalText(Networking.LocalPlayer); } // Cubeをインタラクトした時に呼ばれる public override void Interact() { // 新にOwnerになる var player = Networking.LocalPlayer; Networking.SetOwner(player, this.gameObject); if (player.IsOwner(this.gameObject)) { // カウントアップ処理 CountUp(); } } // Ownerが移行した際の処理 // このイベントはインスタンスにいる全員に発行されるので、Network.LocalPlayerを用いる // 引数のplayerは新たなオーナーを指す public override void OnOwnershipTransferred(VRCPlayerApi player) { SetOptionalText(Networking.LocalPlayer); } // Ownerが値を+1する処理 public void CountUp() { CountData++; // データ更新 RequestSerialization(); // 同期更新 } // 同期変数の値をUIに表示する処理 public void DisplayCountData() { DisplayDataText.text = _countData.ToString(); // データ表示更新 } // プレイヤーがOwnerかどうかをテキストで表示させる処理 public void SetOptionalText(VRCPlayerApi player) { if (player.IsOwner(this.gameObject)) { // Owner側の処理 OptionText.text = $"<color=red>{player.displayName} is Owner!</color>"; } else { // Owner以外の処理 OptionText.text = $"{player.displayName} isn't Owner"; } } }
改変としては先ほど同様、OnDeserializationをOnValueChangedに書き換えただけです。
一つ良くなった点として、OnDeserializationで書いていた時と違い、RequestSerialization()
を遅延しなくてもしっかりと動作出来たということがありました。
これは、OnValueChangedが速く同期出来るという恩恵によるものです。
まとめ
結論として、OnValueChangedは結構早く同期してくれるよということでした。
余談ですが、以前に私がUdonのツイートをした際に、CyanLaserさんから「同期された変数を用いる場合は、SendCustomNetworkEvent
を用いない方がいいよ」というリプライを頂きました。
With OnVariableChanged everything can be handled in two events. When dealing with Synced variables, OnDeserialization or OnVariableChange should be used and not SendCustomNetworkEvent. This causes race conditions as synced data may not arrive in time.
— CyanLaser (@CyanLaser) 2021年8月16日
Translated with DeepL.
SendCustomNetworkEvent
を用いてしまうと、同期変数とSendCustomNetworkEvent
で同期頻度が異なることから、予想し得ない処理が発生する可能性があるため、極力SendCustomNetworkEvent
内で同期変数を処理しない方が良いみたいです。現段階で私がこの新しい設計に慣れていなかったので、今回この記事を書くことで理解を深められたらなと思い、記事を書くことにしました。
この記事が、誰かの役に立つといいなとは同時に思っています。
今回も、サンプルプロジェクトはGithubに置いておきます。
2.3のシーンが今回の内容のものになっているので、詳しく見たい方はご覧ください。
github.com
On Instances.の全貌
目次
はじめに
この文章は私がVRChatにPublishしたワールド「On Instances.」についてのお話です。
ネタバレな話も存在していますので、気に障る方は一度訪れてみることをおすすめします。
vrchat.com
原案
確かVRAA02のテーマがLive / Frontierだったので、最初は生き死にの境目をテーマに作ろうと考えていました。生きることと死ぬことは一切両立の出来ないものであることから、何処かに必ず境界が生じるはずです。今回はその境界を表現してみたいなと当初思ったので、それを意識して作りました。
そのため、境界となる箇所に鳥居をたくさん用意しています。鳥居とは元来、この世と神社の空間との隔たりや境界を示すために置かれているものであり、今回はその境界という面を用いています。結果的には、風情から距離を置かれた景色になりましたけど...。
あと、原案のイラストには実際には作られなかったモノが色々と描かれていますね...w
作られなかったモノのほとんどは、実装が手間になると予想出来たため、没になっています(当時は盛夏音祭'20の準備もあってあまりVRAAの創作に時間が割けなかったことも要因の一つとしてあります)。
というか、元々は自然に囲まれた一風景を作ろうと思ってあのイメージラフを描いていました。ただ、やっぱり自然のような背景を作ろうと思うとどうしてもTerrainが必要になる場面があったので、泣く泣く人工的なデザインに変更することにしました(Terrainでモノづくりするのが私自身苦手なので)。
実際に完成したワールド
完成形はこんな形になりました。ポツンと真ん中に桜の木が育っていて、その周りを幾つもの提灯が昇っています。
途中、作っていて建物の静的感を際立たせたかったため、それ以外のオブジェクトはなるべく動かすようにしました。例えば桜の木の葉やランタン、鏡に設定のUI等...。
後は動かないモノほど暗く、動くものほど明るくしました。というより無意識でそういう風に作ってました。感性的にそう作ってしまうんだと思います。
ちなみに、桜の木は私自身を示しており、提灯は今まで関わってきた人達を示しているつもりです。
隠されたメッセージ
前回のNo time.程ではないですが、今回も私からのメッセージをワールドに閉じ込めてあります。
開放の仕方は以下の通りです(ネタバレ)。
- 桜の木の下にある白いボールを何処かしらの壁等にぶつけて音階を1オクターブ鳴らす
- ポスターを設置しているところにあるSetting PanelのCreditを開く
- 右下の私のアイコンをクリックする
この通りにすると、メッセージが置かれた空間に移動することが出来ます。
メッセージの内容自体は当時の私の心境が書かれています。今ではある程度書いてあることが現実になっているので、私としては大変嬉しく思っています。
ちなみにメッセージの最後にあるクイズの答えは後述の通りですが、「事実」=「バーチャルの死」であり「実態」=「墓場」です。
テーマと目標
今回のワールドのテーマは、ズバリ墓場です。
なので、ポスターに書いてあるサブタイトルは ~That's after world.~ になっています。
私は昔からバーチャル上に自分のお墓を作りたいなと思っていました。なぜなら、人はいつ死ぬかが明確ではないからです。もしかしたら明日いきなり死ぬ可能性だってあります。なので、あらかじめ用意しておきたかったんです。
そのため、このワールド全体は墓場となるように作られており、将来的には私が実質的な死を遂げた際、私の魂が桜の木の下で眠ることを想定しています。
もし、私がバーチャル的な意味で死んだときはお墓参りしてくださると嬉しいです。個人的には、お墓がバーチャル上にあるっていうのがいいですよね。しかもVRChatというプラットフォームに建っているので、いつか壊れるかもしれないし、消えるかもしれないですよね。そういう不確定さが面白いですね。
もう一つ、このワールドを作る際に、ワールドの外観をフルスクラッチで作るということを目標に作成していました。
理由としましては、少しはモデリングに関して知識を蓄えておこうかなと思ったためです。
当時はかなり意気込んでいましたが、結果としては全部自分のモデルにすることは叶いませんでした。圧倒的技術力不足です。
ですが、それでもモデリングソフトを使ってワールドを作成したという経験はとてもタメになりました。今後の制作のノウハウに活かせることを多く学べたと思います。でも、あまり楽しくなかったのでもうやらないかなと率直に思います...w
hatuxes.booth.pm
お墓参り
バーチャルにおける死については、まだバーチャルという概念自体が始まったばかりなのであまり想像する機会はないと思います。
アスタリスクの花言葉というワールドをクリアした方であれば、もしかしたら少し考えたことあるかもしれません。
一番わかりやすい例だと、バーチャルYouTuberが挙げられると思います。
頑張って活動を続けてきたVTuberでも、ある日突然引退を表明し、それ以降何処にも姿を現さなくなる。これこそまさにバーチャル的な死ではないかと私は思います。
現時点ではそういった活動しているVの民に対して起こりやすい現象ですが、決してそれだけではないと思います。
普通にバーチャルで暮らしている我々でも、もしかしたら目の当たりにしてきた瞬間があるかもしれません。ある日突然フレンドが姿を現さなくなったといったようなことが。
その原因は単純に興味がなくなったからやリアルの身体が死んでしまったから、友好関係に亀裂が生じたからなど多岐に及ぶと思います。
友好関係的な意味で死んでしまった人に関しては、記憶を忘れることで解決することがほとんどかなと思います。これは長くインターネットで生活していた人程理解できる話でしょう。
一方で好んでいた人がある日いきなり死んでしまったらどうでしょうか。果たして同じように済まされるでしょうか。
答えはYesです。
人は意外にも物覚えの悪い生き物です。その人のことを覚えていると思っていても、実際には時が経つにつれて概念しか思い出せなくなるといったことがほとんどです。何か意図的にモノや文章として保存しておかない限り思い出すことは難しくなっていきます。
ですが、お墓参りという行動を続けていくことで定期的にその人のことを思い出し、忘れてしまうことを少しは抑制することが出来るのではないかと私は思っています。
そんなことをバーチャルな空間でも当たり前にしてほしいなと思って、私はこのワールドを作りました。
別に「私のことを忘れないでくれぇ!」とだけ言っているわけではなく、単純にこの忘れ去られやすいインターネットの世界において、少しでも忘れ去られない方法を模索したかったのです。
その方法の一つとして、私はお墓参りを提供します。
全員に忘れられないというのは現実でも出来ないので無理な話ですが、数人ぐらいには覚えてもらいたいものですね。やっぱり。
作っていて良いなと思ったところ
ポスター周り
これ私としては凄い綺麗に並んでいるなと我ながらニコニコしています。
前回のワールドであるNo time. と今回のワールドであるOn Instances.のポスターが上下に並んでいます。
橋の下の影
なかなかニヤニヤしちゃいますね。柔らかい影は私の大好物です。
橋の影は横や斜めから光を当てるとどうしても綺麗に見えなかったので、真上から直下に照射する形にしました。
なので、影が垂れたような感じになっていて、リアルさはなく幻想的な形に仕上げられたかなと思います。満足。
反省点
UV展開
UV展開難しすぎます...。本当は床から床下の壁まで綺麗にテクスチャを繋げたかったのですが、上手い展開の仕方が分からなかったので断念しました。
シームの割り方とかいろいろ考えましたけど、経験が足りなかったということで。
中央のパフォーマンス
やはり桜の木が重いですね。
どうしても木の影をリアルタイムで落としたかったのでしょうがないかなとも思います。
やはり木の描画は半透明地獄なので難しいですね。
おわりに
結論としては、お墓作ったので死んだらお墓参りに来てね!という話でした。
お墓ブームはまだ当分来ないかなと思いますが、死が現実味を帯び始めたころに少しずつ現れるかなと個人的に思っています。そのころにはお墓作成代行ブームとか来てそうですよね。
その時に先陣を切ってやっていたということを自慢できたらいいですね。
まぁ、今回は特にVRAAの賞は無かったですが、私としてはとても重要なワールドになったのでいいかなと思います。
今注目を浴びるなら、やっぱりインパクトのあるインタラクティブなワールドじゃないですかね。
もし次のVRAAがあるのなら、皆さんの気持ちが籠ったワールドを是非とも見てみたいなと思います。
それでは。