Source Multiplayer Networking
Source Engineでのマルチプレイヤーネットワーキング
Contents
概要
Source Engine のマルチプレイヤーゲームではクライアント/サーバネットワークアーキテクチャを使用しています。通常サーバは専用のホストとなってゲームを実行し、ワールドシミュレーション、ゲームルール、プレイヤーインプット処理に関して信頼できるもの(authoritative、訳注:ゲームロジック処理を集中して行うのはサーバ)になります。クライアントはゲームサーバに接続したプレイヤーのコンピュータです。クライアントとサーバはお互いに小さなデータパケットを高頻度(通常1秒に20から30パケット)で送りあいます。クライアントはサーバから現在のワールドの状態を受け取り、それらの更新情報をもとにビデオと音声のアウトプットを作り出します。クライアントはまた、入力デバイス(キーボード、マウス、マイクなど)からのデータをサンプルし、これらの情報をさらなる処理のためにサーバに送り返します。クライアントはゲームサーバとのみ通信を行い、クライアント同士の通信(peer-to-peer通信のような)を行いません。シングルプレイヤーゲームとは違い、マルチプレイヤーゲームにおいてはこのパケットに基づいた通信に起因する新しく多様な問題に取り組む必要があります。
ネットワーク帯域には限りがあるので、ワールドでの一つ一つの変更全てに対して、サーバが全てのクライアント向けに更新パケットを送り出すということは不可能です。その代わりに、 サーバは現在のワールドの状態を一定の間隔でスナップショットという形にし、これらのスナップショットをクライアントにブロードキャストします。ネットワークパケットがクライアント、サーバ間を移動するには時間がかかります(これがping時間です)。これにより、クライアント時間はサーバ時間より常に少し遅れているということになります。さらに、クライアントからのインプットパケットが届くのにも遅延があり、サーバは常に遅延したユーザコマンドを処理していることになります。追加して、他のバックグラウンドのトラフィックやクライアントのフレームレートのに影響されて、それぞれのクライアントのネットワーク遅れは様々で、しかも時間とともに変化します。こうしたのサーバとクライアントでの時間の違いは論理的な問題を引き起こし、ネットワークレイテンシ(遅延)が増加すると共に悪化します。早いペースのアクションゲームにおいては、たとえ数ミリ秒の遅れであってもラグのあるゲームプレイ感覚を引き起こし、他のプレイヤーを撃ったり、動くオブジェクトとインタラクションを行うことが難しくなります。帯域の制限とネットワークレイテンシに加え、情報がネットワークパケットロスで失われることもあります。
ネットワーク通信によって発生したこれらの問題全てに対処するために、Source Engineは複数のテクニックを使って、問題を解決、もしくは少なくともプレイヤーが問題に気づきにくくするようにしています。テクニックには、データ圧縮、補間(interpolation)、予測(prediction)、そしてラグ補償(compensation)があります。これらのテクニックが密接に組み合わされているので、1つのシステムの変更は他のシステムにも影響する可能性があります。このドキュメントではこれらのシステムの基本機能と、その連携について説明します。
ネットワークの基本
サーバはtickという非連続的な時間ステップを使ってゲームのシミュレーション計算を行います。初期設定では1秒に66のtickがシミュレーションされますが、MODでは独自のtickrate(1秒間に実行するtick数)を指定することができます。例えば[[Ja/|]]-tickrate
コマンドラインパラメータで上書きすることができますが、こうしたtickrateの変更はmodがデザインされたように動作しない可能性があるため推奨されません。
クライアントが使用可能な帯域は通常ごく限られたものです。最悪の場合においては、プレイヤーがモデムを使用していて、5-7KB/秒以上受け取れないということもありえます。もしサーバがそうしたプレイヤーにより大きなデータ量で更新情報を送ろうとした場合、パケットロスは避けることができないでしょう。そのため、クライアントはサーバに対して自身の受信帯域能力をコンソール変数rate
(byte/秒)を設定して伝える必要があります。これがクライアントでの最も重要なネットワーク変数で、最適なゲームプレイ体験を得るためにはこれを正しく設定する必要があります。クライアントはcl_updaterate
(初期設定20)を変更することでスナップショット頻度の要求をサーバに出すことができますが、サーバはシミュレーションのtick以上やクライアントの要求したrate
以上のスナップショットを送ることはしません。サーバ管理者はクライアントが要求するデータ量をsv_minrate
とsv_maxrate
(どちらもバイト/秒)で制限することができます。スナップショット頻度はsv_minupdaterate
とsv_maxupdaterate
(どちらもスナップショット数/秒)で制限することができます。
クライアントはサーバのtick頻度と同じtick頻度で入力デバイスのサンプリングを行い、ユーザコマンドを作成します。ユーザコマンドは基本的にはそのときのキーボードとマウス状態のスナップショットです。しかしここではそれぞれのユーザコマンドについて新しいパケットを送信するのではなく、クライアントは決まった頻度(通常1秒に30回)でコマンドパケットを送信します。つまり1つのパケットの中で2つ以上のユーザコマンドが送信されることもあります。クライアントにおいてcl_cmdrate
を変更することでコマンド送信頻度を増加させることができます。これにより反応性が上がりますが、より多くの送信帯域が必要になります。
ゲームデータはネットワーク負荷を減らすために差分圧縮(delta compression)を使って圧縮されます。つまりサーバは毎回完全なワールドスナップショットを送信するのではなく、直前の確認されたスナップショットから変化した部分(差分スナップショット)だけを送信します。クライアントとサーバの間に送信されるそれぞれのパケットには、データの流れを把握するために確認番号がつけられています。通常完全な(差分ではない)スナップショットはゲーム開始時や、クライアントが数秒の間重大なパケットロスに苦しんだ時だけに送信されます。クライアントはcl_fullupdate
コマンドで手動で完全スナップショットのリクエストを出すことができます。
反応性、言い換えるとユーザが入力をしてからゲームワールドでのビジュアルフィードバックがあるまでの時間、は様々な要因によって決まります。サーバ/クライアントのCPU負荷、シミュレーションtickrate、データ転送量、スナップショット更新設定などもそれらの要因に含まれますが、大部分はパケットがネットワークを移動するのにかかる時間によるものです。クライアントがユーザコマンドを送信し、サーバが応答し、クライアントがそのサーバの応答を受け取るまでの時間はレイテンシやping(もしくはラウンドトリップタイム)と呼ばれます。マルチプレイヤーオンラインゲームを遊ぶ場合、低レイテンシはかなり有利になります。予測(prediction)、そしてラグ補償(compensation)といったテクニックはこの有利さを最小限にして、遅い接続のプレイヤーも公正なゲームができるようにしようとするものです。必要な帯域とCPUパワーがあればネットワーク設定を調整することはよりよいゲーム体験を得る助けになります。ただ不適切な変更は実際の利益よりマイナスの副作用を起こすので、初期設定を使うことを推奨します。
エンティティ補間
初期設定では、クライアントはおよそ一秒に20のスナップショットを受け取ります。もしゲームワールドのオブジェクト(エンティティ)がサーバから受け取った位置でしか描画されないのなら、動くオブジェクトやアニメーションは不規則でガクガクになるでしょう。パケットが損失した倍イでも気がつくような不都合が起こるでしょう。この問題を解決するトリックはレンダリングにおいて時間を戻して、位置やアニメーションが最新の2つのスナップショットの間で連続的に補間するというものです。このテクニックはクライアントサイドエンティティ補間と呼ばれ、cl_interpolate 1
によって初期設定でオンになっています。1秒間に20のスナップショットが送信される場合、新しい更新情報は大体毎50ミリ秒ごとに届きます。もしクライアントのレンダリング時間が50ミリ秒だけ過去にずれていれば、エンティティは常に最新のスナップショットとその直前のスナップショットで補間し続けることができます。Source Engineはこのエンティティ補間を100ミリ秒遅れ(cl_interp 0.1
)で行っています。これにより、1つのスナップショットが損失しても、補間できる2つの適当なスナップショットが存在することになります。以下の図がワールドスナップショットの到着時間を示しています:
クライアントが受け取った最新のスナップショットはtick 344、いいかえると10.30秒のものでした。クライアントにおける時間はこのスナップショットとクライアントのフレームレートに基づいて増加する形で続きます。新しい画面フレームがレンダリングされるときの、レンダリング時刻はクライアント時刻の10.32から画面補間遅れの0.1秒を引いたものになります。この例では引いた時間は10.22になり、すべてのエンティティとそのアニメーションはスナップショット340と342の間で補間された正しい断片を使用したものになっています。
100ミリ秒の補間遅れがあるので、パケットロスでスナップショット342が損失したとしても補間を行うことができます。そのときは補間はスナップショット340と344を使います。もし2以上のスナップショットが連続して失われた場合、補間は履歴バッファのスナップショットがなくなってしまうため完璧には動作できなくなります。この場合レンダラーは外挿法を使い(cl_extrapolate 1
)今までの履歴に基づいて単純な線形外挿を行います。外挿はパケットロスから0.25秒だけ行われます(cl_extrapolate_amount
)。これより長くなると予測エラーが大きくなりすぎるからです。
エンティティ補間は常に100ミリ秒の画面「ラグ」を引き起こします。これはlistenサーバ(サーバとクライアントが同じマシン)で遊んでいるとしても同じです。そのためsv_showhitboxes
をオンにしてサーバ時間でのプレイヤーヒットボックスを表示させると、描画されているプレイヤーモデルの100ミリ秒先に表示されることになります。これは他のプレイヤーに対して狙いをつけるときに先を狙わないといけないということではありません。というのもサーバサイドラグ補償がクライアントエンティティ補間について知っていて、この違いを直すからです。listenサーバにおいて補間をオフにすれば(cl_interpolate 0
)、ヒットボックス表示と描画されるプレイヤーモデルが一致しますが、アニメーションと動くオブジェクトの表示はとてもガクガクになるでしょう。
入力予測
100ミリ秒のネットワークレイテンシがあるプレイヤーが前進を始めたと仮定しましょう。+FORWARD
キーが押されたという情報はユーザコマンドに保存され、サーバに送られます。サーバではこのユーザコマンドが移動(movement)コードによって処理され、そのプレイヤーのキャラクタがゲームワールドの中で前進します。このワールドの状態変更は全てのクライアントに次のスナップショット更新時に送られます。そのためこのプレイヤーは自身の移動の変化を歩き始めてから100ミリ秒後に見ることになるでしょう。この遅れは移動、武器の発射という全てのプレイヤーアクションに当てはまり、そして高レイテンシではさらに悪化することになるでしょう。
プレイヤー入力とそれに対応したビジュアルフィードバックの間の遅れは奇妙で不自然な感じを生み出し、正確に動いたり狙ったりするのを難しくします。クライアントサイドの入力予測(cl_predict 1
)はこの遅れを取り除き、プレイヤーのアクションがより瞬時に感じるようにさせる方法です。サーバが自分自身の位置を更新するのを待つ代わりに、ローカルのクライアントが自分自身のユーザコマンドの結果を予測します。そのために、サーバがユーザコマンドの処理に使うのとまったく同じコードとルールをクライアントも実行します。予測が完了したら、ローカルのプレイヤーはその新しい場所に即時に移動します。この時サーバはまだ以前の位置にプレイヤーがいると見なしています。
100ミリ秒後に、クライアントは自分が予測したユーザコマンドによる変化を含んだサーバからのスナップショットを受け取ります。そこでクライアントはサーバでの位置と、予測した位置を比較します。もしそれらが違っているのなら、予測エラーが起こったことになります。このことはクライアントはユーザコマンドを処理したときに他のエンティティや環境について正しい情報を持っていなかったということを示します。そしてクライアントは自身の位置を訂正します。というのもサーバの情報がクライアントサイドでの予測より権威ある最終決定だからです。もしcl_showerror 1
がオンになっていると、クライアントは予測エラーが起こったことを見ることができます。予測エラーの訂正はとても目立つことがあり、クライアントでの画面が突然飛ぶことがあります。このエラーを短い時間で段階的に直す(cl_smoothtime
)ようにすることで、エラー訂正はスムーズになります。予測エラーのスムーズ化はcl_smooth 0
でオフにできます。
オブジェクトの振る舞いの予測は、サーバが実行するのと同じオブジェクトのルールと状態を知っている時のみ上手くいきます。これは通常事実とは違っています。サーバはクライアントに比べてより多くのオブジェクトの内部情報を知っているからです。クライアントは世界の一部だけを見ていて、オブジェクトをレンダリングするのに十分な情報だけを得ています。そのため、予測は自分自身のプレイヤーと、自分が操作する武器にのみ働きます。現時点では他のプレイヤーやインタラクティブオブジェクトの正確な予測が不可能です。
ラグ補償
プレイヤーがターゲットをクライアント時間10.5の時点で撃ったとします。この発射の情報はユーザコマンドに詰め込まれてサーバに送信されます。パケットがネットワークを移動する間もサーバはワールドのシミュレーションを続け、ターゲットが別の位置に動いてしまった可能性があります。ユーザコマンドはサーバ時間10.6で到着し、プレイヤーが正確にターゲットを狙ったとしても当たらなかったことになるかもしれません。このエラーはサーバサイドのラグ補償(sv_unlag 1
)によって訂正されます。
ラグ補償システムは約1秒ほど間の(sv_maxunlag
で変更可能)全てのプレイヤー位置の履歴を保管します。ユーザコマンドが実行されるときに、サーバはそのコマンドが作成された時間を推定します。コマンド実行時間は以下のように計算されます:
それからサーバは他の全てのプレイヤをコマンドが実行された時間での場所に巻き戻します。ユーザコマンドはそこで実行され、当たったかどうか正しく検出します。ユーザコマンドが処理されたらプレイヤーは元の位置に戻されます。listenサーバではsv_showimpacts 1
を実行してサーバとクライアントのヒットボックスの違いを見ることができます:
このスクリーンショットは200ミリ秒のラグ(net_fakelag
を使用)があるlistenサーバで、サーバが当たりを確定した直後にとったものです。赤いヒットボックスは100ミリ秒前だったクライアントでのターゲット位置を示しています。それからユーザコマンドがサーバに届くまでの間にターゲットは左に移動を続けました。ユーザコマンドが到着したら、サーバは推定されるコマンド実行時間に基づいて以前のターゲット位置(青いヒットボックス)を復元しました。サーバは射撃を追跡し、当たったことを確定しました(クライアントには血のエフェクトが表示)。クライアントとサーバのヒットボックスは時間計測の小さい正確性エラーによって完全には一致しません。数ミリ秒の違いであったでも速く移動するオブジェクトにおいては数インチのエラーになります。マルチプレイヤーでの当たり判定はピクセル単位で完璧なものではなく、tickrateと移動オブジェクトの速度による正確さの限界があることが知られています。tickrateを上げることは当たり判定の正確性を増しますが、より多くのCPU、メモリ、帯域能力がサーバとクライアントにおいて必要になります。
疑問もあることでしょう。なぜ当たり判定がサーバで補償されるのでしょうか?当たり判定でのプレイヤー位置の巻き戻しと正確性エラーへの対処はクライアントサイドならば簡単にピクセル単位の正確さで行うことができるでしょう。そしてクライアントはプライヤーが撃たれた場合にどの部位をやられたかを"当たった"メッセージをサーバに返すことでしょう。これを許すことができないのは、ゲームサーバはそのような重要な決定をさせるほどクライアントを信じることができないからです。もしクライアントが"クリーン"でValve-Anti-Cheat (VAC) で守られていたとしても、パケットがゲームサーバに届く間にサードパーティのマシンで変更される可能性は残っています。ここで"チートプロキシ"がVACに検出されることなくネットワークパケットに"当たった"メッセージを挿入することもできるでしょう("中間者"攻撃です)。
ネットワークレイテンシとラグ補償は現実世界と比べると非合理的なパラドックスを生み出す可能性があります。例えば、自分が遮蔽を取ってもう見ることができない攻撃者からの攻撃が当たることがあります。ここで起こったのはサーバがあなたのヒットボックスを巻き戻して、まだ攻撃者に見えている状態に移動させたということです。相対的に遅いパケットスピードのせいでこの不整合問題は一般的に解くことはできません。現実世界においてはこの問題に気づかないのは軽い(パケット)は速く移動し、あなたも他のみなも同じ世界を見ているからです。
ネットグラフ
Source Engineではクライアント接続スピードと質をチェックするためのツールをいくつか提供しています。一番有名なものはネットグラフで、net_graph 2
でオンにすることができます。受信パケットは右から左に動く小さい棒で表現されます。それぞれの棒の高さはパケットのサイズを表しています。もし棒の間に隙間があるのなら、パケットが失われたか、順番が狂って到着したかです。棒は含まれるデータ種類によって色分けされています。
ネットグラフの下部の左の列には現在の描画フレームレート、平均レイテンシ、そしてcl_updaterate
の現在値が表示されています。真ん中の列には最新の受信パケット(スナップショット)のバイトサイズ、平均受信帯域幅、そして一秒に受け取ったパケットが表示されます。右の列には送信データ(ユーザコマンド)についての同様のデータが表示されます。