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)
Advertisements

One thought on “Let’s make a multiplayer game (part 9)

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