Let’s make a multiplayer game (part 6)


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.

public void Update(GameTime gameTime)
{
    if ((this.localPlayer != null) && (!this.localPlayer.IsDestroyed))
    {
        var velocityChanged = this.HandlePlayerMovement();

        if (velocityChanged)
        {
            this.OnPlayerStateChanged(this.localPlayer);
        }
    }

    foreach (Player player in this.Players)
    {
        player.Update(gameTime);

        if (!player.IsDestroyed)
        {
            this.ImposeMovementLimits(player);
        }
    }

    if (this.isHost && this.hearbeatTimer.Stopwatch(1000))
    {
        foreach (var player in this.Players)
        {
            this.OnPlayerStateChanged(player);
        }
    }
}

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:

if (this.IsHost)
{
    this.playerManager.AddPlayer(true);
}

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:

case NetConnectionStatus.RespondedAwaitingApproval:
    NetOutgoingMessage hailMessage = this.networkManager.CreateMessage();
    new UpdatePlayerStateMessage(playerManager.AddPlayer(false)).Encode(hailMessage);
    im.SenderConnection.Approve(hailMessage);
    break;

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.

if (!this.IsHost)
{
    var message = new UpdatePlayerStateMessage(im.SenderConnection.RemoteHailMessage);
    this.playerManager.AddPlayer(message.Id, message.Position, message.Velocity, message.Rotation, true);
    Console.WriteLine("Connected to {0}", im.SenderEndpoint);
}
else
{
    Console.WriteLine("{0} Connected", im.SenderEndpoint);
}

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.

Sending 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.

this.asteroidManager = new AsteroidManager(this.resolutionManager, randomNumberGenerator, this.IsHost);
if (this.IsHost)
{
    this.asteroidManager.AsteroidStateChanged += (sender, e) => this.networkManager.SendMessage(new UpdateAsteroidStateMessage(e.Asteroid));
}

this.playerManager = new PlayerManager(this.resolutionManager, randomNumberGenerator, this.inputManager, this.IsHost);
this.playerManager.PlayerStateChanged += (sender, e) => this.networkManager.SendMessage(new UpdatePlayerStateMessage(e.Player));

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:

public void SendMessage(IGameMessage gameMessage)
{
    var om = this.netClient.CreateMessage();
    om.Write((byte)gameMessage.MessageType);
    gameMessage.Encode(om);

    this.netClient.SendMessage(om, NetDeliveryMethod.ReliableUnordered);
}

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.

public void SendMessage(IGameMessage gameMessage)
{
    var om = this.netClient.CreateMessage();
    om.Write((byte)gameMessage.MessageType);
    gameMessage.Encode(om);

    this.netClient.SendMessage(om, NetDeliveryMethod.ReliableUnordered);
}

To start receiving the UpdatePlayerStateMessages we need to add the appropraite case to our switch statement in the ProcessNetworkMessages method.

case NetIncomingMessageType.Data:
    var gameMessageType = (GameMessageTypes)im.ReadByte();
    switch(gameMessageType)
    {
        case GameMessageTypes.UpdateAsteroidState:
            this.HandleUpdateAsteroidStateMessage(im);
            break;
        case GameMessageTypes.UpdatePlayerState:
            this.HandleUpdatePlayerStateMessage(im);
            break;
    }
    break;

The HandleUpdatePlayerStateMessage implementation is as follows:

private void HandleUpdatePlayerStateMessage(NetIncomingMessage im)
{
    var message = new UpdatePlayerStateMessage(im);

    var timeDelay = (float)(NetTime.Now – im.SenderConnection.GetLocalTime(message.MessageTime));

    Player player = this.playerManager.GetPlayer(message.Id) ??
                        this.playerManager.AddPlayer(message.Id, message.Position, message.Velocity, message.Rotation, false);

    //player.EnableSmoothing = true;

    if (player.LastUpdateTime < message.MessageTime)
    {
        player.PrevDisplayState = (EntityState)player.DisplayState.Clone();

        player.SimulationState.Position = message.Position += (message.Velocity * timeDelay);

        player.SimulationState.Velocity = message.Velocity;
        player.SimulationState.Rotation = message.Rotation;

        player.LastUpdateTime = message.MessageTime;
    }
}

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.

Player movement with 200ms latency

In the next article I will cover some techniques in smoothing out the snapping effect caused by latency.

Advertisements

5 thoughts on “Let’s make a multiplayer game (part 6)

  1. Hey Dirk, I have mostly programmed in Java and have been recently dabbling in C# (on account of programming in XNA). I am pretty new to EventHandlers and delegates and am having a hard time figuring out how things like, when and how the OnAsteroidStateChanged() method gets called. I am also confused about the meaning of the following line:
    this.asteroidManager.AsteroidStateChanged +=
    (sender, e) => this.networkManager.SendMessage(new UpdateAsteroidStateMessage(e.Asteroid));

    Any help or explanation is much appreciated 🙂

    • Hi Anurag

      The line in question is attaching an event handler to the AsteroidStateChanged event.

      Whenever the AsteroidStateChanged event is fired the code will call the

      this.networkManager.SendMessage method with a new instance of the UpdateAsteroidStateMessage as input parameter.

      Notice how the UpdateAsteroidStateMessage is instantiated with the Asteroid object received from the event via the event args e.

      Hope that makes sense

      • Hi Dirk, Thanks a lot for the super quick reply. And yeah, that does make sense. Only part is, I still can’t seem to get a handle on where and when exactly the AsteroidStateChanged event gets fired within the code. Is it possible for you to maybe point out a snippet of the code that might fire the AsteroidStateChanged event ?

        Again, thanks for the feedback man. I really do appreciate it !

      • Hi Anurag

        The AsteroidManager is responsible for notifying the rest of the game in changes to the Asteroid state.

        If you check the Update method you will see calls to the OnAsteroidStateChanged method (this raises the actual event).

        The logic is simple, whenever there is a change in an asteroid’s state we raise the AsteroidStateChanged event. In addition whenever asteroids go out of bounds we raise the event and then finally every 200ms the event is raised as well.

      • Finally, I get the bigger picture. On to building the game prototype. Thanks a lot for the answers Dirk. Much appreciated ! 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s