コマンド
コマンドの説明
コマンドは、サーバー権限でクライアントの予測をサポートすることを目的としたBolt内の完全に任意の構成要素です。
対応したくない場合は、ゲームでコマンドを使用する必要はありません。
実際には、Boltの最も簡単な実装 - 完全にクライアントの権限 - では、コマンドを使用する必要がないので、コマンドを使用する必要はありません。
また、完全にサーバー駆動のNPCのようなものは、クライアントが予測していないので、コマンドを使用する必要はありません。
Boltの状態を介して単純な変換同期を使用する必要があります。
コマンドを使用することは、ゲームオーバーフローをより複雑にしますので、広範囲に使用する前に完全に理解するべきです。
Advanced Tutorial
では、コマンドを使った簡単なキャラクターモーターを実装しています。
コマンドを理解せずに独自のモーターを使うことはお勧めできません。
予測
ローカルのエンティティ(通常はプレイヤー)は常に予測されています。
ローカルマシン上で瞬時に移動するのに、コマンドシステムを用いています。プレイヤーにとって即座にレスポンスがあることはとても大切なことです。
プレイヤーの入力は、コマンドの一部としてサーバーに送信されます。
サーバは同じ入力を再生しますが、その結果、ほとんどの場合、クライアントが予測した通りのシミュレーションが行われます。
サーバはその結果(最終的な位置や速度など)を特定のフレームでプレイヤーに返し、プレイヤーは基本的に位置やその他の状態をそのフレームのサーバに時間内に戻してリセットします(コマンド結果の「補正」)。次に、その時点から現在までの入力を再生して、予測した場所に戻るようにします。
オーソリティビティ
クライアントの予測された挙動にしたがって、サーバは状態権限というものを実行します。
サーバのシミュレーションが異なる場合、プレイヤーは異なる位置に行き着くことになります。
シミュレーションはサーバ上で権限を持って実行されており、プレイヤーは単にそのシミュレーションがどのようなものになるかを予測しようとします。
プレイヤーが自分の速度を非常に高速に設定しても、クライアントのシミュレーションはサーバとは全く関係がありません。
Tプレイヤーのプロキシ、つまり他のプレイヤーのボックス上のプレイヤーの表現は、通常、同期されたトランスフォームを使用して動作するためにBoltステートシステムを使用します。
制御しているプレイヤーは自分の動きを予測しているので、プレイヤーの Transform
プロパティの Replication Mode
を Everyone but Controller
に設定するとよいでしょう。自身のPlayerオブジェクトを変換してサーバーから同期しないようにするためです。
これが Everyone
に設定されている場合、プレイヤーは自分の動きを予測しますが、予測されていない自分のプレイヤーの状態の同期もサーバから戻ってきます。これは本質的に予測値と衝突し、明らかなアーチファクトが表示されます。
クライアントが予測した、ネットワーク上のサーバー権限の動きモーターをテストする場合は、レイテンシーシミュレーションを有効にして、Boltが付属しているデフォルトなど、合理的な設定にすることをお勧めします。
これは、遅延が大きいほど、実装でより顕著な(そして明らかな)エラーが発生するためです。
シミュレーションを行わず、サーバをローカルにしている場合、正常に動作しているようでも、レイテンシーシミュレーションを有効にしたとたん、問題が発覚することがあります。
コマンドは、基本的には、コントローラからサーバへの送信速度で前後に送信されるネットワークストリームです(入力はサーバに、結果はサーバに返されます)。
これは、クライアントの予測にコマンドを使用していない場合でも有用な構成になります。
コマンドの定義
コマンドを作成したり変更したりするには、Bolt Assets
ウィンドウで右クリックして New Command
を作成するか、リストから選択して定義を編集します。
Bolt Editor
ウィンドウでは、Inputs
と Results
を含めることができます。これらのフィールドは、クライアントから入力コマンドを送信したり、サーバから結果を返したりするのに使われます。
コマンドには、その動作を変更するためのいくつかの設定もあります。
- Is Instant - このコマンドのキューに入っているすべての入力は、サーバに到達した後、次のフレームで即座に実行されます。
- Enable Frame Limit - SimulateController() につき、キューに入れる入力は 1 つだけに制限されます。これにより、スピードハックを防ぐことができます。
- 修正補間 - サーバから受信した結果を補間します。
- Enable Delta Compression - ネットワークトラフィックと割り当てを削減しますが、その代わりに処理時間が若干増加します。常に結果を測定してください! 入力/結果は決してデルタ圧縮されないので、boolsのみで使用しないでください。
コマンドを使う
コマンドを使用するためのBolt APIは比較的小さいですが、コマンドを利用するためには、すべての可動部分を理解する必要があります。
新しい Bolt Entity
を作成し、その状態を定義し、Bolt.EntityEventListener<[あなたの状態はこち]]>
を継承するコンポーネントクラスを関連付けると、3つの主要なメソッドにアクセスすることができます。
public override void SimulateController()
: はゲームから入力を集めてコマンドに変換するために利用します。SimulateController
はフレームごとに1回ずつ実行されます。public override void ExecuteCommand()
:エンティティの所有者とコントローラの両方で実行されるため、サーバーがスポーンし、それをクライアントに制御するプレイヤーキャラクターがある場合、ExecuteCommand
はサーバーと制御クライアントの両方で実行されます。 他のクライアントでは実行されません。entity.QueueInput(cmd)
: 関数SimulateController()
の内部からこのメソッドを呼び出すと、引数として渡されたコマンドをクライアント上でローカルに実行したり、リモートで実行するためにサーバに送信したりすることができます。これにより、Bolt はクライアント側の予測を行うことができます: コマンドはサーバとクライアントの両方で実行されます。
サーバはコマンドを実行すると、そのコマンドの Result
をそのコマンドを作成したクライアントに送り返し、クライアント上の特定のコマンドの状態をそれ自身の正しい状態で上書きします。
パラメータ resetState
は、resetStateがtrueの場合に渡されたコマンドの Result
にキャラクターモータの状態をリセットすることを要求します。
これはリモートコントロールクライアントでのみ発生し、サーバでは発生しません。
この処理は各フレームの最初に一度だけ行われ、渡されたコマンドはサーバから正しい Result
を受け取ったコマンドになります。
resetState
を持つコマンドが実行された後、Boltは現在の状態に "追いつく "ために、リセットされたフレームから現在のフレームまで、クライアント上で他のすべてのコマンドを再び実行します。
これはフレームごとに行われます(これがシミュレーションレートです)。
よくある質問:クライアント上でプレイヤーのリセット状態のロジックをコメントアウトすると、なぜプレイヤーは高速に移動するのでしょう?
その理由は、1つ1つのティックごとに、ボルトは、特定のフレーム上にあった場所に巻き戻します(リセット状態で)。次に、現在のフレームに、そのフレームからキューに入れられたコマンドをすべてリプレイします。
これは、ほとんどのシナリオでは、ティックの前と同じ位置に戻って配置する必要がありますが、新しいティックからの追加入力を使用します。
再生される入力は、同じネットワーク上のサーバーでプレイしている場合でも、一般的には少なくとも10以上のコマンドが入力されます。
リセット状態のロジックをコメントアウトすると、Boltは、最初に時間を遡って位置をリセットすることなく、10以上のコマンドを(前方を押していた場合は前方入力で)実行することになります。
つまり、1ティックあたり10個以上の「前方への移動」を実行することになります。そのため、動きが速くなります。これは完全にクライアント側の問題であり、サーバーはこの高速な動きを反映しません。
サーバーを完全に無視しているということになります。
コマンドで入力をキューに入れる
Boltのユーザーからよくある質問は、入力を正しくキューに入れる方法です。
多くのユーザーは、ワンショット入力が連続して何度も処理されていたり、場合によっては逆に入力を見逃してしまうことがあります。この理由は非常に単純ですが、UnityのUpdate
/ FixedUpdate
がどのように動作するかを知る必要があります。
UnityはUpdateの最初に入力を収集し、Boltの入力キューイングはFixedUpdateで行われます。
Update
は1フレームに1回発生します。FixedUpdateは一定の間隔で起動します。
フレームレートが高い場合は、各FixedUpdate
の間に複数のUpdates
を実行します。
フレームレートが低い場合、Unityは物理の目盛りを同期させるために、1つのフレーム内に複数の FixedUpdates
を実行します。
次のシナリオを想像してみてください。何もないテストシーンをエディタで実行しています。このときのフレームレートは非常に高いです。
この場合、シミュレーションレートが 60(1 秒間に 60 回の物理ティック)で、フレームレートが 180 であったとします。これは、各ティックの間に 3 回の更新が発生することを意味します。
つまり、このシナリオでは、1回の固定更新で3回の更新が行われることになります。
md
Update - collect input for jump == false
Update - collect input for jump == true (you pressed the jump button)
Update - collect input for jump == false
FixedUpdate - queue input (false)
この例では、入力を追跡するデータ構造体を持っているので、SimulateController
でキューに入れることができます。
2回目の更新でジャンプボタンをクリックします。データ構造体はキューに入れる前の 3 回目の更新時にジャンプを false にリセットしているので、実際にゲーム上でジャンプはしません。
解決策は簡単で、ジャンプフラグをtrueに設定するだけです。入力をポーリングしている間は決してfalseにリセットしないでください。
代わりに、入力のキューイングが終了したときに、ワンショット入力をすべてリセットします。
もちろん、低フレームレートの状況では逆のことが起こる可能性があります。
md
Update - input polled
FixedUpdate - queue input
FixedUpdate - queue same input (again)
FixedUpdate - queue same input (again)
この場合、Boltの SimulateController
の後にワンショット入力をクリアしないと、同じオンショット入力を3回連続でキューに入れてしまいます。