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);
Comments