Export (0) Print
Expand All

Tutorial 5: Adding Multiplayer and Networking Support to the Game

This tutorial adds two-player competitive game play to the game completed in Tutorial 4.
Bb975645.note(en-US,XNAGameStudio.30).gifNote
In order to test the network functionality of this sample, each instance must be running on a separate computer, each with XNA Game Studio installed.
Bb975645.GB5GamePlay(en-US,XNAGameStudio.30).jpg

Before You Begin: Getting the Project Ready

Begin this tutorial by completing the fourth tutorial in the Going Beyond: XNA Game Studio in 3D series, or by downloading the completed code for the fourth tutorial (Video Tutorial 4: Make a Game in 60 Minutes).

A complete code sample for this tutorial is also available for you to download, including full source code and any additional supporting files required by the sample.

Step 1: Simplify the Update Method

This tutorial is going to add some details to the Update and Draw methods of your game, so as a first step we will work on simplifying the code in these methods.

Move the bullet-asteroid collision check into a method to simplify the code.

bool CheckForBulletAsteroidCollision(float bulletRadius, float asteroidRadius)
{
    for (int i = 0; i < asteroidList.Length; i++)
    {
        if (asteroidList[i].isActive)
        {
            BoundingSphere asteroidSphere =
              new BoundingSphere(asteroidList[i].position, asteroidRadius *
                  GameConstants.AsteroidBoundingSphereScale);
            for (int j = 0; j < bulletList.Length; j++)
            {
                if (bulletList[j].isActive)
                {
                    BoundingSphere bulletSphere = new BoundingSphere(bulletList[j].position,
                      bulletRadius);
                    if (asteroidSphere.Intersects(bulletSphere))
                    {
                        asteroidList[i].isActive = false;
                        bulletList[j].isActive = false;
                        score += GameConstants.KillBonus;
                        return true; //no need to check other bullets
                    }
                }
            }
        }
    }
    return false;
}

Similarly, we will make new methods for the ship-asteroid collision check, and replace the corresponding code in the Update method with the call to this new CheckForShipAsteroidCollision method.

public bool CheckForShipAsteroidCollision(float shipRadius, float asteroidRadius)
{
    //ship-asteroid collision check
    if (ship.isActive)
    {
        BoundingSphere shipSphere = new BoundingSphere(ship.Position, shipRadius *
            GameConstants.ShipBoundingSphereScale);
        for (int i = 0; i < asteroidList.Length; i++)
        {
            if (asteroidList[i].isActive)
            {
                BoundingSphere b = new BoundingSphere(asteroidList[i].position, asteroidRadius *
                    GameConstants.AsteroidBoundingSphereScale);
                if (b.Intersects(shipSphere))
                {
                    //blow up ship
                    //soundExplosion3.Play();
                    ship.isActive = false;
                    asteroidList[i].isActive = false;
                    score -= GameConstants.DeathPenalty;
                    return true;
                }
            }
        }
    }
    return false;
}

This class uses the ship model. While you perform this, add a shipModel to the Game class, and initialize it as you did the other models. When we add a player class to the code, the players will all share the same ship model data.

Model asteroidModel;
Model bulletModel;
Model shipModel;
Matrix[] asteroidTransforms;
Matrix[] bulletTransforms;
Matrix[] shipTransforms;

Initialize the ship model with the other models in the game class.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    shipModel = Content.Load<Model>("Models/p1_wedge");
    shipTransforms = SetupEffectDefaults(shipModel);
    asteroidModel = Content.Load<Model>("Models/asteroid1");
    asteroidTransforms = SetupEffectDefaults(asteroidModel);
    bulletModel = Content.Load<Model>("Models/pea_proj");
    bulletTransforms = SetupEffectDefaults(bulletModel);
    stars = Content.Load<Texture2D>("Textures/B1_stars");
    soundEngine = Content.Load<SoundEffect>("Audio/Waves/engine_2");
    soundHyperspaceActivation = Content.Load<SoundEffect>("Audio/Waves/hyperspace_activate");
    soundExplosion2 = Content.Load<SoundEffect>("Audio/Waves/explosion2");
    soundExplosion3 = Content.Load<SoundEffect>("Audio/Waves/explosion3");
    soundWeaponsFire = Content.Load<SoundEffect>("Audio/Waves/tx0_fire1");
    lucidaConsole = Content.Load<SpriteFont>("Fonts/Lucida Console");
}

The Update method should now look something like this:

protected override void Update(GameTime gameTime)
{
    float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;

    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();

    // Get some input.
    UpdateInput();

    // Add velocity to the current position.
    ship.Position += ship.Velocity;

    // Bleed off velocity over time.
    ship.Velocity *= 0.95f;

    for (int i = 0; i < GameConstants.NumAsteroids; i++)
    {
        asteroidList[i].Update(timeDelta);
    }

    for (int i = 0; i < GameConstants.NumBullets; i++)
    {
        if (bulletList[i].isActive)
        {
            bulletList[i].Update(timeDelta);
        }
    }

    if (CheckForBulletAsteroidCollision(bulletModel.Meshes[0].BoundingSphere.Radius,
            asteroidModel.Meshes[0].BoundingSphere.Radius))
    {
        soundExplosion2.Play();
    }

    bool shipDestroyed = CheckForShipAsteroidCollision(shipModel.Meshes[0].BoundingSphere.Radius,
                asteroidModel.Meshes[0].BoundingSphere.Radius);
    if (shipDestroyed)
    {
        soundExplosion3.Play();
    }

    base.Update(gameTime);
}

Next, we will look for ways to simplify the UpdateInput method. The code used to shoot a bullet could be moved into a new method, as well as the code to warp the ship to center and to play the engine sound.

public void ShootBullet()
{
    //add another bullet.  Find an inactive bullet slot and use it
    //if all bullets slots are used, ignore the user input
    for (int i = 0; i < GameConstants.NumBullets; i++)
    {
        if (!bulletList[i].isActive)
        {
            bulletList[i].direction = ship.RotationMatrix.Forward;
            bulletList[i].speed = GameConstants.BulletSpeedAdjustment;
            bulletList[i].position = ship.Position
                + (200 * bulletList[i].direction);
            bulletList[i].isActive = true;
            score -= GameConstants.ShotPenalty;
            return;
        }
    }
}
public void WarpToCenter()
{
    ship.Position = Vector3.Zero;
    ship.Velocity = Vector3.Zero;
    ship.Rotation = 0.0f;
    ship.isActive = true;
    score -= GameConstants.WarpPenalty;
    
}
void PlayEngineSound(GamePadState currentState)
{
    //Play engine sound only when the engine is on.
    if (currentState.Triggers.Right > 0 && !soundEnginePlaying)
    {
        if (soundEngineInstance == null)
            soundEngineInstance = soundEngine.Play(0.5f, 0.0f, 0.0f, true);
        else
            soundEngineInstance.Resume();
        soundEnginePlaying = true;
    }
    else if (currentState.Triggers.Right == 0 && soundEnginePlaying)
    {
        soundEngineInstance.Pause();
        soundEnginePlaying = false;
    }
}

It is also possible to simplify the check for whether a current button is pressed. We will create a new method called IsButtonPressed that takes only one parameter, the button to check. This method will check a set of global variables storing the current and last states of the game pad.

GamePadState currentState;
GamePadState lastState;

...
bool IsButtonPressed(Buttons button)
{
    switch (button)
    {
        case Buttons.A:
            return (currentState.Buttons.A == ButtonState.Pressed &&
                lastState.Buttons.A == ButtonState.Released);
        case Buttons.B:
            return (currentState.Buttons.B == ButtonState.Pressed &&
                lastState.Buttons.B == ButtonState.Released);
        case Buttons.X:
            return (currentState.Buttons.X == ButtonState.Pressed &&
                lastState.Buttons.X == ButtonState.Released);
        case Buttons.Back:
            return (currentState.Buttons.Back == ButtonState.Pressed &&
                lastState.Buttons.Back == ButtonState.Released);
        case Buttons.DPadDown:
            return (currentState.DPad.Down == ButtonState.Pressed &&
                lastState.DPad.Down == ButtonState.Released);
        case Buttons.DPadUp:
            return (currentState.DPad.Up == ButtonState.Pressed &&
                lastState.DPad.Down == ButtonState.Released);

    }
    return false;
}

Using these new methods in UpdateInput will result in the following simplified code:

protected void UpdateInput()
{
    // Get the game pad state.
    currentState = GamePad.GetState(PlayerIndex.One);
    if (currentState.IsConnected)
    {
        if (ship.isActive)
        {
            ship.Update(currentState);
            PlayEngineSound(currentState);
        }
        // In case you get lost, press B to warp back to the center.
        if (IsButtonPressed(Buttons.B))
        {
            WarpToCenter();
            soundHyperspaceActivation.Play();
        }

        //are we shooting?
        if (ship.isActive && IsButtonPressed(Buttons.A))
        {
            ShootBullet();
            soundWeaponsFire.Play();
            bool isFiring = true;
        }
        lastState = currentState;
    }
}

Step 2: Encapsulate Player Code

Just as we encapsulated the ship and asteroid data in Tutorial 4, we now need to encapsulate the player data so we can easily create multiple instances of the data needed to save the state of any player in a multiplayer game. In this step, we are going to refactor the code, moving any relevant data and methods from the Game class and into a new Player class.

The first step is to create a new class to contain the player object code. Right-click on your game project in Solution Explorer, and choose Add and then Class. In the Name field, enter Player.cs and click Add.

In this class, we are going to use some objects from the Framework, Graphics, and Input namespaces. To do this, add a using statement to the top of the class to include these namespaces.

using System;
using System.Collections;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace GoingBeyond5
{
  public class Player
  {
    public Player()
    {
    }
  }
}
    

Next, look at Game1.cs and determine what data should be associated with a player. The player object would keep the information about the game pad state, ship, bullets, asteroids, and score. Move these to Player.cs. Upon inspection of the code, you might notice that the random number generator is only used to reset the asteroids for a player, so let's move this to the Player.cs file as well. You might also notice that the last gamepad state is something that would be associated with a player.

In addition to this data, we will store a picture for each player as a Texture2D.

internal GamePadState lastState;

internal Ship ship = new Ship();
internal Asteroid[] asteroidList = new Asteroid[GameConstants.NumAsteroids];
internal Bullet[] bulletList = new Bullet[GameConstants.NumBullets];

internal int score;

Random random = new Random();

If you compile your project at this time, you will receive some warnings that these member variables no longer exist in Game1.cs. These warnings can help you determine which methods in Game1.cs deal primarily with the player data.

One of the warnings indicates that the asteroidList referenced in the ResetAsteroids function does not exist in the current context. Looking at this, you can see that the ResetAsteroids function deals with resetting the asteroids for a player. Move the ResetAsteroids method into the Player class.

The ResetAsteroids method was previously called during game initialization, so also move the call to ResetAsteroids to the Player constructor.

using System;
using System.Collections;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace GoingBeyond5
{
    public class Player
    {
        internal GamePadState lastState;

        internal Ship ship = new Ship();
        internal Asteroid[] asteroidList = new Asteroid[GameConstants.NumAsteroids];
        internal Bullet[] bulletList = new Bullet[GameConstants.NumBullets];

        internal int score;

        Random random = new Random();

        public Player()
        {
            ResetAsteroids();
        }

        internal void Update(GameTime gameTime)
        {
            float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
            // Add velocity to the current position.
            ship.Position += ship.Velocity;

            // Bleed off velocity over time.
            ship.Velocity *= 0.95f;

            for (int i = 0; i < GameConstants.NumAsteroids; i++)
            {
                asteroidList[i].Update(timeDelta);
            }

            for (int i = 0; i < GameConstants.NumBullets; i++)
            {
                if (bulletList[i].isActive)
                {
                    bulletList[i].Update(timeDelta);
                }
            }
        }

        private void ResetAsteroids()
        {
            float xStart;
            float yStart;
            for (int i = 0; i < GameConstants.NumAsteroids; i++)
            {
                if (random.Next(2) == 0)
                {
                    xStart = (float)-GameConstants.PlayfieldSizeX;
                }
                else
                {
                    xStart = (float)GameConstants.PlayfieldSizeX;
                }
                yStart = (float)random.NextDouble() * GameConstants.PlayfieldSizeY;
                asteroidList[i].position = new Vector3(xStart, yStart, 0.0f);
                double angle = random.NextDouble() * 2 * Math.PI;
                asteroidList[i].direction.X = -(float)Math.Sin(angle);
                asteroidList[i].direction.Y = (float)Math.Cos(angle);
                asteroidList[i].speed = GameConstants.AsteroidMinSpeed +
                   (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed;
                asteroidList[i].isActive = true;
            }
        }

Continue moving methods associated with the player data into the Player class. Here, we move the WarpToCenter, ShootBullet, CheckForShipAsteroidCollision, and CheckForBulletAsteroidCollision methods into the Player class.

There is also some code in the Update method that applies specifically to the player. Remove this code from Game.Update and create a new method called Player.Update with the player specific code. This requires that you add a new instance of the Player object to your Game class.

Player player = new Player();

Make these changes to create the new Update function in Player class.

internal void Update(GameTime gameTime)
{
    float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
    // Add velocity to the current position.
    ship.Position += ship.Velocity;

    // Bleed off velocity over time.
    ship.Velocity *= 0.95f;

    for (int i = 0; i < GameConstants.NumAsteroids; i++)
    {
        asteroidList[i].Update(timeDelta);
    }

    for (int i = 0; i < GameConstants.NumBullets; i++)
    {
        if (bulletList[i].isActive)
        {
            bulletList[i].Update(timeDelta);
        }
    }
}

Change the Update function in the Game to use the new method.

protected override void Update(GameTime gameTime)
{
    player.Update(gameTime);

    // Allows the game to exit
    if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
        this.Exit();

    // Get some input.
    UpdateInput();

    if (player.CheckForBulletAsteroidCollision(bulletModel.Meshes[0].BoundingSphere.Radius,
            asteroidModel.Meshes[0].BoundingSphere.Radius))
    {
        soundExplosion2.Play();
    }

    bool shipDestroyed = player.CheckForShipAsteroidCollision(shipModel.Meshes[0].BoundingSphere.Radius,
                asteroidModel.Meshes[0].BoundingSphere.Radius);
    if (shipDestroyed)
    {
        soundExplosion3.Play();
    }

    base.Update(gameTime);
}

When you are finished, your Player class will look like this:

using System;
using System.Collections;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace GoingBeyond5
{
    public class Player
    {
        internal GamePadState lastState;

        internal Ship ship = new Ship();
        internal Asteroid[] asteroidList = new Asteroid[GameConstants.NumAsteroids];
        internal Bullet[] bulletList = new Bullet[GameConstants.NumBullets];

        internal int score;

        Random random = new Random();

        public Player()
        {
            ResetAsteroids();
        }

        internal void Update(GameTime gameTime)
        {
            float timeDelta = (float)gameTime.ElapsedGameTime.TotalSeconds;
            // Add velocity to the current position.
            ship.Position += ship.Velocity;

            // Bleed off velocity over time.
            ship.Velocity *= 0.95f;

            for (int i = 0; i < GameConstants.NumAsteroids; i++)
            {
                asteroidList[i].Update(timeDelta);
            }

            for (int i = 0; i < GameConstants.NumBullets; i++)
            {
                if (bulletList[i].isActive)
                {
                    bulletList[i].Update(timeDelta);
                }
            }
        }

        private void ResetAsteroids()
        {
            float xStart;
            float yStart;
            for (int i = 0; i < GameConstants.NumAsteroids; i++)
            {
                if (random.Next(2) == 0)
                {
                    xStart = (float)-GameConstants.PlayfieldSizeX;
                }
                else
                {
                    xStart = (float)GameConstants.PlayfieldSizeX;
                }
                yStart = (float)random.NextDouble() * GameConstants.PlayfieldSizeY;
                asteroidList[i].position = new Vector3(xStart, yStart, 0.0f);
                double angle = random.NextDouble() * 2 * Math.PI;
                asteroidList[i].direction.X = -(float)Math.Sin(angle);
                asteroidList[i].direction.Y = (float)Math.Cos(angle);
                asteroidList[i].speed = GameConstants.AsteroidMinSpeed +
                   (float)random.NextDouble() * GameConstants.AsteroidMaxSpeed;
                asteroidList[i].isActive = true;
            }
        }

        internal void ShootBullet()
        {
            //add another bullet.  Find an inactive bullet slot and use it
            //if all bullets slots are used, ignore the user input
            for (int i = 0; i < GameConstants.NumBullets; i++)
            {
                if (!bulletList[i].isActive)
                {
                    bulletList[i].direction = ship.RotationMatrix.Forward;
                    bulletList[i].speed = GameConstants.BulletSpeedAdjustment;
                    bulletList[i].position = ship.Position
                        + (200 * bulletList[i].direction);
                    bulletList[i].isActive = true;
                    score -= GameConstants.ShotPenalty;
                    return;
                }
            }
        }

        internal void WarpToCenter()
        {
            ship.Position = Vector3.Zero;
            ship.Velocity = Vector3.Zero;
            ship.Rotation = 0.0f;
            ship.isActive = true;
            score -= GameConstants.WarpPenalty;

        }
        internal bool CheckForBulletAsteroidCollision(float bulletRadius, float asteroidRadius)
        {
            for (int i = 0; i < asteroidList.Length; i++)
            {
                if (asteroidList[i].isActive)
                {
                    BoundingSphere asteroidSphere =
                      new BoundingSphere(asteroidList[i].position, asteroidRadius *
                          GameConstants.AsteroidBoundingSphereScale);
                    for (int j = 0; j < bulletList.Length; j++)
                    {
                        if (bulletList[j].isActive)
                        {
                            BoundingSphere bulletSphere = new BoundingSphere(bulletList[j].position,
                              bulletRadius);
                            if (asteroidSphere.Intersects(bulletSphere))
                            {
                                asteroidList[i].isActive = false;
                                bulletList[j].isActive = false;
                                score += GameConstants.KillBonus;
                                return true; //no need to check other bullets
                            }
                        }
                    }
                }
            }
            return false;
        }
        internal bool CheckForShipAsteroidCollision(float shipRadius, float asteroidRadius)
        {
            //ship-asteroid collision check
            if (ship.isActive)
            {
                BoundingSphere shipSphere = new BoundingSphere(ship.Position, shipRadius *
                    GameConstants.ShipBoundingSphereScale);
                for (int i = 0; i < asteroidList.Length; i++)
                {
                    if (asteroidList[i].isActive)
                    {
                        BoundingSphere b = new BoundingSphere(asteroidList[i].position, asteroidRadius *
                            GameConstants.AsteroidBoundingSphereScale);
                        if (b.Intersects(shipSphere))
                        {
                            //blow up ship
                            //soundExplosion3.Play();
                            ship.isActive = false;
                            asteroidList[i].isActive = false;
                            score -= GameConstants.DeathPenalty;
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
}

With this step complete, update the UpdateInput method of the game to use the new Player class.

protected void UpdateInput()
{
    // Get the game pad state.
    currentState = GamePad.GetState(PlayerIndex.One);
    lastState = player.lastState;

    if (currentState.IsConnected)
    {

        if (player.ship.isActive)
        {
            player.ship.Update(currentState);
            PlayEngineSound(currentState);
        }
        // In case you get lost, press B to warp back to the center.
        if (IsButtonPressed(Buttons.B))
        {
            player.WarpToCenter();
            soundHyperspaceActivation.Play();
        }

        //are we shooting?
        if (player.ship.isActive && IsButtonPressed(Buttons.A))
        {
            player.ShootBullet();
            soundWeaponsFire.Play();
            bool isFiring = true;
        }
        player.lastState = currentState;
    }
}

Finally, change the Game.Draw so that it accesses the data from the new Player class.

protected override void Draw(GameTime gameTime)
{
    graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
    spriteBatch.Draw(stars, new Rectangle(0, 0, 800, 600), Color.White);
    spriteBatch.End();

    Matrix shipTransformMatrix = player.ship.RotationMatrix
            * Matrix.CreateTranslation(player.ship.Position);
    if (player.ship.isActive)
    {
        DrawModel(shipModel, shipTransformMatrix, shipTransforms);
        
    }
    for (int i = 0; i < GameConstants.NumAsteroids; i++)
    {
        Matrix asteroidTransform =
            Matrix.CreateTranslation(player.asteroidList[i].position);
        if (player.asteroidList[i].isActive)
        {
            DrawModel(asteroidModel, asteroidTransform, asteroidTransforms);
        }
    }
    
    for (int i = 0; i < GameConstants.NumBullets; i++)
    {
        if (player.bulletList[i].isActive)
        {
            Matrix bulletTransform =
              Matrix.CreateTranslation(player.bulletList[i].position);
            DrawModel(bulletModel, bulletTransform, bulletTransforms);
        }
    }
    spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
          SpriteSortMode.Immediate, SaveStateMode.None);
    spriteBatch.DrawString(lucidaConsole, "Score: " + player.score,
                           scorePosition, Color.LightGreen);
    spriteBatch.End();
    base.Draw(gameTime);
}

At this point, you should be able to compile and run your game! We have been making small, iterative changes to make the code more elegant, but when you run the game, it should still look essentially the same. In the next step we will begin making the changes to the game rendering to allow the players to see one another.

Step 3: Split the Screen

Now that we have a way to create multiple players, we need a place to display the second player. In Game1.cs, create three new member variables to store the main viewport, the left viewport, and the right viewport.

Viewport mainViewport;
Viewport leftViewport;
Viewport rightViewport;

When we use only half of the screen, the aspect ratio will change. In the constructor for the game, divide the aspect ratio by 2 to account for the split screen. If you compile and run your game after making this change, the game will be distorted due to the fact that the aspect ratio is no longer equivalent to the width and height of the back buffer.

public Game1()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";

    // this game is split screen, so divide the aspect ratio by 2.
    aspectRatio = (float)GraphicsDeviceManager.DefaultBackBufferWidth / (2 * GraphicsDeviceManager.DefaultBackBufferHeight);
}

In LoadContent, initialize the values for each viewport. We first set the main viewport to equal the graphics device viewport. The left viewport and right viewport will have a width that is half of the main viewport, with the right viewport beginning one pixel past the center of the screen.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);

    shipModel = Content.Load<Model>("Models/p1_wedge");
    shipTransforms = SetupEffectDefaults(shipModel);
    asteroidModel = Content.Load<Model>("Models/asteroid1");
    asteroidTransforms = SetupEffectDefaults(asteroidModel);
    bulletModel = Content.Load<Model>("Models/pea_proj");
    bulletTransforms = SetupEffectDefaults(bulletModel);
    stars = Content.Load<Texture2D>("Textures/B1_stars");
    soundEngine = Content.Load<SoundEffect>("Audio/Waves/engine_2");
    soundHyperspaceActivation = Content.Load<SoundEffect>("Audio/Waves/hyperspace_activate");
    soundExplosion2 = Content.Load<SoundEffect>("Audio/Waves/explosion2");
    soundExplosion3 = Content.Load<SoundEffect>("Audio/Waves/explosion3");
    soundWeaponsFire = Content.Load<SoundEffect>("Audio/Waves/tx0_fire1");
    lucidaConsole = Content.Load<SpriteFont>("Fonts/Lucida Console");

    // Initialize the values for each viewport
    mainViewport = GraphicsDevice.Viewport;
    leftViewport = mainViewport;
    rightViewport = mainViewport;
    leftViewport.Width = leftViewport.Width / 2;
    rightViewport.Width = rightViewport.Width / 2;
    rightViewport.X = leftViewport.Width + 1;
}

Notice that the Draw method in game contains all the code necessary to draw a player. Rename Draw to DrawPlayer, and change the method so that it returns nothing and accepts two arguments, the Player to draw, and the Viewport to draw the player in. At the beginning of DrawPlayer, set the graphics device viewport to the viewport argument. Don't forget to remove the call to the Draw method of the base class, located at the end of the function.

void DrawPlayer(Player player, Viewport viewport)
{
    graphics.GraphicsDevice.Viewport = viewport;

    spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
    spriteBatch.Draw(stars, new Rectangle(0, 0, 800, 600), Color.White);
    spriteBatch.End();

    Matrix shipTransformMatrix = player.ship.RotationMatrix
            * Matrix.CreateTranslation(player.ship.Position);
    if (player.ship.isActive)
    {
        DrawModel(shipModel, shipTransformMatrix, shipTransforms);
        
    }
    for (int i = 0; i < GameConstants.NumAsteroids; i++)
    {
        Matrix asteroidTransform =
            Matrix.CreateTranslation(player.asteroidList[i].position);
        if (player.asteroidList[i].isActive)
        {
            DrawModel(asteroidModel, asteroidTransform, asteroidTransforms);
        }
    }
    
    for (int i = 0; i < GameConstants.NumBullets; i++)
    {
        if (player.bulletList[i].isActive)
        {
            Matrix bulletTransform =
              Matrix.CreateTranslation(player.bulletList[i].position);
            DrawModel(bulletModel, bulletTransform, bulletTransforms);
        }
    }
    spriteBatch.Begin(SpriteBlendMode.AlphaBlend,
          SpriteSortMode.Immediate, SaveStateMode.None);
    spriteBatch.DrawString(lucidaConsole, "Score: " + player.score,
                           scorePosition, Color.LightGreen);
    spriteBatch.End();
}

Finally, recreate the Draw method, calling DrawPlayer in this new, shorter method, and move the call to base.Draw(gameTime) into this method.

protected override void Draw(GameTime gameTime)
{
    DrawPlayer(player, leftViewport);
    base.Draw(gameTime);
}

If you run your game now, you will see that the game for the local player is now showing on the left side of the screen:

Bb975645.GoingBeyond5Step3(en-US,XNAGameStudio.30).jpg

Step 4: Game State Management

In a networked game, you will want the game to display different information depending on the state of the game. For example, if the player has not yet signed in, you will want to display a message with instructions on how to sign in. If the player would like to host or join an multiplayer game on the subnet, you will need to display a list of available games. Once a player selects a game to join you will need to allow the player to wait in the lobby until all players are ready to start the game.

Bb975645.GoingBeyond5-1(en-US,XNAGameStudio.30).gif

To do this, we will create the main Update and Draw methods so that they choose the right area to update or draw - the title screen, the list of available sessions, the lobby, or the game. In this step of the tutorial, we will make separate update and draw methods to handle the particular requirements of the title screen, session selection screen, lobby, and game.

Begin by declaring the new global variables we will need in the game to manage the network session. Declare a variable to hold the current network session for the game, a collection of available network sessions, the index of the network session a player has selected to join, and a packet reader and writer to read network data.

NetworkSession networkSession;
AvailableNetworkSessionCollection availableSessions;
int selectedSessionIndex;
PacketReader packetReader = new PacketReader();
PacketWriter packetWriter = new PacketWriter();

While you are looking at the global variables for the game, comment out the Player instance that is declared there. We are going to instead associate the player data with a person who has signed into the session.

//Player player = new Player();

To use network services in a game, we need to add a GamerServicesComponent to the game. Once this component has been added, we can respond to the event that occurs when a gamer signs in.

In the Game constructor, add a new GamerServicesComponent to the collection of game components. Also in the Game constructor, add a new event handler for the SignedIn event.

public Game1()
{
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";

    aspectRatio = (float)GraphicsDeviceManager.DefaultBackBufferWidth / (2 * GraphicsDeviceManager.DefaultBackBufferHeight);

    // Add Gamer Services
    Components.Add(new GamerServicesComponent(this));

    // Respond to the SignedInGamer event
    SignedInGamer.SignedIn += new EventHandler<SignedInEventArgs>(SignedInGamer_SignedIn);
}

One of the arguments that is passed to the SignedIn event is an instance of SignedInEventArgs. This type contains a property called Gamer. Each Gamer has a Tag object which can be used to attach data to the gamer. We are going to use the Tag property to store the Player that is associated with a particular gamer. Because this data does not exist yet when a gamer signs in, we will create a new Player when responding to the SignedIn event.

void SignedInGamer_SignedIn(object sender, SignedInEventArgs e)
{
    e.Gamer.Tag = new Player();
}

We want to look at the input for the list of signed-in gamers during gameplay. Rename the current Update function HandleGameplayInput and set the accessibility level to "private." Also remove the call to base.Update. Change this method to accept a Player argument in addition to the GameTime. Also update the network session when this method is called.

private void HandleGameplayInput(Player player, GameTime gameTime)
{
    if (IsButtonPressed(Buttons.Back))
        this.Exit();

    // change UpdateInput to take a Player
    UpdateInput(player);

    player.Update(gameTime);

    networkSession.Update();

    //base.Update(gameTime);
}

There is a static property called SignedInGamers that contains the list of all signed-in players. We will use this property in the Update method to get the Player data from a SignedInGamer. Note that this loop wraps the update code that was already in this method. We also change UpdateInput to accept an instance of Player. This method also updates the lastState and currentState member variables, so the update of these variables can be removed from UpdateInput

protected override void Update(GameTime gameTime)
{
    if (!Guide.IsVisible)
    {
        foreach (SignedInGamer signedInGamer in SignedInGamer.SignedInGamers)
        {
            Player player = signedInGamer.Tag as Player;
            lastState = player.lastState;
            currentState = GamePad.GetState(signedInGamer.PlayerIndex);

            if (networkSession != null)
            {
                // Handle the lobby input here...
            }
            else if (availableSessions != null)
            {
                // Handle the available sessions input here..
            }
            else
            {
                // Handle the title screen input here..
            }
            player.lastState = currentState;
        }
    }
    base.Update(gameTime);
}

The UpdateInput method will stay the same, except that it now gets the player to update from the Player argument instead of a global variable. There is also no need for the currentState and lastState variables to be updated in this method, so you can comment out these lines.

protected void UpdateInput(Player player)
{
    //// Get the game pad state.
    //currentState = GamePad.GetState(PlayerIndex.One);
    //lastState = player.lastState;
    ...
        //player.lastState = currentState;
    }
}

Finally, rename the Draw method to DrawGameplay and set the accessibility to "private." In this method, we will check to see if a network session has been created or joined before we start drawing the game. For each person in the networked game, we will draw to a different area of the screen.

private void DrawGameplay(GameTime gameTime)
{
    GraphicsDevice.Viewport = mainViewport;
    GraphicsDevice.Clear(Color.CornflowerBlue);

    Player player;
    if (networkSession != null)
    {

        foreach (NetworkGamer networkGamer in networkSession.AllGamers)
        {
            player = networkGamer.Tag as Player;
            if (networkGamer.IsLocal)
            {
                DrawPlayer(player, leftViewport);
            }
            else
            {
                DrawPlayer(player, rightViewport);
            }
        }
    }
}

If you compile and run your game at this point, you will see a blank screen. Do not panic! This is because we made the game play state of the game contingent on the user signing in and creating or joining a network session. We will implement these game states next.

The Title Screen

First, create a method to draw the title screen.

private void DrawTitleScreen()
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    string message = "";

    if (SignedInGamer.SignedInGamers.Count == 0)
    {
        message = "No profile signed in!  \nPress the Home key on the keyboard or \nthe Xbox Guide Button on the controller to sign in.";
    }
    else
    {
        message += "Press A to create a new session\nX to search for sessions\nB to quit\n\n";
    }
    spriteBatch.Begin();
    spriteBatch.DrawString(lucidaConsole, message, new Vector2(101, 101), Color.Black);
    spriteBatch.DrawString(lucidaConsole, message, new Vector2(100, 100), Color.White);
    spriteBatch.End();
}

Next, change the Draw method so that it draws our new title screen if there is no network session available and no available sessions to list.

protected override void Draw(GameTime gameTime)
{
    if (networkSession != null)
    {
        // If the session is not null, we're either in the lobby or playing the game...
    }
    else if (availableSessions != null)
    {
        // Show the available session...
    }
    else
    {
        DrawTitleScreen();
    }

    base.Draw(gameTime);
}

We also need to handle the title screen input.

protected void HandleTitleScreenInput()
{
    if (IsButtonPressed(Buttons.A))
    {
        CreateSession();
    }
    else if (IsButtonPressed(Buttons.X))
    {
        availableSessions = NetworkSession.Find(
            NetworkSessionType.SystemLink, 1, null);

        selectedSessionIndex = 0;
    }
    else if (IsButtonPressed(Buttons.B))
    {
        Exit();
    }
}

This method creates a session if the user selects this option.

void CreateSession()
{
    networkSession = NetworkSession.Create(
        NetworkSessionType.SystemLink,
        1, 8, 2,
        null);

    networkSession.AllowHostMigration = true;
    networkSession.AllowJoinInProgress = true;

    HookSessionEvents();
}

When creating a session, subscribe to the GamerJoined event.

private void HookSessionEvents()
{
    networkSession.GamerJoined += new EventHandler<GamerJoinedEventArgs>(networkSession_GamerJoined);
}

When responding to the GamerJoined event, we want to either create a new Player if the player is not local, or get the player if the player has already signed in and has a Player object associated with it.

void networkSession_GamerJoined(object sender, GamerJoinedEventArgs e)
{
    if (!e.Gamer.IsLocal)
    {
        e.Gamer.Tag = new Player();
    }
    else
    {
        e.Gamer.Tag = GetPlayer(e.Gamer.Gamertag);
    }
}
Player GetPlayer(String gamertag)
{
    foreach (SignedInGamer signedInGamer in SignedInGamer.SignedInGamers)
    {
        if (signedInGamer.Gamertag == gamertag)
        {
            return signedInGamer.Tag as Player;
        }
    }

    return new Player();
}

If you run your game, you will see the new title screen with sign-in instructions for the player. Because we have a GamerServicesComponent added to the game, you can now sign in or view the Guide when you follow the instructions on the screen.

Bb975645.GB5TitleScreen(en-US,XNAGameStudio.30).jpg

Once a player signs in, the instructions for finding or creating a new network session will also be displayed.

Bb975645.GB5CreateOrSearch(en-US,XNAGameStudio.30).jpg

The Lobby

First, we will create a game screen to display the players waiting in the lobby.

private void DrawLobby()
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    float y = 100;

    spriteBatch.DrawString(lucidaConsole, "Lobby (A=ready, B=leave)", new Vector2(101, y + 1), Color.Black);
    spriteBatch.DrawString(lucidaConsole, "Lobby (A=ready, B=leave)", new Vector2(101, y), Color.White);

    y += lucidaConsole.LineSpacing * 2;

    foreach (NetworkGamer gamer in networkSession.AllGamers)
    {
        string text = gamer.Gamertag;

        Player player = gamer.Tag as Player;

        if (player.picture == null)
        {
            GamerProfile gamerProfile = gamer.GetProfile();
            player.picture = gamerProfile.GamerPicture;
        }

        if (gamer.IsReady)
            text += " - ready!";

        spriteBatch.Draw(player.picture, new Vector2(100, y), Color.White);
        spriteBatch.DrawString(lucidaConsole, text, new Vector2(170, y), Color.White);

        y += lucidaConsole.LineSpacing + 64;
    }
    spriteBatch.End();

}

Next, we will need a method to handle any input from the user while the user is in the lobby.

protected void HandleLobbyInput()
{
    // Signal I'm ready to play!
    if (IsButtonPressed(Buttons.A))
    {
        foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            gamer.IsReady = true;
    }

    if (IsButtonPressed(Buttons.B))
    {
        networkSession = null;
        availableSessions = null;
    }

    // The host checks if everyone is ready, and moves to game play if true.
    if (networkSession.IsHost)
    {
        if (networkSession.IsEveryoneReady)
            networkSession.StartGame();
    }

    // Pump the underlying session object.
    networkSession.Update();

}

Add code to the Draw method so that it will call the DrawLobby function if the user should be in the lobby.

protected override void Draw(GameTime gameTime)
{
    if (networkSession != null)
    {
        // Draw the Lobby
        if (networkSession.SessionState == NetworkSessionState.Lobby)
            DrawLobby();
    }
    else if (availableSessions != null)
    {
        // Show the available session...
    }
    else
    {
        DrawTitleScreen();
    }

    base.Draw(gameTime);
}

Finally, change the Update method of the game so it will call the method that handles the lobby input.

protected override void Update(GameTime gameTime)
{
    if (!Guide.IsVisible)
    {
        foreach (SignedInGamer signedInGamer in SignedInGamer.SignedInGamers)
        {
            Player player = signedInGamer.Tag as Player;
            lastState = player.lastState;
            currentState = GamePad.GetState(signedInGamer.PlayerIndex);

            if (networkSession != null)
            {
                if (networkSession.SessionState == NetworkSessionState.Lobby)
                    HandleLobbyInput();
            }
            else if (availableSessions != null)
            {
                // Handle the available sessions input here...
            }
            else
            {
                HandleTitleScreenInput();
            }
            player.lastState = currentState;
        }
    }
    base.Update(gameTime);
}

Compile and run the game. Sign in and create a session to see the lobby.

Bb975645.GB5MySessionLobby(en-US,XNAGameStudio.30).jpg

List Available Network Sessions

Just as we created a method to draw an update the other game states, we will also create methods to draw and update the list of available network sessions.

private void DrawAvailableSessions()
{
    GraphicsDevice.Clear(Color.CornflowerBlue);
    spriteBatch.Begin();
    float y = 100;

    spriteBatch.DrawString(lucidaConsole, "Available sessions (A=join, B=back)", new Vector2(101, y + 1), Color.Black);
    spriteBatch.DrawString(lucidaConsole, "Available sessions (A=join, B=back)", new Vector2(100, y), Color.White);

    y += lucidaConsole.LineSpacing * 2;

    int selectedSessionIndex = 0;

    for (
        int sessionIndex = 0;
        sessionIndex < availableSessions.Count;
        sessionIndex++)
    {
        Color color = Color.Black;

        if (sessionIndex == selectedSessionIndex)
            color = Color.Yellow;

        spriteBatch.DrawString(lucidaConsole, availableSessions[sessionIndex].HostGamertag, new Vector2(100, y), color);

        y += lucidaConsole.LineSpacing;
    }
    spriteBatch.End();
}

protected void HandleAvailableSessionsInput()
{
    if (IsButtonPressed(Buttons.A))
    {
        // Join the selected session.
        if (availableSessions.Count > 0)
        {
            networkSession = NetworkSession.Join(availableSessions[selectedSessionIndex]);
            HookSessionEvents();

            availableSessions.Dispose();
            availableSessions = null;
        }
    }
    else if (IsButtonPressed(Buttons.DPadUp))
    {
        // Select the previous session from the list.
        if (selectedSessionIndex > 0)
            selectedSessionIndex--;
    }
    else if (IsButtonPressed(Buttons.DPadDown))
    {
        // Select the next session from the list.
        if (selectedSessionIndex < availableSessions.Count - 1)
            selectedSessionIndex++;
    }
    else if (IsButtonPressed(Buttons.B))
    {
        // Go back to the title screen.
        availableSessions.Dispose();
        availableSessions = null;
    }

}

Change the Draw and Update methods to call these methods when the game should show the list of available sessions.

protected override void Draw(GameTime gameTime)
{
    if (networkSession != null)
    {
        // Draw the Lobby
        if (networkSession.SessionState == NetworkSessionState.Lobby)
            DrawLobby();
    }
    else if (availableSessions != null)
    {
        DrawAvailableSessions();
    }
    else
    {
        DrawTitleScreen();
    }

    base.Draw(gameTime);
}
protected override void Update(GameTime gameTime)
{
    if (!Guide.IsVisible)
    {
        foreach (SignedInGamer signedInGamer in SignedInGamer.SignedInGamers)
        {
            Player player = signedInGamer.Tag as Player;
            lastState = player.lastState;
            currentState = GamePad.GetState(signedInGamer.PlayerIndex);

            if (networkSession != null)
            {
                if (networkSession.SessionState == NetworkSessionState.Lobby)
                    HandleLobbyInput();
            }
            else if (availableSessions != null)
            {
                HandleAvailableSessionsInput();
            }
            else
            {
                HandleTitleScreenInput();
            }
            player.lastState = currentState;
        }
    }
    base.Update(gameTime);
}
Bb975645.GB5AvailableSessions(en-US,XNAGameStudio.30).jpg

When a player joins an available session, the other players waiting in the lobby can now be seen.

Bb975645.GB5Lobby(en-US,XNAGameStudio.30).jpg

Step 5: Receive Network Data

We can now create a network session with mulitiple players in the session, but we need a way for players to send and receive data from other players in the game.

First, create a method to receive the data from a network gamer.

void ReceiveNetworkData(LocalNetworkGamer gamer, GameTime gameTime)
{
    while (gamer.IsDataAvailable)
    {
        NetworkGamer sender;
        gamer.ReceiveData(packetReader, out sender);

        if (!sender.IsLocal)
        {
            Player player = sender.Tag as Player;
            player.ship.isActive = packetReader.ReadBoolean();
            player.ship.Position = packetReader.ReadVector3();
            player.ship.Rotation = packetReader.ReadSingle();
            player.score = packetReader.ReadInt32();
            if (packetReader.ReadBoolean())
            {
                player.ShootBullet();
            }
            if (packetReader.ReadBoolean())
            {
                player.ship.isActive = false;
            }
            for (int i = 0; i < GameConstants.NumAsteroids; i++)
            {
                player.asteroidList[i].isActive = packetReader.ReadBoolean();
                player.asteroidList[i].position = packetReader.ReadVector3();
            }
            player.Update(gameTime);
        }
    }
}

Next, change the UpdateInput method to call ReceiveNetworkData for every signed-in player on the local system that might receive data. Note that the ReceiveNetworkData needs an instance of GameTime to pass to the Player.Update method, so we will update UpdateInput to take the GameTime as a parameter. At the end of the UpdateInput Method, send the data from the local gamer to any network gamers in the game.

private void HandleGameplayInput(Player player, GameTime gameTime)
{
    ...
    UpdateInput(player, gameTime);
    ...
}
private void UpdateInput(Player player, GameTime gameTime)
{
    bool isFiring = false;
    bool shipDestroyed = false;

    foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
    {
        ReceiveNetworkData(gamer, gameTime);

        // this code is the same code we have been using to update the player input
        if (currentState.IsConnected)
        {

            if (player.ship.isActive)
            {
                player.ship.Update(currentState);
                PlayEngineSound(currentState);
            }
            // In case you get lost, press B to warp back to the center.
            if (IsButtonPressed(Buttons.B))
            {
                player.WarpToCenter();
                // Make a sound when we warp.
                soundHyperspaceActivation.Play();
            }

            //are we shooting?
            if (player.ship.isActive && IsButtonPressed(Buttons.A))
            {
                player.ShootBullet();
                soundWeaponsFire.Play();
                isFiring = true;
            }

            if (player.CheckForBulletAsteroidCollision(
                                     bulletModel.Meshes[0].BoundingSphere.Radius,
                                     asteroidModel.Meshes[0].BoundingSphere.Radius))
            {
                soundExplosion2.Play();
            }

            shipDestroyed = player.CheckForShipAsteroidCollision(
                        shipModel.Meshes[0].BoundingSphere.Radius,
                        asteroidModel.Meshes[0].BoundingSphere.Radius);

            if (shipDestroyed)
            {
                soundExplosion3.Play();
            }
        }
        packetWriter.Write(player.ship.isActive);
        packetWriter.Write(player.ship.Position);
        packetWriter.Write(player.ship.Rotation);
        packetWriter.Write(player.score);
        packetWriter.Write(isFiring);
        packetWriter.Write(shipDestroyed);
        for (int i = 0; i < GameConstants.NumAsteroids; i++)
        {
            packetWriter.Write(player.asteroidList[i].isActive);
            packetWriter.Write(player.asteroidList[i].position);
        }

        gamer.SendData(packetWriter, SendDataOptions.None);
    }
}

Finally, change Draw and Update so that each method calls the DrawGameplay and HandleGameplayInput method at the appropriate time.

protected override void Draw(GameTime gameTime)
{
    if (networkSession != null)
    {
        // Draw the Lobby
        if (networkSession.SessionState == NetworkSessionState.Lobby)
            DrawLobby();
        else
            DrawGameplay(gameTime);
    }
    else if (availableSessions != null)
    {
        DrawAvailableSessions();
    }
    else
    {
        DrawTitleScreen();
    }

    base.Draw(gameTime);
}
protected override void Update(GameTime gameTime)
{
    if (!Guide.IsVisible)
    {
        foreach (SignedInGamer signedInGamer in SignedInGamer.SignedInGamers)
        {
            Player player = signedInGamer.Tag as Player;
            lastState = player.lastState;
            currentState = GamePad.GetState(signedInGamer.PlayerIndex);

            if (networkSession != null)
            {
                if (networkSession.SessionState == NetworkSessionState.Lobby)
                    HandleLobbyInput();
                else
                    HandleGameplayInput(player, gameTime);
            }
            else if (availableSessions != null)
            {
                HandleAvailableSessionsInput();
            }
            else
            {
                HandleTitleScreenInput();
            }
            player.lastState = currentState;
        }
    }
    base.Update(gameTime);
}

With this final change, your game can now be played by two players over the network.

Where do you go from here? Using the Player class, you could extend the game to allow for two players on the same local machine. You could also change the game to display more than two players by splitting the screen again, or get rid of the split-screen and implement code to allow the players to shoot at one another.

More advanced screen management techniques are available from the XNA Creators Club Online Web site, along with numerous other game programming techniques that can be used to add functionality to this game.

Community Additions

ADD
Show:
© 2014 Microsoft