Introduction to AI Programming for Games

Update: We have an updated version of this tutorial for iOS 9 and GameplayKit here. When you make a game, you often have enemies for the player to combat. You want these enemies to seem intelligent and present a challenge to the player to keep the game fun and engaging. You can do this through […] By Ray Wenderlich.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Introduction to Steering

If you open an game AI texbook, you will find tons of different examples of steering behaviors you can add into your game. They are often in different variants such as "just velocity", or "velocity plus acceleration".

In this game, you want to use acceleration for more natural movement, so you will be focusing on the "velocity plus acceleration" variants.

Here are a few example of steering behaviors:

  • Seek: Move toward a target (but possibly overshoot).
  • Flee: Move away from a target.
  • Arrive: Move toward a target (with the goal of landing exactly on the target by decellerating).
  • Wander: Move around randomly.
  • Separate: Keep a certain distance from a target.
  • Path following: Follow along a given path.

There are many more beyond this too. And the coolest part is you can mix and match different behavior from the above list for some cool and dynamic effects!

In this tutorial, you'll try out a few (but not all) of these steering behaviors. Hopefully by the time you're done with this tutorial your appetite will be whet and you'll be ready to investigate a few more!

Let's start with the simplest of these steering behaviors - seek!

Seeking Success

The idea behind seek is very simple:

  • Figure out the direction to the target.
  • Max acceleration in that direction!

However this turns out to be a bit overzealous in practice. To see what I mean, inside Monster.m add a new seek method as follows:

- (CGPoint)seekWithTarget:(CGPoint)target {
    CGPoint direction = ccpNormalize(ccpSub(target, self.position));
    return ccpMult(direction, self.maxAcceleration);
}

This does exactly as I described above. It creates a unit vector (i.e. a vector of length 1) in the direction of the target, then multiplies the result by the max acceleration. This results in a max acceleration vector in the direction of the target.

Next, replace the if (hasTarget) clause at the end of updateMove: with the following:

if (hasTarget) {        
    // Calculate amount to accelerate, based on goal of arriving at nearest enemy,
    // and separating from nearby enemies
    CGPoint seekComponent = [self seekWithTarget:moveTarget];
    CGPoint newAcceleration = seekComponent;
    
    // Update current acceleration based on the above, and clamp
    self.acceleration = ccpAdd(self.acceleration, newAcceleration);
    if (ccpLength(self.acceleration) > self.maxAcceleration) {
        self.acceleration = ccpMult(ccpNormalize(self.acceleration), self.maxAcceleration);
    }
    
    // Update current velocity based on acceleration and dt, and clamp
    self.velocity = ccpAdd(self.velocity, ccpMult(self.acceleration, dt));
    if (ccpLength(self.velocity) > self.maxVelocity) {
        self.velocity = ccpMult(ccpNormalize(self.velocity), self.maxVelocity);
    }
    
    // Update position based on velocity
    CGPoint newPosition = ccpAdd(self.position, ccpMult(self.velocity, dt));
    CGSize winSize = [CCDirector sharedDirector].winSize;
    newPosition.x = MAX(MIN(newPosition.x, winSize.width), 0);
    newPosition.y = MAX(MIN(newPosition.y, winSize.height), 0);
    self.position = newPosition;        
}

Here you call the seekWithTarget: method you just wrote to figure out the new acceleration, and add it to the current acceleration. You also make sure the acceleration isn't greater than the max acceleration.

Based on the acceleration, you figure out the new velocity, and then based on the velocity you figure out your position. If you're confused about the relationship between position, velocity, and acceleration over time, crack open your old Physics textbook :]

And that's it! Build and run, switch to attack mode, and build a Zap. The AI will build a few Quirks to counter, and you'll see some strange behavior:

Overshooting with seek steering

The Zaps will fly into the Quirk, and then continue flying right past! This is because their acceleration is carrying them past the Zap, and they then have to counter-accelerate to hit the Zap again, so basically end up acting like very drunken monsters :]

What you want is for the Zaps to take their current acceleration into mind and decellerate at the right time so they land at their target - and this is why you need arrive!

Arriving at Awesomeness

On the other hand, the idea behind arrive is the following:

  • If you are getting close to the target, figure out your "target velocity", which should get smaller and smaller the closer you are to the target.
  • Set the acceleration to the amount needed to increase the current velocity to the target velocity, reduced a factor of how long you want the decelleration to take.
  • Also add a wiggle room where it's "close enough", and a target area to start slowing down in.

Let's see what this looks like in code. Add this method to Monster.m:

- (CGPoint)arriveWithTarget:(CGPoint)target {
    
    // 1
    CGPoint vector = ccpSub(target, self.position);
    float distance = ccpLength(vector);
    
    // 2
    float targetRadius = 5; 
    float slowRadius = targetRadius + 25;
    static float timeToTarget = 0.1;
    
    // 3
    if (distance < targetRadius) {
        self.velocity = CGPointZero;
        self.acceleration = CGPointZero;
        return CGPointZero;
    }
    
    // 4
    float targetSpeed;
    if (distance > slowRadius) {
        targetSpeed = self.maxVelocity;
    } else {
        targetSpeed = self.maxVelocity * distance / slowRadius;
    }
    
    // 5
    CGPoint targetVelocity = ccpMult(ccpNormalize(vector), targetSpeed);    
    CGPoint acceleration = ccpMult(ccpSub(targetVelocity, self.velocity), 1/timeToTarget);

    // 6
    if (ccpLength(acceleration) > self.maxAcceleration) {
        acceleration = ccpMult(ccpNormalize(acceleration), self.maxAcceleration);
    }
    return acceleration;
}

Let's go over this section by section:

  1. Figures out the vector in the direction of the target, and how long it is.
  2. Sets up some constants. 5 points is "close enough", and the monster should start slowing down when it's within 25 points of the target. It should take 0.1 seconds to decellerate.
  3. If the monster is "close enough", instantly return.
  4. Figure out the target speed. If the monster is far away, it should be max velocity - otherwise, slower and slower the closer it gets to the target.
  5. Set the acceleration to the amount needed to increase the current velocity to the target velocity, reduced a factor of how long you want the decelleration to take.
  6. Makes sure the acceleration isn't greater than the max acceleration, and truncates it if so.

Then inside updateMove, change the beginning of the if (hasTarget) clause as follows:

//CGPoint seekComponent = [self seekWithTarget:moveTarget];
CGPoint arriveComponent = [self arriveWithTarget:moveTarget];
CGPoint newAcceleration = arriveComponent;

Build and run, and now the Quirks should be much more successful in their attacks!

Arriving at target with arrive steering

Slyly Separating

One last steering behavior example to show you, then we'll move onto another topic. Right now, if you spawn a set of Quirks they sit right on top of each other, making it difficult to see as a player that there are multiple quirks. Woudln't it be better if they spread out a bit!

You can do that with the separate steering behavior. The basic idea behind this is the following:

  • Loop through all objects you want to stay apart from (allies, in your case)
  • For each nearby object, accelerate away from that object

Let's see what this looks like in code. Add this new method to Monster.m:

- (CGPoint)separate {
    CGPoint steering = CGPointZero;    
    NSArray * allies = [self.layer alliesOfTeam:self.team];
    for (GameObject * ally in allies) {
        if (ally == self) continue;
        CGPoint direction = ccpSub(self.position, ally.position);
        float distance = ccpLength(direction);
        static float SEPARATE_THRESHHOLD = 20;
        
        if (distance < SEPARATE_THRESHHOLD) {
            direction = ccpNormalize(direction);
            steering = ccpAdd(steering, ccpMult(direction, self.maxAcceleration));
        }
    }
    return steering;
}

Notice that it just keeps adding on additional acceleration factors for each object that it needs to stay apart from, and returns the sum. The calling method will make sure that the acceleration is truncated to the max acceleration in the end.

Next, inside updateMove: update the beginning of the if (hasClause) block with the following:

//CGPoint seekComponent = [self seekWithTarget:moveTarget];
CGPoint arriveComponent = [self arriveWithTarget:moveTarget];
CGPoint separateComponent = [self separate];
CGPoint newAcceleration = ccpAdd(arriveComponent, separateComponent);

Note that to combine two different steering behaviors, one thing you can do is simply add them together. Alternatively, you could weight each component differently - it depends what you want for your game.

Build and run, and spawn a bunch of Quirks. The movement of the monsters should feel dynamic and really good!

Demonstrating separate steering behavior

It's pretty cool how steering behaviors can combine like this for such cool effects, eh?

Contributors

Over 300 content creators. Join our team.