Skip to main content

AI Controller Update - Week Ending 6/27

Now, this was a week in which some really important work was done.
Last week, I wrote about the roadblocks I was facing with writing my own game engine – guaranteeing a smooth game loop, constant frame rate, and float precision.
While it is quite easy to build a functional game loop, it is harder to build one that does not break under stress. Somehow my windows framework wasn’t sending regular updates to my main loop, and I spent quite a while scratching my head trying to figure it out.
However, time is precious and it was running out. I had to make a decision, and make it quick. I chose to jump into Unreal and port over all my code into Unreal 4.16.

Jumping to Unreal

I wanted to build a follower behavior, but I also wanted to build it right. So, I made sure to have a component-based architecture from the get-go, and made sure to have the steering behaviors as Actor Components, so that they could be attached to any actor and be reused.
The Actor Steering component is a C++ component which has its functions exposed to blueprints. This is done so that scripters can take advantage of this and integrate it into their scripting workflow.
The code is the same as demonstrated in earlier posts. The only difference is using Unreal’s FVector class instead of our own custom vector, and getting the location, velocity, etc from the parent actor. Here is what Arrive looks like in Unreal (look at last week’s post to see how it looked like in the custom engine):

FVector UActorSteeringComponent::Arrive(const FVector& Target)
{
       AActor* Owner = GetOwner();

       FVector ToTarget = Target - Owner->GetActorLocation();
       float Distance = ToTarget.Size();

       if (Distance > 0)
       {
              float Speed = Distance / DecelerationCoefficient;
              Speed = FMath::Min(Speed, mpMovementComponent->GetMaxSpeed());

              FVector DesiredVelocity = ToTarget / Distance * Speed;
              return DesiredVelocity - Owner->GetVelocity();
       }

       return FVector(0, 0, 0);
}

New behaviors and improvements to old ones

The benefits of jumping to Unreal were immediate. I was able to bang out 3 new behaviors:

Pursuit

Given a target Actor, the source Actor will predict the future position of the target (given a lookahead time), and will Seek towards that.

FVector UActorSteeringComponent::Pursuit(const AActor* TargetActor)
{
       AActor* Owner = GetOwner();
       FVector TargetActorLocation = TargetActor->GetActorLocation();
       FVector TargetActorVelocity = TargetActor->GetVelocity();

       FVector ToTargetActor = TargetActorLocation - Owner->GetActorLocation();
      
       float LookAheadTime = (ToTargetActor.Size() / mpMovementComponent->GetMaxSpeed() + TargetActorVelocity.Size()) / LookAheadTimeModifier;
       return Seek(TargetActorLocation + TargetActorVelocity * LookAheadTime);
}

Evade

Similar to Pursuit, but the source Actor will Flee from the predicted future position of the target.

FVector UActorSteeringComponent::Evade(const AActor* TargetActor, float TriggerDistance)
{
       AActor* Owner = GetOwner();
       FVector TargetActorLocation = TargetActor->GetActorLocation();
       FVector TargetActorVelocity = TargetActor->GetVelocity();

       FVector ToTargetActor = TargetActorLocation - Owner->GetActorLocation();

       if (TriggerDistance < 0 || ToTargetActor.SizeSquared() <= TriggerDistance * TriggerDistance)
       {
              float LookAheadTime = (ToTargetActor.Size() / mpMovementComponent->GetMaxSpeed() + TargetActorVelocity.Size()) / LookAheadTimeModifier;
              return Flee(TargetActorLocation + TargetActorVelocity * LookAheadTime);
       }

       return FVector(0, 0, 0);
}

Wander

This simulates a random walk. The problem with true randomness is that it can be extremely unpredictable and unrealistic. The solution to this is to add small variations of force to force the Actor off its path.

FVector UActorSteeringComponent::Wander(float DeltaTime)
{
       float Jitter = WanderJitter * DeltaTime;
       mWanderTarget += FVector(RandomClamped() * Jitter, RandomClamped() * Jitter, 0);
       mWanderTarget.Normalize();
       mWanderTarget *= WanderRadius;
       FVector Target = mWanderTarget + FVector(WanderDistance, 0, 0);
       Target = GetOwner()->GetTransform().TransformPosition(Target);
       FVector ToPos = Target - GetOwner()->GetActorLocation();
       ToPos.Normalize();
       FVector DesiredVelocity = ToPos * WanderMaxSpeed;
       return DesiredVelocity - GetOwner()->GetVelocity();
}

Blueprints

Since the core functionality is being done in code, we expose the interfaces and adjustable variables to the Unreal Editor using decorators:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Steering")
float DecelerationCoefficient;

UFUNCTION(BlueprintCallable, Category = "Steering")
FVector Arrive(const FVector& Target);



And now not only can we configure all of these variables in the editor, but we can build any set of complex behaviors from these simple behaviors, from script.


We'll probably see obstacle avoidance next week.

Find the source code here: https://github.com/GTAddict/UnrealAIPlugin/

Comments

Popular posts from this blog

AI Controller Update - Week ending 7/18

This was another landmark week. I was finally able to get navigation meshes working with all the behaviors I’d implemented so far. To see how important this is, let’s consider an example: The follower (the smaller model) needs to get to the target position (the other character in this case). However, there is an obstacle in between. The steering algorithm doesn’t know anything about obstacles. So it will continually push the follower towards the actor, and into the obstacle. With a navmesh, we can tell the steering algorithms to follow a sequence of points in order to reach the target actor. Now, calculating this every frame is very expensive. So what do we do here? First, we raycast to the target position to see whether the follower can reach there uninterrupted. If it can, fantastic. We just ask the follower to seek to the target position. If there isn’t a direct path, we request the navmesh to provide us with a set of points that we store, and then seek to one by one. Wh...

AI Engine update - Week ending 6/20

This week, I worked on rendering objects with direction and velocity. The agent is rendered as a triangular object, which points at its current heading. The green line represents the velocity of the object. The text underneath represents the speed. I worked on additional behavior, which is the arrive behavior. Unfortunately I ran into a bug, where the velocity doesn't seem to be getting updated consistently. On further investigation, it turns out that the update loop was running so fast that the delta time between frames was turning up to be 0. What confuses me is that if the delta time is 0, then the agent shouldn't be moving at all, based on these lines: Vec2 Acceleration = SteeringForce / mMass ; mVelocity += Acceleration * deltaTime ; mVelocity . Truncate ( GetMaxSpeed ()); mPosition += mVelocity ; On further debugging, I discovered that the delta time is non-zero for the first few updates (suggesting that there was perhaps some setup code ...

AI Controller - Final update

I got the sample level up and running - and I'll let my video do the rest of the talking! :)