YIn this article I will cover controlling the player ship. Handling player input is more involved as it happens both on the server and all the connected clients. Our simulation will contain multiple players which will need to be updated and drawn, but can only be controlled on the instance they belong to. With that introduction I will jump into the player class.
The Player Class
The Player class inherits from our Sprite class and adds some properties needed to manage the player. These properties are related to game logic such as managing player lives and score and I will not cover them in this article.
The Player Manager Class
The PlayerManager class is responsible for updating the player objects, drawing the player objects, handling player input for the locally controlled player and sending notifications when the player state has changed.
The Update method is interesting from a multiplayer game programming perspective.
Firstly we want to handle player input only for the local player and if the player is alive. My HandlePlayerMovement method basically checks the keyboard input and adjusts the Player velocity information based on the user input. The Player will move as long as the user presses a movement key. Releasing a movement key will cause the player to stop moving in that direction. From a networking perspective we don’t want to send movement messages for every update cycle. This would cause the server to process messages rather than actually processing the game logic.
We are more interested in sending changes in velocity and that is why the HandlePlayerMovement method returns true when there was a change in the player velocity vector. As per the previous articles we use an event to notify the game that there was a change in the EntityState of the player.
The next part is purely game logic where we process the movement of each of our players and limit the player movement to the bounds of the screen.
The last part of the Update method is where we send heartbeat updates to all the clients if we are the server instance of the game.
I had to add the concept of a LocalPlayer property to the PlayerManager class. This is how the PlayerManager knows which player instance should respond to user input.
Creating the Player
The next case to handle is when the player class is instantiated for both the client and the server. In the previous articles I mentioned that the Server is the only instance of the game that is allowed to manage the creation of game state objects. This holds true for players as well, the trick here is the timing of when player instances are created.
On the server side there is no difficulty. The server instance just creates a new player when the game starts and that’s the end of it. In this example I create the player instance in my LoadContent method as follows:
The true variable means the player is local to the current instance of the game.
For new clients connecting to the server we need to handle things differently. This is where the NetConnectionStatus.RespondedAwaitingApproval state comes in handy. When the server detects a new client connecting it adds a new player instance to the PlayerManager. The SenderConnection.Approve() method can send back a hail message to the connecting client. I am using this message to send back the newly created Player information to the client as follows:
On the client side we now decode the hail message on the NetConnectionStatus.Connected state and add it to the client-side PlayerManager as the local player.
This neatly allows us to create a new Player instance when a client connects and send back the Player instance information to the client to add as the local client player.
Now we have created new players on both sides and can look at sending and receiving messages.
We send messages in exactly the same way as with the AsteroidManager class. Instead of the messages only coming from the server we now allow the client to also send update messages for changes in the player state.
The Initialize method now instantiates the PlayerManager and the PlayerStateChanged event is handled on both the client and the server.
The only additional change I had to implement was to complete the SendMessage implementation on the ClientNetworkManager as follows:
This implementation is almost exactly the same as the ServerNetworkManager class except that it uses the underlying NetClient class.
I am not going to cover the UpdatePlayerStateMessage since it is exactly the same at the UpdateAsteroidStateMessage previously covered.
To start receiving the UpdatePlayerStateMessages we need to add the appropraite case to our switch statement in the ProcessNetworkMessages method.
The HandleUpdatePlayerStateMessage implementation is as follows:
You will notice that it is exactly the same (or very similar) to the HandleUpdateAsteroidStateMessage. We could probably refactor this at a later stage. For the time being I will keep it separate to avoid confusion.
Notice that player.EnableSmoothing is commented out. With a simulated latency of 200ms you will immediately notice why we need a good smoothing solution.
In the following video I am controlling the client player. Notice how the player movement on the server-side is very jerky. This is due to the fact that the server is only receiving changes in the player position and velocity 200ms after they have actually happened causing the player to snap to the new position.
On the client-side you will notice the player periodically snap to a position. This is due to the heartbeat update from the server with the 200ms latency.
In the next article I will cover some techniques in smoothing out the snapping effect caused by latency.