Wednesday, October 22, 2008

XNA Series - Basic AI

For today's post I want to take our first step towards 'AI'. Now please understand this and the following posts are not truly AI, but at least get us moving in a direction towards it. The Wikipedia entry for AI states the following:

...The study and design of intelligent agents,"where an intelligent agent is a system that perceives its environment and takes actions which maximize its chances of success. John McCarthy, who coined the term in 1956, defines it as
"the science and engineering of making intelligent machines."


So our idea is that some element or 'agent' in our game that can take data from it's surroundings and choose an action based on that data to take it toward it's goal. One of the MAJOR components of an AI is that it has a 'goal' something it is trying to accomplish. For instance an AI controlling a car in a racing game's goal is to get across the finish line first while staying on the course. While an AI for a target in a shooting game might want to avoid being hit. Or a "bad guy" AI who want to seek out a target. If you are serious about game AI, I would suggest reading this paper http://www.red3d.com/cwr/steer/gdc99/ on Steering Behaviors.



To start in this post, we will choose a very basic task. Agent A will start at a random location with a random rotation. Object B will start at a random location and do nothing. Agent A will rotate until it is facing Object B and will move forward until it reaches object B. So our agent has 3 tasks: turn, move and stop. Object
B just takes up space.



So what do we need for our example? An Agent A and an Object B. They will both be GameObjects. We can reuse our most recent GameObject Class from XNA Part 10 to start with. We will add a couple things to the game class to make this work.



float rotation = 0;
public Vector2 origin = Vector2.Zero;

Rotation will hold the current rotation of our object and origin will hold a "pivit point" for our object to rotate on. Then, update your draw call to use these parameters
public void Draw(SpriteBatch sb)
{
sb.Draw(sprite, position, texSource, Color.White, rotation,
origin,1,SpriteEffects.None,0);
}

Another addition is a property called Center
public Vector2 Center
{
get
{
return position + origin;
}
}
Which returns a screen based coordinate of the rotational center of our object.

So here is a tank and a missle for it to seek to. They are on one sprite sheet. We will add it to our project and create a game object for our missle, but hold off on the tank for a moment

//In our game class

GameObject b;

//In our Load Content Method

sprites = Content.Load<Texture2D>("tank-missle");
b = new GameObject(sprites,100,100,new Rectangle(50,0,50,50));
b.origin = new Vector2(15, 15);

Now in our GameObject file, after our GameObject declaration we are going to create a new
class derived from GameObject called GameAgent

class GameAgent : GameObject

{

}

So our new class GameAgent will have all the GameObject Stuff, but we can add to it and give it extra variables and methods. First we need to give it a constructor

public GameAgent(Texture2D inSprite, float x, float y, Rectangle src) :
base(inSprite,x,y,src)
{

}

Basically we pass everything on to our base class and let the GameObject set everything up. We will give GameAgent a couple fields


const float MAX_SPEED = 2;
public float speed = 0;

Now for each update, we will make a call to a method of our GameAgent for it to seek. So in our GameAgent class we will create a Method called Seek that takes a GameObject as a parameter.

public float Seek(GameObject target)
{
//First Move ahead along current angle

// The distance between the 2 elements
float distance = Vector2.Distance(position, target.Center);

//if we are far away, speed up to MAX_SPEED
if (distance > 100)
{
speed = MathHelper.Clamp(speed + 0.1f, 0, MAX_SPEED);
}

// If we are getting close, use the SmoothStep method to slow us down
else if (distance <= 100 && distance > 40)
{
speed = MathHelper.SmoothStep(0,MAX_SPEED, (distance - 30) / 70);
}

// If we are closer than 40, stop!
else
{
speed = 0;
}

// Use our current rotation + some trig to set our new location
// I'll explain later
position.X += (float)Math.Cos(rotation) * speed;
position.Y += (float)Math.Sin(rotation) * speed;


// Rotate Towards the Object

// Find Distance between this object and the target's center
float o = target.Center.Y - position.Y;
float a = target.Center.X - position.X;

// Find the angle between our unrotated object and the target
float theta = (float)Math.Atan((double)o / (double)a);

// If we are on the right of the object, we need to think a little backwards
if (position.X > target.Center.X)
{
theta = MathHelper.WrapAngle(theta + MathHelper.Pi);
}

// Add to our Rotation to point to the object
// theta-rotation gives us the difference between our current rotation and the
// offset of the target object to our 0 rotation point. This is ideally
// how much we want to rotate, but our agent can only turn so fast,
// So we need to clamp that change to our maximum turning amount.

rotation += MathHelper.Clamp(MathHelper.WrapAngle(theta - rotation), -0.05f, 0.05f);

// I return theta here so I can output it to the screen later, you don't really need to.
return theta;
}

I've added comments to the function code so you can see what is happening. Now this may of course not be the most optimal way for us to do steering, but it does work. I have been reading the work of Craig Reynolds and it does seem that he has done extensive work and research in this area. He uses a method in which the key points are more accurate to how objects might behave. I am currently trying to implement his concepts in my code and will get back to you when I accomplish something. But moving on.

Then in our LoadContent method we can initialize our agent.


a = new GameAgent(sprites, 300,300, new Rectangle(0, 0, 50, 50));
a.origin = new Vector2(25, 25);

Then in our Update method
a.Seek(b);
and Finally in our Draw method
            spriteBatch.Begin();
a.Draw(spriteBatch);
b.Draw(spriteBatch);
spriteBatch.End();

I also added this in our Update Class so I can move the target.

            KeyboardState ks = Keyboard.GetState();
if (ks.IsKeyDown(Keys.NumPad4))
{
b.position.X -= 3f;
}
if (ks.IsKeyDown(Keys.NumPad6))
{
b.position.X += 3f;
}
if (ks.IsKeyDown(Keys.NumPad8))
{
b.position.Y -= 3f;
}
if (ks.IsKeyDown(Keys.NumPad2))
{
b.position.Y += 3f;
}

When all is compiled, you end up with a missle that you can move and a tank that turns and drive to it and slows down and stops when it arrives. If you were a little lost by the trigonometry, don't fear, I'll have a XNA sidebar on that soon.

Code for this Post

References:

http://creators.xna.com/en-us/sample/aiming

http://www.red3d.com/cwr/steer/

No comments: