タイムラインを再生する仕組みについて
#記事
#おすすめ記事
#VRC演出ワールド制作固有のTips
当然ですが、タイムラインを再生する仕組み(Udon)が必要です。
作ってみると意外と難しいです。
たいてい以下のどちらかが必要になります。
常設型の場合
任意タイミングからの同期再生
イベント系の場合
所定の開始時刻に再生
私は使ったことが無いですがBoothで配布されているものがあります。
指定した時刻にTimelineを再生するUdon - ziston Shop - BOOTH
あわせてプレビュー機能があると便利です。
制作中はphi16さんのTimelineWandを使わせてもらっているチームが多いと思います。私も使っています。
reeezndさんのrzVRMVTimelinePlayerも使われているのを見たことがあります。
これまで参加した中ではシークバーでの操作がちゃんと同期して動くものが導入されているチームもありました。
私が作ったわけではないので公開できないです。
シークバーだと処理を大きく飛ばしてしまうことになるので、TimelineWandのような早送り・巻き戻しの方がTimelineの内容を問わず正しく再生できる…とかはあるかもしれません。
本番用の仕組みはなんだかんだ微妙に再生条件が違ったりして、都度作ることになりがちです(本当はまとめたい)
再生ONならtrueになる変数だけを同期する実装にした場合、Ownerとそれ以外で同期にかかる1-2秒程度のラグが発生してしまいます。
Ownerが開始時のServerTimeを同期変数に送り、それとの差分でタイムラインの再生開始位置を決めることで全員の再生タイミングを揃えることができます。
Owner以外は同期にかかる1-2秒分タイムラインの先頭が切れてしまうので、それについても対策したほうが良いです。
例えばUndestinedでは薔薇をインタラクトした人だけ薔薇が発光する演出があり、その2秒後から薔薇が消える演出が始まるようになっています。
Owner以外が2秒より早く同期変数を受けた場合は2秒まで待機してから再生開始するようにしています。
Ownerにインタラクトを受け付けたことを知らせるため発光する演出はあった方がいいですが、同期後即座に再生開始してしまうとOwner以外は急に(イージングなしで)発光したと感じてしまうからです。
コード例(抜粋)
code:TimelineManager.cs
public bool IsPlayingLocal = false;
UdonSynced public bool IsPlayingGlobal = false;
UdonSynced public double PlayingStartTime = -1.0; // GetServerTimeInSeconds()
public PlayableDirector _PlayableDirector;
public double ListenerOffset = 0;
private VRCPlayerApi localPlayer;
void Update()
{
if (_PlayableDirector == null) return;
if (localPlayer == null) return;
if (IsPlayingGlobal != IsPlayingLocal)
{
if (IsPlayingGlobal)
{
double now = Networking.GetServerTimeInSeconds();
double delta = Networking.CalculateServerDeltaTime(now, PlayingStartTime);
double duration = _PlayableDirector.duration;
double t = delta + _PlayableDirector.initialTime;
// Owner以外はdeltaがListenerOffset以下の場合、
// IsPlayingLocalを変更せず以降のフレームに処理を先送りする
// 同期する以上、Listenerは必ず遅れて再生が開始されるが、
// 逆にその最低値を指定することでOwnerとは別の意図したタイミングから再生できる
if (!localPlayer.IsOwner(this.gameObject) && delta < ListenerOffset) return;
// 何らかの誤差でdeltaがマイナスの場合、IsPlayingLocalを変更せず以降のフレームに処理を先送りする
if (t < _PlayableDirector.initialTime) return;
// 再生終了後にJoinした場合、Timelineの末尾を評価して終了
if (t >= duration)
{
IsPlayingLocal = IsPlayingGlobal;
_PlayableDirector.time = duration;
_PlayableDirector.Evaluate();
return;
}
_PlayableDirector.time = t;
_PlayableDirector.Play();
IsPlayingLocal = true;
}
else
{
IsPlayingLocal = false;
double t = _PlayableDirector.initialTime;
if (t < 0.0) t = 0.0;
double duration = _PlayableDirector.duration;
if (t > duration) t = duration;
_PlayableDirector.time = t;
_PlayableDirector.Evaluate();
_PlayableDirector.Stop();
}
}
}
// Interactとかで呼ぶ
public void PlayStartFromTop()
{
Networking.SetOwner(localPlayer, this.gameObject);
if (!IsPlayingGlobal)
{
IsPlayingGlobal = true;
PlayingStartTime = Networking.GetServerTimeInSeconds();
RequestSerialization();
}
}