コマンド
コマンドは、サーバー主導によるクライアント予測をサポートすることを目的とした、Bolt の完全に任意の機能です。この機能に対応する必要がなければゲーム内でコマンドを使用する必要は一切ありません。実際、Boltの最も簡単な実装(完全なクライアント主導)では、コマンドの使用は全く必要ありません!さらに、NPCなどの完全にサーバードリブンなものにおいては、クライアント予測を使用しないためコマンドを使用する必要がありません。Bolt State を介したシンプルな Transform 同期を使用してください。
コマンドの実装手順は複雑ですのでご注意ください。アドバンスチュートリアルではコマンドを搭載したシンプルなモーターを実装していますが、実装開始時には是非このモーターを使用するようにしてください。コマンドの理解を深める前に独自のモーターを使用することは推奨していません。
コマンドを使用すると、ローカルで予測されたエンティティ(通常はプレイヤー)がコマンドシステムを使用してローカルマシン上で即座に移動します(つまり「予測」の部分)。移動に瞬時に応答することがプレイヤーにとっては重要です。プレイヤーの入力はコマンドの一部としてサーバーに送られます。サーバー側でも同じ入力を再生し、クライアントがすでに予測したシミュレーションとまったく同じ結果になることを期待します。それから、一定のフレームでプレイヤーにその結果(最終位置、速度など)を返します。するとプレイヤーは、位置やその他の状態をそのフレーム時点でのサーバーの状態(コマンド結果に含まれる「補正」)まで巻き戻します。そして、その時点から現在まで入力を再実行し、最終的に自分が予測していた位置・状態に戻ることを期待します。サーバーのシミュレーションが異なる場合はプレイヤーが別の位置にいることになります。ここがサーバー主導となる部分で、*シミュレーションはサーバー主導で実行され、プレイヤーはそのシミュレーションがどうなるかを予測するだけです。*プレイヤーが移動速度を非常に速く設定したとしても、サーバーはそれを気にしません。クライアント側のシミュレーションはサーバーにまったく影響を与えないからです。
プレイヤーのプロキシ、つまり他のプレイヤーの端末上に表示されるそのプレイヤーは、通常、Transform の同期による移動に Bolt State システムを使用します。コントローラープレイヤーは自分自身の移動を予測しているため、Player オブジェクトの Bolt State の Transform 同期はEveryone but Controller(コントローラー以外の全員)に設定されています。これは、自分の Player オブジェクトの Transform がサーバーから同期されないようにするためです。Everyone(全員)に設定されていると、プレイヤーは自分の移動を予測しているのに、サーバーからも自分のプレイヤーに対して予測なしの状態同期が返ってきてしまい、自分で予測した値と競合し、目に見えるアーティファクトが生じてしまいます。
クライアント予測とサーバーオーソリティを採用した移動システムをネットワーク上でテストする場合は、レイテンシシミュレーションを有効にし、Boltのデフォルト設定などの適切な値に設定した状態でテストすることをおすすめします。これは、レイテンシが大きいほど、実装時のエラーが顕著になるからです。シミュレーションをオフにして、サーバーをローカルで実行しいる場合、問題ないように見えるかもしれません。ただしレイテンシシムをオンにしてみるとすぐに何が問題なのかわかるはずです。
コマンドは本質的には、設定したSendRateに従ってコントローラーとサーバーの間で送受信されるネットワークストリームです(入力はサーバーへ送られ、結果はコントローラーへ返されます)。これはクライアント予測を使用していなくても便利な機能です。
コマンドの仕組み
まず初めに、SimulateController/ExecuteCommandの仕組みを説明します。SimulateControllerは、エンティティをコントロールしているホスト上でのみ起動します。このホストは、自分自身にTakeControl()を呼び出す場合はオーナー、オーナーが AssignControl(connection)を呼び出す場合はプロキシになります。
SimulateControllerは、ゲームから入力を取得してCommandやコントローラ固有のその他のタスクに格納するために使用します。SimulateControllerの実行は1フレームにつき1回のみです。
ExecuteCommand は、エンティティのオーナーとコントローラーの両方で実行されます。例えば、サーバーがプレイヤーキャラクターを生成し、その制御権をクライアントに渡した場合、ExecuteCommand はサーバーとそのキャラクターを操作しているクライアントの両方で実行されます。他のクライアントでは実行されません。
次によくある疑問が、入力・状態はコマンド上でどのような仕組みになっているのか、そしてresetStateはどんな働きをしているのか、というものです。
So, first of - input is very obvious, this is set on the command in SimulateController and polls the local state of whatever input scheme you are using for that specific frame. When you call entity.QueueInput(cmd); the command is scheduled both for local execute on the client and is sent to the server for remote execution. This is what lets Bolt do client side prediction: the command will execute on both the server and client.
When the server executes a command, it will send the State of the command back to the client which created the command, and override the state of that specific command on the client with its own correct state.
So how does resetState fit into all of this? The resetState parameter asks you to reset the state of the character motor to the state of the command passed in when resetState is true. This will only happen on remote controlling clients, never on the server. This happens once at the beginning of every frame, and the command which is passed in is the command which has received its correct state from the server.
After the command with resetState has execute, Bolt will execute all other commands again on the client from the frame of the reset frame to the current frame, to "catch up" to the current state. This happens every frame (this is your simulation rate).
A common question is: Why does my player move really fast when I comment out his reset state logic on the client?
The reason is that every single tick, Bolt rewinds you to where you were on a certain frame (with reset state), and then it replays every queued command from that frame to the current frame. This should in most scenarios place you back at the same position you were before the tick, but with the additional input from the new tick. Replayed inputs generally are at least 10+ commands, even when playing on a server on the same network. If you comment out the reset state logic, Bolt will end up executing 10+ commands (with forward input if you were pressing forward), without resetting your position back in time first. So you will end up executing 10+ “move forwards” per tick! This is why you move so fast. Keep in mind this is completely client side and the server doesn’t reflect this rapid movement. You are basically just ignoring the server completely.
Queuing Input with Commands
A common question from users of Bolt is how to queue input correctly. Most users find that their one shot input is being acted on multiple times in a row or in some cases the opposite occurs and their input is simply missed. The reason for this is quite simple but requires knowledge of how Unity’s Update / FixedUpdate works. Note that Unity collects input in the beginning of Update and Bolt’s input queuing occurs in FixedUpdate. Alternatively you can use Rewired and detect input in both Update and FixedUpdate.
Update fires once per frame. FixedUpdate fires on a fixed interval. If your frame rate is high, you will fire multiple Updates between each FixedUpdate. If your framerate is low, Unity will fire a number of FixedUpdates in a single frame in order to try to keep the physics ticks in sync.
Imagine the following scenario. You are running in the editor on an empty test scene. Your frame rate is very high. In this case, say your simulation rate was 60 (60 physics ticks per second), and your frame rate was 180. This means three updates fire between each tick.
So in this scenario you have three updates per one fixed update:
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)
In this example you have a data structure that is tracking your input so you can queue it in SimulateController. You click the jump button on the second update. However, you don’t end up actually jumping in the game, because your data structure resets the jump back to false on the third update before you queued it. The solution is simple: only set the jump flag to true - never reset it to false while polling input. Instead you reset all of your one shot inputs when you have finished queueing your input.
Of course in low frame rate situations the opposite can happen:
Update - input polled
FixedUpdate - queue input
FixedUpdate - queue same input (again)
FixedUpdate - queue same input (again)
In this case if you don’t clear your one-shot input after Bolt’s SimulateController, you will queue the same on-shot input three times in a row.