Can we make the CPU drift?

In part 1, we laid the foundation for a CPU driving system for a video game using waypoints. In this part, we'll add in drifting as a simple example of how you can use waypoints to "script" your CPU to do stuff beyond basic driving.

What is drifting?

In car racing, drifting is when the car essentially "slides" around a turn. Here is a simple example of RC cars drifting. To put it simply, the direction the car is going and the direction it is facing are quite different. By controlling those two angles, the driver can make a car drift around corners in a smooth and satisfying way.

Here is our simulator with drifting added

frame: 0
The simulator also has a "drift boost", a quick burst of speed when the car comes out of its drift. That's not realistic, but hey it's a video game. My actual game is mimicking how Mario Kart does drifting. It's a bit subtle, but it's there.

Implementing drifting

This blog series isn't really about drifting. The implementation here is very simple.

To drift, the car now keeps track of steeringAngle and velocityAngle. Whenever the car turns, it updates steeringAngle. This happens just like before, only last time it was updating velocityAngle. Then each frame, it calls moveTowardsSteeringAngle

moveTowardsSteeringAngle(isDrifting: boolean) {
    const diff = this.steeringAngle - this.velocityAngle;
    const stepMagnitude = degreesToRadians(isDrifting ? 2 : 8);
    const stepDirection = sign0(diff);
    const step = stepDirection * stepMagnitude;
    this.velocityAngle += step;
    if (
        stepDirection > 0 &&
        this.velocityAngle > this.steeringAngle) {
        this.velocityAngle = this.steeringAngle;
    }
    if (stepDirection < 0 &&
        this.velocityAngle < this.steeringAngle) {
        this.velocityAngle = this.steeringAngle;
    }
}

This function just has the velocity angle (ie, the actual direction the car is going in), "catch up" to the steering angle (the direction the car is pointing). When isDrifting is true, it catches up more slowly, and that ultimately enables our simple drift.

Having the CPU car decide when to start drifting

When should the CPU drift? Generally when it enters large, sweeping turns. This can easily be done by adding a shouldDrift flag to our waypoints. In this case, shouldDrift was set to true for waypoint 1. When the car arrives at waypoint 0, it will then focus on waypoint 1, and there it will learn it should go into drifting mode

The waypoints tell it to drift here
The waypoints tell it to drift here

That can be seen when the car moves onto its next waypoint

updateCurrentWaypoint(waypoints: Waypoint[]) {
    const currentWaypoint = waypoints[this.targetWaypoint];
    const distance = getDistance(
        this.x,
        this.y,
        currentWaypoint.x,
        currentWaypoint.y
    );
    if (distance <= currentWaypoint.radius) {
        this.targetWaypoint += 1;
        if (this.targetWaypoint >= waypoints.length) {
            this.targetWaypoint = 0;
        }
        // here our waypoint tells the car to start drifting
        this.shouldDrift =
            this.allowDrifting &&
            waypoints[this.targetWaypoint].shouldDrift;
    }
}

When should the CPU stop drifting?

Starting to drift is easy, knowing when to stop drifting is a bit trickier. When does a human driver stop drifting? They usually stop when they are aligned in the direction they want to go in coming out of the turn. If we can just convince the CPU to do that, it will drift pretty well.

A first stab at this might be "stop drifting once you are pointed at your next waypoint". So in this case, the car will start drifting when it hits waypoint zero, and stop once it is pointed at waypoint 1, let's see how that goes

frame: 0

I mean it's decent? But not really what we want. We really want the car to slide throughout the entire turn. To pull this off, let's have the car drift until it's both pointing at waypoint 1 and waypoint 2.

The car is pointing at both of its upcoming waypoints
The car is pointing at both of its upcoming waypoints

And if you think about it, that's also what human drivers do. They want a nice solid, straight line to exit the turn on.

We need to make sure the next waypoint is positioned well for this, but that's not a big deal.

In the code, this just means calc a turn decision for the next waypoint and the one after it. If both come up 0 (ie, straight), stop drifting

determineSteeringAngleChangeRate(waypoint: Waypoint, nextWaypoint: Waypoint) {
    const turnDecision = this.calcTurnDecision(waypoint);
    if (turnDecision !== 0) {
        return this.calcSteeringAngleChangeRate(
            waypoint,
            turnDecision
        );
    } else {
        if (this.shouldDrift) {
            // check the next waypoint to see
            // if the car is pointed
            // well to exit its drift
            this.shouldDrift =
                this.calcTurnDecision(nextWaypoint) != 0;
        }
        return 0;
    }
}

Conclusion

And that's it. Our CPU can now drift pretty well around corners.

frame: 0

If you watch the whole simulation, the CPU will drift around corner 0-1 and corner 8-9. But the second time it drifts around 0-1, it goes way out of bounds. Correcting for that is the topic of a future post :)

The real purpose of this post was just to show you can use waypoints to convey more information to your CPU driver. Drifting was one simple example. You can use waypoints to make your CPU act more naturally and do more interesting things. Maybe a waypoint can tell it to speed up for an upcoming jump.

Oh and here is the code for the drift car.

Anyway, that will do it for this one. But if you want more, why not head to part 3?.