Part Nine - Game Structure



So far, we have build a lot of components for our game, but we still are missing a lot of the trimmings that actually make it playable. In this segment we are going to look at wrapping our project with things that will make it into an actual game.

First, lets add two possible modes the game can be in:

  • Title Screen Mode
  • Game Play Mode


  • In Title Screen mode, we display a title screen (I know, shocking!) and wait for the player to press a button to start the game. In game play mode, we play as we have been all along.

    This, of course, implies that there is a way to get back to Title Screen Mode (by losing the game) so we will need to account for that as well.

    Lets do the easy parts first and add a title screen to our resources. Here is the screen Jason put together for me:



    Save it to your Content/Textures folder and add it to your project.

    We will need to add a declaration for the image (We will also add an integer that determines if we are in Game Mode or Title Screen mode):

            int iGameStarted = 0;
            Texture2D t2dTitleScreen;
    


    And we will need to initialize it in LoadContent:

                t2dTitleScreen = Content.Load<Texture2D>(@"Textures\TitleScreen");


    While we are here, take out the line that says "StartNewWave()", since we will be implementing that as part of our code below.

    So lets look at what we need to do in our Update() and Draw() routines to account for the title screen. We need to wrap an If statement around most of our Update code, but I also want to clean the code up a bit, so I'm going to include a full replacement Update method for our Game1 class. I've cleaned it up, changed a few things (I'll talk about them below) and commented the code. Additionally, I've created the "if" statement for iGameStarted and set up a couple of #regons to make things easier to deal with.

    Big Code Block Warning!

            protected override void Update(GameTime gameTime)
            {
                // Store values for the Keyboard and GamePad so we aren't
                // Querying them multiple times per Update
                KeyboardState keystate = Keyboard.GetState();
                GamePadState gamepadstate = GamePad.GetState(PlayerIndex.One);
     
                // If the Escape Key is pressed, or the user presses
                // the "Back" button on the game pad, exit the game.
                if ((keystate.IsKeyDown(Keys.Escape) || 
                     gamepadstate.Buttons.Back == ButtonState.Pressed))
                    this.Exit();
     
                // Get elapsed game time since last call to Update
                float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
     
                if (iGameStarted == 1)
                {
                    #region GamePlay Mode (iGameStarted==1)
                    //Accumulate time since the last bullet was fired
                    fBulletDelayTimer += elapsed;
     
                    // Accumulate time since the player's speed changed
                    player.SpeedChangeCount += elapsed;
     
                    // If enough time has passed that the player can change
                    // speed again, call CheckHorizontalMovementKeys
                    if (player.SpeedChangeCount > player.SpeedChangeDelay)
                    {
                        CheckHorizontalMovementKeys(keystate, gamepadstate);
                    }
     
                    // Accumulate time since the player moved vertically
                    player.VerticalChangeCount += elapsed;
     
    
                    // If enough time has passed, call CheckVerticalMovementKeys
                    if (player.VerticalChangeCount > player.VerticalChangeDelay)
                    {
                        CheckVerticalMovementKeys(keystate, gamepadstate);
                    }
     
                    // Check any other key presses
                    CheckOtherKeys(keystate, gamepadstate);
     
                    // Update all enemies and explosions
                    for (int i = 0; i < iTotalMaxEnemies; i++)
                    {
                        if (Enemies[i].IsActive)
                            Enemies[i].Update(gameTime,
                              background.BackgroundOffset);
     
                        if (Explosions[i].IsActive)
                            Explosions[i].Update(gameTime,
                              background.BackgroundOffset);
     
                    }
     
                    // Update the player's star fighter
                    player.Update(gameTime);
     
                    // Move any active bullets
                    UpdateBullets(gameTime);
     
                    // See if any active bullets hit any active enemies
                    CheckBulletHits();
     
                    // Accumulate time since the game board was last updated
                    // This reflects the actual movement rate of the screen
                    // as opposed to speed changes by the player
                    fBoardUpdateDelay += elapsed;
     
                    // If enough time has elapsed, update the game board.
                    if (fBoardUpdateDelay > fBoardUpdateInterval)
                    {
                        fBoardUpdateDelay = 0f;
                        UpdateBoard();
                    }
                    #endregion
                }
                else
                {
                    #region Title Screen Mode (iGameStarted==0)
                    if ((keystate.IsKeyDown(Keys.Space)) || 
                        (gamepadstate.Buttons.Start == ButtonState.Pressed))
                    {
                        StartNewGame();
                    }
                    #endregion
                }
                base.Update(gameTime);
            }
    


    As you can see, I've tried to comment everything here. The first big change is that we store a KeyboardState and GameState so that we can just reference those later instead of having a bunch of calls to Keyboard.GetState() and GamePad.GetState(PlayerIndex.One).

    We do the same with our Elapsed Game Time. Instead of converting it to a float every time we need it, we just make a float called "elapsed" at the beginning. Note that this and the KeyboardState code happen outside of the "if" statement checking iGameStarted, since we can make use of it in both cases.

    Right inside the curly bracket of the "if" statement you will notice a "#region" line. This is not actual code, but a handy feature of the C# environment that allows you to have collapsable code regions. A #region and #endregion pair will at a little plus/minus sign on the left side of the screen that will expand/collapse the code inside the region, leaving just the name of the region behind.

    Inside the "if" statement, I've modified a few lines of code to use "elapsed" instead of "(float)gameTime.ElapsedGameTime.TotalSeconds" and to use the KeyboardState and GamePadState objects we created earlier.

    I've also got the UpdateBullets being called before CheckBulletHits to keep things consistant.

    Inside the "Title Screen Mode" region, which is the code we will be running during Update() if iGameStarted==0, we check for the player to press the Start button on the GamePad or the Space bar on the keyboard. If that happens, we call "StartNewGame()", which we haven't yet written. Here is a basic StarNewGame function:

            protected void StartNewGame()
            {
                iGameStarted = 1;
                StartNewWave();
            }
    


    Here we simply set iGameStarted to 1 and execute the StartNewWave() method we created when we added enemies to the game.

    Next we need to consider our Draw method. Once again, I've cleaned it up a little, added some comments and the like:

            protected override void Draw(GameTime gameTime)
            {
                // Clear the Graphics Device
                graphics.GraphicsDevice.Clear(Color.Black);
     
                // Start a SpriteBatch.Begin which will be used
                // by all of our drawing code.
                spriteBatch.Begin();
     
                if (iGameStarted == 1)
                {
                    #region Game Play Mode (iGameStarted==1)
     
                    // Draw the Background object
                    background.Draw(spriteBatch);
     
                    // Draw the Player's Star Fighter
                    player.Draw(spriteBatch);
     
                    // Draw any active bullets on the screen
                    for (int i = 0; i < iMaxBullets; i++)
                    {
                        if (bullets[i].IsActive)
                        {
                            bullets[i].Draw(spriteBatch);
                        }
                    }
     
                    // Draw any active enemies and explosions
                    for (int i = 0; i < iMaxEnemies; i++)
                    {
                        if (Enemies[i].IsActive)
                        {
                            Enemies[i].Draw(spriteBatch,
                              background.BackgroundOffset);
                        }
     
                        if (Explosions[i].IsActive)
                        {
                            Explosions[i].Draw(spriteBatch, false);
                        }
                    }
     
                    // Draw the player's explosion if it is happening
                    if (Explosions[iTotalMaxEnemies].IsActive)
                        Explosions[iTotalMaxEnemies].Draw(spriteBatch, true);
                    #endregion
                }
                else
                {
                    #region Title Screen Mode (iGameStarted==0)
                    spriteBatch.Draw(t2dTitleScreen, new Rectangle(0, 0, 1280, 720), Color.White);
                    #endregion
                }
                // Close the SpriteBatch
                spriteBatch.End();
     
                base.Draw(gameTime);
            }
    


    There are only a few changes here other than spacing and comments. I've got our "if" statement in there, and if we are in title screen mode, we just use SpriteBatch.Draw() to throw our title screen up on the display.

    If you launch your game, you should get the title screen. Pressing Back or Escape will exit, while pressing Space or Start will start the game.

    That gets us started, but we need to go a bit further.

    Lets add a few more declarations we are going to need:

            int iProcessEvents = 1;
            int iLivesLeft = 3;
            int iGameWave = 0;
            int iPlayerScore = 0;
            float fPlayerRespawnTimer = 4f;
            float fPlayerRespawnCount = 0f;
    


    • iProcessEvents will be used as another "state" variable. When iGameStarted==1 we will use iProcessEvents to determine if we are going to continue moving enemies, bullets, and the screen. We will use this when the player crashes into an enemy to stop the action but allow the explosion animations to continue playing.

    • iLivesLeft is the number of ships the player has until the game is over.

    • iGameWave is the level the player is currently on. Whenever all of the enemies on a wave are cleared, the number of enemies is increased by one and a new wave is spawned.

    • iPlayerScore is the score the player has accumulated in this game.

    • fPlayerRespawnTimer and fPlayerRespawnCount determine the delay between the player exploding and respawning (if they have ships left)

    Lets edit our StartNewGame() method to set these variables as well as some of our existing variables when a game is started:

            protected void StartNewGame()
            {
                iGameStarted = 1;
                iProcessEvents = 1;
                iLivesLeft = 3;
                player.ScrollRate = 0;
                iPlayerScore = 0;
                iGameWave = 0;
                iMaxEnemies = 9;
     
                StartNewWave();
            }
    


    Lets update our StartNewWave() method to make sure iGameStarted and iProcessEvents are running:

            protected void StartNewWave()
            {
                iProcessEvents = 1;
                iGameStarted = 1;
                iGameWave++;
                GenerateEnemies();
            }
    


    Lets add another helper function. This one will get expanded upon when we add Power Ups later:

            protected void PlayerKilled()
            {
                // Reset the iGameWave and iMaxEnemies, since they will 
                // both be bumped automatically when the new wave is 
                // generated.
                iGameWave--;
                iMaxEnemies--;
     
                // Stop the player's ship
                player.ScrollRate = 0;
            }
    


    Since our GenerateEnemies method automatically bumps up the iMaxEnemies and StartNewWave bumps up the current wave number, we simply subtract one from each in preparation for calling StartNewWave(). (When a player is killed, the wave starts over with a full load of enemies)

    Now, while I hate to do this to you for the second time in the same installment, here is the whole update method (again) adding our iProcessEvents checks:

            protected override void Update(GameTime gameTime)
            {
                // Store values for the Keyboard and GamePad so we aren't
                // Querying them multiple times per Update
                KeyboardState keystate = Keyboard.GetState();
                GamePadState gamepadstate = GamePad.GetState(PlayerIndex.One);
     
                // If the Escape Key is pressed, or the user presses
                // the "Back" button on the game pad, exit the game.
                if ((keystate.IsKeyDown(Keys.Escape) || 
                     gamepadstate.Buttons.Back == ButtonState.Pressed))
                    this.Exit();
     
                // Get elapsed game time since last call to Update
                float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
     
                if (iGameStarted == 1)
                {
                    #region GamePlay Mode (iGameStarted==1)
     
                    if (iProcessEvents == 1)
                    {
                        #region Processing Events (iProcessEvents==1)
                        //Accumulate time since the last bullet was fired
                        fBulletDelayTimer += elapsed;
     
                        // Accumulate time since the player's speed changed
                        player.SpeedChangeCount += elapsed;
     
                        // If enough time has passed that the player can change
                        // speed again, call CheckHorizontalMovementKeys
                        if (player.SpeedChangeCount > player.SpeedChangeDelay)
                        {
                            CheckHorizontalMovementKeys(keystate, gamepadstate);
                        }
     
                        // Accumulate time since the player moved vertically
                        player.VerticalChangeCount += elapsed;
     
                        // If enough time has passed, call CheckVerticalMovementKeys
                        if (player.VerticalChangeCount > player.VerticalChangeDelay)
                        {
                            CheckVerticalMovementKeys(keystate, gamepadstate);
                        }
     
                        // Check any other key presses
                        CheckOtherKeys(keystate, gamepadstate);
     
                        // Update all enemies and explosions
                        for (int i = 0; i < iTotalMaxEnemies; i++)
                        {
                            if (Enemies[i].IsActive)
                                Enemies[i].Update(gameTime,
                                  background.BackgroundOffset);
     
                            if (Explosions[i].IsActive)
                                Explosions[i].Update(gameTime,
                                  background.BackgroundOffset);
     
                        }
     
                        // Update the player's star fighter
                        player.Update(gameTime);
     
                        // Move any active bullets
                        UpdateBullets(gameTime);
     
                        // See if any active bullets hit any active enemies
                        CheckBulletHits();
     
                        // Accumulate time since the game board was last updated
                        // This reflects the actual movement rate of the screen
                        // as opposed to speed changes by the player
                        fBoardUpdateDelay += elapsed;
     
                        // If enough time has elapsed, update the game board.
                        if (fBoardUpdateDelay > fBoardUpdateInterval)
                        {
                            fBoardUpdateDelay = 0f;
                            UpdateBoard();
                        }
                        #endregion
                    }
                    else
                    {
                        #region Not Processing Events (iProcessEvents==0)
     
                        if (Explosions[iTotalMaxEnemies].IsActive)
                            Explosions[iTotalMaxEnemies].Update(gameTime, 
                                background.BackgroundOffset);
     
                        fPlayerRespawnCount+=elapsed;
     
                        if (fPlayerRespawnCount > fPlayerRespawnTimer)
                        {
                            iLivesLeft -= 1;
                            if (iLivesLeft > 0)
                            {
                                PlayerKilled();
                                StartNewWave();
                            }
                            else
                            {
                                iGameStarted = 0;
                                iProcessEvents = 1;
                            }
                        }
                        #endregion
                    }
                    #endregion
                }
                else
                {
                    #region Title Screen Mode (iGameStarted==0)
                    if ((keystate.IsKeyDown(Keys.Space)) ||
                       (gamepadstate.Buttons.Start == ButtonState.Pressed))               
                    {
                        StartNewGame();
                    }
                    #endregion
                }
                base.Update(gameTime);
            }
    


    Other that the addition of the if block for iProcessEvents, this is unchanged, but explaining where to put the if statement to surround everything would probably end up with a mess in the editor. Inside the "Not Processing Events" region we see what we will do when iProcessEvents is false. We continue to animate the player's explosion, and we accumulate time in fPlayerRespawnCount until it is greater than fPlayerRespawnTimer (4 seconds).

    Then we subtract one from iLivesLeft and check to see if the player has any lives remaining. If so, we call our PlayerKilled() method and then StartNewWave(). If the player is out of lives, we set iGameStarted to 0 and turn iProcessEvents back on (just to be safe later).

    Fortunately, or draw code when the player's ship is exploding is much easier to update. Find the line that says "player.Draw(spriteBatch);" and replace it with:

                    if (iProcessEvents == 1)
                    {
                        player.Draw(spriteBatch);
                    }
    


    Since the only thing we don't want to draw is the player's ship (because it is exploding) that is the only thing we need to eliminate if iProcessEvents==0. (Bullets and enemies will freeze in place, but will still be drawn).

    At the end of your CheckBulletHits() method, add the following:

                // If we have run out of active enemies, generate new ones
                if (iActiveEnemies < 1)
                    StartNewWave();
    


    Which will start a new wave if we run out of enemies. In order for our game to run out of enemies, though, we need up update our DestroyEnemy() method by adding a line:

                iActiveEnemies--;


    This can be placed anywhere in the call, but I have it after the call to the Activate() method of the explosion.

    One more helper function for this installment:

            protected void CheckPlayerHits()
            {
                for (int x = 0; x < iTotalMaxEnemies; x++)
                {
                    if (Enemies[x].IsActive)
                    {
                        // If the enemy and ship sprites  collide...
                        if (Intersects(player.BoundingBox, Enemies[x].CollisionBox))
                        {
                            // Stop event processing
                            iProcessEvents = 0;
     
                            // Set up the ship's explosion
                            Explosions[iTotalMaxEnemies].Activate(
                                player.X - 16,
                                player.Y - 16,
                                Vector2.Zero,
                                0f,
                                background.BackgroundOffset);
     
                            fPlayerRespawnCount = 0.0f;
     
                            return;
                        }
                    }
                }
            }
    


    Here we are checking each active enemy's CollisionBox against the player's BoundingBox to see if they have collided. If they have, we set iProcessEvents to 0, which will freeze the action. We then set up the player's ship explosion and activate it. Finally, we set fPlayerRespawnCount to 0 so we can start a four second countdown to respawing. We throw in a return here because if we have exploded once there is no need to keep checking other enemies.

    We are ALMOST DONE! Promise. The last thing we need to do is add a call to CheckPlayerHits() to our Update() method. Right after CheckBulletHits(); add:

                        // Check to see if the player has collided with any enemies
                        CheckPlayerHits();
    


    Run your game, and you should have a pretty much playable game! You have enemies that "attack" in waves as you kill them off, you have a limited number of lives, after which you return to the title screen, you have explosions flying all over the place, and life is good!

    We have a few more things to do, though, so check back for our next installment, where we will handle setting up our "game screen" overlay, displaying information like your score and the number of lives you have left, and such. We'll also throw in Super Bombs.

    I've packed up the project as it stands right now and am uploading the code in case you are having any trouble getting things to work.



    (Continued in Part 10...)


































     

     
     
    Site Contents Copyright 2006 Full Revolution, Inc. All rights reserved.
    This site is in no way affiliated with Microsoft or any other company.
    All logos and trademarks are copyright their respective companies.
    RSS FEED