Part Six - Enemies



It is time for us to add some bad guys for us to eventually shoot down. By the end of this segment, we will have our enemy class in place and generate a few enemies that will fly around our game world.

As usual, we'll add a new asset to the project. Remember when I said I wasn't an artist? Here is the "enemy ship" I will be using:



I know, I know… but Jason claims he has been too busy to make me some new graphics. Go ahead and save it to Content/Textures and add it to your project.

You could certainly expand on this enemy graphic, perhaps make a sprite sheet with several animated frames of lights blinking or things whirling around.

Lets add a new class called "Enemy" to our project and, as always, add our using statements to the top:

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


And our declarations:

        AnimatedSprite asSprite;
 
        static int iMapWidth = 1920;
        static int iPlayAreaTop = 30;
        static int iPlayAreaBottom = 630;
        static Random rndGen = new Random();
        int iX = 0;
        int iY = -100;
        int iBackgroundOffset = 0;
        Vector2 v2motion = new Vector2(0f, 0f);
        float fSpeed = 1f;
        float fEnemyMoveCount = 0.0f;
        float fEnemyDelay = 0.01f;
        bool bActive = false;


Most of this should look pretty standard by now. We have an AnimatedSprite to hold our image, an X/Y location, and an "Active" boolean.

Some of the new things here are:

iMapWidth - We will need to know how big the game board is when we translate from World Coordinates to Screen Coordinates. This static value (shared by all enemies) holds that size.

iPlayAreaTop and iPlayAreaBottom - These two integers hold the pixel positions of where we will consider the active play area to end. Enemies can't move above the 30 pixel mark, nor below the 630 pixel mark. Remember that a similar restriction is placed on the player's vertical movement.

rndGen - This instance of the Random class will allow us to generate random numbers to control our enemies movements. It is static because there is no need for a different random number generator instance for each enemy.

iBackgroundOffset - This is passed into the enemy during the Update routine and lets the enemy know how far the player has scrolled on the screen. We will need to know this when position our graphic on the screen.

v2motion - This Vector2 value will hold the direction that the enemy ship is currently moving in. Our "AI" system (ok, we can't really call it that since it will be just random movements) will set this value periodically during the Update method.

fSpeed - The speed at which v2motion is appled to the position of the enemy.

fEnemyMoveCount and fEnemyDelay - These floats represent our standard timing functions to keep a consistant gameplay rate.

We will need some public properties to manipulate our enemies:

        public int X
        {
            get { return iX; }
            set { iX = value; }
        }
 
        public int Y
        {
            get { return iY; }
            set { iY = value; }
        }
 
        public bool IsActive
        {
            get { return bActive; }
        }
 
        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);
            }
        }
 
        public int Offset
        {
            get { return iBackgroundOffset; }
            set { iBackgroundOffset = value; }
        }
 
        public float Speed
        {
            get { return fSpeed; }
        }
 
        public Vector2 Motion
        {
            get { return v2motion; }
            set { v2motion = value; }
        }


We use the background offset to determine the on-screen location of our enemy, accounting for crossing the "zero line". (See the GetDrawX() method below)

As with our explosions, our constructor for the Enemy class is very simple:

        public Enemy(Texture2D texture, 
                     int X, int Y, int W, int H, int Frames)
        {
            asSprite = new AnimatedSprite(texture, X, Y, W, H, Frames);
        }


Again we are simply passing the parameters along to the AnimatedSprite class. We will handle all of the parameters for the enemy when they are generated by the Generate() method (coming up shortly).

First, a couple of helper functions:

        public void Deactivate()
        {
            bActive = false;
        }
 
        private int GetDrawX()
        {
            int X = iX - iBackgroundOffset;
            if (X > iMapWidth)

                X -= iMapWidth;
            if (X < 0)
                X += iMapWidth;
 
            return X;
        }


As it's name implies, Deactivate simply deactivates and enemy. Non-active enemies won't update or draw.

The GetDrawX() method translates the enemy's "world position" X coordinate to a screen position by subtracting the value of iBackgroundOffset from the world-X position. It then accounts for wrapping off of either end of the map by adding or subtracting the width of the map.

So, for example, if the enemy is located at world-position 500 and we are it our starting position of 0 iBackgroundOffset, the enemy will be drawn starting at pixel 500. If we are scrolled right to postion 250, the enemy needs to be drawn at postion 250 (500-250).

Where it gets tricky is, as you can imagine, surrounding the "zero-line". Lets say our enemy is locasted at world position 100, but we are scrolled so that our current background offset is 1800 pixels. If we simply subtract the iBackgroundOffset from iX, we get -1700 pixels, which won't draw anything to the screen at all. But if we add the iMapWidth to the -1700, we get 220 pixels, which is the correct draw location for our enemy.

Our last helper function will generate a random direction and speed for our enemy ship:

        public void RandomizeMovement()
        {
            v2motion.X = rndGen.Next(-50, 50);
            v2motion.Y = rndGen.Next(-50, 50);
            v2motion.Normalize();
            fSpeed = (float)(rndGen.Next(3,6));
        }


We use our rndGen Random object to generate X and Y values for our vector between -50 and +50 using the Random class' Next method. This way of calling the method generates a random integer between the two passed values.

We then use the Vector2's "Normalize" method to turn the v2motion vector into a "unit vector" which a a vector with a length of 1. This means that our vector now contains directional information but not speed information. This means we can multiple the vector by a scalar (fSpeed in our case) in order to apply a speed when the vector is added to our object's location.

Speaking of fSpeed, the last thing our RandomizeMovement() method does is generate a speed between 3 and 6. This value will be multiple into the v2motion vector when it is added to the enemy position during Update().

Next, lets add a helper function to our class:

        public void Generate(int iLocation, int iShipX)
        {
            // Generate a random X location that is NOT 
            // within 200 pixels of the player's ship.
            do
            {
                iBackgroundOffset = iLocation;
                iX = rndGen.Next(iMapWidth);
            } while (Math.Abs(GetDrawX() - iShipX) < 200);
 
            // Generate a random Y location between iPlayAreaTop 
            // and iPlayAreaBottom (the area of our game screen)
 
            iY = rndGen.Next(iPlayAreaTop,iPlayAreaBottom);
            RandomizeMovement();
            bActive = true;
        }


We will call Generate() from our game whenever we need to set up a new enemy. The Generate() method will randomize the location of the enemy ship, using Math.Abs (absolute value) to make sure that it doesn't end up within 200 pixels of the player's ship horizontally. This way we can start a new game wave and not worry about clobbering the player unfairly.

The vertical location of the ship is generated beteween the bounds of the Play Area, and our RandomizeMovement() method is called to establish an initial direction and speed for our enemy. Finally, the enemy is made active by setting bActive to true.

I'm going to go ahead and add the Draw method here since it is very simple. We'll handle update last because it will take some discussion:

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


Nothing surprising here… if the enemy is active, we ask the AnimatedSprite to draw itself. As with our explosions, we use GetDrawX() to map the World Coordinates to Screen Coordinates.

Finally, we need our Update() method:

        public void Update(GameTime gametime, int iOffset)
        {
            iBackgroundOffset = iOffset;
 
            fEnemyMoveCount += (float)gametime.ElapsedGameTime.TotalSeconds;
            if (fEnemyMoveCount > fEnemyDelay)
            {
                iX += (int)((float)v2motion.X * fSpeed);
                iY += (int)((float)v2motion.Y * fSpeed);
 
                if (rndGen.Next(200) == 1)
                {
                    RandomizeMovement();
                }
 
                if (iY < iPlayAreaTop)
                {
                    iY = iPlayAreaTop;
                    RandomizeMovement();
                }
 
                if (iY > iPlayAreaBottom)
                {
                    iY = iPlayAreaBottom;
                    RandomizeMovement();
                }
 
                if (iX < 0)
                    iX += iMapWidth;
 
                if (iX > iMapWidth)
                    iX -= iMapWidth;
 
                fEnemyMoveCount = 0f;
            }
            asSprite.Update(gametime);
        }


As with our Explosion class, we update the passed in background offset for our enemy, after which we check to see if enough time has passed to allow us to do something again.

If so, we have a lot of things to do. First off, we update the enemy position based on the v2motion vector and fSpeed by simply multiplying the magnitude of the X and Y components of the vector by the fSpeed variable and adding them to the position.

Next ,we generate a random number between 0 and 199. If the result is a 1 (so 0.5% chance) we will generate a new random movement for our enemy.

The next couple checks determine if we have moved off of the top or bottom of the play area. If we have, we clamp back to the play area and generate a new random movement. It is entirely possible that this new movement will take us off of the screen on the next Update cycle, but we will hit the same "if" condition again and try again if that happens.

If our X position moves off of either side of the game map (ie, becomes less than 0 or greater than the map width) we add or subtract the map width as appropriate to wrap it around.

We then reset our delay timer.

Finally, we allow the sprite to update it's animate frames (since it keeps track of it's own framerate we don't need to control it with our timing values here).

Adding Enemies to Our Game

That handles our enemy class, but of course we still can't see them. Lets add some to the game. In the declarations area of your Game1.cs file, lets add the values we will need to support our enemies:

        int iMaxEnemies = 9;
        int iActiveEnemies = 9;
        static int iTotalMaxEnemies = 30;
        Enemy[] Enemies = new Enemy[iTotalMaxEnemies];
        Texture2D t2dEnemyShip;


Here, iTotalMaxEnemies represents to overall maximum number of enemies we can ever have on a level. We are setting this to 30 (and recall it needs to be static for us to use it as the size of our array).

iMaxEnemies will control how many enemies are in the "current wave". We will expand on this when we actually add collisions and waves. iActiveEnemies represents how many undestroyed enemies remain on the current level.

In our LoadContent, lets add the following to set up our enemies:

            t2dEnemyShip = Content.Load<Texture2D>(@"Textures\enemy");
 
            for (int i = 0; i < iTotalMaxEnemies; i++)
            {
                Enemies[i] = new Enemy(t2dEnemyShip, 0, 0, 32, 32, 1);
            }


Here we simply load the texture and execute all of the constructors for our enemies.

Now lets add a helper function for our game:

        protected void GenerateEnemies()
        {
            if (iMaxEnemies < iTotalMaxEnemies)
                iMaxEnemies++;
 
            iActiveEnemies = 0;
 
            for (int x = 0; x < iMaxEnemies; x++)
            {
                Enemies[x].Generate(background.BackgroundOffset,
                                    player.X);
                iActiveEnemies += 1;
            }
        }


This helper function checks to see if iMaxEnemies is less than iTotalMaxEnemies and, if it is, adds one to iMaxEnemies. Whenever this routine is called, a new wave of enemies, one enemy larger than the last, will be generated. Note that existing enemies will simply be "regenerated" if they have been destroyed. We update our iActiveEnemies count for each enemy we create.

Lets add one more helper function that will be expaneded later:

        protected void StartNewWave()
        {
            GenerateEnemies();
        }


At the moment we are simply calling GenerateEnemies, but down the road we will expand this to have other impacts on our game state. Since we don't currently have anything in place to handle our game state, we need to temporarially put a call to StartNewWave() somewhere that it won't get run over and over and over again. The easiest place to do this for now is at the very end of the LoadContent method, so add this line there:

            StartNewWave();


Next we will need to handle updating our enemies during our Update method. This is as simple as looping through the active enemies and calling their Update method. Add the following to your Update method after the call to CheckOtherKeys:

                    for (int i = 0; i < iTotalMaxEnemies; i++)
                    {
                        if (Enemies[i].IsActive)
                            Enemies[i].Update(gameTime,
                              background.BackgroundOffset);
                    }


Finally we need to draw our enemies to the screen. Update your Draw code by adding the following right after all of the bullets are drawn:

                for (int i = 0; i < iMaxEnemies; i++)
                {
                    if (Enemies[i].IsActive)
                      Enemies[i].Draw(spriteBatch,
                        background.BackgroundOffset);
                }


Go ahead and run your game! It should generate 10 enemies that fly randomly around on your game board.

At the moment, you can fly right through them and they are maddeningly immune to your ship's cannons, but not for long!

(Continued in Part 7...)



































 

 
 
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