UdonSharp 同期編
概要
UdonSharpで書く命令は、コンポーネント含め同期させる行動を意図的に取らなきゃ全てローカル動作になります。
同期、もとい特定のプレイヤーの世界での動作や状態などを他のプレイヤーに渡す/参照させる方法は3種類あります。
1.同期変数
全プレイヤーで1つの共通の変数を見に行ける仕組みです。
ただし、変数を更新できるのは権限者の一人のみです。
2.イベント同期
SendCustomNetworkEventメソッドを使う方法です。
これは変数ではなく関数をほぼ同時に発動させる方法です。
が、関数内で変数を変えればいいだけなので
変数変更とてこの方法でもできます。
3.セーブデータ機能
セーブデータ機能は、当然上書きは当人のセーブデータしかできませんが
覗く分には他人のセーブデータを覗きに行けます。
ですがセーブデータを同期用として悪用(?)するような使い方は、「バグりそう」や「容量」という点でお勧めしません。
今回では、2.のSendCustomNetworkEventでの方法について取り扱います。
Late-Joiner(後から入ってきた人)に自動的に調整してくれる機能はないですが、
後から入ってきた人用の関数を用意してあげればほとんどの場合で対応可能でしょう。
以降、長いので「SendCustomNetworkEvent」を「SCNE」と略します。
[NetworkCallable]も省略しています。
目次
特定プレイヤーに実行させる方法
SCNEのNetworkEvevtTargetでは
・All (全員)
・Others (送信者以外の全員)
・Owner (オブジェクトオーナー)
・Self (送信者自身)
と受信者指定はこの四種類しかないですが、特定のプレイヤーだけに内容を実行させたい場合もあるでしょう。
public void HogeHoge01(int targetPlayerId)
{
if (Networking.LocalPlayer.playerId == targetPlayerId)
{
// ここに処理
}
}
このように宛先プレイヤーのIDを引数にして送り、関数内のif文で判定してあげる方法で実行できます。
受信したい人にフラグを付けておいて、if文でフラグtrueかどうかの判定を行えば
特定の複数人相手にも同様に受信させられます。
重複エントリーを防ぐ方法
一般的な同期変数の方法では、
1.自身をオーナーにする
2.同期変数を更新する
の2ステップの為、オーナー状態の僅かなラグで同時押しに弱い性質があります。
これもSCNEで解決できるうちの代表的な例でしょう
誰かひとりを基準者にし、基準者で判定や処理を行いその結果を全員に返す
という方法が使えます。
private void HogeHogeEvent()
{
SendCustomNetworkEvent(NetworkEventTarget.Owner, nameof(HogeHogeRequest), /* 任意のデータ */);
}
public void HogeHogeRequest(/* 任意のデータ */)
{
// Ownerがすべき何らかの処理
// 全体に伝達する情報
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(HogeHogeShare), /* 任意のデータ */);
}
public void HogeHogeShare(/* 任意のデータ */)
{
// Ownerから受け取ったデータを処理する
}
HogeHogeEventはなんでもいい関数です、ここでエントリーをいきなりするのではなく
エントリーのリクエストをオブジェクトオーナーに発信します。
HogeHogeRequestを受信したオブジェクトオーナーは判定や処理等を行った後に、その結果を全体に返します。
往復する分時間はかかりますが、高速化された今あまり気にならないのと
エントリーする等、反映に時間がかかってもあまり問題がない部分に向いている方法になります。
これをすることで、エントリーは必ず重複等なく行われ
エントリー結果はそれぞれローカルの世界で保有できます。
部分グローバル
上記二つの方法を組み合わせる手法です。
自分がVRC上のゲムワでの、RPGのパーティーを結成する仕組みに使っているものなので
それを例にとり紹介します。
Join時にプレイヤーは、自身のPlayerIDと同じ値をPartyIDとして格納します。
つまり、最初は自分ひとりのパーティー状態になっています。
このPartyIDは、一致するPlayerIDが自身のパーティーリーダーである事を示しています。
誰かのパーティーに混ざりたいときは、混ざりたいプレイヤーのPlayerIDを付けSCNE発信をします。
受信者は引数のPlayerIDと自身のPlayerIDが一致する場合のみ処理をします。
処理内容は、自身のPartyIDをSCNEで送り返すことです。
送り返すとき、発信者のIDはNetworkCalling.CallingPlayerから取得します。
パーティに混ざりたいと発信した人は、PartyIDが返ってきたら
今度はそのPartyIDを付けてSCNEをします。
受け取る側はPartyIDと自身のPlayerIDが一致する事を条件に処理をします。
RPGの場合では、ここでパーティーリーダーにメッセージ通知を出し、パーティー参加の可否を選択させ
その結果をパーティに混ざりたい人に送りパーティー処理をします。
この二段階処理を行うことの利点は、パーティーリーダーがパーティー加入に対し操作を割り込ませられる点と
処理の基準者をパーティーリーダーと定めているので、参加するタイミングが他の操作とのタイミングと重なっても影響が出ない点です。
public void HogeHogeEvent()
{
// 一緒のパーティーになりたい人のPlayerIDを引数に
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(HogeHogePartyIDRequest), targetPlayerId);
}
public void HogeHogePartyIDRequest(int targetPlayerId)
{
// 自身がパーティーになりたいと思われている人であれば実行
if (targetPlayerId == Networking.LocalPlayer.playerId)
{
// 自身のPartyIDをリクエストした人に返す
int RequesterID = NetworkCalling.CallingPlayer.playerId;
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(HogeHogePartyIDShare), RequesterID, myPartyID);
}
}
public void HogeHogePartyIDShare(int RequesterId, int targetPartyId)
{
// 自分がリクエストした人であれば実行
if (RequesterId == Networking.LocalPlayer.playerId)
{
// 参加したいPartyIDを取得できたのでそれを送る
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(HogeHogePartyJoinRequest), targetPartyId);
}
}
public void HogeHogePartyJoinRequest(int targetPartyID)
{
// PartyIDとPlayerIDが一致する場合、そのパーティーのリーダーである
if (targetPartyID == Networking.LocalPlayer.playerId)
{
// 何かしらUIを表示させて、関数を途切れさせても良い
// 今回は自動承認だとする、dmyPartyIDという仮予約
dmyPartyID = targetPartyID;
int RequesterID = NetworkCalling.CallingPlayer.playerId;
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(HogeHogePartyJoinShare), RequesterID, 0);
}
}
public void HogeHogePartyJoinShare(int RequesterID, byte answer)
{
// 自分がリクエストした人であれば実行
if (RequesterID == Networking.LocalPlayer.playerId)
{
// answerが0であればパーティー加入成功
if (answer == 0)
{
myPartyID = dmyPartyID;
}
// 加入失敗以外にも場合分け等する用にanswerはbool型でなくbyte型にしてある
}
}
特定のプレイヤーを基準者にし、特定のプレイヤー群に受信させる方式の強みは
同期変数では全員が一つの変数を見に行くため、全員が一致しているか個人個人バラバラかの二択しか取れない部分の解消と
同期変数を更新すべき人(つまり何かしらの動作の判定者、基準者)が固定の一人でなくても問題がない部分です。
ゲームによっては、攻撃者側の世界で攻撃を当てた事をHit判定に使いたい場合や
被攻撃者の世界で攻撃をくらった事をHit判定に使いたい場合等、判定基準を自由に取りたい場合などに強い方法になります。
後から入ってきた人も合わせる方法
といっても単純ですが、誰かがワールドに入ってきた時等遅れて同期をとりたいタイミングでの実行で
オブジェクトオーナーが同期させたいプレイヤーに対しSCNE発信させれば可能です。
例として、何らかの対戦ゲームで各チームがポイントを稼ぎあうゲームを想定します。
localpoint系が各プレイヤーが保存しているポイントになります。
今回はプレイヤーがJoinした時に同期させる事を想定している為OnPlayerJoinedにしています。
麻雀ワールド等では、後から入ってきたPlayerのインタラクト等で同期させるタイプもありますが
SCNEを発信する為のトリガーは自由に応用できます。
public override void OnPlayerJoined(VRCPlayerApi player)
{
// 自身がオブジェクトオーナーであれば実行
if (Networking.IsOwner(gameObject))
{
// 自身が保有してる各チームのポイントを共有
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(LateJoinerShare), localTeam1point, localTeam2point);
}
}
public void LateJoinerShare(/* 必要な分のデータ 今回は各チームのポイント数*/ int team1point, int team2point)
{
localTeam1point = team1point;
localTeam2point = team2point;
}
特定時刻 or 特定時間後に実行 をSCNEでやる方法
内部の時間が違うのか、着弾のDateTime.Nowのズレはほぼ無くとも着弾タイミングが1秒ほど空いたりするので良くない方法かも…
直接引数にDateTimeを送りたいとことですが、構造体の送信はVector系、Color系しか許してくれないので
DateTimeの年、月、日、時、分、秒、ミリ秒の7要素をint配列で渡してやることにします。
発火時刻(DateTime)を送信し、受信側は現在時刻と比較し
最終的に発火までの秒数(float)に変換し、DelayedSeconds関数でディレイさせます。
int[] timeInt = new int[7];
public void HogeHogeEvent()
{
// 現在時刻 + 10秒分 のDateTimeを取得
DateTime dt = DateTime.Now;
dt += new TimeSpan(0, 0, 10);
// int配列に代入
timeInt[0] = dt.Year;
timeInt[1] = dt.Month;
timeInt[2] = dt.Day;
timeInt[3] = dt.Hour;
timeInt[4] = dt.Minute;
timeInt[5] = dt.Second;
timeInt[6] = dt.Millisecond;
// SCNEでint配列を送る
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PreDelayEvent), timeInt);
}
public void PreDelayEvent(int[] ints)
{
// 送られた発火時刻を発火までの秒数(float)に変換
DateTime dt = new DateTime(ints[0], ints[1], ints[2], ints[3], ints[4], ints[5], ints[6]);
TimeSpan ts = dt - DateTime.Now;
float delaySecond = (float)ts.TotalSeconds;
// 指定時間後に発火
SendCustomEventDelayedSeconds(nameof(MainEvent), delaySecond);
}
public void MainEvent()
{
// 何かの処理
}
こうすればSCNE通信の時間のラグを吸収できます。
突発的に起こるイベントには使えないですが
RPGの敵モンスターのスポーンタイミングなどの、些細なラグを吸収するのに使っています。