ハツェの真時代傾向璋

興味を持ったことを書いていく鱗片的な場所から先の未来の場

On Instances.の全貌

目次


はじめに

この文章は私がVRChatにPublishしたワールド「On Instances.」についてのお話です。
f:id:hatuxes:20210627175340p:plain ネタバレな話も存在していますので、気に障る方は一度訪れてみることをおすすめします。
vrchat.com

原案

f:id:hatuxes:20210626184029p:plain

最初に描いたデザイン案


確かVRAA02のテーマがLive / Frontierだったので、最初は生き死にの境目をテーマに作ろうと考えていました。生きることと死ぬことは一切両立の出来ないものであることから、何処かに必ず境界が生じるはずです。今回はその境界を表現してみたいなと当初思ったので、それを意識して作りました。
そのため、境界となる箇所に鳥居をたくさん用意しています。鳥居とは元来、この世と神社の空間との隔たりや境界を示すために置かれているものであり、今回はその境界という面を用いています。結果的には、風情から距離を置かれた景色になりましたけど...。
あと、原案のイラストには実際には作られなかったモノが色々と描かれていますね...w
作られなかったモノのほとんどは、実装が手間になると予想出来たため、没になっています(当時は盛夏音祭'20の準備もあってあまりVRAAの創作に時間が割けなかったことも要因の一つとしてあります)
というか、元々は自然に囲まれた一風景を作ろうと思ってあのイメージラフを描いていました。ただ、やっぱり自然のような背景を作ろうと思うとどうしてもTerrainが必要になる場面があったので、泣く泣く人工的なデザインに変更することにしました(Terrainでモノづくりするのが私自身苦手なので)



実際に完成したワールド

f:id:hatuxes:20210627175259p:plain

出来上がった風景


完成形はこんな形になりました。ポツンと真ん中に桜の木が育っていて、その周りを幾つもの提灯が昇っています。
途中、作っていて建物の静的感を際立たせたかったため、それ以外のオブジェクトはなるべく動かすようにしました。例えば桜の木の葉やランタン、鏡に設定のUI等...。
後は動かないモノほど暗く、動くものほど明るくしました。というより無意識でそういう風に作ってました。感性的にそう作ってしまうんだと思います。
ちなみに、桜の木は私自身を示しており、提灯は今まで関わってきた人達を示しているつもりです。




隠されたメッセージ

前回のNo time.程ではないですが、今回も私からのメッセージをワールドに閉じ込めてあります。
開放の仕方は以下の通りです(ネタバレ)。

  1. 桜の木の下にある白いボールを何処かしらの壁等にぶつけて音階を1オクターブ鳴らす
  2. ポスターを設置しているところにあるSetting PanelのCreditを開く
  3. 右下の私のアイコンをクリックする

この通りにすると、メッセージが置かれた空間に移動することが出来ます。
メッセージの内容自体は当時の私の心境が書かれています。今ではある程度書いてあることが現実になっているので、私としては大変嬉しく思っています。
ちなみにメッセージの最後にあるクイズの答えは後述の通りですが、「事実」=「バーチャルの死」であり「実態」=「墓場」です。




テーマと目標

今回のワールドのテーマは、ズバリ墓場です。
なので、ポスターに書いてあるサブタイトルは ~That's after world.~ になっています。
f:id:hatuxes:20210702025436p:plain

On Instances.のポスター


私は昔からバーチャル上に自分のお墓を作りたいなと思っていました。なぜなら、人はいつ死ぬかが明確ではないからです。もしかしたら明日いきなり死ぬ可能性だってあります。なので、あらかじめ用意しておきたかったんです。
そのため、このワールド全体は墓場となるように作られており、将来的には私が実質的な死を遂げた際、私の魂が桜の木の下で眠ることを想定しています。
もし、私がバーチャル的な意味で死んだときはお墓参りしてくださると嬉しいです。個人的には、お墓がバーチャル上にあるっていうのがいいですよね。しかもVRChatというプラットフォームに建っているので、いつか壊れるかもしれないし、消えるかもしれないですよね。そういう不確定さが面白いですね。

もう一つ、このワールドを作る際に、ワールドの外観をフルスクラッチで作るということを目標に作成していました。
理由としましては、少しはモデリングに関して知識を蓄えておこうかなと思ったためです。
当時はかなり意気込んでいましたが、結果としては全部自分のモデルにすることは叶いませんでした。圧倒的技術力不足です。
ですが、それでもモデリングソフトを使ってワールドを作成したという経験はとてもタメになりました。今後の制作のノウハウに活かせることを多く学べたと思います。でも、あまり楽しくなかったのでもうやらないかなと率直に思います...w
hatuxes.booth.pm



お墓参り

バーチャルにおける死については、まだバーチャルという概念自体が始まったばかりなのであまり想像する機会はないと思います。
アスタリスクの花言葉というワールドをクリアした方であれば、もしかしたら少し考えたことあるかもしれません。
一番わかりやすい例だと、バーチャルYouTuberが挙げられると思います。
頑張って活動を続けてきたVTuberでも、ある日突然引退を表明し、それ以降何処にも姿を現さなくなる。これこそまさにバーチャル的な死ではないかと私は思います。
現時点ではそういった活動しているVの民に対して起こりやすい現象ですが、決してそれだけではないと思います。
普通にバーチャルで暮らしている我々でも、もしかしたら目の当たりにしてきた瞬間があるかもしれません。ある日突然フレンドが姿を現さなくなったといったようなことが。
その原因は単純に興味がなくなったからやリアルの身体が死んでしまったから、友好関係に亀裂が生じたからなど多岐に及ぶと思います。
友好関係的な意味で死んでしまった人に関しては、記憶を忘れることで解決することがほとんどかなと思います。これは長くインターネットで生活していた人程理解できる話でしょう。
一方で好んでいた人がある日いきなり死んでしまったらどうでしょうか。果たして同じように済まされるでしょうか。
答えはYesです。
人は意外にも物覚えの悪い生き物です。その人のことを覚えていると思っていても、実際には時が経つにつれて概念しか思い出せなくなるといったことがほとんどです。何か意図的にモノや文章として保存しておかない限り思い出すことは難しくなっていきます。
ですが、お墓参りという行動を続けていくことで定期的にその人のことを思い出し、忘れてしまうことを少しは抑制することが出来るのではないかと私は思っています。
そんなことをバーチャルな空間でも当たり前にしてほしいなと思って、私はこのワールドを作りました。
別に「私のことを忘れないでくれぇ!」とだけ言っているわけではなく、単純にこの忘れ去られやすいインターネットの世界において、少しでも忘れ去られない方法を模索したかったのです。
その方法の一つとして、私はお墓参りを提供します。
全員に忘れられないというのは現実でも出来ないので無理な話ですが、数人ぐらいには覚えてもらいたいものですね。やっぱり。




作っていて良いなと思ったところ

ポスター周り

f:id:hatuxes:20210701015808p:plain

最初に描いたデザイン案


これ私としては凄い綺麗に並んでいるなと我ながらニコニコしています。
前回のワールドであるNo time. と今回のワールドであるOn Instances.のポスターが上下に並んでいます。

橋の下の影

f:id:hatuxes:20210627175209p:plain

最初に描いたデザイン案


なかなかニヤニヤしちゃいますね。柔らかい影は私の大好物です。
橋の影は横や斜めから光を当てるとどうしても綺麗に見えなかったので、真上から直下に照射する形にしました。
なので、影が垂れたような感じになっていて、リアルさはなく幻想的な形に仕上げられたかなと思います。満足。





反省点

UV展開

UV展開難しすぎます...。本当は床から床下の壁まで綺麗にテクスチャを繋げたかったのですが、上手い展開の仕方が分からなかったので断念しました。
シームの割り方とかいろいろ考えましたけど、経験が足りなかったということで。

中央のパフォーマンス

やはり桜の木が重いですね。
どうしても木の影をリアルタイムで落としたかったのでしょうがないかなとも思います。
やはり木の描画は半透明地獄なので難しいですね。




おわりに

結論としては、お墓作ったので死んだらお墓参りに来てね!という話でした。
お墓ブームはまだ当分来ないかなと思いますが、死が現実味を帯び始めたころに少しずつ現れるかなと個人的に思っています。そのころにはお墓作成代行ブームとか来てそうですよね。
その時に先陣を切ってやっていたということを自慢できたらいいですね。
まぁ、今回は特にVRAAの賞は無かったですが、私としてはとても重要なワールドになったのでいいかなと思います。
今注目を浴びるなら、やっぱりインパクトのあるインタラクティブなワールドじゃないですかね。
もし次のVRAAがあるのなら、皆さんの気持ちが籠ったワールドを是非とも見てみたいなと思います。
それでは。

Writed by Hatze

シン・U# 入門 ②

こんばんは。ハツェです。
今回も、Udonスクリプトで制作するための記事を書いていきます。
今回は、Udonの同期関連について話して行けたらなと思っています。
あくまでもU#を扱っているので、この記事はコードを基礎的に書ける方を対象としています。予めご了承ください。
動作環境は、Unity : 2019.4.29f1、VRCSDK : 2021.08.11.15.16、U# : v0.20.2です。

目次


前回

前回は導入と簡単な処理をしました。
hatuxes.hatenablog.jp

今回の概要

今回は同期について紹介します。
変数操作自体は普通のコードとなんら変わりはないですが、同期の方法についてはVRChat独特の概念を持つので、少し混乱するかもしれません。
ですが、しっかりと頭で分けて一つずつ考えれば、内容自体はそこまで難しい話ではないと思います。
また、同期のいろはを書いているため前回よりもかなり長めな記事となっています。あらかじめご了承ください。
では、やっていきましょう!

同期について

SDK2では、ObjectSyncとかTriggerで同期を行っていましたが、Udonにも同期を行う仕組みがあります。
VRC_Trigger等を過去に使用したことが無い状態で初めてUdonを触るという方は、最初に以下の公式動画を見ると良いと思います。
www.youtube.com

基本となる同期は以下の三つです。

  1. ObjectSyncによる同期
  2. 変数単体の同期
  3. 処理の同期

それぞれ説明していきます。

ObjectSyncによる同期

SDK2と同様にObjectSyncを用いると、気軽にオブジェクトの同期が行えます。

f:id:hatuxes:20210319010639p:plain

ここで同期出来るのは、TransformとRigidBodyのみです。
ちなみに、ObjectSyncのOwnerは、最後にオブジェクトを動かした人になるらしいです。
一番下のAllow Ownership Transfer on Collisionは、他のコライダーと接触した時にOwnerを移すかどうかのチェックボックスです。あんまりちゃんと動いてくれないので、オフにしておくのが良いと思います。

また、連続同期設定をしたUdonBehaviorを使用すると、ObjectSyncが同期させるタイプを変更することは出来ます。
Transform同期のスムージングを解除したり、重力やキネマチックの可否を設定することが出来るみたいです。
詳しいことは、公式サイトをご覧ください。
docs.vrchat.com

変数の同期

Udonになって、変数ごとに同期がとれるようになりました。
UdonGraphでいうところのここですね。

f:id:hatuxes:20210319011028p:plain

このSyncはワールド全員に同期するだけでなく、後から来た人にも同期します。
この後から来た人にも同期するというのが、変数同期の一番の強みです。
U#では、変数の属性として[UdonSynced]を指定すると同期変数になります。
ただし、この属性をつけた変数はそのUdonBehaviorのOwnerにしか編集出来なくなるため、変数をいじる際にはSetOwner()等でOwnerを渡す必要が出てきます。

ちなみに、対応している変数の型はbool, char, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, Vector2, Vector3, Vector4, Quaternion, VRCUrl, Color, Color32ですが、配列の同期に関しては現在string, VRCUrlのみ同期出来ないので注意が必要ですです。現在は変数か配列か気にせず同様の型が同期可能となっています。

変数の同期方法

変数を同期させる方法は二種類存在します。

  1. Continuous Sync - 連続同期
  2. Manual Sync - 手動同期

どの手法で同期させるかはGameObjectごとに決めることができ、加えて同期方法を指定する方法は、Inspectorから選択する方法とスクリプトから指定する方法の二種類があります。

Inspectorから同期方法を指定する

Inspectorから選択する場合、Synchronization Methodから選択出来ます。

f:id:hatuxes:20210319012805p:plain

欠点としては、複数人で扱うプロジェクトの場合、他人に勝手に変更されてしまう危険性もあるので注意が必要です。

スクリプト内から同期方法を指定する

一方、スクリプトから指定する場合、クラスの属性として指定します。

[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]
public class ClassName : UdonSharpBehaviour { }

設定はAny Continuous Manual NoVariableSyncの四つから指定できます。 Any 以外を設定した場合、Inspectorから変更出来ないようにすることが出来ます。
f:id:hatuxes:20210319015657p:plain

また、NoVariableSyncを指定すると、同期変数が存在するコードを記述した場合にコンパイルエラーを出してくれるようになります。

f:id:hatuxes:20210522160729p:plain

便利でもありますが、裏では自動的に連続同期にされるようなので、通信量を少しでも節約したい人はManualを指定しておくと良いと思います。

Continuous Sync- 連続同期

連続同期とは、プレイヤーが更新するタイミングを指定せず、自動的に変数の値を同期させ続ける手法であり、現在はおおよそ0.2~2秒間隔で値を同期させ続ける仕様になっています。
例えば、色情報を常に更新し続けたいみたいな時に使えますね。
常に更新し続けるというのは便利なときもありますが、反対にこの特性から他の通信のために帯域を節約する理由で同期が更新されないこともあるという仕様になっています。つまり、同期の優先度は低めに設定されているということです。そのため、全ての同期設定をこれにするのはあまり良い手法ではないかな思います。

Manual Sync- 手動同期

手動同期とは、任意のタイミングでプレイヤーが変数の値を同期させる手法です。
基本的にこっちの方が確実に値を同期してくれます。
手動で値を更新するので、更新するためには変数の値を変更した後、UdonBehavior.RequestSerialization()を呼んであげると次の通信時に更新されます。
こちらは手動で更新するため帯域を喰う心配はないですが、弱点として唯一挙げるとするとならば、ObjectSyncを扱えないということがあります。

処理の同期

処理同期は、変数同期とは異なります。
例えば銃から弾を発射するというギミックを考えた際、弾が銃口から発射されるという処理が同期していないと完成しないわけです。
これはこれで便利なのですが一つ注意点として、変数同期と異なり現在のインスタンスにいる人には同期されますが、その後に入ってきた人には同期されません。この点は変数同期とかなり異なります。

その処理同期ですが、使うのはSendCustomNetworkEventです。
SendCustomNetworkEventは同期させたい処理を記述している関数の名前を指定してあげることで、処理を同期させることが出来ます。
関数名を指定する手法であるため、現状引数を扱う関数をSendCustomNetworkEventで呼ぶことは出来ません。引数を用いる手法は、要望が多いのでそのうち実装されるかなと...。
因みにUdonGraphにももちろん同じノードがあります。

f:id:hatuxes:20210319014210p:plain

Ownerを渡す

先述しましたが、同期変数はOwnerのみ編集することが出来ます。
そのため、別の人が同期変数に変更を加えるような操作を行う場合、基本的にはその人をOwnerにさせてあげる必要があります。
具体的にOwnerを別の人に移す場合、Network.SetOwnerを呼べば良いです。
Network.SetOwner関数を呼んだ場合、以下の一連の処理が発生します。

  1. (必須) Networking.SetOwner()を呼び、オーナー権限のリクエストをする。
  2. (任意) リクエストが発生した際にはOnOwnershipRequestイベントが呼ばれるので、そのリクエストを許可する場合はtrueを返す。falseを返した場合、Ownerは譲渡されずにこのまま処理が終わる。
  3. (任意) 前述の処理でtrueを返した場合はオーナーが譲渡され、その後OnOwnershipTransferredイベントがインスタンス内のすべてのユーザーのGameObjectで呼び出される。


SDKから移行してきた方へ
特徴的な箇所としては、リクエストの可否が出来るようになった点だと思います。
Udonをアップデートする過程で、旧Udonから移行してきて既存のシステムをそのまま使いたいと思う方も多いと思います。
仮にOnOwnershipRequestを記述しなかった場合、無条件でOwnerを移行する仕様になる(以下のコードを記述した時と同じ処理になる)ので特にコードを変更しなくても対応出来るため、そこまで意識する必要はありません。

public override OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
{
    return true;
}


実例

ここからは、いくつか例になるものを作っていこうと思います。
以下で使用するCanvasとParticleは、全てこのunitypackageに入れてあります。
必要に応じて使用してみてください。(自由に使用して結構です)
drive.google.com

パーティクルを発射する装置を作る

まずは処理同期からやってみましょう。
概要としては、Cubeをインタラクトしたら上からパーティクルが数秒ポロポロと落ちてくるものです。
まず、インタラクトに使用するCubeと以下からダウンロードしたパーティクルをシーンに設置します。
だいたいこんな感じで設置します。

f:id:hatuxes:20200413162524p:plain

そしたらCubeに以下のようなスクリプトをアタッチしましょう。

using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;

public class PlayParticle : UdonSharpBehaviour
{
    public ParticleSystem Particle;  // 再生するパーティクル本体

    public override void Interact()
    {
        // パーティクルが再生されてなかったら、再生する
        if (!Particle.isPlaying)
        {
            SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "Emit");
        }
    }

    public void Emit()
    {
        Particle.Play();
    }
}


これで、一応完成となります。少し解説しますね。
気になる所は20行目のここ。

if (!Particle.isPlaying)
{
    SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.All, "Emit");
}

SendCunstomNetworkEventが処理同期のための関数ということは同期の説明時にお話ししました。
この関数は第一引数に処理を同期させる人の種類、第二引数に同期させる処理を記述した関数の名前を指定します。
第一引数は現状VRC.Udon.Common.Interfaces.NetworkEventTarget.AllVRC.Udon.Common.Interfaces.NetworkEventTarget.Ownerの二つから選べます。
Allの方はインスタンスにいる全員に関数内の処理を実行します。
一方Ownerの方は、オーナーにのみ関数内の処理を実行します。
加えて、ifの中身はパーティクルの重複起動防止のために再生中かどうかの判定を行っています。
ちなみに、SendCunstomNetworkEventで呼ぶ関数はpublicである必要があります。

実行

実行画面は以下の通りです。(三人称視点の映像)

f:id:hatuxes:20200413165038g:plain

押した人は別の人なのに、ちゃんと自分の画面にもパーティクルが降り注いでますね。
これで処理同期が確認出来たことになりました。

カウントアップする装置を作る

次は、カウントアップ装置を作りながら変数同期の例を作成していきます。
まず始めに、上記のprefabの名前とかを少し変更して、以下のように三つのCanvasを作成しました。全く同じ感じでも良いですし、好きにアレンジしても構いません。
内容は左から、ローカル版、Owner経由版、Owner変更版として使って行く予定です。

f:id:hatuxes:20210519184305p:plain

まずはローカルのカウントアップ装置を作る

最初に、ベースとなるカウントアップ装置を作っていきます。ここは同期とは一切関係ありません。
概要としては、Cubeをインタラクトしたら数字が1ずつ増えていくものを作ります。
インタラクトさせるCubeにUdonBehaviorを追加して、以下のスクリプトをアタッチします。

using UdonSharp;
using UnityEngine.UI;

public class Local_Countup_System : UdonSharpBehaviour
{
    public Text DisplayDataText;  // データを表示するためのText

    private int _countData;           // データ本体

    void Update()
    {
        // 常時データをTextに出力する
        DisplayDataText.text = _countData.ToString();
    }

    // Cubeをインタラクトすることで、値を+1する処理
    public override void Interact()
    {
        _countData++;
    }
}


仕組みは単純で、インタラクトしたらint型の変数_countDataを1ずつ増やしていくだけです。

実行

実行画面は以下の通りです。

f:id:hatuxes:20210520144716g:plain

今度は、これを同期させてみましょう。

Ownerに加算処理をさせることで同期変数を操る

同期の説明時にもお話ししましたが、同期変数はそのUdonBehaviorのOwnerしか変更出来ません。
最初の手法では、Ownerに処理を任せることで同期変数を扱ってみようというパターンを紹介します。
ローカル版と同様、Cubeに以下のスクリプトをアタッチします。

using UdonSharp;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class ViaOwner_Countup_System : UdonSharpBehaviour
{
    [UdonSynced(UdonSyncMode.None)] private int _countData;        // データ本体

    public Text DisplayDataText;            // データを表示するText
    public Text OptionText;                 // 誰がOwnerかを表示するText



    private void Start()
    {
        // Onwerかどうかを表示
        SetOptionalText(Networking.LocalPlayer);
    }



    // Cubeをインタラクトしたときに呼ばれる
    public override void Interact()
    {
        var player = Networking.LocalPlayer;

        if (player.IsOwner(this.gameObject))
        {
            // Ownerが押したら、純粋にカウントアップする
            CountUp();
        }
        else
        {
            // Owner以外が押したら、Ownerにカウントアップさせるように命令する
            SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, nameof(CountUp));
        }
    }

    // Owner以外のデータ表示処理
    public override void OnDeserialization()
    {
        DisplayDataText.text = _countData.ToString();
    }



    // Ownerが値を+1する処理
    public void CountUp()
    {
        _countData++;                                   // データ更新
        RequestSerialization();                     // 同期更新
        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";
        }
    }
}


少し情報が多いので、一つずつ紹介していきましょう。

同期変数の書き方

まずは、後から来た人にも同様な値を見せたい変数に[UdonSynced]属性をつけます。

[UdonSynced(UdonSyncMode.None)] private int _countData;

UdonSyncModeは同期の仕方を設定するオプションです。
設定はNone Linear Smooth NotSyncedの四つから指定できます。
それぞれただの同期 リニア補間同期 スムーズ補間同期 同期しないです。
Linear Smoothについては、連続同期でのみ使用出来ます。手動同期では使えないので注意が必要です。
それぞれ動かしてみた感じとしては以下の通りで、体感としては下図のような感じになります。

f:id:hatuxes:20200413161827p:plain

上記動画を見てもわかる通り、IntやFloatなら問題ないですが、Quaternion等のLinearSmoothが正常に同期されていないため、素直にNoneを使用するのがオススメです。
また、今回は同期変数を使用し、かつ手動同期を用いるため、クラスの属性に[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]を付けました。(クラス属性を付けずにInspectorからManualSyncを選択しても構いません)

ManualSyncで変数を同期させる

同期の説明時にもお話ししましたが、UdonBehavior.RequestSerialization()を呼んだフレームから次の通信(正確には次のネットワークティック)で他の人にも同期されます。
難しいことを言っているように聞こえますが、結局RequestSerialization()関数を呼べば同期してくれるわけです。
実際に、変数を同期させている箇所はCountup()関数内で行っていました。

// Ownerが値を+1する処理
public void CountUp()
{
    _countData++;                                   // データ更新
    RequestSerialization();                     // 同期更新
    DisplayDataText.text = _countData.ToString();   // データ表示更新
}

まず、同期変数の値を+1し、その値を他の人にも同期させ、最後に自分の表示を更新するという流れです。
この関数はOwnerのみが実行されるように作っているため、問題はありません。

処理をOwnerに渡す

処理をOwnerに渡す方法は、SendCustomNetworkEventの第一引数をOwnerにするだけです。ちなみに、第二引数の関数名にはnameof()関数を用いると、楽にコードが書けるのでオススメです。

SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, "関数の名前");

今回は、インタラクトしたプレイヤーがOwnerかどうかで処理を分けました。

public override void Interact()
{
    var player = Networking.LocalPlayer;

    if (player.IsOwner(this.gameObject))
    {
        // Ownerが押したら、純粋にカウントアップする
        CountUp();
    }
    else
    {
        // Owner以外が押したら、Ownerにカウントアップさせるように命令する
        SendCustomNetworkEvent(VRC.Udon.Common.Interfaces.NetworkEventTarget.Owner, nameof(CountUp));
    }
}

Ownerの場合は普通に処理を実行し、そうでない場合はOwnerに同じ処理を渡しています。
今回は説明のために丁寧に作っていますが、結局処理がOwnerに渡る意味では条件分岐なんかせずに、SendCustomNetworkEventだけInteract()関数内に書いても問題ないと思います。

Owner以外に同期データを反映させる

これはテクニックですが、OnDeserialization()内に同期変数を扱ったデータに関する処理を書くのが良いです。
Update()じゃないの?と思う方もいると思いますが、OnDeserialization()自体が同期変数を受け取った時に呼ばれる関数になっています。そのため、データの確実性的にOnDeserialization()内に書いた方が良いという訳です。
今回は、Owner以外の人に向けて同期変数_countDataの表示処理をOnDeserialization()内に書いています。

// Owner以外のデータ表示処理
public override void OnDeserialization()
{
    DisplayDataText.text = _countData.ToString();
}


追記
現在では、同期変数を扱う処理はOnDeserializationよりOnValueChangedを用いる方が良いとされています。
理由は、OnDeserializationよりも更新速度が速いため、OnDeserializationを用いた場合に同期ズレが発生していたような処理でもOnValueChangedを用いた場合は素直に同期処理出来るからです。
OnValueChangedについては、C#でいうプロパティの仕組みを用いるため、当記事では取り上げていません。
当記事を読んで同期の仕組みが理解できた上で気になる方は、以下の別記事を見てみることをオススメします。
hatuxes.hatenablog.jp

実行

実行画面は以下の通りです。(三人称視点の映像)
他人が押しても、自分が押しても問題なく動いています。英文が間違ってるのはミスです...

f:id:hatuxes:20210520153828g:plain

Ownerを移して自分で同期変数を操る

今度はもう一つの方法として、自分がOwnerになって同期変数を操作するパターンを紹介します。
ローカル版と同様、Cubeに以下のスクリプトをアタッチします。

using UdonSharp;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class OwnerTransfer_Countup_System : UdonSharpBehaviour
{
    [UdonSynced(UdonSyncMode.None)] private int _countData;      // データ本体

    public Text DisplayDataText;          // データを表示するText
    public Text OptionText;               // 誰がOwnerかを表示するText



    private void Start()
    {
        // Onwerかどうかを表示
        SetOptionalText(Networking.LocalPlayer);
    }



    // Cubeをインタラクトした時に呼ばれる
    // 新にOwnerになって、データを自身で更新する
    public override void Interact()
    {
        var player = Networking.LocalPlayer;
        Networking.SetOwner(player, this.gameObject);

        if (player.IsOwner(this.gameObject))
        {
            CountUp();
        }
    }

    // Owner以外のデータ表示処理
    public override void OnDeserialization()
    {
        DisplayDataText.text = _countData.ToString();
    }

    // SetOwnerのリクエストが飛んだ際に呼ばれる関数
    // 譲渡の可否を返す必要がある
    // trueを返すだけなので、書かなくても良い
    public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
    {
        return true;  // 譲渡を許可
    }

    // Ownerが移行した際の処理
    // このイベントはインスタンスにいる全員に発行されるので、Network.LocalPlayerを用いる
    // 引数のplayerは新たなオーナーを指す
    public override void OnOwnershipTransferred(VRCPlayerApi player)
    {
        SetOptionalText(Networking.LocalPlayer);
    }




    // Ownerが値を+1する処理
    public void CountUp()
    {
        _countData++; // データ更新

        // Owner変更後に即時更新すると、OnDeserializationに追いつかないことがあるため。少し遅延させている
        SendCustomEventDelayedSeconds(nameof(SerializeData), 0.4f);


        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";
        }
    }

    public void SerializeData()
    {
        RequestSerialization();       // 同期更新
    }
}


仕組みは先程のOwnerに処理を渡す方法と同じです。そのため、異なっている箇所のみ説明を挟もうと思います。

Ownerを渡す流れ

同期の説明時にもお話ししましたが、SetOwner()をするとOnOwnershipRequest, OnOwnershipTransferredと順に処理が発生します。
今回の例で同じ箇所を追ってみましょう。
今回は、まずインタラクト時にSetOwner()を呼びました。

// Cubeをインタラクトした時に呼ばれる
// 新にOwnerになって、データを自身で更新する
public override void Interact()
{
    var player = Networking.LocalPlayer;
    Networking.SetOwner(player, this.gameObject);

    if (player.IsOwner(this.gameObject))
    {
        CountUp();
    }
}

その後、OnOwnershipRequest内でOwnerの譲渡を無条件で許可するように記述しました。

// SetOwnerのリクエストが飛んだ際に呼ばれる関数
// 譲渡の可否を返す必要がある
// trueを返すだけなので、書かなくても良い
public override bool OnOwnershipRequest(VRCPlayerApi requestingPlayer, VRCPlayerApi requestedOwner)
{
    return true;  // 譲渡を許可
}

そして、Ownerが受け渡された時に自身がOwnerかどうかのテキスト表記を更新するという書き方をしました。

// Ownerが移行した際の処理
// このイベントはインスタンスにいる全員に発行されるので、Network.LocalPlayerを用いる
// 引数のplayerは新たなオーナーを指す
public override void OnOwnershipTransferred(VRCPlayerApi player)
{
    SetOptionalText(Networking.LocalPlayer);
}

ちなみに、OnOwnershipTransferredの引数のplayerは新しくOwnerになったプレイヤーを指し、Network.LocalPlayerは単純にローカルプレイヤーを指します。
OnOwnershipTransferredは、Ownerだけとかではなくインスタンスの全員に発行されるので、この違いには注意が必要です。

実行

実行画面は以下の通りです。(三人称視点の映像)
他人が押しても動作しており、自分が押した際にもしっかりとOwnerが移った状態で変更されていることが分かります。英文が間違ってるのはミスです...

f:id:hatuxes:20210520155120g:plain


以上です!お疲れ様でした。
今回も、サンプルプロジェクトはGithubに置いておきます。
github.com

次回

次回は、UIでUdonを使う方法について触れていきます。
hatuxes.hatenablog.jp