Part Eleven - Power Ups


We have come a LONG way in developing our game, and the last planned part of the series is to add some Power Ups to our game to alter the game play a bit. One of the reasons we have split a lot of simple things into their own functions is so we can come back and expand on them with this installment of the tutorial.

Lets start out with our Power Up graphic. This will be an animated spinning barrel:



Here we have a 23 frame animation that, when played, will display a slowly tumbling barrel on our screen. Save the image above to your Content/Textures folder and add it to your project.

We are going to add a new feature to our AnimatedSprite class. Since we are going to have multiple types of Power Ups in our game, we want to be able to differentiate them by color. We can use the last parameter of the SpriteBatch.Draw() method (which we have always left as Color.White) to colorize our sprite any way we want. In order to do that, we need to add a declaration to our AnimatedSprite class:

        Color cTinting = Color.White;


We will default the tinting to Color.White so we don't break anything we already have. Next, lets add a property so we can access the tint value:

        public Color Tint
        {
            get { return cTinting; }
            set { cTinting = value; }
        }


Finally, in the Draw() method, change the last parameter of the call to spriteBatch.Draw() from Color.White to cTinting. The whole call should look like this:

            spriteBatch.Draw(
                t2dTexture,
                new Rectangle(
                  iScreenX + XOffset,
                  iScreenY + YOffset,
                  iFrameWidth,
                  iFrameHeight),
                GetSourceRect(),
                cTinting);


This won't impact your existing game, since our tint defaults to white.

Lets add a new class called "PowerUp.cs" to our project. As always, we need to add our Using statements:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


We will also need a couple of declarations for the PowerUp class:

        static int iMapWidth = 1920;
        static Color[] colorPowerUpColors = new Color[5] 
          { Color.White, Color.Aquamarine, Color.Maroon, 
            Color.Yellow, Color.Orange };
 
        AnimatedSprite asSprite;
        int iX = 0;
        int iY = -100;
        bool bActive = false;
        int iBackgroundOffset = 0;
        int iPowerUpType = 0;


Since Power Ups will exist in world coordinates, we need the same information to track them as we did for our Enemy and Explosion classes. In addition to our normal variables, we also include iPowerUpType, which will have one of 5 values:

0 : Grants the player an extra ship
1 : Grants the player an extra Super Bomb
2 : Improves the handling of the player's star fighter
3 : Give ths player "dual cannons" on their star fighter
4 : increases the fire rate of the player's weapons

All three of the enhancement powerups can be active at the same time, and powerups 2 and 4 can "stack" multiple times. For example, there are 3 different fire rates, so picking up a #4 powerup increases your fire rate to the second level, and picking up another #4 increases it to the fastest rate.

The iPowerUpType is used as an index into colorPowerUpColors to determine what tint to draw the powerup sprite with. This allows the powerups to be distinguished on the screen.

The next thing our PowerUp class will need is some public properties:

        public int X
        {
            get { return iX; }
            set { iX = value; }
        }
 
        public int Y
        {
            get { return iY; }
            set { iY = value; }
        }
 
        public bool IsActive
        {
            get { return bActive; }
            set { bActive = value; }
        }
 
        public int Offset
        {
            get { return iBackgroundOffset; }
            set { iBackgroundOffset = value; }
        }
 
        public int PowerUpType
        {
            get { return iPowerUpType; }
            set { iPowerUpType = value;
                  asSprite.Tint = colorPowerUpColors[iPowerUpType];
            }
        }
 
        public Rectangle BoundingBox
        {
            get
            {
                int X = iX - iBackgroundOffset;
                if (X > iMapWidth)
                    X -= iMapWidth;
                if (X < 0)
                    X += iMapWidth;
                return new Rectangle(X, iY, 32, 32);
            }
        }


Most of these are our standard stuff, with the exception of PowerUpType. Whenever this property is set, we also set the Tint of our AnimatedSprite to the appropriate color for this power up.

Our constructor is very simple, just passing the texture and size information to our AnimatedSprite class:

        public PowerUp(Texture2D texture)
        {
            asSprite = new AnimatedSprite(texture, 0, 0, 32, 32, 23);
        }


We don't actually need to make this a method, as we could always use IsActive to set a power up active, but if we ever want to do other things when activating a power up it is nice to have it as a separate method, so lets add a simple Activate() method:

        public void Activate()
        {
            bActive = true;
        }


Just like our Enemies and Explosions, we need a helper function to translate world coordinates to screen coordinates:

        private int GetDrawX()
        {
            int X = iX - iBackgroundOffset;
            if (X > iMapWidth)
                X -= iMapWidth;
            if (X < 0)
                X += iMapWidth;
 
            return X;
        }


Since our powerups don't move, our Update method is simple as well:

        public void Update(GameTime gametime, int iOffset)
        {
            if (bActive)
            {
                asSprite.Update(gametime);
                iBackgroundOffset = iOffset;
            }
        }


And finally, our standard simple draw method which just draws the AnimatedSprite if it is active:

        public void Draw(SpriteBatch sb)
        {
            if (bActive)
            {
                asSprite.Draw(sb, GetDrawX(), iY, false);
            }
        }


That's it for the PowerUp class, but we need to make some modifications to our Player class in order to support these power ups. Lets start by adding a few declarations:

        float[] fFireRateDelay = new float[3] { 0.15f, 0.1f, 0.05f };
        float fSuperBombDelayTimer = 2f;
 
        int iMaxSuperBombs = 5;
        int iMaxWeaponLevel = 1;
        int iShipMaxFireRate = 2;
        int iMaxAccelerationModifier = 5;
 
        int iSuperBombs = 2;
        int iWeaponLevel = 0;
        int iWeaponFireRate = 0;
        int iAccelerationModifier = 1;


  • fFireRateDelay is an array of floats that represent the delay between bullets being fired. The smaller the number, the faster bullets can be fired by the player's ship.

  • fSuperBombDelayTimer is a limiter on how rapidly the player can trigger super bombs. If we didn't put some kind of delay in here, all of the player's available superbombs would go off in a single button press.

  • iMaxSuperBombs is the maximum number of super bombs the player's ship is allowed to carry at any time.

  • iMaxWeaponLevel is how many weapon upgrades the player can get. In our case, the limit is 1, meaning that the player can have either no upgrades (0) or dual cannons (1).

  • iShipMaxFireRate is the maximum index of fFireRateDelay. With our array above, 0=0.15f, 1=0.1f, and 2=0.05f.

  • iMaxAccelerationModifier is used to limit the "handling" improvements our ship can receive. We will detail this below.

  • iSuperBombs, iWeaponLevel, iWeaponFireRate, and iAccelerationModifier are the actual values that the limiting variables above are applied to.

In order to handle the ship handling power up as simply as possible, we will modify our AccelerationRate property:

        public int AccelerationRate
        {
            get { return iShipAccelerationRate * iAccelerationModifier; }
        }


Now, instead of simply returning the value of iShipAccelerationRate we will modify it by multiplying it by iAccelerationModifier. By doing it this way, we don't have to change any of our existing movement code to allow us to implement the ship handling powerup. When the user has no acceleration powerups, iAccelerationModifier==1, resulting in the base iShipAccelerationRate being returned.

Next, lets add public properties for the variables we created above:

        public int SuperBombs
        {
            get { return iSuperBombs; }
            set { iSuperBombs = (int)MathHelper.Clamp(value, 
                    0, iMaxSuperBombs); }
        }
 
        public int FireRate
        {
            get { return iWeaponFireRate; }
            set { iWeaponFireRate = (int)MathHelper.Clamp(value, 
                    0, iShipMaxFireRate); }
        }
 
        public float FireDelay
        {
            get { return fFireRateDelay[iWeaponFireRate]; }
        }
 
        public int WeaponLevel
        {
            get { return iWeaponLevel; }
            set { iWeaponLevel = (int)MathHelper.Clamp(value, 
                    0, iMaxWeaponLevel); }
        }
 
        public float SuperBombDelay
        {
            get { return fSuperBombDelayTimer; }
        }
 
        public int AccelerationBonus
        {
            get { return iAccelerationModifier; }
            set { iAccelerationModifier = (int)MathHelper.Clamp(value,
                    1, iMaxAccelerationModifier); }
        }


As you can see in most of these we use the MathHelper.Clamp() method to impose the limits we defined in our declarations section. We do have a couple of Read Only properties here:

FireDelay returns the index into the fFireRateDelay equivalent to the iWeaponFireRate variable.

SuperBombDelay returns the amount of time required between Super Bomb firings.

Lets add a helper method to our Player class to return all of the ship upgrade variables to their defaults:

        public void Reset()
        {
            iAccelerationModifier=1;
            iWeaponFireRate=0;
            iWeaponLevel=0;
            iSuperBombs = (int)MathHelper.Max(1, iSuperBombs);
            iScrollRate = 0;
            iFacing = 0;
        }


You can also see that we don't take away the player's existing Super Bombs, and in fact give them one if they don't have at least one already.

That's it for modifying our Player class, so lets now head on over to the Game1.cs file and implement the changes we will need to support our power ups. We will need a few new declarations here as well:

        Vector2 vSuperBombTextLoc = new Vector2(250, 677);
 
        static int iMaxPowerups = 5;
        PowerUp[] powerups = new PowerUp[iMaxPowerups];
        float fSuperBombTimer = 2f;
        float fPowerUpSpawnCounter = 0.0f;
        float fPowerUpSpawnDelay = 30.0f;


vSuperBombTextLoc is similar to all of our other text positioning vectors. It holds the location on the screen where the number of super bombs the player has left is displayed.

iMaxPowerups determines the maximum number of power ups that can be spawned (awaiting being picked up by the player) at any one time.

The powerups array holds the actual PowerUp objects we will use.

fSuperBombTimer is the amount of time since the last super bomb was fired. We start it off at 2 seconds so that the player could trigger a super bomb as soon as the game starts if they wish.

fPowerUpSpawnCounter and fPowerUpSpawnDelay determine how fast new power ups will be generated. Here we are saying that a new power up will be generated every 30 seconds.

We will need to initialize our power ups in LoadContent(), so add the following:

            for (int i = 0; i < iMaxPowerups; i++)
            {
                powerups[i] = new PowerUp(Content.Load<Texture2D>(@"Textures\PowerUp"));
            }


We simply loop through our powerup array and call the constructor for each object.

Now, lets look at how we will implement a super bomb explosion. Add this helper function to Game1:

        protected void ExecuteSuperBomb()
        {
            for (int x = 0; x < iMaxEnemies; x++)
            {
                if (Intersects(Enemies[x].BoundingBox, 
                      new Rectangle(0,30,1280,630)))
                    DestroyEnemy(x);
            }
        }


Lets also add two lines to the top of our StartNewGame() method:

            player.Reset();
            player.SuperBombs = 2;


These make sure all of our power up settings start at their correct values when a new game is started.

All we are doing here is using our existing Intersects() method to see if any of our enemies intersect with the screen. If they do, we destroy them. Since our BoundingBox returns screen coordinates, this makes it very easy to determine if any given enemy is on the screen, and if so destroy them.

Now we need to update our CheckOtherKeys() method to account for the Dual Cannons, Firing Rate, and Super Bombs Power Ups. We need to fire the second bullet (remember that we included a Vertical Offset parameter in our FireBullet method back in Part 5? That was so we could use it now to fire the second bullet just above the first). We also will set up the B button (or Backspace key) to trigger our Super Bombs. Here is the whole CheckOtherKeys() method:

        protected void CheckOtherKeys(KeyboardState ksKeys, GamePadState gsPad)
        {
 
            // Space Bar or Game Pad A button fire the 
            // player's weapon.  The weapon has it's
            // own regulating delay (fBulletDelayTimer) 
            // to pace the firing of the player's weapon.
            if ((ksKeys.IsKeyDown(Keys.Space)) ||
                (gsPad.Buttons.A == ButtonState.Pressed))
            {
                if (fBulletDelayTimer >= player.FireDelay)
                {
                    FireBullet(0);
                    fBulletDelayTimer = 0.0f;
                    if (player.WeaponLevel == 1)
                    {
                        FireBullet(-4);
                    }
                }
            }
 
            // The Backspace (keyboard) or B (gamepad)
            // button is used to trigger a "Super Bomb"
            if ((ksKeys.IsKeyDown(Keys.Back)) ||
                (gsPad.IsButtonDown(Buttons.B)))
            {
                if ((fSuperBombTimer > player.SuperBombDelay) &&
                    (player.SuperBombs > 0))
                {
                    player.SuperBombs--;
                    fSuperBombTimer = 0f;
                    ExecuteSuperBomb();
                }
            }
        }


There are two changes to the "A" button code. First, we replaced our Game1 based fFireDelay variable (which you can now remove from the declarations area if you wish) with the player.FireDelay property. As you will remember this will be the result at an index into the fFireRateDelay array.

Second, we check to see if player.WeaponLevel is 1. If so, we fire a second bullet with a vertical offset of -4.

All of the "B" button code is new, but it is fairly straightforward. If enough time has passed since the last super bomb (and if we have any left) we subtract a super bomb, reset the timer, and execute the super bomb.

Remember the Reset() method we added to our Player class? Open your PlayerKilled method and add it at the end:

            player.Reset();


That way, when the player's ship is destroyed and they respawn, they return to an unupgraded star fighter.

We need to determine when the player runs into a power up (hench picking it up). We already have a method called "CheckPlayerHits()" that checks to see if the player runs into any enemies, so lets expand on it to test for player hits into powerups. Add the following to your CheckPlayerHits() method after all of the code to check for running into enemies:

            for (int x = 0; x < iMaxPowerups; x++)
            {
                if ((powerups[x].IsActive) &&
                     (Intersects(player.BoundingBox, powerups[x].BoundingBox)))
                {
                    switch (powerups[x].PowerUpType)
                    {
                        case 0:
                            iLivesLeft++;
                            break;
 
                        case 1:
                            player.SuperBombs++;
                            break;
 
                        case 2:
                            player.AccelerationBonus++;
                            break;
 
                        case 3:
                            player.WeaponLevel++;
                            break;
 
                        case 4:
                            player.FireRate++;
                            break;
 
                    }
                    powerups[x].IsActive = false;
                }
            }


This is where we actually handle what the power up does to the game as well. A simple switch statement tests the value of PowerUpType and takes the appropriate action. We don't need to worry about going off of the end of our upgrade values because the properties we implemented in the Player class use MathHelper.Clamp to limit them to the appropriate ranges. This makes implementing them here a snap.

What about actually getting power ups into our gameplay? Well, lets add a helper function to generate them:

        protected void GeneratePowerup()
        {
            for (int x = 0; x < iMaxPowerups; x++)
            {
                if (!powerups[x].IsActive)
                {
                    powerups[x].X = rndGen.Next(0, 1920);
                    powerups[x].Y = rndGen.Next(30, 630);
                    powerups[x].PowerUpType = rndGen.Next(0, 5);
                    powerups[x].Offset = background.BackgroundOffset;
                    powerups[x].Activate();
                    break;
                }
            }
        }


We use the same method as our Bullets array to loop until we find a free PowerUp object. Then we generate the values for it and activate it. The "break;" knocks us out of the loop once we have generated the PowerUp so we don't generate all 5 at the same time. If we don't find an inactive PowerUp, the method will simply exit and not generate one.

As with everything else in our code, we need to update our power ups during our Update method. Right after the call to UpdateBullets(gameTime) add:

                    // Update Powerups
                    for (int x = 0; x < iMaxPowerups; x++)
                        powerups[x].Update(gameTime,
                          background.BackgroundOffset);


Which simply loops through all the power ups and calls their Update() methods. Also in our update method, we will need to account for our super bomb timer, and for the power up spawn timer. Right "after fBulletDelayTimer += elapsed;" add the following:

                    //Accumulate time since the last super bomb was fired
                    fSuperBombTimer += elapsed;
 
                    //Accumulate time since the last powerup was generated
                    fPowerUpSpawnCounter += elapsed;
                    if (fPowerUpSpawnCounter > fPowerUpSpawnDelay)
                    {
                        GeneratePowerup();
                        fPowerUpSpawnCounter = 0.0f;
                    }


And finally, we need to add them to our Draw() code. First, in the section where we write all of the rest of our text (lives, score, etc) add:

                spriteBatch.DrawString(spriteFont, player.SuperBombs.ToString(), 
                    vSuperBombTextLoc, Color.White);


Which will draw the number of super bombs the player has remaining.

Then, scroll up a little and find the section where we loop through and draw all of the enemies. Right after that, add:

                for (int i = 0; i < iMaxPowerups; i++)
                {
                    powerups[i].Draw(spriteBatch);
                }


Which simply calles the draw method for all of our power ups.

This should all be old hat by now, simply updating our delay timers and, in the same of the power up spawner, generating one and resetting the timer if the time has elapsed.

One last quick bug fix. Currently, when the player dies, all of their bullets remain active and may destroy enemies as soon as the player respawns and everything starts moving again. Edit your StartNewWave() method and add the following at the end:

            for (int x = 0; x < iMaxBullets; x++)
                RemoveBullet(x);


Run your game! Besides sound, you should have a fully functional space shooter game.



Here is the whole project as it exists at the end of Part 11:



(Continued in Part 12...)


































 

 
 
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