Recent Changes

Sunday, April 25

  1. page Project 3 edited ... } At this point, the game will play exactly the same as the original game, except it will onl…
    ...
    }
    At this point, the game will play exactly the same as the original game, except it will only occupy the left half of the screen.
    Step Four: gameGame State Management
    ...
    can start. SinceThis behavior is illustrated in the following diagram, courtesy of the XNA documentation.
    {Game_State_Diagram.PNG}
    Since
    each phase
    NetworkSession networkSession;
    AvailableNetworkSessionCollection availableSessions;
    (view changes)
    5:35 pm
  2. page Project 3 edited Adding Multiplayer and Network Support to a Game Introduction For this project we will be dis…

    Adding Multiplayer and Network Support to a GameIntroduction
    For this project we will be discussing adding network and multiplayer support to a game. Multiplayer capability is very important for games. These days there are an increasing number of casual gamers, who often prefer to play video games together with friends or over the internet with strangers rather than play by themselves. Games with only a single player component will not be able to reach as wide a market and will not do as well commercially. We will base this project off of a finished game from the Tutorials for Microsoft XNA Game Studio. This game involves a ship flying around and shooting asteroids. The finished code for the game without multiplayer support is available for download in the XNA Game Studio documentation. We have covered that version of the game in a previous project, and the scope of this project is modifying that game to allow two players to play the game over a network.
    Step One: Simplify the Update Method
    (view changes)
    5:24 pm
  3. page Project 3 edited ... } } ... this code: bool CheckForBulletAsteroidCollision(float bulletRadius, float ast…
    ...
    }
    }
    ...
    this code:
    bool CheckForBulletAsteroidCollision(float bulletRadius,
    float asteroidRadius) {
    ...
    internal int score;
    Random random = new Random();
    ...
    as well.
    We will also move some of the information from the Update method of the Game class to the Player class. The update in the Game class will then call Player's Update method in order to perform the actions that are brought over. This is what Player's Update should look like. In the Game class, you simply remove this code and put in a call to the Player Update.
    internal void Update(GameTime gameTime) {
    ...
    base.Draw(gameTime);
    }
    ...
    the game.
    Step Three: Split the Screen
    Now that we have a Player class, we have the ability to support multiple players at the same time. We will also need a display for each player. The method we will use is to split the screen in half and let each player's display occupy one side. We will need a Viewport variable for the main viewport and each side viewport.
    (view changes)
    5:23 pm
  4. page Project 3 edited Adding Multiplayer and Network Support to a Game For this project we will be discussing adding …

    Adding Multiplayer and Network Support to a Game
    For this project we will be discussing adding network and multiplayer support to a game. Multiplayer capability is very important for games. These days there are an increasing number of casual gamers, who often prefer to play video games together with friends or over the internet with strangers rather than play by themselves. Games with only a single player component will not be able to reach as wide a market and will not do as well commercially. We will base this project off of a finished game from the Tutorials for Microsoft XNA Game Studio. This game involves a ship flying around and shooting asteroids. The finished code for the game without multiplayer support is available for download in the XNA Game Studio documentation. We have covered that version of the game in a previous project, and the scope of this project is modifying that game to allow two players to play the game over a network.
    Step One: Simplify the Update Method
    To begin adding multiplayer functionality to our game, we must first clean up the Update and Draw methods some. To do this, we will remove some portions of the code from these large methods, and place it in separate methods to handle that functionality. To start, lets find the Bullet-Asteroid collision check code in the Update method. In the method, this code looks like this:
    //bullet-asteroid collision check
    for (int i = 0; i < asteroidList.Length; i++) {
    if (asteroidList[i].isActive) {
    BoundingSphere asteroidSphere =
    new BoundingSphere(asteroidList[i].position,
    asteroidModel.Meshes[0].BoundingSphere.Radius *
    GameConstants.AsteroidBoundingSphereScale);
    for (int j = 0; j < bulletList.Length; j++) {
    if (bulletList[j].isActive) {
    BoundingSphere bulletSphere = new BoundingSphere(
    bulletList[j].position,
    bulletModel.Meshes[0].BoundingSphere.Radius);
    if (asteroidSphere.Intersects(bulletSphere)) {
    soundBank.PlayCue("explosion2");
    asteroidList[i].isActive = false;
    bulletList[j].isActive = false;
    score += GameConstants.KillBonus;
    break; //no need to check other bullets
    }
    }
    }
    }
    }
    This code can all be pulled out into its own method, and then replaced in the Update method with a call to that method. Here is the method that will replace this 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;
    }
    Notice we did change a few things in the code. Instead of a break statement in the last if, we now return true, to say that we do not need to check any other bullets to see if they are colliding with that particular asteroid. Otherwise, we will return false to show that no asteroid has been hit.
    Next, we need to do the same thing to the code to handle the collisions between an asteroid and a ship. In the Update method, this looks like this:
    if (ship.isActive) {
    BoundingSphere shipSphere = new BoundingSphere(
    ship.Position, ship.Model.Meshes[0].BoundingSphere.Radius *
    GameConstants.ShipBoundingSphereScale);
    for (int i = 0; i < asteroidList.Length; i++) {
    if (asteroidList[i].isActive) {
    BoundingSphere b = new BoundingSphere(asteroidList[i].position,
    asteroidModel.Meshes[0].BoundingSphere.Radius *
    GameConstants.AsteroidBoundingSphereScale);
    if (b.Intersects(shipSphere)) {
    //blow up ship
    soundBank.PlayCue("explosion3");
    ship.isActive = false;
    asteroidList[i].isActive = false;
    score -= GameConstants.DeathPenalty;
    break; //exit the loop
    }
    }
    }
    }
    This code will be extracted to its own method, just like we did with the asteroid-bullet collisions:
    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;
    }
    Again, we replaced the break statement with return statements to reflect the change from a loop to a method. At this point, we can replace both of those loops with a call to the methods we just made, so our Update method should look like this now:
    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);
    }
    Now that we have our Update method cleaned up some, lets look at one of the other methods in our Game class that got a little bit larger than it should be. We will remove a few things from the UpdateInput method the same way we did for the Update method. There are four possible things to remove from this method. We will pull out the code to shoot a bullet, to warp to the center, to play an engine sound, and to detect whether a button is pressed. Here are all four of those methods:
    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) {
    if (soundEngineInstance.State == SoundState.Stopped) {
    soundEngineInstance.Volume = 0.75f;
    soundEngineInstance.IsLooped = true;
    soundEngineInstance.Play();
    }
    else
    soundEngineInstance.Resume();
    }
    else if (currentState.Triggers.Right == 0) {
    if (soundEngineInstance.State == SoundState.Playing)
    soundEngineInstance.Pause();
    }
    }
    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;
    }
    With all of these methods extracted, our new UpdateInput method will look like this:
    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;
    }
    }
    This is considerably easier to understand what is going on than the old code with all of the other functions in it.
    Step Two: Encapsulate The Player Code
    With our code cleaned up, we can now begin setting up the program to having multiple players. To do this, we need to encapsulate all of our player data, allowing us to create multiple instances of a player. To do this, we first need a class called Player.
    using System;
    using System.Collections;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    namespace Project3 {
    public class Player {
    public Player() {
    }
    }
    }
    We will need functions from the Graphics, Input, and Framework libraries of the Microsoft XNA Studio for the player class, so they are included. Now we need to go back to the Game class and move anything that should be associated with the player over to out new class. This includes information on the gamepad, ship, bullets, asteroids, and the score. All of these should be moved over to the player class.
    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();
    A good way to see what else needs to be moved over now is to try to compile the code. You will receive errors from the Game class stating that there are missing references. These warnings should help you determine what in the Game class deals primarily with player data. One of the things that shows these errors is the resetAsteroids method. This method deals with resetting the game for a player. It should be moved over as well.
    We will also move some of the information from the Update method of the Game class to the Player class. The update in the Game class will then call Player's Update method in order to perform the actions that are brought over. This is what Player's Update should look like. In the Game class, you simply remove this code and put in a call to the Player Update.
    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);
    }
    }
    }
    We will also end up bringing all of the methods we created in Step One to the player class as well. In the end, the Player class should 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;
    }
    }
    }
    Now we need to do a few more things to the Game class to finish this step. In the Game class, we need to update the UpdateInput and Draw methods to use the new 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;
    }
    }
    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);
    }
    You should now be able to compile and run the game. If this was all done correctly, there should be very little difference between the execution before we began and now. All we have done thus far is to encapsulate and clean up the code we already had, without adding anything. Now we are ready to begin actually programming the multiplayer functionality of the game.
    Step Three: Split the Screen
    Now that we have a Player class, we have the ability to support multiple players at the same time. We will also need a display for each player. The method we will use is to split the screen in half and let each player's display occupy one side. We will need a Viewport variable for the main viewport and each side viewport.
    Viewport mainViewport;
    Viewport leftViewport;
    Viewport rightViewport;
    Next we have to update the aspect ratio to reflect the fact that the viewing area is only half the original width. The initialization of the aspect ratio is located inside the constructor for Game1.
    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);
    }
    We now need to add a section to the LoadContent method to initialize the parameters for each viewport.
    protected override void LoadContent() {
    // ... old code omitted for brevity
    // 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;
    }
    The only difference between the left and right viewports and the main viewport is the width, and the x coordinate of the right viewport. This is reflected in the code above. Next, since all the code for drawing the player is located inside the Draw method, further encapsulation is required. We will add all the player-drawing code to its own method and call that method from inside the Draw method, and also add a parameter to select which viewport to draw the player inside of. The DrawPlayer method looks like this:
    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();
    }
    And the reduced Draw method looks like this:
    protected override void Draw(GameTime gameTime) {
    DrawPlayer(player, leftViewport);
    base.Draw(gameTime);
    }
    At this point, the game will play exactly the same as the original game, except it will only occupy the left half of the screen.
    Step Four: game State Management
    Since we want the game to support multiplayer, we will need to add some new functionality to the project. We want the players to be able to sign into the game, create new sessions or browse existing sessions, and join sessions. Once all the players are ready, the game can start. Since each phase of this process will have different behavior and graphics, it makes sense to add new methods to handle each phase. First we will need some variables to get the network session up and running.
    NetworkSession networkSession;
    AvailableNetworkSessionCollection availableSessions;
    int selectedSessionIndex;
    PacketReader packetReader = new PacketReader();
    PacketWriter packetWriter = new PacketWriter();
    We need a variable to store the current network session, a variable that lists all the available sessions, and a reader and writer to send data back and forth. GamerServicesComponent is required to use the network functionality, so we will add it to our project in the Game1 constructor.
    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);
    }
    We will also add an event handler for the SignedIn event. The arguments to the SignedIn event include a Gamer property, and the Gamer contains a Tag. The Tag can be loaded with any data you want to associate with the Gamer. We will use the Tag to keep track of the Player object for the player, so we will create a new Player inside the SignedIn event handler and get rid of the earlier global Player declaration.
    void SignedInGamer_SignedIn(object sender, SignedInEventArgs e) {
    e.Gamer.Tag = new Player();
    }
    Next we will modify the input collection routine, since we will need to accept input from each signed in player instead of only one. First, we need to modify the UpdateInput method to accept a Player parameter. Also we will modify the Update method to accept a Player argument as well, which it will pass to the UpdateInput method. Since the Update method now represents only a portion of the update logic, we will rename it to HandleGameplayInput in order to reflect this.
    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);
    }
    We will create a new Update method to handle all the different possibilities (player has not signed in yet, player has signed in but hasn't joined a game, etc.) for each player.
    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);
    }
    Next we will update the Draw method to only draw the game if the game has actually started. Also, similarly to what we did with the Update method, we will rename the Draw method to DrawGameplay to reflect its new status as a small part of a greater whole.
    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);
    }
    }
    }
    }
    The next step is to implement the portions of the game that deal with signing in and creating and joining games.
    Title Screen
    The first thing the player will see is the title screen. Let's draw it now.
    private void DrawTitleScreen() {
    GraphicsDevice.Clear(Color.CornflowerBlue);
    string message = "";
    if (SignedInGamer.SignedInGamers.Count == 0) {
    message = "No profile signed in! \n" +
    "Press the Home key on the keyboard or \n" +
    "the Xbox Guide Button on the controller to sign in.";
    }
    else {
    message += "Press A to create a new session\n" +
    "X 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();
    }
    The title screen is very simple; it just displays a message with instructions for how the player should proceed, depending on whether they have signed in already or not. Next we will modify the Draw method so that it will call this DrawTitleScreen method at the appropriate times.
    protected override void Draw(GameTime gameTime) {
    if (networkSession != null) {
    }
    else if (availableSessions != null) {
    // Show the available session...
    }
    else {
    DrawTitleScreen();
    }
    base.Draw(gameTime);
    }
    We will also need a new method to handle player input while in the title screen.
    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();
    }
    }
    These input options of course reflect the choices that were offered to the player on the title screen display, namely that the A button creates a new session, X joins an existing session, and B exits. We will need a method to create the session, if the player presses A. The CreateSession method looks like this:
    void CreateSession() {
    networkSession = NetworkSession.Create(
    NetworkSessionType.SystemLink,
    1, 8, 2,
    null);
    networkSession.AllowHostMigration = true;
    networkSession.AllowJoinInProgress = true;
    HookSessionEvents();
    }
    The numeric parameters refer to the maximum number of local gamers, the maximum number of total gamers, and the number of private gamer slots. The HookSessionEvents needs to attach a new event handler to the GamerJoined event.
    private void HookSessionEvents() {
    networkSession.GamerJoined +=
    new EventHandler<GamerJoinedEventArgs>(
    networkSession_GamerJoined);
    }
    Now we will implement this event handler, which needs to associate the correct Player object to the Gamer that just joined, depending on whether the Gamer is a local player or not. Nonlocal players will need a new Player object, but local players may already have a Player object associated with them.
    void networkSession_GamerJoined(object sender, GamerJoinedEventArgs e) {
    if (!e.Gamer.IsLocal) {
    e.Gamer.Tag = new Player();
    }
    else {
    e.Gamer.Tag = GetPlayer(e.Gamer.Gamertag);
    }
    }
    And the GetPlayer method will get the Player object that is currently associated with the Gamer if there is one, otherwise it will create a new Player object.
    Player GetPlayer(String gamertag) {
    foreach (SignedInGamer signedInGamer in
    SignedInGamer.SignedInGamers) {
    if (signedInGamer.Gamertag == gamertag) {
    return signedInGamer.Tag as Player;
    }
    }
    return new Player();
    }
    With this, the title screen is finished. It displays the appropriate instructions for the player to continue depending on the current state of the game. The next step is to set up the lobby.
    The Lobby
    The lobby is the state of the game in which the players who have decided which session they wish to join wait for all the players to be ready. Once all the players are ready, the lobby phase is over and the game begins. The first step is to create the method to draw the lobby. The lobby display will show all the players who have joined the game and are waiting to start.
    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();
    }
    The lobby displays each player along with their picture and an indication of whether they are ready to play. Also, it shows the input options for the lobby state. Pressing A sets the player to ready state, and pressing B leaves the lobby and takes the player back to search for another game session. We will need a method that handles these inputs.
    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();
    }
    In addition to handling the input, the lobby input method for the host machine checks whether all the players are ready, and if they are, starts the game. Now that the lobby code is up, we have to modify the Draw and Update methods to use it.
    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...
    // Draw the Lobby
    if (networkSession.SessionState == NetworkSessionState.Lobby)
    DrawLobby();
    }
    else if (availableSessions != null) {
    // Show the available session...
    }
    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) {
    // Handle the available sessions input here...
    }
    else {
    HandleTitleScreenInput();
    }
    player.lastState = currentState;
    }
    }
    base.Update(gameTime);
    }
    With the lobby finished, the last component that needs to be added is the part that lists the available network sessions.
    List Available Network Sessions
    This phase is entered after the player signs into the game and presses X to join a existing game. Like the title screen and lobby, this phase also needs its own methods to draw and to handle input logic.
    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();
    }
    This method will draw a message that shows input instructions (A to join, B to back up to the title screen) as well as a list of all the available sessions. In this screen the player can use the directional buttons to select a session from the list. The selected session is drawn in a different color so the player can see which one is selected. These input options are implemented in the HandleAvailableSessionsInput method.
    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;
    }
    }
    And of course, the Draw and Update methods must once again be modified to include calls to these methods at the appropriate times.
    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...
    // 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);
    }
    With this, all the necessary phases of the game are implemented. However, the game is not quite complete yet. All the necessary elements are in place, but the network link isn't actually sending and receiving information yet.
    Step Five: Receive Network Data
    First we will add a method to handle receiving data from the network.
    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);
    }
    }
    }
    Remember the packetReader and packetWriter objects we put in with the global variables a while back? This method uses the packetReader to read data received from a remote player in a predefined order and updates the local copy of that player according to the received data. This method will be called from the UpdateInput method. We also need to modify the UpdateInput method to send out data to the other players in the game. Naturally, the order we send the information out in is the same as the order in which the data is read.
    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);
    }
    }
    The very last thing we need to do is to update the Draw and Update methods for the last time to call the drawing and input methods for the gameplay state.
    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);
    }
    That is the last step in writing the game. At this point, two players on different machines can connect to each via a network. Each player will have his own game, but will be able to view his partner's game in progress. This game is fairly simple, because the two networked players don't actually interact with each other, however, all of the pieces are in place to implement that kind of thing, or to add the capability for more than two players.

    (view changes)
    5:11 pm

Monday, March 22

  1. 9:31 pm
  2. 9:30 pm
  3. page home edited Write-up is below. Code is here. Introduction In this second project of SSE 643, Spring 2010, …
    Write-up is below.
    Code is here.

    Introduction
    In this second project of SSE 643, Spring 2010, I will use the XNA documentation's tutorial on how to build an Asteroids® look-alike using Microsoft Visual C# and the XNA Framework to demonstrate some key concepts of game programming. This game will incorporate 3D graphics, user input, and sound effects. The tutorial contains all the models, textures, and audio files needed for the game. This document assumes the reader is familiar with the concepts covered in project 1, which can be found at the course's ftp site.
    (view changes)
    9:29 pm
  4. 9:29 pm
  5. 9:28 pm

More