オンライン対戦ゲームの動作原理を理解するためには、「ディレイ方式」と「ロールバック方式」の二つの主要な概念を理解することが重要です。今回は、ディレイ方式の同期メカニズムに焦点を当てて解説します。
ディレイ方式の概要
ディレイ方式の同期メカニズムは、ユーザーの入力からその結果が画面に表示されるまでに一定の遅延(ディレイ)が発生する仕組みです。例えば、パンチボタンを押すとキャラクターがパンチを出すまでに、一定のディレイが生じます。この遅延は、入力バッファの大きさに依存します。大きい入力バッファは長いディレイを生じさせます。
ディレイ方式では、画面の更新(フレーム)は対戦相手の入力が届いた前提で行われます。もし対戦相手の入力が届かない場合、フレームの進行を停止し(フレームストップ)、入力が届くのを待ちます。対戦相手とのレイテンシーが高いと、入力バッファが追いつかず、フレームストップが頻繁に発生する可能性があります。
ディレイ方式のメカニズム
ディレイ方式のメカニズムは、入力バッファを利用して入力と表示の間にディレイを設けることです。この入力バッファは「対戦相手の入力は遅れてくる」という前提のもと、遅れが予想されるフレーム数分のバッファを持つことで設計されます。予測される遅延フレーム数は予測値なので、実際の遅延がそれ以上になった場合、相手からの入力がないため、バッファが補填されるまで待機しなければなりません。
ディレイ方式の種類
ディレイ方式には以下の三種類が存在します。
- 固定ディレイ方式
- 可変ディレイ方式
- 動的ディレイ方式
これらの方式は、ディレイの大きさをどのように決定するかが異なります。
固定ディレイ方式
ディレイ方式で一番簡単でシンプルな実装です。予め固定されたサイズ分のディレイ用の入力バッファを用意します。ゲーム中はこの固定されたサイズの入力バッファが利用されます。
固定ディレイ方式の適用範囲
-
ネットワーク品質がよい環境
ゲーム中にネットワークの品質に揺らぎがない、安定したネットワーク環境(日本国内のみ)では問題は発生しにくいと思います。 -
開発コストをかけたくない
オンライン対戦ゲームのデータ交換の仕組みにそれほどコストをかけることができない場合は、実装もシンプルなため、選択するもの良いかと思います。 -
ディレイは固定にしたい
可変ディレイ方式や動的ディレイ方式ではディレイが変わるため、対戦によってはディレイがどれくらいあるのかを考慮してゲームをプレイする必要がありますので、それならいっその事、常に固定でディレイがかかったほうがよいということであれば、固定ディレイ方式を選択するのもありです。 -
ネットワークプログラム初心者
ネットワークプログラムは、落とし穴が多く、ノウハウがないと問題になりやすいですが、この固定ディレイ方式であれば、実装もシンプルですので導入しやすいと思います。
可変ディレイ方式
可変ディレイ方式では、ゲーム開始前に対戦相手とのレイテンシーを計測して、そのレイテンシーを元に入力バッファのサイズを決めます。一番簡単な方法は、対戦相手とのラウンドトリップタイムを計測して、それを / 2 した値を元にバッファサイズを決めます。例えば、ラウンドトリップタイムが 100ms の場合、片道の時間が 50ms と仮定できますので、60fps の場合であれば、相手からの遅延フレーム数は 4 フレームとなります。これを入力バッファのサイズとして使用します。可変ディレイ方式でも、固定ディレイ方式と同様に、ゲーム中はこの固定されたサイズの入力バッファが利用されます。
可変ディレイ方式の適用範囲
-
ネットワーク品質がよい環境
ゲーム中にネットワークの品質に揺らぎがない、安定したネットワーク環境(日本国内のみ)では問題は発生しにくいと思います。 -
フレームストップをできるだけなくしたい
固定ディレイ方式と違ってディレイサイズをゲーム開始時に決定するため、固定ではない分、ある程度、対戦相手との通信状況を考慮したサイズになります。
動的ディレイ方式
動的ディレイ方式では、ゲーム開始時には可変ディレイ方式と同様に対戦相手とのレイテンシーを元に入力バッファサイズを決定します。可変ディレイ方式の場合であれば、ゲーム開始前に決めたバッファサイズには変更はありませんが、動的ディレイ方式では、ゲーム中に入力バッファサイズに変更を加えます。入力バッファサイズの変更は、入力バッファに補填されるまでの待機フレーム数分を増やします。全てのディレイ方式で共通の考え方ではありますが、「入力バッファサイズはレイテンシーの最大値を基に算出」されるため、動的ディレイ方式の入力バッファサイズも同じだけ増やせば良いということになります。
動的ディレイ方式の適用範囲
-
ネットワークの品質の揺らぎが激しい環境
ゲーム中にネットワークの品質の揺らぎが多い環境で適用すると効果があります。特に欧米では使用している ISP によって、かなりの揺らぎが発生します。
ディレイ方式の同期の実装
「ディレイ方式」の同期メカニズムを実現するために必要な仕様を列挙します。
- ゲームは毎フレームでの入力値を必ずバッファ(一時保存)します。
- 入力値はすぐには使用せず、一定のフレーム数(ディレイ)分をバッファに保存します。
- ゲーム画面の更新は、バッファの最も古い入力値を元にして行います。
- 処理したい特定のフレームでは、対戦相手の入力値がすでに自分のシステムに届いていることが必要です。
- もし処理したいフレームで相手の入力値がまだ届いていない場合、その入力値が到着するまでフレームの進行を一時停止します。
- フレームの進行が一時停止されている間、新たな入力値はバッファに保存せず、無視されます。
以下は固定ディレイ方式でのプログラムのサンプルです。
// 遅延させるフレーム数
// ゲーム中ではこのフレーム数分の遅延が発生する
#define DELAY_FRAME_COUNT 6
// バッファは循環バッファとして機能させるため
// バッファリングするフレームの本数を指定する
#define DELAY_FRAME_BUFFER_COUNT 10
// 遅延させるフレーム数とその本数によってバッファサイズが決定される
#define DELAY_FRAME_BUFFER_SIZE (DELAY_FRAME_COUNT * DELAY_FRAME_BUFFER_COUNT)
// 対戦するプレイヤー二人分の入力バッファ
Input FrameInput[2][DELAY_FRAME_BUFFER_SIZE];
// 画面更新を行いたいフレーム番号
int DisplayFrame = 0;
// 自分の入力値を設定するフレーム番号
int InputFrameNo[2];
// 相手からのデータ送信待ちの場合は true にする
bool IsStoppedFrame = false;
void SetFrameInput(int playerNo, int frame, Input input)
{
FrameInput[playerNo][frame % DELAY_FRAME_BUFFER_SIZE] = input;
}
// 自分自身の入力値を保存するため
// このメソッドは必ず毎フレーム呼び出す
void UpdateFrameInput(Input input)
{
if (!!IsStoppedFrame) {
return;
}
SetFrameInput(0, InputFrameNo[0], input);
InputFrameNo[0]++;
}
Input * GetFrameInput(int playerNo, int frame)
{
// 入力値を設定するフレームよりも新しいフレーム番号が指定されているということは
// 自分もしくは相手の入力値が設定されていないのでデータは存在しないため nullptr を返す。
if (InputFrameNo[playerNo] <= frame) {
return nullptr;
}
return &FrameInput[playerNo][frame % DELAY_FRAME_BUFFER_SIZE];
}
ディレイ方式の問題点
ディレイ方式は非常にシンプルなメカニズムで、プログラミングの初心者でも実装することが可能なメリットがあります。しかし、そのシンプルさゆえに、一部の問題点が存在します。
-
レイテンシーによるゲーム体験の悪化
ディレイ方式では、ユーザーが入力を行った後に、その入力がゲーム画面に反映されるまでに一定の遅延が生じます。この遅延時間は、ネットワークのレイテンシーによって変化します。したがって、ネットワークの状態が悪い場合や、遠隔地のプレイヤーと対戦している場合、この遅延時間が長くなり、ゲーム体験が悪化する可能性があります。 -
フレームストップの頻発
対戦相手からの入力が自分に届くまでの時間が長い場合、ディレイ方式ではゲームのフレーム進行が一時停止されることがあります。このフレームストップが頻発すると、ゲームの流れが滑らかでなくなり、また、プレイヤーの操作体験が悪化します。