Artificial intelligence, huh? It probably sounds a little bit scary and pretty cool at the same time. We touched on the concept of artificial intelligence in previous chapters, but now let’s take a look at what artificial intelligence really is.
Since the beginning of the computing age, researchers have pondered and debated ways to make machines act more like humans and/or give them some form of artificial intelligence. The biggest problem with the entire line of artificial intelligence science is that there really is no way to define intelligence. What makes somebody or something intelligent? That’s an excellent question, and perhaps one that we will never be able to answer fully. Numerous other questions crop up as well. How do you define typical human behavior? What forms of human behavior constitute intelligence? What forms of human behavior are worthy of replication in machines?
You could argue that the application you have written is “intelligent” because the sprites animate on their own (that is, the user doesn’t have to tell them to continually animate). So, they must be intelligent, right? Others would argue that they are not intelligent, though, because they don’t “do” anything; they just sit there and spin. Even in this example, where it’s clear that the sprites aren’t really intelligent, you can start to see how this area of research is inherently ambiguous.
In this line of science, it’s a blessing and a curse that the idea of creating artificially intelligent beings is so fascinating to humans. It’s a blessing because that’s what drives this science to begin with: researchers and casual observers alike are so interested in the possibilities in this field that more and more money and time are spent on artificial intelligence every year.
At the same time, it’s a curse because that fascination, dating from the early days of civilization, has led to the dramatization of highly advanced artificially intelligent beings in books, movies, and beyond. The expectations in this field are set so high by Hollywood and authors alike that there may never be a way for science to catch up to what is depicted in the latest science fiction.
Alan Turing, widely regarded as the father of modern computer science, invented one of the most famous methods for determining whether or not a machine truly is intelligent. Turing called this method the Imitation Game, but universally it is known as the Turing Test.
Essentially, a Turing Test begins with a human sitting at a keyboard. Using the keyboard, the user interrogates both a computer and another human. The identities of the other subjects are not disclosed to the interrogator. If the interrogator is unable to determine which one is the computer and which one is the human, the computer used in the test is deemed “intelligent.” Although it seems simplistic, programming something that would be able to fool somebody regardless of the line of questioning is extremely difficult.
How does that apply to what we’re talking about with XNA? Well, even though the Turing Test wasn’t a video game, the same principle is behind the essence of nearly all artificial intelligence as related to video games. When programming a computer-controlled entity in any game, the idea is to make that entity play so much like a human that a real human opponent wouldn’t know the difference.
That’s definitely easier said than done, and we aren’t going to get to that level in this game. However, you can clearly see that if you used a Turing Test as your standard, there’s no way that your current application would cut it.
So, what’s the next step? Let’s program some basic movement for your automated sprites, and then we can look at taking things a step further with some basic artificial intelligence algorithms.
This chapter picks up with the code that you finished writing in Chapter 6. Open that project and use it throughout this chapter.
You have already created a sprite manager that draws and updates all the sprites in your application. However, right now all you have is a handful of skull ball sprites that are created when the application starts. Even worse, those sprites don’t move—they just sit there and animate. That just isn’t going to cut it; you need some action and excitement in this game. In this section, you’ll add some code that will create automated sprites at random intervals and send them flying onto the screen to force the player to move around and work a little to avoid hitting them.
Rather than creating the objects in waves or all at once, you want to create them at random intervals. This adds a bit of variety to the game and also serves to keep the player guessing. The first thing you need to do is create some variables that will help you define how often to create your automated sprites.
First, to handle the random factor in your game, create the
following variable at the class level in your Game1
class:
public Random rnd { get; private set;}
Then, initialize the Random
object in the constructor of the Game1
class:
rnd = new Random( );
You now have a Random
variable
that you’ll use for all random aspects of your game. When using random
number generators, it’s important to make sure that you don’t create
multiple random number generators inside a tight loop. This is because
if you create multiple random number generators within a close enough
time frame, there is a chance that they will be created with the same
seed. The seed is what the random number generators
use to determine which numbers are generated and in which order. As you
can probably guess, having multiple random number generators with the
same seed would be a bad thing; you could potentially end up with the
same list of numbers being generated by each, and then your randomness
would be thrown out the window.
One way to avoid this is to have only one random number generator object in your application and reuse that object for all random numbers. Otherwise, just make sure that you create the random number generators in areas of the application that won’t be executed within a short time frame.
System.Random
really isn’t the
greatest of random number generation tools, but it will have to do for
now.
Next, add to the SpriteManager
class some class-level variables that will be used to spawn
sprites:
int enemySpawnMinMilliseconds = 1000; int enemySpawnMaxMilliseconds = 2000; int enemyMinSpeed = 2; int enemyMaxSpeed = 6;
These two sets of variables represent the minimum number of
seconds and the maximum number of seconds to wait to spawn a new enemy,
and the minimum and maximum speeds
of those enemies. The next step is to use these two variables in your
SpriteManager
class to spawn enemies
at some random interval between these two variables and at random speeds between
your two speed threshold values.
Next, you need to get rid of the code that created the AutomatedSprite
s that didn’t move. Because
you’ll now be periodically spawning new enemies, you don’t need those
test sprites anymore. The code to create those objects is in the
LoadContent
method of your SpriteManager
class. Once you remove the code
that creates the AutomatedSprite
s,
the code in the LoadContent
method of
your SpriteManager
class should look
like this:
protected override void LoadContent( ) { spriteBatch = new SpriteBatch(Game.GraphicsDevice); player = new UserControlledSprite( Game.Content.Load<Texture2D>(@"Images/threerings"), Vector2.Zero, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), new Vector2(6, 6)); base.LoadContent( ); }
At this point, you’ll want to make the game window a bit larger so
you have more room to work with. Add this code at the end of the
constructor in your Game1
class:
graphics.PreferredBackBufferHeight = 768; graphics.PreferredBackBufferWidth = 1024;
All right, let’s spawn some sprites. You want to make your sprites spawn at somewhat random intervals, and you want them to spawn from the top, left, right, and bottom sides of the screen. For now, you’ll just have them traveling in a straight direction across the screen, but they’ll do so at varying speeds.
You need to let your SpriteManager
class know when to spawn the
next enemy sprite. Create a class-level variable in your SpriteManager
class to store a value
indicating the next spawn time:
int nextSpawnTime = 0;
Next, you need to initialize the variable to your next spawn time.
Create a separate method that will set the next spawn time to some value
between the spawn time thresholds represented by the class-level
variables you defined previously in the Sprite
Manager
class:
private void ResetSpawnTime( ) { nextSpawnTime = ((Game1)Game).rnd.Next( enemySpawnMinMilliseconds, enemySpawnMaxMilliseconds); }
You’ll then need to call your new ResetSpawnTime
method from the Initialize
method of your SpriteManager
class, so the variable is
initialized when the game starts. Add the following line at the end of
the Initialize
method of the SpriteManager
class, just before the call to
base.Initialize
:
ResetSpawnTime( );
Now you need to use the GameTime
variable in the SpriteManager
’s Update
method to determine when it’s time to
spawn a new enemy. Add this code to the beginning of the Update
method:
nextSpawnTime −= gameTime.ElapsedGameTime.Milliseconds; if (nextSpawnTime < 0) { SpawnEnemy( ); // Reset spawn timer ResetSpawnTime( ); }
This code first subtracts the elapsed game time in milliseconds
from the NextSpawnTime
variable
(i.e., it subtracts the amount of time that has passed since the last
call to Update
). Once the NextSpawnTime
variable is less than zero, your
spawn timer expires, and it’s time for you to unleash the fury of your
new enemy upon the pitiful human player—err…I mean…it’s time to spawn an
AutomatedSprite
. You spawn a new
enemy via the SpawnEnemy
method,
which you’ll write in just a moment. Then, you reset the NextSpawnTime
to determine when a new enemy
will spawn again.
The SpawnEnemy
method will need
to, well, spawn an enemy. You’ll be choosing a random starting position
for the enemy, at the left, right, top, or bottom of the screen. You’ll
also be choosing a random speed for the enemy based on the speed
threshold variables in the Game1
class. To add the enemy sprite to the game, all you need to do is add a
new AutomatedSprite
to your SpriteList
variable. Add the SpawnEnemy
to your code as follows:
private void SpawnEnemy( ) { Vector2 speed = Vector2.Zero; Vector2 position = Vector2.Zero; // Default frame size Point frameSize = new Point(75, 75); // Randomly choose which side of the screen to place enemy, // then randomly create a position along that side of the screen // and randomly choose a speed for the enemy switch (((Game1)Game).rnd.Next(4)) { case 0: // LEFT to RIGHT position = new Vector2( -frameSize.X, ((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferHeight - frameSize.Y)); speed = new Vector2(((Game1)Game).rnd.Next( enemyMinSpeed, enemyMaxSpeed), 0); break; case 1: // RIGHT to LEFT position = new Vector2( Game.GraphicsDevice.PresentationParameters.BackBufferWidth, ((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferHeight - frameSize.Y)); speed = new Vector2(-((Game1)Game).rnd.Next( enemyMinSpeed, enemyMaxSpeed), 0); break; case 2: // BOTTOM to TOP position = new Vector2(((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferWidth - frameSize.X), Game.GraphicsDevice.PresentationParameters.BackBufferHeight); speed = new Vector2(0, -((Game1)Game).rnd.Next(enemyMinSpeed, enemyMaxSpeed)); break; case 3: // TOP to BOTTOM position = new Vector2(((Game1)Game).rnd.Next(0, Game.GraphicsDevice.PresentationParameters.BackBufferWidth - frameSize.X), -frameSize.Y); speed = new Vector2(0, ((Game1)Game).rnd.Next(enemyMinSpeed, enemyMaxSpeed)); break; } // Create the sprite spriteList.Add( new AutomatedSprite(Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision")); }
First, this method creates variables for the speed and position of
the soon-to-be-added sprite. Next, the speed
and position
variables are set by randomly
choosing in which direction the new sprite will be heading. Then, the
sprite is created and added to the list of sprites. The frameSize
variable defined at the top of the
method is used to determine how far to offset the sprite from all sides
of the window.
Compile and run the application at this point, and you’ll find that it’s looking more and more like a real game. The enemy sprites are spawned from each edge of the screen, and they head across the screen in a straight line at varying speeds (see Figure 7-1).
OK, quiz time. Let’s see how well you understand what’s going on here, and what problems you might run into. Let the game run for a minute or so without user input. Some objects may hit the user-controlled sprite and disappear, but most of them will fly harmlessly off the edge of the screen. What’s the problem, and how can you fix it?
If you said that you’re not deleting your objects, you’re really picking this up and understanding game concepts—great job! If you’re confused by that, let me explain: when an automated sprite hits the user-controlled sprite, the automated sprite is removed from the list of sprites and destroyed. However, when an automated sprite makes it all the way across the screen, it simply disappears; you aren’t doing anything with that object to destroy it and the player can no longer collide with the object to destroy it, either. The result is that these objects will continue forever outside the field of play, and in every frame, you will continue to update and draw each of them—not to mention running pointless collision checks on them. This problem will grow worse and worse until at some point it affects the performance of your game.
This brings us to a fundamental element of game development. One thing that is absolutely essential to any game is the definition of what makes an object “irrelevant.” An object is considered irrelevant when it can no longer affect anything in the game.
Irrelevancy is handled differently in each game. Some games allow objects to leave the screen and then ultimately return. Other games destroy objects before they ever leave the screen. An example of the latter is seen in most renditions of the game Asteroids. In most versions of Asteroids, when shooting from one side of the screen to the other, a ship’s bullet actually disappears before it leaves the screen. This is because the shot has a maximum distance that it can travel before it is deleted from the game. Although I’m not a huge fan of that functionality (yeah, I like guns that can shoot as far as I can see), the developers made the call that a bullet wouldn’t be able to reach from one side of the screen to the other. You can argue the merits of that choice, but that’s not the point. The point is that the developers decided what constituted irrelevancy for those bullets, and when they reached that point, they deleted them.
It’s interesting to look at the Asteroids game further, because while its developers decided to remove bullets before they hit the edge of the screen, they did the opposite with the asteroids themselves: the asteroids are recycled immediately when they leave the screen, and they pop into view on another side of the screen. Again, you can argue about whether you like this behavior and whether it’s realistic, but that’s not the point. One of the great things about game development is that you control the world, and you can do whatever you want. The developers of Asteroids made that call, and hey, who can argue with one of the best classic games ever made, right?
Currently, you aren’t doing anything about your irrelevant sprites. Your sprites leave the screen and never have any chance to return (you only have logic for the sprites to move forward, not to turn or double back), and therefore at that point they become irrelevant. Once one of your automated sprites leaves the screen, you need to detect that and get rid of it so that you don’t waste precious processor time updating and drawing objects that will never come into play in the game again.
To do this, you need to add a method in your Sprite
base class that will accept a Rectangle
representing the window rectangle
and return true
or false
to indicate whether the sprite is out of
bounds. Add the following method to your Sprite
class:
public bool IsOutOfBounds(Rectangle clientRect) { if (position.X < -frameSize.X || position.X > clientRect.Width || position.Y < -frameSize.Y || position.Y > clientRect.Height) { return true; } return false; }
Next, you’ll need to add to the Update
method of your SpriteManager
class some code that will loop
through the list of AutomatedSprite
s
and call the IsOutOfBounds
method on
each sprite, deleting those that are out of bounds. You already have
code in the Update
method of your
SpriteManager
class that loops
through all your AutomatedSprite
objects. The current code should look something like this:
// Update all sprites for (int i = 0; i < spriteList.Count; ++i) { Sprite s = spriteList[i]; s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions if (s.collisionRect.Intersects(player.collisionRect)) { // Play collision sound if(s.collisionCueName != null) ((Game1)Game).PlayCue(s.collisionCueName); // Remove collided sprite from the game spriteList.RemoveAt(i); −−i; } }
Add some code to check whether the sprite is out of bounds. If the sprite is out of bounds, remove it from the game. The preceding loop should now look like this (added lines are in bold):
// Update all sprites for (int i = 0; i < spriteList.Count; ++i) { Sprite s = spriteList[i]; s.Update(gameTime, Game.Window.ClientBounds); // Check for collisions if (s.collisionRect.Intersects(player.collisionRect)) { // Play collision sound if(s.collisionCueName != null) ((Game1)Game).PlayCue(s.collisionCueName); // Remove collided sprite from the game spriteList.RemoveAt(i); −−i; } // Remove object if it is out of bounds if (s.IsOutOfBounds(Game.Window.ClientBounds)) { spriteList.RemoveAt(i); −−i; } }
Now your irrelevant objects will be deleted after they leave the screen. Your game will have to update, draw, and run collision checks only on objects that are on the screen, and this will greatly improve performance, especially as the game progresses.
As mentioned previously, when it comes to computer-controlled objects, the goal of any game is to make those objects appear intelligent to the point where a user may not be able to tell the difference between an object controlled by a human and an object controlled by a computer. We clearly aren’t even close to that.
The automated sprites you’ve added do nothing more than move
forward in a straight line. You’ve done some great work on your SpriteManager
, but we haven’t discussed how to
do anything to improve the movement of your automated sprites.
Let’s create a couple of different objects that do something a little more intelligent than simply moving in a straight line.
In this section, you’ll create a new sprite type that will chase your user-controlled object around the screen. You’ll do this with the following very simple chase algorithm:
if (player.X < chasingSprite.X) chasingSprite.X −= 1; else if (player.X > chasingSprite.X) chasingSprite.X += 1; if (player.Y < chasingSprite.Y) chasingSprite.Y −= 1; else if (player.Y > chasingSprite.Y) chasingSprite.Y += 1;
Essentially, the algorithm compares the position of the player with that of the chasing sprite. If the player’s X coordinate is less than the chasing sprite’s X coordinate, the chasing sprite’s coordinate is decremented. If the player’s X coordinate is greater than the chasing sprite’s X coordinate, the chasing sprite’s X coordinate is incremented. The same is done with the Y coordinate.
To implement the chasing sprite, you’ll want to create a new class
that derives from Sprite
. But before
you do that, you can see from the preceding algorithm that the new class
is going to need to know the position of the player object. Looking at
your current Sprite
class and its
derived classes, there is no way to get that information. So, you’ll
need to add a public accessor to the Sprite
base class that will return the
position of the sprite object:
public Vector2 GetPosition { get { return position; } }
Then, add a method in your SpriteManager
class that will return the
position of the player object:
public Vector2 GetPlayerPosition( ) { return player.GetPosition; }
That done, you’ll need to create a new class within your project (right-click on the project in the Solution Explorer and select Add→Class…). Name it ChasingSprite.cs, and replace the code that’s generated with the following:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace AnimatedSprites { class ChasingSprite : Sprite { // Save a reference to the sprite manager to // use to get the player position SpriteManager spriteManager; public ChasingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, SpriteManager spriteManager) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName) { this.spriteManager = spriteManager; } public ChasingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, SpriteManager spriteManager) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName) { this.spriteManager = spriteManager; } public override Vector2 direction { get { return speed; } } public override void Update(GameTime gameTime, Rectangle clientBounds) { // Use the player position to move the sprite closer in // the X and/or Y directions Vector2 player = spriteManager.GetPlayerPosition( ); // Because sprite may be moving in the X or Y direction // but not both, get the largest of the two numbers and // use it as the speed of the object float speedVal = Math.Max( Math.Abs(speed.X), Math.Abs(speed.Y)); if (player.X < position.X) position.X −= speedVal; else if (player.X > position.X) position.X += speedVal; if (player.Y < position.Y) position.Y −= speedVal; else if (player.Y > position.Y) position.Y += speedVal; base.Update(gameTime, clientBounds); } } }
There are a couple of things to note about this code. First, the
namespace that you’re using is AnimatedSprites
. This was what you should have
named your project way back in the first
couple of chapters in this book. If the namespace is giving you problems, you most likely named your project
something else. Look at the namespace in your Game1
class, and use the same namespace that
you have listed there in this file.
Next, notice that the constructor is essentially the same as the
one in your AutomatedSprite
class,
with one key exception: here, you’ve added a SpriteManager
parameter and set a local
SpriteManager
variable to keep track
of the object passed in via that parameter. During the Update
method call, this object is used to
retrieve the position of the player via the method you added
previously.
The other important thing to understand is what’s going on in the
Update
method. You’re retrieving the position of the player and then
running your chasing algorithm using the largest of the two coordinates
specified in the speed
member of the
Sprite
base class (because the
sprites will be moving only in the X or the Y direction, not
both).
The final thing that you’ll need to change in order to get a
functional chasing sprite is the SpriteList.Add
call in your SpriteManager
’s SpawnEnemy
method. You’ll need to change the
type of sprite you’re creating to ChasingSprite
instead of AutomatedSprite
. This will result in creating
ChasingSprite
objects at random
intervals rather than AutomatedSprite
s, and when you run your
application, they should give you a good run for your money. Your
SpriteList.Add
call, which is at the
end of the SpawnEnemy
method in the
SpriteManager
class, should look like
this:
spriteList.Add( new ChasingSprite (Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this));
Run the application, and get ready to run for your life. You can try to avoid the objects, but eventually there’ll be too many of them and you’ll hit them. Try using a gamepad or the keyboard rather than the mouse for an even tougher challenge. Your application should, at this point, look something like Figure 7-2.
You can easily increase or decrease the difficulty of this
algorithm by multiplying the speed
member of the base class by some value. Increasing the
speed will make your sprites chase the player faster, whereas decreasing
the speed will slow them down. As it is, the objects definitely chase
the player around the screen, but we’re going to tweak them a little bit
for the purposes of this game. Instead of having the objects chase the
player indefinitely all over the screen, you’re going to program them to
continue on their course across the screen while veering toward the
player. This will cause the chasing sprites to continue their course off
the screen toward deletion from the game if the player successfully
avoids them.
To accomplish this, you’ll first need to figure out which direction a given sprite is moving in (remember that the sprites only move in one direction—up, down, left, or right). If the sprite is moving horizontally, you’ll adjust only the sprite’s vertical movement to chase the player. If the sprite is moving vertically, you’ll adjust only the horizontal movement to chase the player. This will make the sprites continue in their original direction (horizontal or vertical) while chasing the player at the same time.
Replace the Update
method of
your ChasingSprite
class with this
one:
public override void Update(GameTime gameTime, Rectangle clientBounds) { // First, move the sprite along its direction vector position += speed; // Use the player position to move the sprite closer in // the X and/or Y directions Vector2 player = spriteManager.GetPlayerPosition( ); // If player is moving vertically, chase horizontally if (speed.X == 0) { if (player.X < position.X) position.X −= Math.Abs(speed.Y); else if (player.X > position.X) position.X += Math.Abs(speed.Y); } // If player is moving horizontally, chase vertically if (speed.Y == 0) { if (player.Y < position.Y) position.Y −= Math.Abs(speed.X); else if (player.Y > position.Y) position.Y += Math.Abs(speed.X); } base.Update(gameTime, clientBounds); }
This is a slightly modified chasing algorithm that will chase in
only one direction. The method starts by adding the speed
member to the sprite’s position
member. This will move the sprite
forward in the direction of the speed
vector.
After the position is updated, the player object’s position is
retrieved. Recall from when you wrote the code that generates the
automated sprites that the code generates a Vector2
for speed that will have a zero value
in the X or Y coordinate (i.e., sprites move only vertically or
horizontally, not diagonally). Because of this, the algorithm next
detects whether the ChasingSprite
is
moving horizontally or vertically by determining which coordinate in the
Speed
variable is zero. If the X
coordinate is zero, that means that the object is moving vertically, and
the algorithm will then adjust only the X coordinate of the ChasingSprite
to “chase” the player in only
the horizontal direction. The result is that the sprite will continually
move up or down across the screen, but while doing so, will sway to the
left or the right to chase the player. The algorithm then runs the same
checks and calculations for objects moving horizontally.
Compile and run the game now, and you’ll see that the sprites move
horizontally or vertically across the screen, but bend slightly to chase
the player. As you’ll probably notice when playing, the sprites that
move more quickly are definitely more difficult to evade than the slower ones. That’s
because your objects are chasing the player at the same speed at which they are cruising
across the screen (i.e., you’re using the speed
member
variable to chase the player by using Math.Abs(speed.X)
and Math.Abs(speed.Y)
).
Congratulations! Now you’re really getting somewhere. Not only does your game actually look and feel more like a real game, but you’ve just written an artificial intelligence algorithm that makes the sprites respond to the movements of the real human player. Pretty cool!
You now have two types of automated sprites in your application: one that moves across the screen without changing direction and one that moves across the screen but changes direction slightly to chase the player.
In this section, you’ll build one more type of sprite that is similar to the chasing sprite, but this one will actually try to avoid the player. Why would you want to write a sprite that avoids the player? This sprite type will be used for something that the player will want to run into (maybe a power-up, or extra life, or something), so the sprite will tease the player by letting her get close to it but then, when she gets too close, taking off in another direction. This should add a nice different element to the game, as well as making it more challenging.
Let’s get started. Add a new class to your project, and call it
EvadingSprite.cs. The code for this sprite will be
very similar to the code you just wrote for the ChasingSprite
—so similar, in fact, that it
will be easier to start with that code than to start from scratch.
Remove the code generated for you in the EvadingSprite
class and replace it by copying
the code in the ChasingSprite
class
and pasting that code into the EvadingSprite.cs
file. You’ll need to change the name of the class from ChasingSprite
to EvadingSprite
and also change the names of the
constructors. Your EvadingSprite.cs file should now
look like this:
using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace AnimatedSprites { class EvadingSprite : Sprite { // Save a reference to the sprite manager to // use to get the player position SpriteManager spriteManager; public EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, SpriteManager spriteManager) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName) { this.spriteManager = spriteManager; } public EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, SpriteManager spriteManager) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName) { this.spriteManager = spriteManager; } public override Vector2 direction { get { return speed; } } public override void Update(GameTime gameTime, Rectangle clientBounds) { // First, move the sprite along its direction vector position += speed; // Use the player position to move the sprite closer in // the X and/or Y directions Vector2 player = spriteManager.GetPlayerPosition( ); // If player is moving vertically, chase horizontally if (speed.X == 0) { if (player.X < position.X) position.X −= Math.Abs(speed.Y); else if (player.X > position.X) position.X += Math.Abs(speed.Y); } // If player is moving horizontally, chase vertically if (speed.Y == 0) { if (player.Y < position.Y) position.Y −= Math.Abs(speed.X); else if (player.Y > position.Y) position.Y += Math.Abs(speed.X); } base.Update(gameTime, clientBounds); } } }
Because this code is exactly the same as the code you used for
your ChasingSprite
object, creating
an object of this type at this point will create an object that will
chase the user while moving across the screen. However, you want to
program this class to actually run away from the
player.
First, you’ll need to tell the SpriteManager
to create objects of the type
EvadingSprite
rather than ChasingSprite
. To do this, modify the SpriteList.Add
call in the Sprite
Manager
’s
SpawnEnemy
method:
spriteList.Add( new EvadingSprite (Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this));
Now you’re ready to get to work on the evasion algorithm. The algorithm is really simple. In fact, it’s essentially the opposite of the chasing algorithm: if the X coordinate of the player’s position is less than the X coordinate of the evading sprite’s position, rather than decreasing the value of the X coordinate of the evading sprite’s position to move the sprite closer to the player, you’ll increase the value to move the sprite farther away from the player.
You can do this by swapping the additions and subtractions in your
chasing algorithm. Also, because you are now evading the player, you
don’t care about continuing in a straight line across the screen, so you
can remove the two if
statements
detecting the direction in which the sprite is traveling. After making
these changes, your EvadingSprite
’s
Update
method should look like
this:
public override void Update(GameTime gameTime, Rectangle clientBounds) { // First, move the sprite along its direction vector position += speed; // Use the player position to move the sprite closer in // the X and/or Y directions Vector2 player = spriteManager.GetPlayerPosition( ); // Move away from the player horizontally if (player.X < position.X) position.X += Math.Abs(speed.Y); else if (player.X > position.X) position.X −= Math.Abs(speed.Y); // Move away from the player vertically if (player.Y < position.Y) position.Y += Math.Abs(speed.X); else if (player.Y > position.Y) position.Y −= Math.Abs(speed.X); base.Update(gameTime, clientBounds); }
Compile and run your project at this point, and you’ll see that the objects are nearly impossible to catch. Instead of traveling across the screen, they veer off to one side to avoid even coming close to the player.
Although the sprites are effectively avoiding the player, this
really isn’t very fun. You’re losing at your own game, and that’s just
lame. Let’s modify the new sprite so that it travels across the screen
just like an AutomatedSprite
object,
but then, when the player gets within a certain range of the object, the
evasion algorithm turns on and the sprite turns and runs.
Add a few variables to your EvadingSprite
class: one that will be used to
detect when to activate the evasion algorithm, one that will determine
the speed at which the sprite runs from the player, and one that will
keep track of the sprite’s state (possible states are evading and not
evading). By default, you want this variable to indicate that your
sprite is in a not-evading state:
float evasionSpeedModifier; int evasionRange; bool evade = false;
Why use a separate speed for evasion? You don’t have to do this, but the evasion tactic will be somewhat unexpected for the user. All of the other sprites in the game either move forward only or actually chase after the player. Having a sprite turn and book it in a different direction will be a bit of a surprise and therefore will be a little harder for the player to handle. Using a modifier like this will enable you to increase or decrease the speed of the sprite while in evasion mode. You’ll be able to play with the numbers and find a speed that feels right to you as a player/developer.
Next, update your constructors to accept parameters for the
evasionSpeedModifier
and evasionRange
variables. You’ll want to assign
the values from those parameters to your member variables in the body of
your constructors as well:
public EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, string collisionCueName, SpriteManager spriteManager, float evasionSpeedModifier, int evasionRange) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, collisionCueName) { this.spriteManager = spriteManager; this.evasionSpeedModifier = evasionSpeedModifier; this.evasionRange = evasionRange; } public EvadingSprite(Texture2D textureImage, Vector2 position, Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize, Vector2 speed, int millisecondsPerFrame, string collisionCueName, SpriteManager spriteManager, float evasionSpeedModifier, int evasionRange) : base(textureImage, position, frameSize, collisionOffset, currentFrame, sheetSize, speed, millisecondsPerFrame, collisionCueName) { this.spriteManager = spriteManager; this.evasionSpeedModifier = evasionSpeedModifier; this.evasionRange = evasionRange; }
Now you’ll need to modify your Update
method to add some logic that will make
the sprite operate just like an AutomatedSprite
would until the distance
between the player’s position and the sprite’s position is less than the
value in the evasionRange
variable.
You can use the Vector2.Distance
method to determine the distance between two vectors.
Once the sprites are closer than the evasionRange
, you need to reverse the
direction of the sprite and activate the evasion algorithm. That
algorithm should continue to run until the sprite is destroyed.
Your Update
method should look
something like this:
public override void Update(GameTime gameTime, Rectangle clientBounds) { // First, move the sprite along its direction vector position += speed; // Use the player position to move the sprite closer in // the X and/or Y directions Vector2 player = spriteManager.GetPlayerPosition( ); if (evade) { // Move away from the player horizontally if (player.X < position.X) position.X += Math.Abs(speed.Y); else if (player.X > position.X) position.X −= Math.Abs(speed.Y); // Move away from the player vertically if (player.Y < position.Y) position.Y += Math.Abs(speed.X); else if (player.Y > position.Y) position.Y −= Math.Abs(speed.X); } else { if (Vector2.Distance(position, player) < evasionRange) { // Player is within evasion range, // reverse direction and modify speed speed *= -evasionSpeedModifier; evade = true; } } base.Update(gameTime, clientBounds); }
Finally, you’ll need to change the
SpriteList.Add
call in the SpriteManager
’s Spawn
Enemy
method once again, adding the two
parameters to the constructor for the EvadingSprite
object. For starters, pass
in .75f
as the modifier for the
evasion speed and 150
for the evasion
range. These values will cause the sprite to begin evading the player
when the two are within a range of 150 units, and the sprite will evade
at three quarters of its normal speed:
spriteList.Add( new EvadingSprite (Game.Content.Load<Texture2D>(@"imagesskullball"), position, new Point(75, 75), 10, new Point(0, 0), new Point(6, 8), speed, "skullcollision", this, .75f, 150));
Compile and run the game now, and you’ll see that the sprites seem to have a bit more “intelligence.” They detect when the player is near, and they turn and head off in the opposite direction. Also, you’ll find that you probably can catch most of them, but they’re still a bit tricky. Once you add the other sprites floating around that the player actually has to avoid, this evasion technique will be just enough to represent a good challenge.
So that’s it? Is that artificial intelligence?
Well, not quite. We haven’t even scratched the surface of true artificial intelligence algorithms. In fact, many hardcore AI experts would argue that this isn’t artificial intelligence at all—and they might be right. Again, the science is somewhat ambiguous, and who’s to say whether what you’ve done here truly represents intelligence or not?
The point here is that you can definitely go overboard in the name of science sometimes. True artificial intelligence research and algorithms absolutely have a place in the world, but probably not in a 2D sprite-avoidance game. When programming artificially intelligent objects—especially in video games—there is a certain level of “intelligence” that is typically “good enough.” You could spend months or years fine-tuning an algorithm for this game so you could argue that it is truly intelligent, but at what cost, and at what advantage to the player?
Unfortunately, there is no right or wrong answer in relation to the degree or quality of artificial intelligence that should be implemented, and it comes down to a decision that you as the developer must make. Ultimately, it’s up to you to decide when your algorithm needs improvement and at what point it’s good enough for what you’re trying to accomplish.
You’re now very close to having something worthy of your newly developed XNA prowess. In the next chapter, you’ll fine-tune the game and wrap up your 2D development. In the meantime, let’s reflect on what you just accomplished:
You learned some background to artificial intelligence.
You created a factory for sprites that creates sprites at random intervals.
You learned about irrelevant objects and what to do with them to improve game performance.
You created a chasing sprite that follows a player across the screen.
You created an evading sprite that runs from a player.
You drank from the fount of XNA goodness.
Artificial intelligence means many different things, mainly because the term “intelligence” itself is ambiguous and difficult to define.
Alan Turing made great strides in the field of artificial intelligence. Much of his work is directly relevant to what game developers attempt to accomplish.
Irrelevant objects are objects that will no longer affect gameplay (e.g., a bullet that’s shot into the sky and doesn’t hit anything). These objects must be removed and deleted in order to not negatively impact performance as they accrue.
To implement a chase algorithm, detect the position of the player in relation to the chaser’s current position, and then move the chasing object in the direction of the player.
Implementing an evasion algorithm is the opposite of the chase algorithm: detect the position of the player, and move the evading object in the opposite direction.
“Artificial intelligence is no match for natural stupidity.” —Anonymous
What is the Turing Test?
Why is artificial intelligence so difficult to perfect?
What constitutes irrelevancy for an object in a video game? What should be done with irrelevant objects, and why?
If you have a player whose position is stored in a Vector2
object called PlayerPos
and a chasing object whose
position is stored in a Vector2
object called ChasePos
, what
algorithm will cause your chasing object to chase after your
player?
In the beginning, what was created that made a lot of people very angry and has been widely regarded as a bad move?
Take what you’ve learned in this chapter and make yet another type of sprite object, one that moves randomly around the screen. To do this, you’ll want to create a random timer that signifies when the object should change directions. When the timer expires, have the object move in a different direction, and then reset the random timer to a new random time at which the object will again shift its direction.
3.144.107.193