Let’s make a multiplayer game (part 4)


In this article I will demonstrate how to get some asteroids flying around the screen synchronized on both the client and server.

The Asteroid Class

Each of our asteroids are represented by an Asteroid class which inherits from a general Sprite class. I’m not going to cover the Sprite class in detail but would like to highlight the areas that are relevant for networking.

public double LastUpdateTime { get; set; }

public EntityState PrevDisplayState { get; set; }

public EntityState SimulationState { get; set; }

public EntityState DisplayState { get; set; }

public bool EnableSmoothing { get; set; }

The LastUpdateTime property is used to check when last a particular sprite was updated. I use this to make sure I don’t process old messages out of sequence. You will notice that in this example game I make use of the ReliableUnordered NetDeliveryMethod. This means that the Lidgren library guarantees message delivery but not the sequence in which the messages will be delivered.

The PrevDisplayState property is used to store the last rendered EntityState values. This is used during the smoothing of the drawing which I will cover in more detail in a later article.

The SimulationState property is used to store the current simulation state of the entity. This means the real values used by the simulation i.e. the actual position, velocity and rotation.

The DisplayState property is used to store the current rendered EntityState values. If smoothing is disabled then the DisplayState will equal the SimulationState i.e. we have not made any adjustments to how we render the EntityState between frames.

The EnableSmoothing property is used to determine if we should smooth the rendering of the sprite between frames. The smoothing is done from the PrevDisplayState to the SimulationState.

The EntityState class stores the Position (Vector2), Velocity (Vector2) and Rotation (float) of our sprite.

You can see how the Draw and Update methods makes use of the DisplayState and SimulationState properties respectively.

The Asteroid Manager Class

This game architecture makes use of Manager classes to control objects of the same type. You will see an AsteroidManager, PlayerManager, EnemyManager, ShotManager, CollisionManager, etc.

Each manager class is responsible for loading its own content, creating it’s objects, drawing the objects and updating the objects. Now in the previous articles I mentioned that the server is responsible for the simulation state. In the case of the AsteroidManager the server is the only one that can add or remove asteroids from the simulation. It also has the final say over the real SimulationState of each of the asteroids.

Because we want our simulation to be responsive on the client-side (even with some latency) we need to do some client-side processing of the simulation. What this means is that the client-side instance of the AsteroidManager should be allowed to draw, bounds asteroid against each other and update the asteroid positions.

To separate these two behaviours I have introduced a bool called IsHost to the AsteroidManager class. When IsHost is true our AsteroidManager will perform server functions as well as the functionality common to both the client and the server.

Most of the work is done in the Update method. The purpose of this method is to update each of the individual asteroids and then to perform some game logic such as handle asteroids that have move offscreen, handle collisions between asteroids, ensure the correct number of asteroids exist and finally to send out notifications of the current asteroid EntityStates.

public void Update(GameTime gameTime)
{
    foreach (Asteroid asteroid in this.Asteroids)
    {
        asteroid.Update(gameTime);
        if (this.isHost)
        {
            if (!this.IsOnScreen(asteroid))
            {
                asteroid.SimulationState = this.SelectRandomEntityState();
                this.OnAsteroidStateChanged(asteroid);
            }
        }
    }

    var processedList = new List<Asteroid>();
    foreach (Asteroid a1 in this.Asteroids)
    {
        processedList.Add(a1);
        foreach (Asteroid a2 in this.asteroids.Values.Except(processedList))
        {
            if (a1.IsCircleColliding(a2.Center, a2.CollisionRadius))
            {
                this.BounceAsteroids(a1, a2);
            }
        }
    }

    if (this.isHost)
    {
        for (int i = this.asteroids.Count; i < this.asteroidCount; i++)
        {
            this.AddAsteroid();
        }

        if (this.hearbeatTimer.Stopwatch(200))
        {
            foreach (var asteroid in this.Asteroids)
            {
                this.OnAsteroidStateChanged(asteroid);
            }
        }
    }
}

In the first part of the Update method we enumerate all the asteroids and call their Update method. This will allow the asteroids to move around on the screen due to their positions being updated with the velocity vector.

Here we see the first server specific code where we test if an asteroid has been moved outside the bounds of the screen. When an asteroid is outside the bounds we choose a new random position and velocity and then raise the AsteroidStateChanged event.

Note: I decided that I would make use of events to manage notifications of state changes in my simulation. The server instance of the game will bind to this event and send the appropriate network message to each of the clients. The client instance of the game does not bind to this event so even if it was raised it would never do anything about it. I found this technique to work quite nicely for this game.

The second part of the update method checks for collisions between asteroids and invokes a method to let the asteroids bounce by changing their velocity vectors. Once the colliding asteroids have been updated I once again raise the AsteroidStateChanged event so that the new state update will be sent to the clients.

The last part of the update method is purely server focused. The first thing I do is ensure that there are enough asteroids in the game. This code will never run on the client as it is the server’s responsibility to create asteroids.

The next part makes use of a Timer class to send updated EntityState values for each of the asteroids every 200ms. In an earlier design I was handling the heartbeat update in the game code itself, but it made sense for each manager to handle its own update. This heartbeat is basically our overriding update message to ensure that the client does not deviate too far from the server simulation.

Sending Messages

Our AsteroidManager class is now raising events when the asteroid EntityState changes. The next step is for our Game code to handle the events and send the appropriate network messages.

The following code shows the Initialize method in the game class:

protected override void Initialize()
{
    // TODO: Add your initialization logic here
    this.networkManager.Connect();

    var randomNumberGenerator = new MersenneTwister();

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

    base.Initialize();
}

You will notice that I use the Initialize method to instantiate my Manager objects. I used to have the code in the constructor but decided this felt more like initialization.

Note: You might be wondering why the AsteroidManager isn’t an XNA game object. I will cover this in more detail in another blog post as it is more XNA related than mutliplayer game related.

The interresting part in the above code is the event handler I’m attaching to the AsteroidStateChanged event. Firstly it is only done when the game is running as a Host. Secondly I am calling a method on the INetworkManager interface called SendMessage. The message I’m sending is called the UpdateAsteroidStateMessage.

I like to encapsulate my network communications in message objects. I find this a really elegant way of handling messages with the added benefit of reducing message encoding errors.

Each of my messages inherit from the IGameMessage interface.

public interface IGameMessage
{
    GameMessageTypes MessageType { get; }

    void Encode(NetOutgoingMessage om);

    void Decode(NetIncomingMessage im);
}

The MessageType property is encoded as the first value in any game message we send over the network. When we receive a game message we can then safely decode the first byte, cast it to the GameMessageType and then know what the rest of the message content should contain. You will see this in action later.

The Encode method is used to encode or write the contents of the message to the NetOutgoingMessage.

The Decode method is used to decode or read the contents from the NetIncomingMessage into the IGameMessage instance.

The implementation of the UpdateAsteroidStateMessage is as follows:

public class UpdateAsteroidStateMessage : IGameMessage
{
    #region Constructors and Destructors

    public UpdateAsteroidStateMessage(NetIncomingMessage im)
    {
        this.Decode(im);
    }

    public UpdateAsteroidStateMessage(Asteroid asteroid)
    {
        this.Id = asteroid.Id;
        this.Position = asteroid.SimulationState.Position;
        this.Velocity = asteroid.SimulationState.Velocity;
        this.Rotation = asteroid.SimulationState.Rotation;
        this.MessageTime = NetTime.Now;
    }

    #endregion

    #region Properties

    public long Id { get; set; }

    public double MessageTime { get; set; }

    public Vector2 Position { get; set; }

    public float Rotation { get; set; }

    public Vector2 Velocity { get; set; }

    #endregion

    #region Public Methods

    public GameMessageTypes MessageType
    {
        get { return GameMessageTypes.UpdateAsteroidState; }
    }

    public void Decode(NetIncomingMessage im)
    {
        this.Id = im.ReadInt64();
        this.MessageTime = im.ReadDouble();
        this.Position = im.ReadVector2();
        this.Velocity = im.ReadVector2();
        this.Rotation = im.ReadSingle();
    }

    public void Encode(NetOutgoingMessage om)
    {
        om.Write(this.Id);
        om.Write(this.MessageTime);
        om.Write(this.Position);
        om.Write(this.Velocity);
        om.Write(this.Rotation);
    }

    #endregion
}

The key thing to note is how the Decode and Encode methods are mirrored. This is a very useful technique in managing your game messages and I can highly recommend this pattern.

Simply put, the Encode method writes each of the game message class variables to the NetOutgoingMessage. The sequence is very important.

Note: I do not write the MessageType property. The reason for this is that your typically read this property before you call the Decode method and I want to keep them as similar as possible.

The Decode method then reads the variable types from the NetIncomingMessage class in the same sequence as it was encoded in.

The properties in the class store the values we want to send over the wire. The MessageTime property is interesting as it is not required to update the asteroid EntityState values. The MessageTime property is used to store the NetTime.Now value when the message was sent. This value is used by the client to determine the time delay from when then message was sent to when the message was received. This information is used to perform some client-side prediction on the asteroid position.

The next step is to look at how the server will send messages to the connected clients.

The server version of the SendMessage method on the INetworkManager interface is as follows:

public void SendMessage(IGameMessage gameMessage)
{
    NetOutgoingMessage om = netServer.CreateMessage();
    om.Write((byte)gameMessage.MessageType);
    gameMessage.Encode(om);

    netServer.SendToAll(om, NetDeliveryMethod.ReliableUnordered);
}

The first thing we do is to create a new NetOutGoingMessage by calling the CreateMessage method on the NetServer.

We then write the game message header which is a single byte defined by our MessageType property.

Thereafter we encode the rest of the message properties onto the NetOutgoingMessage.

And finally we call the SendToAll method on the NetServer which will send the message to all the connected clients.

As I mentioned earlier I am using the NetDeliveryMethod.ReliableUnordered to send my game messages.

Receiving Messages

The receiving of messages is handled with the rest of our message processing code. You will notice that I have introduced a new case in the switch statement for messages of type NetIncomingMessageType.Data as follows:

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

Our own game messages will always be of type Data. We can then ready the first byte, cast it to our GameMessageTypes enum and handle the appropriate message type.

I like to encapsulate my message handling code in a method with the corresponding name. In this case it is the HandleUpdateAsteroidStateMessage method.

private void HandleUpdateAsteroidStateMessage(NetIncomingMessage im)
{
    var message = new UpdateAsteroidStateMessage(im);

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

    Asteroid asteroid = this.asteroidManager.GetAsteroid(message.Id) ??
                        this.asteroidManager.AddAsteroid(
                            message.Id, message.Position, message.Velocity, message.Rotation);

    asteroid.EnableSmoothing = true;

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

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

        asteroid.SimulationState.Velocity = message.Velocity;
        asteroid.SimulationState.Rotation = message.Rotation;

        asteroid.LastUpdateTime = message.MessageTime;
    }
}

The first thing we do is to construct our UpdateAsteroidStateMessage from the NetIncomingMessage which will call our Decode method and populate all the game message properties.

The next step is to calculate the time delay since the message was sent and the current NetTime. The MessageTime we sent along with our game message comes in handy to do this.

Using the id from the game message I test to see if the asteroid already exists (if it does I retrieve it) and whether I need to create a new asteroid. Once the asteroid has been created or retrieved we can proceed to update it’s EntityState values.

The next check is to ensure that old messages i.e. messages received out of order are discarded.

Ignore the EnableSmoothing and PrevDisplayState code for now as I will cover this in a later article.

The important part here is the updating of the asteroid Position. You will notice that I am updating the asteroid position with the message position PLUS the velocity multiplied by the timeDelay. This is how I implement client-side prediction to make sure that our client is not running behind the server simulation.

You will notice that when you run the code that even with a simulated delay of 200ms the two simulations are synchronized.

Example output from Client and Server
Advertisements

3 thoughts on “Let’s make a multiplayer game (part 4)

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