Skip to main content

AI Controller Update - Week ending 7/4


This week, I worked on Obstacle Avoidance and a slight hint of navigation.

Obstacle Avoidance

Though the concept is simple enough, the execution is quite tricky. The first part is to detect obstacles in your area. We define a lookahead, which can vary based on the object’s current velocity. I’ve left it at a fixed value for now. Then we raycast forwards to find the first obstacle in our path.
Next, we compute the normal from the point of impact. We steer our object away from the point in the direction of the normal. The code looks like this:
bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, StartLocation, EndLocation, Channel, QueryParams, ResponseParam);
       if (bHit)
       {
              FVector PenetratedAlongHit = Hit.ImpactPoint - EndLocation;
              FVector PenetratedAlongNormal = PenetratedAlongHit.ProjectOnToNormal(Hit.ImpactNormal);

              float PenetrationDepth = PenetratedAlongNormal.Size();
              return (Hit.ImpactNormal * PenetrationDepth);
       }

Hiding

Hiding is when an Actor places themselves behind an obstacle, such that the obstacle is directly between said Actor and the “Enemy” or other Actor. The first step is to get the closest obstacle to hide behind. This is done by doing a SphereTrace and getting a list of Actors. From that list, we pick the Actors closest to us and proceed. The code for that looks like this:
       static ETraceTypeQuery TQuery = UEngineTypes::ConvertToTraceType(ECC_WorldStatic);

       TArray<AActor*> ActorsToIgnore = { pOwner };
       TArray<FHitResult> OutHits;

       if (UKismetSystemLibrary::SphereTraceMulti(
              GetWorld(),
              pOwner->GetActorLocation(),
              pOwner->GetActorLocation(),
              ObstacleSearchRadius,
              TQuery,
              false,
              ActorsToIgnore,
              EDrawDebugTrace::ForOneFrame,
              OutHits,
              true))
       {
              // Find closest object
              FHitResult ClosestHit;
              ClosestHit.Distance = ObstacleSearchRadius;
              for (auto Hit : OutHits)
              {
                     // Don't allow hiding behind other pawns.
                     if (Hit.Distance < ClosestHit.Distance && Cast<APawn>(Hit.Actor.Get()) == nullptr && !IsOver(Hit.Actor.Get()))
                     {
                           ClosestHit = Hit;
                     }
              }
       }
When we go through the obstacles in this manner, we have to make sure we consider those only above us. In the future we might consider objects that only cover us fully. For now, we check if the bottommost point of the character’s capsule is over the topmost point of the obstacle. If so, we reject this obstacle:
bool UActorSteeringComponent::IsOver(const AActor* Actor)
{
       FVector Origin, Extent;
       Actor->GetActorBounds(true, Origin, Extent);
       float ActorHighestZ = Origin.Z + Extent.Z;
       float CapsuleLowestZ = mpCapsuleComponent->GetComponentLocation().Z - mpCapsuleComponent->GetScaledCapsuleHalfHeight();
       return CapsuleLowestZ > ActorHighestZ;
}

The next step is to select a suitable hiding spot behind this obstacle.
To do this, we draw a line from the Target to the Obstacle, and scale that by the distance away from the obstacle we want our Actor to stand. Now, we perform a raycast from the opposite direction to find out where the exit point of the initial vector would be on the obstacle. We add the scaled vector to this point.
FVector UActorSteeringComponent::GetHidingSpot(const AActor* Obstacle, const FVector& Target)
{
       FVector ToObstacle = Obstacle->GetActorLocation() - Target;
       ToObstacle.Normalize();

       FVector CheckFromPoint = Obstacle->GetActorLocation() + ToObstacle * SafeRaycastDistanceFromObstacle;
       FVector OutPoint;
       UPrimitiveComponent* OutComponent;
       Obstacle->ActorGetDistanceToCollision(CheckFromPoint, ECC_WorldStatic, OutPoint, &OutComponent);

       return OutPoint + ToObstacle * DistanceFromObstacle;
}
The final step is to Arrive at this location. If there is no obstacle available, we simply Evade the target.
if (ClosestHit) return Arrive(GetHidingSpot(ClosestHit.Actor.Get(), Target->GetActorLocation()));
else return Evade(Target);




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 Controller - Final update

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