前言
Unreal为AI寻路提供了非常完善的支持系统(NavigationSystem、PathFollowingComponent等),并且和AIController、MovementComponent以及AITask等其他模块间有密切的调用关系,很多参数都支持定制化,但学习成本较高,本文旨在结合源码以浅显易懂的方式分析一次AI寻路流程中涉及的各个模块和数据类型
AI寻路流程
首先我们可以从几个常用的寻路API入手,从源码梳理出一次寻路的函数调用栈图
接下来我们就按照顺序,从上到下看看这些函数内都做了什么,涉及到哪些数据类型
AITask_MoveTo
通过Task进行一次AI寻路应该是最常见也是支持配置选项最多的方式,我们就以它为例
FNavPathSharedPtr FollowedPath;
const FPathFollowingRequestResult ResultData = OwnerController->MoveTo(MoveRequest, &FollowedPath);
switch (ResultData.Code)
{
case EPathFollowingRequestResult::Failed:
FinishMoveTask(EPathFollowingResult::Invalid);
break;
case EPathFollowingRequestResult::AlreadyAtGoal:
MoveRequestID = ResultData.MoveId;
OnRequestFinished(ResultData.MoveId, FPathFollowingResult(EPathFollowingResult::Success, FPathFollowingResultFlags::AlreadyAtGoal));
break;
case EPathFollowingRequestResult::RequestSuccessful:
MoveRequestID = ResultData.MoveId;
PathFinishDelegateHandle = PFComp->OnRequestFinished.AddUObject(this, &UAITask_MoveTo::OnRequestFinished);
SetObservedPath(FollowedPath);
break;
default:
checkNoEntry();
break;
}
它通过调用AIController::MoveTo进行寻路,并获取导航路径(FollowedPath)和寻路移动结果(ResultData),根据寻路移动结果,如果失败或者已经到达则立即结束Task,如果RequestSuccessful则对寻路移动结束事件OnRequestFinish进行监听,并且对路径更新监听,取消了路径失效的自动重新计算,而由自己来负责路径更新(ConditionalUpdatePath)和其他路径事件
void UAITask_MoveTo::SetObservedPath(FNavPathSharedPtr InPath)
{
Path = InPath;
if (Path.IsValid())
{
// disable auto repaths, it will be handled by move task to include ShouldPostponePathUpdates condition
Path->EnableRecalculationOnInvalidation(false);
PathUpdateDelegateHandle = Path->AddObserver(FNavigationPath::FPathObserverDelegate::FDelegate::CreateUObject(this, &UAITask_MoveTo::OnPathEvent));
}
}
void UAITask_MoveTo::OnPathEvent(FNavigationPath* InPath, ENavPathEvent::Type Event)
{
const static UEnum* NavPathEventEnum = StaticEnum<ENavPathEvent::Type>();
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT("%s> Path event: %s"), *GetName(), *NavPathEventEnum->GetNameStringByValue(Event));
switch (Event)
{
case ENavPathEvent::NewPath:
case ENavPathEvent::UpdatedDueToGoalMoved:
case ENavPathEvent::UpdatedDueToNavigationChanged:
//Partial表示路径只有一部分因为目标不可达等各种原因
if (InPath && InPath->IsPartial() && !MoveRequest.IsUsingPartialPaths())
{
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT(">> partial path is not allowed, aborting"));
UPathFollowingComponent::LogPathHelper(OwnerController, InPath, MoveRequest.GetGoalActor());
FinishMoveTask(EPathFollowingResult::Aborted);
}
break;
case ENavPathEvent::Invalidated:
ConditionalUpdatePath();
break;
case ENavPathEvent::Cleared:
case ENavPathEvent::RePathFailed:
UE_VLOG(GetGameplayTasksComponent(), LogGameplayTasks, Log, TEXT(">> no path, aborting!"));
FinishMoveTask(EPathFollowingResult::Aborted);
break;
case ENavPathEvent::MetaPathUpdate:
default:
break;
}
}
在这里涉及到的一个重要的数据类型为FAIMoveRequest,这个结构体中存储了一次寻路移动请求中的所有参数包括终点和到达距离(AcceptRadius)等,也包括和A*寻路相关的最大寻路消耗因子(CostLimitFactor)等,源码每个参数都有注释就不详细解释了
AIController::MoveTo
进入到AIController中,实际上AIController并不实际处理寻路,只负责准备按路径移动前的所有资源,然后通过自己的一个组件PathFollowingComponent来处理,这个MoveTo函数很长,但实际上只做了4件事
1.如果MoveRequest的终点类型为Location并且配置了ProjectGoalOnNavigation,则将其投影到导航网格上,用投影点作为移动目标
if (!MoveRequest.IsMoveToActorRequest())
{
// fail if projection to navigation is required but it failed
if (bCanRequestMove && MoveRequest.IsProjectingGoal())
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
const FNavAgentProperties& AgentProps = GetNavAgentPropertiesRef();
FNavLocation ProjectedLocation;
if (NavSys && !NavSys->ProjectPointToNavigation(MoveRequest.GetGoalLocation(), ProjectedLocation, INVALID_NAVEXTENT, &AgentProps))
{
bCanRequestMove = false;
}
MoveRequest.UpdateGoalLocation(ProjectedLocation.Location);
}
}
FNavAgentProperties是寻路Agent的属性参数,在引擎中可以通过FNavDataConfig配置预设的寻路Agent参数(ProjectSettings/NavigationSystem/Agents),包括AgentRadius以及PreferredNavData等影响寻路结果的数据,实际寻路的Agent的会通过在NavMovementComponent配置AgentProperties或者使用默认的FNavAgentProperties::DefaultProperties
FNavigationData是为每个NavAgent生成的实际导航数据包括网格体等(基于NavAgentProperties的PreferredNavDataClass生成),在NavigationSystem中存有NavAgentProperties向NavigationData的映射,并且提供了GetNavDtaProps获取Agent对应的NavData(如果Agent是预设的话则直接查表即可,否则根据Agent的Radius等参数使用最接近的Agent生成的NavData)
TMap<FNavAgentProperties, TWeakObjectPtr<ANavigationData> > AgentToNavDataMap;
2.如果已经在目标点的话则立即结束移动
bAlreadyAtGoal = bCanRequestMove && PathFollowingComponent->HasReached(MoveRequest);
if (bAlreadyAtGoal)
{
UE_VLOG(this, LogAINavigation, Log, TEXT("MoveTo: already at goal!"));
ResultData.MoveId = PathFollowingComponent->RequestMoveWithImmediateFinish(EPathFollowingResult::Success);
ResultData.Code = EPathFollowingRequestResult::AlreadyAtGoal;
}
3.生成寻路路径结果(调用底层A*寻路算法)
const bool bValidQuery = BuildPathfindingQuery(MoveRequest, PFQuery);
if (bValidQuery)
{
FNavPathSharedPtr Path;
FindPathForMoveRequest(MoveRequest, PFQuery, Path);
}
BuildPathfindingQuery
通过NavigationSystem获取该Agent对应的NavData,如果移动目标是实现INavAgentInterface的Actor的话则对目标位置进行一个偏移,否则直接用ActorLocation作为目标点,然后用NavData的Filter和MoveRequest的一些参数组合成FPathFindingQuery
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
const ANavigationData* NavData = (NavSys == nullptr) ? nullptr :
MoveRequest.IsUsingPathfinding() ? NavSys->GetNavDataForProps(GetNavAgentPropertiesRef(), GetNavAgentLocation()) :
NavSys->GetAbstractNavData();
FVector GoalLocation = MoveRequest.GetGoalLocation();
if (MoveRequest.IsMoveToActorRequest())
{
const INavAgentInterface* NavGoal = Cast<const INavAgentInterface>(MoveRequest.GetGoalActor());
if (NavGoal)
{
const FVector Offset = NavGoal->GetMoveGoalOffset(this);
GoalLocation = FQuatRotationTranslationMatrix(MoveRequest.GetGoalActor()->GetActorQuat(), NavGoal->GetNavAgentLocation()).TransformPosition(Offset);
}
else
{
GoalLocation = MoveRequest.GetGoalActor()->GetActorLocation();
}
}
FindPathForMoveRequest
本质上是调用NavigationSystem::FindPathSync进行寻路,如果目标是Actor的话要监听Actor的移动等准备更新路径
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
if (NavSys)
{
FPathFindingResult PathResult = NavSys->FindPathSync(Query);
if (MoveRequest.IsMoveToActorRequest())
{
PathResult.Path->SetGoalActorObservation(*MoveRequest.GetGoalActor(), 100.0f);
}
PathResult.Path->EnableRecalculationOnInvalidation(true);
}
前面已经说过,NavData实际上就是为每一类预设的Agent生成的实际寻路网格数据,所以NavigationSystem::FindPathSync本质上还是调用对应NavData的FindPath或者FindHierarchicalPath进行寻路,其中的寻路算法实际上就是A* 寻路算法,openlist和closelist等概念也完全适用,如果对A*寻路算法有基础的话读起来应该不太困难,这里就不深入了。我之前有几篇文章写了一个自己的A *寻路系统,不过是基于Unity+C#的,感兴趣的话可以看一下
3D沙盒游戏开发日志4——网格寻路系统
3D沙盒游戏开发日志9——寻路模块优化附带网格建筑系统修改
寻路的最终结果就存在PathResult.Path(FNavigationPath)中
4.调用PathFollowingComponent进行寻路移动
const FAIRequestID RequestID = Path.IsValid() ? RequestMove(MoveRequest, Path) : FAIRequestID::InvalidRequest;
FAIRequestID AAIController::RequestMove(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr Path)
{
uint32 RequestID = FAIRequestID::InvalidRequest;
if (PathFollowingComponent)
{
RequestID = PathFollowingComponent->RequestMove(MoveRequest, Path);
}
return RequestID;
}
通过RequestMove将请求参数MoveRequest(FAIMoveRequest)和寻路结果Path(FNavigationPath)传给PathFollowingComponent进行实际的按路径移动
PathFollowingComponent
RequestMove
PathFollowingComponent在同一时间只能有一个移动请求,所以RequestMove会恢复之前暂停的Move或者中断正在进行的Move
if (Status == EPathFollowingStatus::Paused && Path.IsValid() && InPath.Get() == Path.Get() && DestinationActor == RequestData.GetGoalActor())
{
ResumeMove();
}
else
{
if (Status != EPathFollowingStatus::Idle)
{
// setting to false to make sure OnPathFinished won't result in stoping
bStopMovementOnFinish = false;
OnPathFinished(EPathFollowingResult::Aborted, FPathFollowingResultFlags::NewRequest);
}
}
然后会绑定PathUpdate的监听,初始化MetaNavPath(不知道有啥用,好像和异步有关),然后通过SetMoveSegment为StartingPathPoint来开始移动,Segment实际上就是寻路路径上的每个点,用一个index值代表。检查CurrentRequestID == MoveId是为了只在第一次创建移动时设置参数和起点,在之后更新或者暂停后恢复不会将寻路移动重置
if (CurrentRequestId == MoveId)
{
AcceptanceRadius = UseAcceptanceRadius;
bReachTestIncludesAgentRadius = RequestData.IsReachTestIncludingAgentRadius();
bReachTestIncludesGoalRadius = RequestData.IsReachTestIncludingGoalRadius();
GameData = RequestData.GetUserData();
SetDestinationActor(RequestData.GetGoalActor());
SetAcceptanceRadius(UseAcceptanceRadius);
UpdateDecelerationData();
// with async pathfinding paths can be incomplete, movement will start after receiving path event
if (!bIsUsingMetaPath && Path.IsValid() && Path->IsValid())
{
Status = EPathFollowingStatus::Moving;
// determine with path segment should be followed
const uint32 CurrentSegment = DetermineStartingPathPoint(InPath.Get());
SetMoveSegment(CurrentSegment);
}
}
Tick
接下来就在tick中持续的进行寻路移动,逻辑也相当简单,通过UpdatePathSegment判断是否到达了当前目标segment,如果是的话就更新下一个目标segment,如果该segment为终点就结束移动;然后通过FollowPathSegment进行实际移动,目标是指定的Segment
void UPathFollowingComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (Status == EPathFollowingStatus::Moving)
{
// check finish conditions, update current segment if needed
UpdatePathSegment();
}
if (Status == EPathFollowingStatus::Moving)
{
// follow current path segment
FollowPathSegment(DeltaTime);
}
};
迭代下一个目标点的核心函数是SetMoveSegment(GetNextPathIndex()),实际上就是从寻路结果FNavigationPath中获取PathPointList,取当前MoveSegmentEndIndex对应的NavPathPoint然后不断自增
在FollowPathSegment中实际上是调用了AIPawn上挂载的NavMovementComponent中的RequestPathMove/RequestDirectMove进行移动,注册PostProcessMove可以对MoveInput进行修改
const bool bAccelerationBased = MovementComp->UseAccelerationForPathFollowing();
if (bAccelerationBased)
{
CurrentMoveInput = (CurrentTarget - CurrentLocation).GetSafeNormal();
if (bStopMovementOnFinish && (MoveSegmentStartIndex >= DecelerationSegmentIndex))
{
const FVector PathEnd = Path->GetEndLocation();
const FVector::FReal DistToEndSq = FVector::DistSquared(CurrentLocation, PathEnd);
const bool bShouldDecelerate = DistToEndSq < FMath::Square(CachedBrakingDistance);
if (bShouldDecelerate)
{
bIsDecelerating = true;
const FVector::FReal SpeedPct = FMath::Clamp(FMath::Sqrt(DistToEndSq) / CachedBrakingDistance, 0., 1.);
CurrentMoveInput *= SpeedPct;
}
}
PostProcessMove.ExecuteIfBound(this, CurrentMoveInput);
MovementComp->RequestPathMove(CurrentMoveInput);
}
else
{
FVector MoveVelocity = (CurrentTarget - CurrentLocation) / DeltaTime;
const int32 LastSegmentStartIndex = Path->GetPathPoints().Num() - 2;
const bool bNotFollowingLastSegment = (MoveSegmentStartIndex < LastSegmentStartIndex);
PostProcessMove.ExecuteIfBound(this, MoveVelocity);
MovementComp->RequestDirectMove(MoveVelocity, bNotFollowingLastSegment);
}
void UPawnMovementComponent::RequestPathMove(const FVector& MoveInput)
{
if (PawnOwner)
{
PawnOwner->Internal_AddMovementInput(MoveInput);
}
}
当到达终点后在UpdatePathSegment中调用OnPathFinished结束本次寻路移动
if (bCollidedWithGoal)
{
// check if collided with goal actor
OnSegmentFinished();
OnPathFinished(EPathFollowingResult::Success, FPathFollowingResultFlags::None);
}
else if (MoveSegmentEndIndex > PreciseAcceptanceRadiusCheckStartNodeIndex && HasReachedDestination(CurrentLocation))
{
OnSegmentFinished();
OnPathFinished(EPathFollowingResult::Success, FPathFollowingResultFlags::None);
}
总结
- 在整套流程当中可以配置的地方非常多,在MoveRequest、NavAgentProperties等结构中可以自由配置参数,在PathFollowingComponent和NavMovementComponent等组件中也可以配置参数和注册回调方法,寻路过程中也可以向NavData添加关于路径更新的回调;
- 是整个寻路系统是嵌入在AI框架中的,基于从Task到AIController最后调用AIPawn上的Component完成实际效果这一套流程,通过为AIController添加PathFollowingComponent结合NavigationSystem实现了寻路系统