In this chapter we will handle taking control of entities and moving around in the world, this will familiarize you with how the control concept in Bolt works and how it handles authoritative movement.
Hiding server and client differences
Before we go onto taking control of our entities we are going to deal with and explain something which comes up a lot when dealing with both Bolt and multiplayer in general.
The problem: If we want let the server be just another player in the game, how do we deal with the fact that the server doesn't exist as a connection on itself?
Each client that connects to the server is represented by a
BoltConnection object and on each client the server is represented as a single
BoltConnection object. However when we want to do something to the "server player" on the server itself we have no easy way to refer to it, since there is no object which represents the server on itself.
The answer to this is that we need to create a simple abstraction, which lets us deal with a Player object instead of a specific connection, in this player object we will hide if we have a connection or not, so that the rest of our code does not have to think about it.
Create two new C# files and call them TutorialPlayerObject.cs and TutorialPlayerObjectRegistry.cs. Let's start in the
This is a standard C# class; it does not inherit from unity's
MonoBehaviour class. This is very important. It also contains two fields called
character field will contain the instantiated object which represents the player's character in the world. The
connection field will contain the connection to this player if one exists, this will be
null on the server for the servers player object.
We are going to add two properties also, which lets us check if this is a client or a server player object without having to deal with the
connection field directly.
isClient simply check if the connection is or isn't null, which tells us if the player represents the server or a client. Before we add more functionality to our
TutorialPlayerObject we are going to open up the
TutorialPlayerObjectRegistry class. We are using this class for managing instance of our
The only thing in this entire class which isn't just standard C# code is that we are accessing the
userToken property on the
BoltConnection class. This property is simply a place where you can stick any type of other object/data that you want to pair with the connection. In our case we are going to pair the
TutorialPlayerObject we create with the connection it belongs to (if it belongs to one).
The remainder of this class contains very little which is specific to Bolt, if you want read through the code and comments below but we're not going to go into more detail on it.
Open up the TutorialServerCallbacks.cs file and update the class we have in there, remove the two calls to
Implement the unity
Awake function and call
TutorialPlayerObjectRegistry.CreateServerPlayer inside it, this creates the server player for us whenever this callback object becomes active.
TutorialServerCallbacks override the method called
Connected which is inherited from
Bolt.GlobalEventListener. Inside of it call
TutorialPlayerObjectRegistry.CreateClientPlayer and pass in the connection argument.
It's finally time to spawn the characters for each of our players and also assign control of them correctly, open up the
TutorialPlayerObject class again and we are going to add two new methods:
Spawn will spawn our character and
RandomPosition simply picks a random position in the world to spawn at.
I won't explain
RandomPosition as it simply returns a random vector (position) in the world, but let's look closer at the
Spawn function. In
Spawn we first check if we have a character or not and if we don't have one we call
BoltNetwork.Instantiate and create one. Then we simply check if we are the server or not and call the proper method for taking or giving control.
We set the transform.position property on the character object which moves our player to a random position in the world.
Before we start our game, there are two more things we need to handle. First open up the
TutorialPlayerCallbacks class found in tutorial/Scripts/Callbacks. We are going to override a callback called
ControlOfEntityGained which notifies us of when we get control of an entity.
The last thing we need to do is go back to the
TutorialServerCallbacks and call the
Spawn method when we are done loading the scene, since this behaviour exists on the server (courtesy of the
[BoltGlobalBehaviour(BoltNetworkModes.Server, "Level2")] attribute) we need to check both for the SceneLoadLocalDone for the server itself and for the SceneLoadRemoteDone for the clients.
If you go to the Bolt Scenes window and click Play As Server you will see a screen like the one below.
We have spawned our character, we have been assigned control of it and the camera is looking at it. Next up is moving around and controlling our character. You can also build a separate client and connect to the server you are starting in the editor with it; you will see that the client gets spawned properly and assigned a character just like the server is.
Note: Due to the way the camera code works you cannot rotate it around your character, it's completely static if the character is not moving, this is intentional
This section of the tutorial deals with something a lot of people ask about: Authoritative movement with client side prediction for instant movement on the clients that is still controlled and verified by the server. It also does this completely transparently and removes the differences between being a client and a server from movement codes point of view.
We are going to start by creating a new Command, right-click in the Bolt Assets window and select New Command.
Select the command by clicking on it and name it TutorialPlayerCommand.
At the top left you can see that instead of the states New Property button you have two buttons called New Input and New Result. Before we start adding data to our command, let's go into detail on exactly what the Input and Result represents.
Input in general terms encapsulates player input from one player. This is often something like "Forward" and "Backward" for movement, or "YRotation" and "XRotation" for mouse rotation. But it can also be a bit more abstract such as "SelectedWeapon".
Result encapsulate the state which is the result of applying the Input to your object, common properties here are values for position and velocity but also flags for different types of state as isGrounded, etc.
With this in mind, let's start adding some input to our command. We want the following input properties.
- Forward - Bool. If we are holding down the forward key.
- Backward - Bool. If we are holding down the backward key.
- Left - Bool. If we are holding down the left key.
- Right - Bool. If we are holding down the right key.
- Jump - Bool. If we pressed the down the jump key.
- Yaw - Float. Our current rotation on the Y axis.
- Pitch - Float. Our current rotation on the X axis.
The result of applying our input is represented by the following four properties.
- Position - Vector3.
- Velocity - Vector3.
- IsGrounded - Bool. If we are touching the ground or not.
- JumpFrames - Integer. This one is a bit odd, it is the number that represents how many "frames" we have left of applying our jump force on. This is a detail specific to the character motor we will be using.
Our command is done, we are going to add more to it later but for now it's all we need to get movement working. Compile Bolt again by going to Assets/Compile Bolt Assets (All), doing this Bolt will update its internal data with the new command we created.
The next thing we need to setup is the character motor, Bolt comes with an already working character motor which supports all of the features we need. You can find it in bolt_tutorial/Scripts/Player/PlayerMotor.cs. Locate the script and also our TutorialPlayer prefab, attach a copy of the motor to our prefab.
We need to adjust a couple of settings on both the Player Motor component and also the Character Controller component which gets added automatically.
- Set Step Offset to 0.5
- Set Center to (0, 1, 0)
- Set Height to 2.2
- Set Layer Mask to World
Create a new script called TutorialPlayerController.cs in the tutorial/Scripts/Player folder.
Our new class should inherit from
Bolt.EntityBehaviour<ITutorialPlayerState> which gives us direct and static access to the data on our TutorialPlayerState asset.
Now we're going to write pretty large chunk of code and we will split it into several small pieces, begin by adding fields for our inputs, motor and also a constant to the
Then we are just defining the standard Unity
Awake method and get a reference to the motor in it.
We also need to make Bolt aware of the transform of our entity. Override the 'Attached' method which is provided by the
Bolt.EntityBehaviour base class, inside this method access
state.Transform and call the method
SetTransforms on it and supply the transform of our game object.
Next up is PollKeys which is used for buffering the input data of the local player, this includes all of our buttons and the mouse movement.
This is pretty much standard Unity Input code, so not a lot to talk about - the only thing really interesting is the
bool mouse parameter which tells us if we should poll the mouse input or not, more on this later.
Since Unity refreshes the state of the
Input class inside
Update, we are going to define a very simple
Update function like this, just calling our
PollKeys function. We pass true to the
PollKeys function so that we read our mouse movement also.
Time to get into some Bolt specific stuff, we are going to override a method called
SimulateController, which is only invoked on the computer which has been assigned control of an entity. The first thing we do is call
PollKeys again, but we pass in
false saying we don't want it to read the mouse data. The reason for this is that if we had read the mouse data here again we would double our mouse movement.
Next we are calling
TutorialPlayerCommand.Create() to create an instance of our command input that Bolt has compiled for us from the TutorialPlayerCommand asset. Now we just need to copy over all of the input data from our local variables to the input.
The last thing we will do is to call
QueueInput on our entity, this sends the input to both the server and client for processing and is what lets Bolt do its client prediction but still maintain authority on the server.
Phew ... we are soon done, one more method. I would argue that this method is the most important one in all of Bolt, this is where the magic happens when it comes to authoritative movement and control. Meet
ExecuteCommand, it executes on both the controller and the owner of an entity.
The first parameter to this function is always a command which contains the input you have sent with
SimulateController. The second parameter called
resetState it is important to note that this can only be true on the controller, it tells the controller (usually the client) that the owner (usually the server) sent a correction to it, and we should reset the state of our motor.
Inside the code of the function we check if
resetState is true, if it is we don't "execute" the command and we simply reset the local state of the motor. If it's not true we apply the input of the command to the motor by calling
Move, which returns a new state which we copy over to the
Result property on the command that was passed in.
Assigning the state of the motor to the command passed in is very important, this is what lets Bolt apply corrections result of the command to the controller from the owner.
Attach a copy of the TutorialPlayerController component to your TutorialPlayer prefab and if you hit Play As Server now, you should be able to run around in the world (without animations) and move your character. For reference here is the complete code for
Here is a screenshot of the server (in the editor) with two clients connected (from an older version of Bolt so the UI looks a bit different).
Here is a video demonstrating that the movement is infact 100% authoritative (from an older version of Bolt so the UI looks a bit different).