Let’s make a multiplayer game (part 9)

In this article I am going to cover the handling of enemy ships. The first thing to consider when adding a new piece of functionality to the game is where the responsibilities lie. Based on the classification discussed in article 5 we can classify the creation of enemies and enemy shots fired at the player as the responsibility of the server. All other enemy behaviour is handled on both the client and server side.

Managing Enemies

Adding enemies follows exactly the same pattern as the previous game logic. We define an Enemy class that inherits from the Sprite class. The Enemy class is responsible for handling enemy movement across the screen and the rendering of the enemy sprite. The EnemyManager class is responsible for the creation of enemies and whether an enemy has fired at the player. These activities happen on the server with the client-side merely adding enemies created on the server to the local list.

public void Update(GameTime gameTime)
        {
            for (int x = this.enemies.Count – 1; x >= 0; x–)
            {
                this.enemies[x].Update(gameTime);
                if (this.enemies[x].IsActive == false)
                {
                    this.enemies.RemoveAt(x);
                }
                else
                {
                    if (this.isHost)
                    {
                        if ((float)this.randomNumberGenertor.Next(0, 1000) / 10 <= this.shipShotChance)
                        {
                            Vector2 fireLoc = this.enemies[x].SimulationState.Position;
                            fireLoc += this.gunOffset;

                            Vector2 shotDirection =
                                this.playerManager.Players.ToList()[
                                    this.randomNumberGenertor.Next(0, this.playerManager.Players.Count() - 1)].Center
                                - fireLoc;
                            shotDirection.Normalize();

                            this.shotManager.FireShot(fireLoc, shotDirection, this.enemies[x].Id, false);
                        }
                    }
                }
            }

            if (this.isActive && this.isHost)
            {
                this.UpdateWaveSpawns();
            }
        }

In the above Update method of the EnemyManager class we loop through the enemy object instances and call their Update methods. If an enemy instance is no longer active (due to it moving out of the screen bounds or being destroyed) we remove it from the local list of enemies.

Note: This happens on both the server and client side without any updates from the server to the client. this is due to the deterministic nature of the simulation.

Next only the server side will do a check to see if an enemy has fired at a randomly selected player. This code makes use of the ShotManager class to create the shot and to notify the client that a shot was fired.

Lastly, the server side will spawn a new wave of enemies based on the configured game timer. The UpdateWaveSpawns method makes use of the server-side SpawnEnemy method which will raise an event for the message to be sent to the client-side.

Sending Messages

As in the previous articles we attach an event handler to the EnemyManager class on the server-side as follows.

this.enemyManager = new EnemyManager(
    randomNumberGenerator, this.shotManager, this.playerManager, this.IsHost);
if (this.IsHost)
{
    this.enemyManager.EnemySpawned +=
        (sender, e) => this.networkManager.SendMessage(new EnemySpawnedMessage(e.Enemy));
}

In the above code we send the EnemySpawned game message to all the connected clients if the code is running as the server.

public class EnemySpawnedMessage : IGameMessage
{
    #region Constructors and Destructors

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

    public EnemySpawnedMessage(Enemy enemy)
    {
        this.Id = enemy.Id;
        this.Position = enemy.SimulationState.Position;
        this.Velocity = enemy.SimulationState.Velocity;
        this.Rotation = enemy.SimulationState.Rotation;
        this.MessageTime = NetTime.Now;
        this.Path = enemy.Path;
    }

    #endregion

    #region Public Properties

    public long Id { get; set; }

    public double MessageTime { get; set; }

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

    public int Path { get; set; }

    public Vector2 Position { get; set; }

    public float Rotation { get; set; }

    public Vector2 Velocity { get; set; }

    #endregion

    #region Public Methods and Operators

    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();
        this.Path = im.ReadInt32();
    }

    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);
        om.Write(this.Path);
    }

    #endregion
}

The EnemySpawned game message follows exactly the same pattern as all the other game message classes. I am only including it to reinforce the pattern. There is nothing different in terms of multiplayer programming.

Receiving Messages

The receipt and processing of messages follows the same pattern as before and the ProcessNetworkMessages method is modified as follows.

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

    break;

The HandleEnemySpawnedMessage method is used to create the enemy instance on the client-side.

private void HandleEnemySpawnedMessage(NetIncomingMessage im)
{
    var message = new EnemySpawnedMessage(im);

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

    Vector2 adjustedPosition = message.Position + (message.Velocity * timeDelay);

    this.enemyManager.SpawnEnemy(message.Id, message.Path, adjustedPosition, message.Velocity, message.Rotation);
}

The above code makes use of the client-side version of the SpawnEnemy method on the EnemyManager. This is important since we don’t want to send game messages in this case.

We did not introduce any additional code to handle the enemies firing at the player. This is because we could reuse the ShotManager class introduced in the previous article.

The following video shows an example of the new enemy functionality in action.

Asteroids with Enemies (200ms delay)

Let’s make a multiplayer game (part 8)

In the previous article we implemented smoothing of player movements on both the client and server side. In this article we will implement the functionality required to allow the player to fire shots.

As per the previous articles the first class to implement is the Shot class. This class will represent a bullet flying across the screen and inherits from the Sprite base class. The Shot class has two additional properties as shown below.

public long FiredById { get; private set; }

public bool FiredByPlayer { get; private set; }

The FiredById property is used to store the Id of the entity that fired the shot.

The FiredByPlayer property is used to indicate whether the player fired the shot. When we implement enemies this property will be false when an enemy ship fires at the player.

Shots are managed by the ShotManager class, meaning that the ShotManager is responsible for drawing all the shots and updating the movement of the shots. Shots are removed from the ShotManager when they move out of the screen bounds.

public Shot FireShot(Vector2 position, Vector2 velocity, long firedById, bool playerFired)
{
    Shot shot = this.FireShot(
        Interlocked.Increment(ref shotIdCounter), position, velocity * this.shotSpeed, firedById, playerFired);
    this.OnShotFired(shot);
    return shot;
}

The FireShot method is used to fire a shot and takes as input parameter the position from where the shot is fired and the velocity (direction and speed) of the shot. Additional meta data includes the Id of the entity that fired the shot and whether the firing entity is a player. The FireShot method raises an event that is handled by the Game class to notify the client or server (depending who is firing the shot) when a shot is fired.

Sending Messages

As per the previous articles we send messages by handling the events raised by the Manager type classes. In this case we handle the ShotFired event as follows.

this.soundManager = new SoundManager(randomNumberGenerator);
this.shotManager = new ShotManager(this.resolutionManager, this.soundManager);
this.shotManager.ShotFired += (sender, e) => this.networkManager.SendMessage(new ShotFiredMessage(e.Shot));

The ShotFired event is handled for both the client and the server.

The ShotFiredMessage is similar to the message classes defined previously.

public class ShotFiredMessage : IGameMessage
{
    #region Constructors and Destructors

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

    public ShotFiredMessage(Shot shot)
    {
        this.Id = shot.Id;
        this.Position = shot.SimulationState.Position;
        this.Velocity = shot.SimulationState.Velocity;
        this.FiredByPlayer = shot.FiredByPlayer;
        this.MessageTime = NetTime.Now;
        this.FiredById = shot.FiredById;
    }

    #endregion

    #region Public Properties

    public long FiredById { get; set; }

    public bool FiredByPlayer { get; set; }

    public long Id { get; set; }

    public double MessageTime { get; set; }

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

    public Vector2 Position { get; set; }

    public Vector2 Velocity { get; set; }

    #endregion

    #region Public Methods and Operators

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

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

    #endregion
}

The above ShotFiredMessage is very similar to the other GameMessages we have defined thus far. In this case we have made provision for the FiredById and FiredByPlayer properties.

Receiving Messages

The ShotFiredMessage is handled using the same pattern as the previous message types. The ProcessNetworkMessages method in the game code is updated as follows:

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

    break;

With the HandleShotFired method implemented as follows:

private void HandleShotFiredMessage(NetIncomingMessage im)
{
    var message = new ShotFiredMessage(im);

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

    Vector2 adjustedPosition = message.Position + (message.Velocity * timeDelay);

    this.shotManager.FireShot(
        message.Id, adjustedPosition, message.Velocity, message.FiredById, message.FiredByPlayer);
}

As before, we decode the game message into our ShotFiredMessage class. We calculate the time delay and update the position of the Shot taking into account any latency. We then call the FireShot message on the ShotManager class to add the shot to the collection of shots to be managed.

Note: We are using the non event raising override of the FireShot method. This pattern is repeated in all the manager implementations. On each manager we have a version of the method that will raise the “create” or “update” event and a version that will add the object instance but not raise any events. The latter version is used when processing game network messages to update the local state in our manager classes.

By adding a few lines of code to the PlayerManager class we now have the player ship firing bullets on both the client and server.

if (this.inputManager.IsKeyDown(Keys.Space))
{
    this.FireShot();
}

private void FireShot()
{
    if (this.shotTimer.Stopwatch(200))
    {
        this.shotManager.FireShot(
            this.localPlayer.SimulationState.Position + this.gunOffset, new Vector2(0, -1), this.localPlayer.Id, true);
    }
}

The Update method will check if the Spacebar is pressed and call the FireShot method. We have limited the shots to be fired to fire every 200ms. Shots are fired from the SimulatedState position taking into account a GunOffset to set the initial position of the shot.

The video below shows an example of shots being fired by the client player instance and how the shots are displayed on the server-side.

Firing shots (200ms delay)

Let’s make a multiplayer game (part 7)

In the previous article I demonstrated how latency impacts the smoothness of the gameplay experience. The deterministic part of the game simulation i.e. the asteroids are not affected as much due to the fact that their movement will follow a predictable course. The player movement however, is very unpredictable with the jittery effect being magnified by frequent player movement changes.

The way to correct the snapping of networked entities is to make use of some kind of smoothing technique. The simplest of these is to make use of Linear Interpolation. Here are some links to articles I found useful.

http://gafferongames.com/game-physics/networked-physics/. This article is really good with a LOT of useful information in the comments section.

This Gamasutra article contains a good general description of dead-reckoning.

The Source engine articles from Valve are also really useful.

You will notice that this code makes use of the ideas from Shawn Hargreaves’ Blog entry.

In my previous articles I showed how I keep three versions of the EntityState (Position, Velocity, Rotation) for each of my sprites. The reason for this is so that we can separate the display state from the simulation state. The simulation state will be used for all game logic decisions such as collision detection. The display state will be used for rendering.

In order to perform linear interpolation we will be interpolating between the previous display state (where we last rendered) and the simulation state (where the object currently is). A linear interpolation requires the from state, to state and an interpolation factor. I’m sure there are sophisticated techniques for determining the interpolation factor. My calculation is based on the fact that by default XNA calls the update method 1000ms / 60 = every 16.67ms. I reasoned that I would like the display state to reach the simulation state after 200ms meaning I would need 200ms / 16.67ms = 12 updates to achieve it. The resulting interpolation factor is therefore 1 / 12.

The following code shows the Update method for all my sprites.

public virtual void Update(GameTime gameTime)
{
    var elapsedSeconds = (float)gameTime.ElapsedGameTime.TotalSeconds;

    if (this.frameTimer.Stopwatch(100))
    {
        this.currentFrameIndex = (this.currentFrameIndex + 1) % this.frames.Count;
    }

    this.SimulationState.Position += this.SimulationState.Velocity * elapsedSeconds;

    if (this.EnableSmoothing)
    {
        this.PrevDisplayState.Position += this.PrevDisplayState.Velocity * elapsedSeconds;

        this.ApplySmoothing(1/12f);
    }
    else
    {
        this.DisplayState = (EntityState)this.SimulationState.Clone();
    }
}

First I get the elapsed seconds since the last update. This value is used to advance the sprite position by it’s velocity.

The FrameTimer code is just looping between the animation frames every 200ms.

Next we use the elapsed time to update the SimulationState of the entity.

If smoothing is disabled then the DisplayState is equal to the SimulationState.

When smoothing is enabled we need to apply smoothing. Firstly we update the previous display state position based on its velocity and the elapsed time. Then we apply the smoothing based on the interpolation factor as described above.

private void ApplySmoothing(float delta)
{
    this.DisplayState.Position = Vector2.Lerp(
        this.PrevDisplayState.Position, this.SimulationState.Position, delta);

    this.DisplayState.Velocity = Vector2.Lerp(
        this.PrevDisplayState.Velocity, this.SimulationState.Velocity, delta);

    this.DisplayState.Rotation = MathHelper.Lerp(
        this.PrevDisplayState.Rotation, this.SimulationState.Rotation, delta);

    this.PrevDisplayState = (EntityState)this.DisplayState.Clone();
}

Using the Vector2.Lerp method our DisplayState values now become the interpolated value from the PrevDisplayState to the SimluationState according to the interpolation factor.

Lastly the PrevDisplayState is updated to the current DisplayState. The above code will enable a smooth linear interpolation of the DisplayState to the SimulationState within 200ms.

I have modified the Draw method to render the SimulationState when smoothing is enabled. This is purely for debugging purposes and will be disabled during actual gameplay.

The HandleUpdatePlayerStateMessage method in the game class is now 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.SimulationState.Position = message.Position += (message.Velocity * timeDelay);
        player.SimulationState.Velocity = message.Velocity;
        player.SimulationState.Rotation = message.Rotation;

        player.LastUpdateTime = message.MessageTime;
    }
}

No major changes, except for the fact that smoothing is now enabled. I had some code referencing the PrevDisplayState. This was a bug and has been removed.

I played around with the idea of ignoring LOCAL player state updates. The effect of is a 100% match between the DisplayState and SimulationState for the local player with no snapping. The only issue is that the player will never be updated with the server values which could lead to some inconsistencies. I did however, reduce the player update heartbeat to once per second, reasoning that I could tolerate local player adjustments in high latency conditions every second.

The following video shows the effects of enabling player smoothing.

Smoothing Enabled (200ms latency)

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.

Let’s make a multiplayer game (part 5)

At this point I am going to spend to time to recap on the concepts I covered in the previous articles.

The game is designed to be a single entry point that can either operate in server mode or client mode. This is a design choice and not a requirement.

The game has the following common responsibilities in both server and client mode.

  • Update game objects. Each of our game objects have their own logic which needs to be processed. In this game it is typically the physics logic to allow the objects to move around the screen.
  • Apply game logic. An example of game logic is the processing of collisions between game objects. This behaviour needs to be handled consistently on both the client and server to ensure the responsiveness of the game.
  • Draw game objects. The game obviously needs to render the scene.

When the game is running in client mode it is also responsible for the following.

  • Notify the Server of player actions. When the player performs an action we need to execute it on the client to provide immediate feedback. We also need to inform the server of the action so that it does not override the player state during the next update cycle.
  • Respond to messages from the server. The client needs to respond to server messages to ensure that the client version of the simulation is synchronised with the server version.

When the game is running in server mode it responsible for the following (in addition to the common elements).

  • Respond to messages from the client. The server needs to process messages received from the client. Here the server can override any invalid requests and the action will be reverted on the client.
  • Notify clients of game object changes. The server needs to send out periodic state updates to all the clients. I call this the heartbeat update as it should happen at a regular time interval.
  • Create game objects. The server is responsible for the game state, by not allowing clients to create objects we can be fairly confident of a synchronised simulation.
  • Delete game objects. This is the inverse of creating objects again we do this on the server to ensure consistency.

We encapsulate our game network messages in a message class that implements the following IGameMessage interface.

public interface IGameMessage
{
    GameMessageTypes MessageType { get; }

    void Encode(NetOutgoingMessage om);

    void Decode(NetIncomingMessage im);
}

The MessageType property is used to identify our game messages and should be the first byte sent and received for every game message.

The Encode and Decode methods should mirror each other exactly. This will eliminate a lot of errors that could occur when sending and receiving messages.

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);
}

I recommend always sending the NetTime.Now value along with the game message as it is useful in determining the exact latency when the message is processed as well as filtering out of sequence messages.

I think that covers the main ideas I wanted to review. In the next article I will add the PlayerManager class which will allow us to control the player ship.