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


マルチプレイヤーネットワーキングの同期方法について

リアルタイムのマルチプレイヤーゲームと言ったら、最初思いついたのはプレイヤー間のデータ同期問題だと思います。
この同期問題の内、一番面倒なのは即時性を高く要求されるデータの同期。
代表的なのはプレイヤーの位置だと思いますので、今回はそれについて紹介しようと思います。

プレイヤー位置を例とした即時性を要求されるデータはユーザに端末Aで操作したら端末BですぐAと同じものを見れることを期待されています。(少なくとも、そのように思わせる必要があります)

唯一の位置データを正としてすべてのクライアント間にデータの統一するため、ユーザが操作が発生したクライアントとサーバ、どちらのデータを正とするかによって、同期方法は大体Client Authoritative MovementとServer Authoritative Movement二種類と分けっています。

Client Authoritative Movement V.S. Server Authoritative Movement

では、Client Authoritative MovementとServer Authoritative Movement具体的にどんなことを指しているでしょう?どんな違いがあるんでしょう?

Client Authoritative Movementというのはクライアント主導型の同期方法で、あるキャラクターをコントロールするクライアントでそのキャラクター位置の計算結果を正として、他のクライアントにそのデータに合わせることを要求する同期方法です。
この同期方法の内、サーバが存在しても、あくまで他のクライアントへ位置データを転送するぐらい仲介のような役目しかはたしていません。

以下の図で表示できます。

この種類の同期方法のメリットは実装の簡単さ(サーバが転送以外何もやらなくていいから)とキャラクター反応の速さ(自分のキャラクターの位置情報は常に正しいので、サーバの返事を待たなくでもすぐ位置変更出来ます)です。
プロトタイプ作りやチートを気にしない場合にとっては結構いい方法です。

しかし、残念ながらこの方法には結構重大なデメリットが存在しています。それはユーザのチートを阻止することが困難であることです。
なので、もし悪意のユーザが存在していれば、改ざんしたクライアントで自分のキャラクターの位置を勝手に送信すると、瞬間移動などゲームバランスを転覆することも出来でしまいます。
クライアントから送信するデータが正しいとして認識され、サーバが一切計算ロジックを持っていないから、それを検証することは無理です。
PUN(Photon Unity Networking)でよく使われるのはこちらです。むしろ、Photon Serverを使わない限り、PUNはこちらしか使えません。

一方、Server Authoritative Movementはまさにチートを防ぐために存在するものです。
この方法だと、クライアントの計算結果ではなく、サーバの計算結果を正として扱い、各クライアントにサーバの結果に合わせるを要求します。
いわゆる、サーバ主導型の同期方法です。
クライアントは位置計算ロジックを持たせても、持たせなくても常にサーバの計算結果が正しいと認識しなければなりません。
それ以外、クライアントがサーバへ送信する主なデータは位置ではなく、ユーザ操作となります。

同じく図で表示すると、以下となります。

この方法のメリットは言わなくでも分かると思います。計算・検証がサーバから行うから、チートを阻止することは可能となり、ゲームの公平性を維持できます。
もちろん、代償はないではありません。図を見たら既に気づいたかも知れませんが、それは比較的に複雑な実装とユーザ操作を入力してからキャラクターが反応するまでの時間の長さです。

ちなみに、実行効率及びネットワークロード軽減のため、どちらの方法でもサーバが受信したらすぐ結果を送信ではなく、受信のタイミングと関わらず一定な周期で各クライアントへ送信します。

Server Authoritative Movementの基本実装

では早速基本的な実装を見ましょう。使っているネットワーキングフレームワークはPUNと似ている形で実装されたカスタマイズのフレームワークです。具体的な実装は本文のスコープに離れすぎますから、省略させていただきます。
ここで見せるのは位置同期コードのみとなります。

まずはクライアントのコード。

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

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

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

    return read;
}

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

    return write;
}

private void Update()
{
    Move(new Vector3(Input.GetAxis("Horizontal"), 0.0f, Input.GetAxis("Vertical")));
}

private void Move(Vector3 dir)
{
    m_Direction = dir.magnitude > 0.0f ? dir.normalized : Vector3.zero;
}

見た通りに、クライアントは単純にユーザの入力情報を収集し、サーバからの計算結果を使うだけです。
次はサーバのコードです。

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

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

    m_Direction = ((Vec3f)direction).ToUnityVector3();

    return read;
}

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

    return write;
}

private void Update()
{
    var new_pos = transform.position + m_Direction * m_Speed * Time.deltaTime;
    transform.position = new_pos;
}

基本実装の問題及び対策

この基本実装は一応Server Authoritative Movementを実現していますが、でも決して完璧ではありません。
ラグによってユーザが入力してキャラクターが反応するまで相当時間がかかり、ユーザに操作感が鈍いと感じさせます。
しかも、サーバの送信頻度がクライアント更新頻度より低いため(そうでないとネットワークロードが高い過ぎます)、クライアント側のキャラクターの移動がスムーズに見えません。

言葉だけでは明確的に感じされないかもしれませんので、以下の比較図を参照してください。
その前に、名前の説明をさせていただきます。
1. ”ローカル”は比較対象として用意したサーバと通信せずユーザの操作にすぐ反応するキャラクターです。白いキューブで表示します。
2. ”クライアント”は、まー、クライアントですね。ユーザの入力をサーバへ送信し、サーバの計算結果を表示するキャラクターです。青いキューブで表示します。
3. ”サーバ”はサーバ側のキャラクター表示です。もちろん、これは単純に比較目的に表示しているだけで、普通は表示しないです。赤いキューブで表示します。
4. 画面下部の矢印はユーザが押したボタンです。

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

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

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

ローカルよりサーバの反応はちょっと遅くなっていて、クライアントの方はさらに遅く見えるんでしょう?
もしこの反応の遅さは他のユーザのキャラクターであれば自分の操作で動いているわけではないので、気にしなくてもいいですが
しかし、自分が操作しているキャラクターであれば、結構”操作が重い”と感じられます。
それに、サーバはローカル並みのスムーズさですが、クライアントの方はグダグダに見えません?
以上の比較はサーバの同期頻度が20fps(一秒間20回同期データをクライアントに送信する)になっていて、これは結構いい状況でないと出来ない頻度です。
こんな理想的な状況でもグダグダならば、もっと厳しい状況だとどうなるんでしょう?では、以下の5fpsバージョンを見てください。

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

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

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

結論として、やはり現象はもっと酷くなりました(特にクライアントの方)。

では、Server Authoritative Movementは使えないではないんでしょうか?そうでもありません。
以上の各デメリットに実は対策が存在します。ちゃんと対策を使ったら、Client Authoritative Movementに負けない効果が出来ます。
(ちなみに、Client Authoritative Movementの方も単純に実装すると自分のクライアント以外のクライアントのキャラクターも以上グダグダに見えます)

本文は既に長くなってしまいましたので、残りはPart2で説明します。