こんにちは。プログラマーのAです。現在 『Warlander』 チームでギミックやネットワークの処理を担当しています。今回は私が担当したところから、サーバー経由でのネットワーク同期について工夫した点などをご紹介します。
サーバーの値を書き換えることで状態を共有する
ネットワークゲームはロジックをサーバーで処理するか、ローカルで処理するか、大きく2つのタイプに別れます。『Warlander』はゲームロジックを主にローカルで処理しており、操作しているプレイヤーに関わる部分を処理し、他のプレイヤーに関わる部分を送信してもらってロジックを進めていきます。ただし特定のプレイヤーに関わらない部分、タワーや兵器などについてはサーバー経由で整合性を取るようにしています。
少し具体的に説明します。クライアントからは、値を書き換えるパケットをサーバーに送ります。書き換わった値は全クライアントに伝わります。
下図は乗り物の空いてる席に Player.0
が乗る際のパケットの流れです。クライアントAからのパケットをサーバーが受け取り、Empty
を Player.0
に書き換え、全クライアントに送信しています。
ただしこのように単純に値を書き換えるパケットを送るやり方だと問題があります。
以下のようにクライアントBが Player.1
に書き換えるパケットを送信してしまうと、サーバーの値が上書きされてしまい、Player.0
が先に乗ったはずなのに Player.1
が乗ったことになってしまいます。クライアントBが送信するタイミングではまだサーバーからのパケットを受信しておらず、値が Empty
に見えているため、他のプレイヤーが先に乗ったという判断ができません。
都合の悪い要求を失敗させる
このような状況を回避するため Compare And Swap という考え方を採用しました。これは以下のような処理をアトミックに実行します。
if (X == Old) {
X = New;
}
本来はメモリを書き換える CPU 命令ですが、排他制御を使わずに整合性を取れるという点でネットワークにも流用できると考えました。
具体的には以下の図になります。先程は書き換え後の値だけを送っていたところを「書き換え前の値 → 書き換え後の値」というパケットに置き換えています。図の「Empty → Player.0」というのは「Empty だったら Player.0 に書き換える」という意味です。このクライアントAからのパケットが成功すると値が Empty
ではなくなるので、クライアントBからのパケット「Empty → Player.1」
の実行は失敗します。
別の例を見てみます。以下は扉を処理している図になりますが、[開閉, 生死]
という2つの状態をペアとして持っています。クライアントAからのパケットで扉は Dead 状態になっていますが、クライアントBは古い状態を見ているため、扉を開けるというパケットを送ってしまっています。このパケットを処理してしまうと壊れたはずの扉が元に戻ってしまいますが、[Close, Alive]
という条件を満たさないため、サーバー側で失敗させることができます。
冗長性を持たせることで破綻を回避する
さらに別の例です。以下は、壊したり作ったりできる建築物の例ですが、値がシンプル過ぎて整合性が取れなくなっています。クライアントAもBも「壊れた」というパケットを送っていますが、クライアントAは、クライアントBが作り直したことが見えないままパケットを送信しており、またサーバーはそのパケットが古いことが判別できず受け付けてしまっています。
もっとも簡単な対応方法は、状態遷移を一方向にすることです。つまり Alive → Dead
という遷移のみを許可し、Dead → Alive
という遷移は禁止します。この例に限らず、状態遷移を制限するのは多くのケースで有効です。
もうひとつ考えられるのは、カウンタを追加して整合性を取るという方法です。この例でうまく処理できない理由は「何回目の破壊かを判別できない」ということなので、その回数をペアで保持し、「Alive → Dead に変化する際にインクリメントする」というルールにします。こうすることによってクライアントAからのパケットが古いという判定になり、失敗させることができます。
最後に
以上、いくつか例を上げながら Compare And Swap で同期を取る方法について解説しました。
この方法のメリットは大きく2つあります。
- 古い状態を根拠として判断したパケットを失敗させることができます。
- サーバー側の処理を単一の実装で処理できます。サーバー側の変更なしにクライアント側の処理を追加、変更することができます。
あらゆる処理にこのパケットを使えるという訳ではありませんが、今回実装した通信処理の中ではかなり汎用的なものだったので紹介しました。