Commands
Commands are completely optional constructs within Bolt intended to support client prediction with server authority. You do not need to use commands at all in your game if you do not want to support this. In fact, with the easiest implementation in Bolt - completely client authoritative - you do not need to use commands at all! In addition, anything completely server driven - such as NPCs, do not need to use commands since they are not client predicted. They should use simple transform syncing through the Bolt State.
Please keep in mind implementing commands are complicated. The Advanced Tutorial implements a simple motor with commands and it is highly suggested you start with this motor when you start. Trying to use your own motor without first understanding commands is not recommended.
With commands the locally predicted entity (usually the player) uses the command system to move on his local machine, instantly (i.e. the “prediction” part). Instant response to movement is critical for players. The player’s inputs are sent to the server as part of the command. The server plays these same inputs as well, hopefully resulting in the same exact simulation that the client already predicted. The server then returns the result (the final position, speed, etc) back to the player for a certain frame, who then essentially resets his position and other state back in time to the server’s on that frame (the “correction” in the command result) and then he replays his inputs from that point back to the present to hopefully be back where he predicted he would be. If the server’s simulation was different, the player will end up at a different position. This is the server authoritative part, because the simulation is authoritatively running on the server and the player simply tries to predict what that simulation will be. If the player sets his speed to a very high speed, the server doesn’t care, because the client simulation has no bearing at all on the server.
The player’s proxy - i.e. the representation of his player on other player’s boxes, typically just uses the Bolt state system for movement with synced transforms. Since the controlling player is predicting his own movement, the state sync for the Player object’s Bolt State transform is set to Everyone but Controller
because you don’t want his own Player objects transform to be synced back from the server. If it was set to Everyone
, the player would predict his own movement, but then the non-predicted state sync for his own player would also come back from the server, which would essentially collide with his predicted value and you would see obvious artifacts.
As a suggestion, if you are working on testing your client predicted, server authoritatve movement motor on the network, make sure you do it with latency simulation enabled and set to something reasonable, like the defaults Bolt comes with. This is because the larger the latency the more pronounced (and obvious) errors will be with your implementation. With no simulation and the server being local you might think it works great - turn on latency sim and you will very quickly see where there are problems.
Finally, note that Commands are essentially network streams being sent on your send rate back and forth from the controller to the server (inputs to the server, results back). This can be a useful construct even if you are not using commands for client prediction.
How Do Commands Work?
First, let's break down how SimulateController/ExecuteCommand
works. SimulateController
is only invoked on the host which is in control of the entity, this can be both the owner (if it calls TakeControl()
for itself) or a proxy (if the owner calls AssignControl(connection)
).
SimulateController
is used to collect inputs from your game and putting it into a Command
, and other tasks specific to the controller. SimulateController
executes one time per frame only.
ExecuteCommand
runs on both the owner and the controller of an entity, so if you have a player character which the server spawns and then gives control of it to a client, ExecuteCommand
will run on both the server and the controlling client. It will not run on any other clients.
The next question usually is: so how does the input/state work on the command, and what does resetState
do?
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.