Part Five - Lock and Load



So we have our star fighter flying merrily through our virtual space sector. Time to bring on the cannons!

This is one of those cases where I break away from the pure object oriented system and use a static array of objects to handle the player's "bullets."

The player will be able to fire A LOT of bullets. If we were creating an destroying objects (with associated textures, etc) every time a bullet was fired, left the screen, or impacted an enemy, we would be adding quite a bit of unneeded overhead to our code.

We have a very small image for our bullet, composed of two frames. In the first frame the bullet is firing to the right, in the second the bullet is firing to the left. Recall that in previous objects we have used the values of 0 and 1 to represent a "facing" of right and left respectively. We will stick to that system here.

Our bullet sprite will be 16 pixels wide by 1 pixel high, so the overall image is 32x1 pixels:



Save the "bullet" graphic to your Content/Textures folder and include it in your project as normal.

As with the rest of our game, we will encapsulate most of the code to handle bullets into a class, so right click on your project and add a new class file called "Bullet.cs".

Also as with the rest of our classes, we will need access to the XNA framework and graphics classes, so add the standard using statements to the top of the file:

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


On with the declarations! We are going to handle our AnimatedSprite a little differently this time around. Bullets always have the same graphic, and nothing in our AnimatedSprite class prevents the sprite from being drawn multiple times, especially since we provide drawing offsets (and haven't been using the AnimatedSprite's internal X and Y coordinates anyway). We will add two animated sprites to our class, both of them static:

        static AnimatedSprite asLeft;
        static AnimatedSprite asRight;


The "static" qualifier indicates that the same "asLeft" and "asRight" objects will be shared across ALL instances of the Bullet class. All of our previous declarations have become their own instances for each instance of the class (for example, the Player object has the integers iX and iY. If we created 100 Player objects, each iX and iY would be different integer instances. When we define an object as static, there is only one copy, no matter how many instances of the parent object we create, and all instances share it).

Because we are declaring the Bullet's sprites static, we can't use a single AnimatedSprite with two frames (one for each facing) because updating the frame on one bullet would update the frame on all bullets in the game, making them flop back and forth. Hence we will have two animated sprites, each pointing to a single frame of our bullet texture.

Next we will need a fairly standard set of variables:

        int iX;
        int iY;
        bool bActive;
        int iFacing = 0;
        float fElapsed = 0f;
        float fUpdateInterval = 0.015f;
        public int iSpeed = 12;


As normal, iX and iY will track the location of our bullet. Some may ask why I'm not using a Vector2 object instead of two integers, and there isn't much of a reason beyond old habits and a vague opinion that the floating point path associated with Vector2's is less efficient than integer math. You could very easily change iX and iY to a single Vector2 and reference the X and Y components.

The bActive variable is set to true when the bullet is fired and remains true while it is on the screen. It will be disabled when the bullet leaves the screen or when it impacts an enemy.

iFacing is our standard directional variable. Bullets fired to the right have a facing of 0, while bullets fired to the left have a facing of 1.

fElapsed and fUpdateInterval will handle our timing values much like we did with acceleration with our ship. fElapsed will accumulate game time until it is greater than fUpdateInterval, at which time the bullet's position will be updated and fElapsed will be reset to zero.

Finally iSpeed determines the pixels per frame that the bullet will move.

Speaking of pixels, I should note that we don't track "world position" of our bullets but only "screen position". This is for a couple of reasons:

By tracking and updating screen position, bullets will always visually move at the same speed no matter which direction and at what speed the player is scrolling the background. This means we won't have situations where bullets "bunch up" while the player accelerates.
Since we are only going to test for collisions on the screen, there is no need to worry about where the bullet is in "world space". While we will be tracking our enemies in world space, we will translate that position to screen space when we test for bullet-to-enemy collisions.
As always, we will need some public properties to access these variables:

        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 Facing
        {
            get { return iFacing; }
            set { iFacing = value; }
        }
 
        public int Speed
        {
            get { return iSpeed; }
            set { iSpeed = value; }
        }
 
        public Rectangle BoundingBox
        {
            get { return new Rectangle(iX, iY, 16, 1); }
        }


Nothing too out of the ordinary here. As with the player ship, we have a BoundingBox property which we will use when detecting collisions later. The BoundinbBox property returns a rectangle that represents the screen position of the bullet (a 16x1 rectangle at iX, iY).

Also, we don't expose fElapsed and fUpdateInterval as properties since all of the timing code will be handled within the class itself. This is a change from the way we handled the Player class, as user input has a more direct impact on what happens to the ship. After a bullet is fired, the player has no control over what happens to it.

Now it's constructor time. We are going to have two constructors for our Bullet class, and here they are:

        public Bullet(Texture2D texture)
        {
            asRight = new AnimatedSprite(texture, 0, 0, 16, 1, 1);
            asRight.IsAnimating = false;
            asLeft = new AnimatedSprite(texture, 16, 0, 16, 1, 1);
            asRight.IsAnimating = false;
            iFacing = 0;
            iX = 0;
            iY = 0;
            bActive = false;
        }
 
        public Bullet()
        {
            iFacing = 0;
            iX = 0;
            iY = 0;
            bActive = false;
        }


The first (full) constructor will be called only once by our game (It could be used every time, and won't hurt anything if it is, but there is really no need to do so). This will take the image we are passing into it and set up our two static AnimatedSprites as single frames.

After the sprite setup, both constructors do the same thing. They set defaults (zeros) for the iFacing, iX, and iY variables, and set the bullet to inactive.

Since we will be using a static array of Bullets in our game, we will set them all up during our LoadContent phase and just activate them when we need them.

Next, we will include a public function to "fire" a bullet:

        public void Fire(int X, int Y, int Facing)
        {
            iX = X;
            iY = Y;
            iFacing = Facing;
            bActive = true;
        }


When called, the position of the bullet will be set, along with it's facing. It is then made active. We'll call this function when the player presses the fire button, passing it the location of the player's ship (with a few offsets) to start the bullet off at the players cannon.

In order to make our bullets as self-contained as possible, we'll include the following Update routine:

        public void Update(GameTime gameTime)
        {
            if (bActive)
            {
                fElapsed += (float)gameTime.ElapsedGameTime.TotalSeconds;
                if (fElapsed > fUpdateInterval)
                {
                    fElapsed = 0f;
                    if (iFacing == 0)
                    {
                        iX += iSpeed;
                    }
                    else
                    {
                        iX -= iSpeed;
                    }
 
                    // If the bullet has moved off of the screen, 
                    // set it to inactive
                    if ((iX > 1280) || (iX < 0))
                    {
                        bActive = false;
                    }
                }
            }
        }


After making sure we are working with an "active" bullet, we accumulate elapsed time, checking to see if we have passed the fUpdateInterval value. If so, we reset the fElapsed time and check the facing of the bullet and then either add or subtract the iSpeed value to the horizontal position.

Lastly, we check to see if the bullet has moved off of the screen as a result of this movement. If it has, we set it to inactive.

Drawing our bullet is very simple. We will simply pass a SpriteBatch object along to the underlying AnimatedSprite depending on the bullet's facing:

        public void Draw(SpriteBatch sb)
        {
            if (bActive)
            {
                if (iFacing == 0)
                {
                    asRight.Draw(sb, iX, iY, false);
                }
                else
                {
                    asLeft.Draw(sb, iX, iY, false);
                }
            }
        }


Once again, we determine if the bullet is active first (and if not, do nothing). Then we simply draw the AnimatedSprite associated with the iFacing value for the bullet.

Adding Bullets to our Game


We are now finished with our Bullet class and ready to add them into our game and allow the player to fire them. To begin with, we'll add a few declarations to our Game1.cs declarations area:

        int iBulletVerticalOffset = 12;
        int[] iBulletFacingOffsets = new int[2] { 70, 0 };
        static int iMaxBullets = 40;
        Bullet[] bullets = new Bullet[iMaxBullets];
        float fBulletDelayTimer = 0.0f;
        float fFireDelay = 0.15f;


iBulletVerticalOffset will be added to our bullet's initial position in order to make it look like the bullet is coming from the star fighter's cannon (moving 12 pixels down from the upper edge of the ship sprite).

iBulletFacingOffsets is similar in that we use it to make the bullet appear to be coming from the ship's cannon, but this time in the horizontal direction. When the ship is facing left (player.Facing==1) a 0 offset is fine, but when facing right we want to add 70 pixels to the bullet's starting location so it doesn't end up coming from the back end of the ship. A simple two element array will let us index it by the facing to get the appropriate offset.

Next we have iMaxBullets which is a static int that determines the maximum number of bullet objects we will be able to have on the screen at any one time. We use a static int so that we can use that value as the array size for the bullets array (you can't use non-static values to declare the size, so would have to have "new Bullet[40]" here if iMaxBullets wasn't static.

The bullets array, of course, holds our actual bullet objects.

fBulletDelayTimer and fFireDelay control how fast bullets can be fired by the player. As normal, we will accumulate time in fBulletDelayTimer and check it against fFireDelay to determine when a bullet can be fired.

After we have our declarations, we need to initialize the bullet objects, so in our LoadContent method lets add the following (It doesn't really matter where, but I have it after the "player" initialization):

            bullets[0] = new Bullet(Content.Load<Texture2D>(@"Textures\PlayerBullet"));
 
            for (int x = 1; x < iMaxBullets; x++)
                bullets[x] = new Bullet();


Next, lets add a few helper functions we will use to manage our Bullet array. The first we will call from our game's update routine to handle updating all of our bullets:

        protected void UpdateBullets(GameTime gameTime)
        {
            // Updates the location of all of thell active player bullets. 
            for (int x = 0; x < iMaxBullets; x++)
            {
                if (bullets[x].IsActive)
                    bullets[x].Update(gameTime);
            }
        }


This function simply loops through all of the bullets in our array and calls the Update method for each active bullet, passing it the current GameTime. We could have inlined this into our Update code, but the Update method is going to get complex enough as we add more things to the game, so I chose to break it out into a separate function.

We'll add another helper function that will be called whenever the player presses the fire button:

        protected void FireBullet(int iVerticalOffset)
        {
            // Find and fire a free bullet
            for (int x = 0; x < iMaxBullets; x++)
            {
                if (!bullets[x].IsActive)
                {
                    bullets[x].Fire(player.X + iBulletFacingOffsets[player.Facing],
                                             player.Y + iBulletVerticalOffset + iVerticalOffset,
                                             player.Facing);
                    break;
 
                }
            }
        }


Since we are using an array of bullet objects, we need to find a "free" bullet to fire. This is a bullet object that isn't currently active, so a simple loop finds the first non-active bullet execute's it's Fire() method using the player's position and the offsets we discussed above to establish its initial position. (As soon as we have found one, we can exit the loop, so we "break;" out of it to prevent the method from firing all the available bullets with each button press.)

You will notice that we add a second vertical offset here that is passed into our helper function. We will use this when we add powerups, as one of the powerups will be "dual cannons" which will fire a second bullet that is 4 pixels above the normal bullet.

If we didn't find a free bullet nothing will happen, but that would mean that every bullet is currently on the screen! With 40 bullets and a delay between firing, this should never happen.

One more helper function, this time to check for the player pressing the fire button:

        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 >= fFireDelay)
                {
                    FireBullet(0);
                    fBulletDelayTimer = 0.0f;
                }
            }
        }


We will call this routine from our Update method (we'll expand it later to account for SuperBombs and other Power Ups). Either the space bar or the "A" button on the gamepad will fire the player's weapon. We use our standard timing method to determine if a bullet can be fired. If it can, we call FireBullet with a vertical offset of 0.

At the top of our Update method (Right after the check to see if the user has pressed the Back button on the game pad to exit), add the following line:

            fBulletDelayTimer += (float)gameTime.ElapsedGameTime.TotalSeconds;


This will handle controlling how fast bullets can be fired by the player.

Add the following to your Game1.cs file's Update method, right after the Explosion.Update(gameTime) call:

                UpdateBullets(gameTime);


Which will call our helper function to update any active bullets. Next, add the following right after the call to CheckVerticalMovementKeys (but outside the if statement about vertical movement timing):

                    CheckOtherKeys(Keyboard.GetState(), GamePad.GetState(PlayerIndex.One));


Which will allow the player to actually fire a bullet by pressing the fire button/key.

Finally, we need to actually draw our bullets. In your draw method, after the call to draw the player's ship, add the following:

                // Draw any active player bullets on the screen
                for (int i = 0; i < iMaxBullets; i++)
                {
                    // Only draw active bullets
                    if (bullets[i].IsActive)
                    {
                        bullets[i].Draw(spriteBatch);
                    }
                }


Here we simply loop through our bullet array and draw any active bullets.

Fire up your game! You should be able to fly around with either the Game Pad or the keyboard and fire bullets!

In our next installment, we will revise our Game project to allow for a Title Screen phase and a Gameplay phase, as well as put in our framework for starting new games and new game waves.

(Continued in Part 6...)


































 

 
 
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