#Quake Engine code review : Network (2/4)
QuakeWorldのネットワークアーキテクチャは当時、画期的な画期的な革新と見なされていました。 すべてのネットワークゲームの後継者は同じアプローチを採用しました。 ここに詳細があります。
この記事は4つのセクションに分かれています
- Architecture section
- Network section(本記事)
- Prediction section
- Rendition section
#Network stack
Quakeの基本的なコミュニケーションユニットがコマンドです。 コマンドはプレーヤーの位置、方向、健康、損傷などを更新するために使用されます。
TCP / IPには、リアルタイムシミュレーション(フロー制御、信頼性、パケットシーケンシング)には都合が良いが、Quake World Engine(元のQuakeにあった)には使えません。FPSでは、ASAP(as soon as possible)で受信されない情報は再送信する価値がありません。 したがって、UDP / IPが選択され、信頼性とパケットシーケンスを実装するために、ネットワーク抽象化レイヤ "NetChannel"が作成されました。
OSIの観点から、NetChannelはUDPの上に非常にうまく座っています。
要約すると、エンジンは主にコマンドを扱います。 送信または受信が必要な場合は、タスクをnetchan.cのNetchan_TransmitメソッドとNetchan_Processメソッドに委譲します(これらのメソッドはクライアントとサーバーで同じです)。
#NetChannel Header
NetChannelヘッダーの構造は次のとおりです。
Bit offset | Bits 0-15 | 16-31 |
---|---|---|
0 | Sequence | Sequence |
32 | ACK Sequence | ACK Sequence |
64 | QPort | Commands |
94 | ... | ... |
シーケンスは、送信者によって初期化され、パケットが送信されるたびに1ずつインクリメントされるintです。 シーケンスの目的は複数ですが、最も重要なのは、受信者に失われた/重複した/順序付けられていないUDPパケットを認識させる方法を提供することです。 intの最強ビットはシーケンスの一部ではありませんが、ペイロード(コマンド)に信頼できるデータが含まれていることを示すフラグが付いています(詳細は後で説明します)。
ACK Sequenceもintであり、受信した最後のシーケンス番号と等しい。 これにより、NetChannelのもう一方の端で、パケットが失われたかどうかを確認できます。
QPortはNATルータのバグを回避するためにここにあります(このページの末尾にある続きを読んでください)。 この値は、クライアントの起動時に設定される乱数です。
コマンド:ペイロードです
#Reliable messages
信頼できないコマンドは、最後の発信シーケンス番号でマークされて送信されたUDPパケットにグループ化されます。彼らが迷子になった場合、送信者にとっては問題になりません。
信頼性の高いコマンドは別々に扱われますが、重要なのは、送信者と受信者の間で肯定応答されない信頼できるUDPパケットが1つしかないことを理解することです。
すべてのゲームループは、新しい信頼できるコマンドが生成された場合、
message_buf配列に追加されます(メッセージ変数を介してパイロットされます)(1)。
信頼できるコマンドのセットは、メッセージからreliable_bufアレイ(2)に移動されます。これは、reliable_bufが空の場合にのみ発生します(空でない場合、これは他のコマンドセットが先に送信され、まだ確認応答されていないことを意味します)。
最終的なUDPデータグラムが作成されます。NetChannelヘッダーが追加されます(3)。十分なスペースがある場合は、reliable_bufのコンテンツと信頼性の低いコマンドが追加されます。
受信側では、UDPメッセージが解析され、入ってくるシーケンス番号は(パケットが信頼できるデータを含むことを示すビットフラグと共に)発信シーケンスACK(4)に転送される。
次のメッセージを受信したとき:
信頼できるビットフラグがtrueに設定されている場合、UDPパケットはそれを受信者に送信します。 NetChannelはreliable_buf(5)をクリーンアップして、新しいコマンドセットを送信する準備ができています。
信頼できるビットフラグがfalseに設定されている場合、UDPはそれを受信者に送信しません。 NetChannelはreliable_bufの内容を再度送信しようとします。 新しいコマンドがmessage_bufに積み重なります。この配列がオーバーフローした場合、クライアントは破棄されます。
##Flow-Control
私が読むことができる限り、サーバ側でのみフロー制御があります。 クライアントは状態更新をできるだけ速く送信します。
サーバ上でアクティブな最初のフロー制御ルールは、データグラムがクライアントから受信された場合にのみデータグラムを送信することです。 制御フローの第2の形態は、クライアントがコンソール内のレートコマンドを介して設定できるパラメータである「choke」である。 これにより、サーバーは更新メッセージをスキップし、クライアントに送信されるデータの量を減らします。
##Important commands
コマンドにはバイトに格納されたタイプコードと、それに続くコマンドのペイロードがあります。 おそらく最も重要なのは、ゲームの状態(frame_t)に関する情報を与えるコマンドです。
- svc_packetentitiesとsvc_deltapackententities:ロケットトレイル、爆発、パーティクルなどのエンティティを更新する
- svc_playerinfo:プレーヤーの位置、最後のコマンドとコマンドの継続時間をmsec単位で更新する
##More on the qport
Qportはバグを回避する為にNetChannelヘッダーに追加されたものです。 Qport導入以前は、Quakeサーバーはその組み合わせ(リモートIP、リモートUDPポート)によってクライアントを識別しました。 ほとんどの場合、これはうまくいきましたが、特定のNATルータは、ポート変換(リモートUDPポート)のスキーマを散発的に変更できます。 UDPポートが信頼できない場合、John Carmack氏は彼の計画の1つで、(リモートIP、NetChannelヘッダーのQport)でクライアントを識別することに決めたと説明しました。 これにより、混乱が修正され、サーバはターゲットUDP応答ポートを即座に調整することができました。
##Latency calculation
Quakeエンジンは、最後に送信された64個のコマンド(frame_t配列:フレーム内)を送信時刻と共に格納します。これらのコマンドは、転送に使用されたシーケンス番号(outgoing_sequence)を介して直接アクセスできます。
frame = &cl.frames[cls.netchan.outgoing_sequence & UPDATE_MASK];
frame->senttime = realtime;
//Send packet to server
サーバからの肯定応答に応じて、コマンドが送信された時刻がシーケンスACKを介して取得されます。 レイテンシは次のように計算されます。
//Receive response from server
frame = &cl.frames[cls.netchan.incoming_acknowledged & UPDATE_MASK];
frame->receivedtime = realtime;
latency = frame->receivedtime - frame->senttime;
##Some elegant things
配列インデックスサイクリング
エンジンのネットワーク部分には、最後に受信した64個のUDPデータグラムが格納されます。 配列を循環する素朴なアプローチは、モジュロ演算子を使うことでした。
arrayIndex =(oldArrayIndex + 1)%64;
代わりに、新しい値は、UPDATE_MASKで "AND"バイナリ演算で計算され、UPDATE_MASKは64-1に等しくなります。
(2のべき乗の剰余を処理する場合、ビット単位AND演算を使用することができる。)
arrayIndex =(oldArrayIndex + 1)&UPDATE_MASK;
実際のコードは実際には:
frame_t * newpacket; newpacket =&frames [cls.netchan.incoming_sequence&UPDATE_MASK];