January, 2011

Posts
• Adding Multiple Enemies with AI (TriangleShooter)

Another week, another article bringing us one step further in the creation of TriangleShooter. Last week, we did a bit of refactoring and broke Player and Enemy out into classes. At the time, this didn’t change anything in how the application worked, but this week, we’ll take advantage of it by adding in some basic AI, and then showing how easily you can scale out to more enemies. We’re going to be starting with the project we created worked on last week, so you can grab that from the SkyDrive Share.

The first thing we can do is make the enemy seem a bit more impressive. He was just sitting out there while we flew all around him just making fun of his inability to move. Well, let’s change that. We’ll begin by some basic animation. We’re not going into sprite frames or anything like that, a rotation should do. In the update method, we’ll add a line to update the enemy’s rotation. Let’s drop it in right before the collision detection. Rotation is handled as a float, in radians. That means that when the rotation is equal to 2*pi, it will be a single rotation. Since rotation is cyclical, and bounded between 0 and 2*pi, we can make use of the modulus operator to make sure that it doesn’t eventually exceed the boundary of what a float can handle.

Rotating the Enemy
1. enemy.Rotation += ((float)Math.PI / 30f) % ((float)Math.PI * 2);

We’ll need to update the Draw function as well, to make use of the newly updated rotation. We’ll use the same format as we did for the player. The main difference will be that the origin of rotation for the enemy will be the center.

Drawing the Rotated Enemy
1. spriteBatch.Draw(enemy.Avatar, enemy.Position, null, Color.White, enemy.Rotation, new Vector2(enemy.Avatar.Width, enemy.Avatar.Height) / 2, 1f, SpriteEffects.None, 0f);

If you run the application now, it works much like it did at the end of the last article, but now the enemy rotates. That simple change makes the visuals of the game quite a bit more interesting.  It also gives us a great view into how easy it is going to be to add in some additional AI.

Let’s jump right into that. We’ve already seen how easy it is to make the player move towards a touch point. Let’s do the same thing with the enemy, and set the destination to the player. That’s the basis for follow AI. Add the following lines to the Update method right after the line that updates the rotation.

1. Vector2 enemyDirection = player.Position - enemy.Position;
2.
3. if (enemyDirection.LengthSquared() > enemy.Speed * enemy.Speed)
4. {
5.     enemyDirection.Normalize();
6.
7.     enemy.Position += enemyDirection * enemy.Speed;
8. }

That’s it. We run the application, and now the enemy follows you around the screen. Programming is really easy, when you think about it.

The next step is to add more enemies. This is actually a lot easier than you might think. First, let’s look at how we are currently creating instances of the Enemy class.

Creating an Enemy
1. enemy = new Enemy();
2. enemy.Avatar = txEnemy6;
3. enemy.Position = new Vector2(600f, 200f);
4. enemy.Rotation = 0f;

As you can see, it takes four lines of code, and we aren’t even setting speed at this point. There are a couple of ways to condense this down, including creating a constructor, and using Object Initialization. In this case, let’s go ahead and use Object Initialization, which means that we will use the default constructor, then assign the values directly after in braces. I prefer this method in this case because when I read through the code, it is more explicit. You may prefer to use a constructor, which works just fine by me. Using Object Initialization, the previous code changes to the following:

Using Object Initialization
1. enemy = new Enemy() { Avatar = txEnemy6, Position = new Vector2(600f, 200f), Rotation = 0f, Speed =  3f};

You’ll notice that I added Speed in there while I was at it.

With this setup, it’s much easier to instantiate a full new Enemy with just a single line of code.

We had been using a variable called enemy defined throughout the class. To allow for more enemies, we’ll make use of a generic collection called List. Replace the line towards the top of game1 defining enemy

Single Enemy
1. Enemy enemy;

with a List of Enemy

List of Enemy
1. List<Enemy> enemies;

and initialize it in the Initialize method.

Initializing enemies
1. enemies = new List<Enemy>();

in the LoadContent method, replace the line

enemy initialization
1. enemy = new Enemy() { Avatar = txEnemy6, Position = new Vector2(600f, 200f), Rotation = 0f, Speed = 3f };

with the following few lines. This will create three separate enemies, each with a different speed.

1. enemies.Add(new Enemy() { Avatar = txEnemy6, Position = new Vector2(600f, 200f), Rotation = 0f, Speed = 3f });
2. enemies.Add(new Enemy() { Avatar = txEnemy6, Position = new Vector2(600f, 300f), Rotation = 0f, Speed = 3.5f });
3. enemies.Add(new Enemy() { Avatar = txEnemy6, Position = new Vector2(600f, 400f), Rotation = 0f, Speed = 4f });

the Update method needs to add a foreach loop around the area we’re using enemy. It will look like this

Updating each Enemy
1. foreach (Enemy enemy in enemies)
2. {
3.     enemy.Rotation += ((float)Math.PI / 30f) % ((float)Math.PI * 2);
4.
5.     Vector2 enemyDirection = player.Position - enemy.Position;
6.
7.     if (enemyDirection.LengthSquared() > enemy.Speed * enemy.Speed)
8.     {
9.         enemyDirection.Normalize();
10.
11.         enemy.Position += enemyDirection * enemy.Speed;
12.     }
13.
14.     if (new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, txEnemy6.Width, txEnemy6.Height).Contains((int)player.Position.X, (int)player.Position.Y))
15.     {
16.         triangleColor = Color.Red;
17.     }
18. }

and a foreach around the enemy in Draw

Drawing each Enemy
1. foreach (Enemy enemy in enemies)
2. {
3.     spriteBatch.Draw(enemy.Avatar, enemy.Position, null, Color.White, enemy.Rotation, new Vector2(enemy.Avatar.Width, enemy.Avatar.Height) / 2, 1f, SpriteEffects.None, 0f);
4. }

And again, that’s all. A couple more simple changes, and we’ve added support for as many enemies as we want.

Next week, we’ll add in some spawning code, and add in the rest of the enemy graphics to add in some variety.

• Spawning More Enemies (TriangleShooter)

Time for my favorite part of Monday – Another article improving my TriangleShooter game! Last week, I promised you that I would spawn some more enemies, and that I would add in some other enemy types to make it more visually appealing. The great news is, both of those things are pretty easy. We’ll get started with the code base from last week, so you can grab that from my SkyDrive share.

We can start by adding in some additional enemy types. To begin with, we’ll drag and drop the rest of the Enemies from the Enemies file from SkyDrive into the Content project with the first enemy. We’ll then create some variables to hold the textures.

Textures for Enemy
1. Texture2D txEnemy3;
2. Texture2D txEnemy4;
3. Texture2D txEnemy5;
4. Texture2D txEnemy6;

We’ll then load them from the content project in the LoadContent method.

We can try this out by changing the lines further down in LoadContent to make use of the new textures.

Creating Diverse Enemies
1. enemies.Add(new Enemy() { Avatar = txEnemy3, Position = new Vector2(600f, 200f), Rotation = 0f, Speed = 3f });
2. enemies.Add(new Enemy() { Avatar = txEnemy4, Position = new Vector2(600f, 300f), Rotation = 0f, Speed = 3.5f });
3. enemies.Add(new Enemy() { Avatar = txEnemy5, Position = new Vector2(600f, 400f), Rotation = 0f, Speed = 4f });

Next up, we need to start spawning some more enemies. What we’re looking to do here is to bring in new enemies every so often. The faster you spawn the enemies, the more difficult the game would be, so you can play with the amount of time between spawns. In order to track them, though, we’ll need a variable to track how long we have left until the next spawn. Since we’ve got some different enemy types, we may as well add in some randomization, so we’ll create a variable to handle randomness as well. As always, we’ll drop the variable up at the top of the game file.

Spawn Timer and Randomization
1. TimeSpan timeToSpawn;
2.
3. Random random;

And then we initialize them down in Initalize()

Initializing Spawn and Random
1. timeToSpawn = new TimeSpan(0, 0, 0, 0, 1000);
2.
3. random = new Random();

This sets up timeToSpawn as one second. We’ll count down until this reaches zero, then spawn another enemy. Luckily, there’s a easy way to get elapsed game time in the Update Method with the gameTime argument. We’ll subtract the elapsed game time from the timeToSpawn variable, then check to see if it is less than or equal to zero. If it is, we create a new enemy and add it to the enemies list. To add variety, we can pick a random edge, and spawn the enemy somewhere along it so that it doesn’t just always come from the same spot. Finally, we’ll reset the timeToSpawn so that we can spawn another. All of this will go in the Update method after we check the enemy collision.

Spawning More Enemies
1. timeToSpawn -= gameTime.ElapsedGameTime;
2.
3. if (timeToSpawn < TimeSpan.Zero)
4. {
5.     Vector2 spawnPosition;
6.     switch (random.Next(4))
7.     {
8.         case 0:
9.             spawnPosition = new Vector2(0, random.Next(480));
10.             break;
11.         case 1:
12.             spawnPosition = new Vector2(800, random.Next(480));
13.             break;
14.         case 2:
15.             spawnPosition = new Vector2(random.Next(800), 0);
16.             break;
17.         case 3:
18.             spawnPosition = new Vector2(random.Next(800), 480);
19.             break;
20.         default:
21.             spawnPosition = Vector2.Zero;
22.             break;
23.     }
24.
25.     switch (random.Next(4))
26.     {
27.         case 0:
28.             enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy3, Speed = 3f });
29.             break;
30.         case 1:
31.             enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy4, Speed = 3.5f });
32.             break;
33.         case 2:
34.             enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy5, Speed = 4f });
35.             break;
36.         case 3:
37.             enemies.Add(new Enemy() { Position = spawnPosition, Rotation = 0f, Avatar = txEnemy6, Speed = 4.5f });
38.             break;
39.         default:
40.             break;
41.     }
42.
43.     timeToSpawn = new TimeSpan(0, 0, 0, 0, random.Next(500) + 250);
44. }

There’s just so many of them!

Next week, we’ll add in shooting! Until this point, we were just a triangle moving around, but now we’ll finally be a triangle that shoots!

Let’s make TriangleShooter a better game. This time around, we’ll be putting the shooter in TriangleShooter, by adding in the ability for the player to shoot. We’ll be starting with last week’s code, so you can go over and grab that from my SkyDrive share.

I tried a few different ways of making the player shoot in my development of the game, and the best one I have found so far is to have you always laying out a steady stream of shots. Towards the end of the series, I’ll be doing some play mechanics tweaking, and we can investigate some of the other ways, but that’s how I’m going to do things in this article.

If you’re going to shoot, you need a bullet. You can get my highly stylized, square bullet from the SkyDrive share. To begin with, download that, then add it into the TriangleShooterContent project by dragging it and dropping it in there. We’ll also need a class to handle the bullet, which is going to look familiar. Create a new class in the TriangleShooter project named Bullet.cs, and set the content to the following:

Bullet.cs
1. using Microsoft.Xna.Framework;
2. using Microsoft.Xna.Framework.Graphics;
3.
4. namespace TriangleShooter
5. {
6.     class Bullet
7.     {
8.         public Texture2D Avatar { get; set; }
9.         public Vector2 Position { get; set; }
10.         public float Rotation { get; set; }
11.         public float Speed { get; set; }
12.     }
13. }

We’ll set up the variables at the top of the game class to hold the texture, the list of Bullets, and to monitor the passing time to shoot, along with a constant to tweak the timer a bit.

Variables for Bullets
1. const int ShootDelay = 250;
2. TimeSpan timeToShoot;
3.
4. List<Bullet> bullets;
5. Texture2D txBullet;

In the Initialize method, we set up the list for the bullets, and set the time to shoot from the constant value we set up.

Initialize
1. bullets = new List<Bullet>();
2.
3. timeToShoot = new TimeSpan(0, 0, 0, 0, ShootDelay);

And now we’ve got everything we need. We only have the Update and Draw to go. The Update is more interesting, so let’s start there.

Update
1. timeToShoot -= gameTime.ElapsedGameTime;
2.
3. if (timeToShoot <= TimeSpan.Zero)
4. {
5.     timeToShoot = new TimeSpan(0, 0, 0, 0, ShootDelay);
6.     bullets.Add(new Bullet() { Avatar = txBullet, Position = new Vector2(player.Position.X + player.Avatar.Width * (float)Math.Cos(player.Rotation), player.Position.Y + player.Avatar.Height * (float)Math.Sin(player.Rotation)), Rotation = player.Rotation, Speed = 15f });
7.
8. }
9.
10. foreach (Bullet b in bullets.ToList())
11. {
12.     b.Position = new Vector2(b.Position.X + b.Speed * (float)Math.Cos(b.Rotation), b.Position.Y + b.Speed * (float)Math.Sin(b.Rotation));
13.
14.     if (!graphics.GraphicsDevice.Viewport.Bounds.Contains(new Point((int)b.Position.X, (int)b.Position.Y)))
15.     {
16.         bullets.Remove(b);
17.     }
18.     else
19.     {
20.         foreach (Enemy enemy in enemies.ToList())
21.         {
22.             if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)b.Position.X, (int)b.Position.Y))
23.             {
24.                 bullets.Remove(b);
25.                 enemies.Remove(enemy);
26.                 break;
27.             }
28.         }
29.     }
30. }

The first part of the code is similar to what we do to spawn enemies. We subtract the elapsed time from the remaining time, and if the time is less than zero, we spawn a new bullet, setting it’s position to the front of the player and the rotation to the players rotation. We then update all existing bullets. Because we may need to remove them from the master list, we make a copy in the foreach by using the .ToList() function. We move them in the direction based on their rotation, and see if they have left the bounds of the screen. If they have, we remove them from the update list. To optimize things here, we should do something like create a list of decommissioned bullets, and reuse them rather than creating new ones each time, but we won’t do that this time around. If they are within the bounds of the screen, we check collision with each of the enemies, again using the .ToList() function because we might need to remove items. If they collide, we remove both the bullet, and the enemy that collided with it.

This is all we need to do, but we do need to draw the bullets. Luckily, this is a simple process, just add a few lines into Draw.

Draw
1. foreach (Bullet b in bullets)
2. {
3.     spriteBatch.Draw(b.Avatar, b.Position, null, Color.White, b.Rotation, new Vector2(b.Avatar.Width / 2, b.Avatar.Height / 2), 1f, SpriteEffects.None, 0f);
4. }

And now we have shooting!

Next week, we’ll do some refactoring to make the code easier to work with, and see if we can add in some death behavior.

• Refactoring and Death Behavior (TriangleShooter)

This time around, we’ll be doing a bit of refactoring to make the code easier to deal with, and adding in some death behavior. To begin with, we’ll need to start with the code from last week, so go ahead and pick that up from the SkyDrive share.

To begin with, let’s update the Initialize and LoadContent methods, and move some of the setup into a new method called NewGame. This will allow us to reset the game back to it’s initial state more easily, such as when you get killed by running into an enemy.

Initialize
1. protected override void Initialize()
2. {
3.     random = new Random();
4.
5.     base.Initialize();
6. }

2. {
3.     // Create a new SpriteBatch, which can be used to draw textures.
4.     spriteBatch = new SpriteBatch(GraphicsDevice);
5.
12.
13.     NewGame();
14. }

NewGame
1. private void NewGame()
2. {
3.     triangleColor = Color.White;
4.
5.     timeToSpawn = new TimeSpan(0, 0, 0, 0, 1000);
6.     timeToShoot = new TimeSpan(0, 0, 0, 0, ShootDelay);
7.
8.     player = new Player() { Avatar = txPlayer, Position = Vector2.Zero, Rotation = 0f };
9.     bullets = new List<Bullet>();
10.
11.     enemies = new List<Enemy>();
12.     enemies.Add(new Enemy() { Avatar = txEnemy3, Position = new Vector2(600f, 200f), Rotation = 0f, Speed = 3f });
13.     enemies.Add(new Enemy() { Avatar = txEnemy4, Position = new Vector2(600f, 300f), Rotation = 0f, Speed = 3.5f });
14.     enemies.Add(new Enemy() { Avatar = txEnemy5, Position = new Vector2(600f, 400f), Rotation = 0f, Speed = 4f });
15. }

If we update the collision detection, we can call NewGame now if the collision happens, which means we can reset the game with ease. Also, while I was setting this up, I found a bug in the code based around the origin of the drawing and the rectangle we were using for collision. The updated code in the Update method looks like the following:

Updated Collision Detection
1. if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)player.Position.X, (int)player.Position.Y))
2. {
3.     triangleColor = Color.Red;
4.     NewGame();
5. }

Now, when you die, the game resets. It might make more sense to freeze the game, and wait for the player to choose to continue before resetting the game. To do that, let’s create a new variable to say whether the player is alive or not. Let’s call it isPlayerDead

And initialize it to false in the NewGame method

Then check if the player is dead in the Update method

Update
2. {
4.     {
5.         NewGame();
6.     }
7. }
8. else
9. {
10.     // Everything else in Update
11. }

This means that the Collision detection from above needs to be updated once more to set the boolean rather than calling NewGame directly.

Collision Detection
1. if (new Rectangle((int)enemy.Position.X - enemy.Avatar.Width / 2, (int)enemy.Position.Y - enemy.Avatar.Height / 2, enemy.Avatar.Width, enemy.Avatar.Height).Contains((int)player.Position.X, (int)player.Position.Y))
2. {
3.     triangleColor = Color.Red;
5. }

And we’ve now got a game that actually ends when you die, and let’s you start a new game. Sounds like we’re ready for scoring. We’ll add that in next week. See you then.

• Breaking the Player and Enemy out into Classes (TriangleShooter)

This week, I’m going to go over how to break out Player and Enemy into classes, which will make it easier to handle multiple enemies, collision, AI, that sort of thing. We’ll be starting things off with the project from last week, so you can go grab that now.

If you take a look at how we’ve been doing things, you’ll notice that I have been using only the file game1.cs, which was automatically generated for me. When I added an enemy, in order to track its position, I created a variable called enemyPosition, because I was already using position for my player. If I wanted to add another enemy, I would have to do something like enemyTwoPosition or enemy2Position, and the next enemy after that would have to be enemy3Position, and it would just be a pain to have to keep track of all of those and copy all of the attributes for each new instance. It would also be a pain to have to check every instance for their collision, and to have to draw each one. If I changed something in one, I’d have to change it in all of them, and missing one of them could result in a bug that might not be noticed right away, when it would be easiest to fix. That’s why I’m going to break out the player and enemy into their own classes. I’m not going to go into subclassing, although there are a good number of shared attributes that would make it make sense to do so.

Let’s take a look at what we have, first.

Current Player and Enemy
1. Texture2D triangle;
2. Vector2 position;
3. float rotation;
4. Color triangleColor;
5.
6. Texture2D enemy;
7. Vector2 enemyPosition;

We only have a texture and a position for the enemy, because it was static in the last article. It would make sense to add rotation to it, as well as a speed variable that will allow us to have different classes of enemies with different speeds. To begin, we’ll need to create a new class. Right click on the TriangleShooter project in the Solution Explorer, and choose Add –> Class. When it asks for a filename, type in Player.cs. Add another class, and name this one Enemy.cs. The contents of the two files is below:

Player.cs
1. using Microsoft.Xna.Framework;
2. using Microsoft.Xna.Framework.Graphics;
3.
4. namespace TriangleShooter
5. {
6.     class Player
7.     {
8.         public Texture2D Avatar { get; set; }
9.    ��    public Vector2 Position { get; set; }
10.         public float Rotation { get; set; }
11.         public float Speed { get; set; }
12.     }
13. }

Enemy.cs
1. using Microsoft.Xna.Framework;
2. using Microsoft.Xna.Framework.Graphics;
3.
4. namespace TriangleShooter
5. {
6.     class Enemy
7.     {
8.         public Texture2D Avatar { get; set; }
9.         public Vector2 Position { get; set; }
10.         public float Rotation { get; set; }
11.         public float Speed { get; set; }
12.     }
13. }

Essentially, they are the same thing. That’s why I said that you could do well with subclassing, but we’ll keep it like this. I also added Speed onto Player, which will be useful a bit later in the series when I get into fine-tuning your game to make it more fun to play. To take advantage of the new classes, we’ll make a few changes to the game1.cs file. Namely, we’ll modify the variable declaration, the Initalize method, LoadContent, Update, and Draw. We’re just refactoring here. No changes to how the game plays are made in this modification.

Variable Declaration
1. GraphicsDeviceManager graphics;
2. SpriteBatch spriteBatch;
3.
4. Player player;
5. Texture2D txPlayer;
6.
7. Enemy enemy;
8. Texture2D txEnemy6;
9.
10. Color triangleColor;
11.
12. int movementId = 0;

We move most of the initialization until after LoadContent, since we need to load the textures.

Initialize
1. protected override void Initialize()
2. {
3.     triangleColor = Color.White;
4.
5.     base.Initialize();
6. }

Here is where we set up the Player and Enemy instances.

2. {
3.     // Create a new SpriteBatch, which can be used to draw textures.
4.     spriteBatch = new SpriteBatch(GraphicsDevice);
5.
8.
9.     player = new Player();
10.     player.Avatar = txPlayer;
11.     player.Position = Vector2.Zero;
12.     player.Rotation = 0f;
13.
14.     enemy = new Enemy();
15.     enemy.Avatar = txEnemy6;
16.     enemy.Position = new Vector2(600f, 200f);
17.     enemy.Rotation = 0f;
18. }

In the Update method, we switch everything over to use the classes.

Update
1. protected override void Update(GameTime gameTime)
2. {
3.     // Allows the game to exit
5.         this.Exit();
6.
7.     foreach (TouchLocation tl in TouchPanel.GetState())
8.     {
9.         if (tl.State == TouchLocationState.Pressed)
10.         {
11.             if (movementId == 0)
12.             {
13.                 movementId = tl.Id;
14.             }
15.         }
16.
17.         if (tl.Id == movementId)
18.         {
19.             Vector2 direction = tl.Position - player.Position;
20.
21.             if (direction.LengthSquared() > 100)
22.             {
23.                 direction.Normalize();
24.
25.                 player.Rotation = (float)(Math.Atan2(direction.Y, direction.X));
26.
27.                 player.Position += direction * 10;
28.             }
29.         }
30.
31.         if (tl.State == TouchLocationState.Released)
32.         {
33.             if (tl.Id == movementId)
34.             {
35.                 movementId = 0;
36.             }
37.         }
38.     }
39.
40.     if (new Rectangle((int)enemy.Position.X, (int)enemy.Position.Y, txEnemy6.Width, txEnemy6.Height).Contains((int)player.Position.X, (int)player.Position.Y))
41.     {
42.         triangleColor = Color.Red;
43.     }
44.
45.     base.Update(gameTime);
46. }

And the same with Draw

Draw
1. protected override void Draw(GameTime gameTime)
2. {
3.     GraphicsDevice.Clear(Color.Black);
4.
5.     spriteBatch.Begin();
6.     spriteBatch.Draw(player.Avatar, player.Position, null, triangleColor, player.Rotation, new Vector2(0, txPlayer.Height / 2), 1f, SpriteEffects.None, 0f);
7.     spriteBatch.Draw(enemy.Avatar, enemy.Position, Color.White);
8.     spriteBatch.End();
9.
10.     base.Draw(gameTime);
11. }

This sets us up perfectly to begin adding additional enemies into the game. Next week, I’ll go ahead and do that, and add some basic AI to the enemies.

Page 1 of 1 (5 items)