マルチプレイヤーネットワーキングのプレイヤー位置同期について Part2~Interpolation


リキャップ

Part1はServer Authoritative Movementについてちょっと紹介しまして、いくつの問題も表しました。
今回は前回出現した問題の対策を紹介しようと思います。

Interpolation

まずはネットでよく紹介されるInterpolationから始めましょう。

Interpolationとは

Interpolation(補間)というのは不連続的なフレームの間に前後のフレームを元に更に細かいフレームを生成し、インサートすることです。
これを実現するinterpolation方法はいくつありますが、一番簡単+位置同期に使われるのはlinear interpolationです。
Linear interpolationの公式は結構直観です。
P = (1 - t) * P1 + t * P2
上記公式の内、Pは補間結果、P1は前フレーム、P2は後フレーム、tはP1からP2まで正規化された時間です(t = 0 ~ 1)。
結果的には不連続的なフレームがより連続的に見えることになります。つまり、スムーズになります。

しかし、以上公式見れば分かると思いますが、この方法を使うには前後フレームの情報を持っている前提があります。
つまり、表示するものは常に実際の状況と少なくとも1フレームずれることがあります。ユーザが見ている”今”は実は過去だと言うことです!
でも1フレームの時間実は人の目が気づかないぐらい短いです(参照物がない限り)!レンダリングの世界にはユーザに騙せばありなので、ここの1フレームずれはほぼ影響なしとも言えます。

位置同期のおいてもっと具体的な話だと、以下のような感じになります。
t = P2の前一フレームを受信した時点から今レンダリングする時点まで経過した時間 / P1を受信した時点からP2を受信した時点まで経過した時間
P1 = P2を受信した時点でキャラクターの位置
P2 = 今回受信したキャラクターの位置
P = ”今”(実は過去)の位置

ちなみに、この方法は一つの仮定がありまして、それは前後にフレーム間の時間は常に一緒・大した差が存在しないということです。
つまり、今回のinterpolationが完了・間もなく完了のタイミングでちょうど次のフレームが来るということです。

図で表示すると、こんな感じですね。

実装

では、Part1のコードを改修してinterpolationを実装してみましょう。
変更必要があるのはクライアント側のみなので、クライアントのコードだけを書くと思います。

// Client
private float m_DelayBetween2SyncFrames = 0.0f;
private float m_ElapsedTimeSinceSync = 0.0f;
private float m_LastSyncFrameReceivingTimestamp = 0.0f;
private Vector3 m_From = Vector3.zero;
private Vector3 m_To = Vector3.zero;

public override int OnReceive(MemoryStream stream)
{
    var read = 0;

    object position;
    read += m_PackerSet.Unpack(stream, out position);

    m_From = transform.position;
    m_To = ((Vec3f)position).ToUnityVector3();

    m_DelayBetween2SyncFrames = Time.time - m_LastSyncFrameReceivingTimestamp;
    m_LastSyncFrameReceivingTimestamp = Time.time;
    m_ElapsedTimeSinceSync = 0.0f;

    return read;
}

public override int OnSend(MemoryStream stream)
{
    var write = 0;
    write = m_PackerSet.Pack(m_Direction.ToVec3f(), stream);

    return write;
}

protected override void OnUpdate()
{
    m_ElapsedTimeSinceSync += Time.deltaTime;
    transform.position = Vector3.Lerp(m_From, m_To, m_ElapsedTimeSinceSync / m_DelayBetween2SyncFrames);
}

効果

早速ですが、効果を見ましょう!今回もPart1と同じく20fpsと5fpsバージョン各三種類の状況で見ようと思います。
説明:
1. ”ローカル”は比較対象として用意したサーバと通信せずユーザの操作にすぐ反応するキャラクターです。緑のキューブで表示します。
2. ”クライアント”は、まー、クライアントですね。ユーザの入力をサーバへ送信し、サーバの計算結果を表示するキャラクターです。青いキューブで表示します。
3. ”サーバ”はサーバ側のキャラクター表示です。もちろん、これは単純に比較目的に表示しているだけで、普通は表示しないです。赤いキューブで表示します。
4. 画面下部の矢印はユーザが押したボタンです。

1.ローカルVSクライアント(同期頻度:20 fps)

2.ローカルVSサーバ(同期頻度:20 fps)

3.ローカルVSサーバVSクライアント(同期頻度:20 fps)

4.ローカルVSクライアント(同期頻度:5 fps)

5.ローカルVSサーバ(同期頻度:5 fps)

6.ローカルVSサーバVSクライアント(同期頻度:5 fps)

Part1と比べたら、スムーズさは結構向上したんでしょう?

問題

グダグダ感は既にあまり感じないですが、でも完全に消えたわけでもありません。
青いキューブの動きをよく見ると(特にローカルVSサーバVSクライアントの場合は更に気付きやすいです)、ちょっとだけ前へ移動したらすぐ後ろへジャンプする感じがあります。(実はそれがジャンプではなく、スピードが一瞬変わったことです)

これの原因実はネットワーク環境の現実と理想の差です。

前にも話したように、interpolationが完璧役目を果たす仮定は前後二フレーム間経過した時間の一致性です。
しかし、そうれは理想的な状況、現実だと前後二フレーム毎の経過時間は多少差があります。
つまり、今回のinterpolationが完了した時点で次のフレームがまた来ていないか、またはその前に来てしまうかどちらです。
Interpolationされるキャラクターの移動スピードはもう定数ではなく、interpolationのインターバル(前後フレームの経過時間)と前後フレームの距離です。
なので、サーバ側が一定なスピードで移動、一定な周期でクライアントへ送信だとしても、保証られるのは距離が一致になっていることだけで
ネットワークスピードの変動によってinterpolationのインターバルが変わってしまい、最終的にクライアントで見える効果は微弱なグダグダです。

こんな微弱なグダグダ感はアニメーションでうまく隠せることはできますが、それ以外もっと明らかな問題が存在します。
それはこの方法が全く操作のラグ感を解決していなくて、むしろ更に悪化させたことです。(一フレームずらせたから)。
これはユーザにとってとても望ましくないことです。

上記二つの問題にも実は完璧に解決出来る方法が存在します。一言というと、クライアントをスマート化してもっと役目を果らせることです。
具体的な方法はまた次のPart3に紹介します。