적의 기본적인 로직인 Patrol을 구현해 볼 것이다.

 

 다만, 현재 본인이 참고하는 유데미 강의를 포함해 거의 모든 Patrol은 아래와 같이 구현 한다.

 

 1. 네비메쉬 볼륨 설치.

 2. 목적지의 지정 또는 랜덤 생성.

 3. AAIController의 MoveTo를 이용한 이동.

 4. 실시간으로 목적지에 도착했는 지를 추적하여, 도착했다면 settimer(블루프린트는 Delay)로 다시 Patrol 호출.

 

 유튜브나 넷상의 Patrol 구현은 위와 같은 틀을 크게 벗어 나지 않는다.

 

 하지만 본인은 Tick과 같이(또는 settimer) 매프레임 거리를 계산하는 4번째 방법이 마음에 들지 않았다. 때문에 이동시 매번 임의의 함수를 호출하지 않는 방법을 생각해 보았다.

 

 아이디어는 간단하다. MoveTo 함수를 실행 하면 캐릭터는 설정된 타겟으로 가고, 해당 지점에 도착하면 알아서 멈춘다. 그렇다면 분명 해당 목표지점에 도착했다는 것을 알리는 함수나 이벤트가 있을 거라고 생각했다.  

 

 우선 AAIController의 MoveTo 함수에 들어가 보았다. 막연하게 MoveTo 함수에 이동 로직과 그와 관련된 로직이 있을 것이라 생각 했지만 아니었다.



FPathFollowingRequestResult AAIController::MoveTo(const FAIMoveRequest& MoveRequest, FNavPathSharedPtr* OutPath)
{ 
    SCOPE_CYCLE_COUNTER(STAT_MoveTo);
    UE_VLOG(this, LogAINavigation, Log, TEXT("MoveTo: %s"), *MoveRequest.ToString());
    FPathFollowingRequestResult ResultData;
    ResultData.Code = EPathFollowingRequestResult::Failed;
    if (MoveRequest.IsValid() == false)
    {
        UE_VLOG(this, LogAINavigation, Error, TEXT("MoveTo request failed due MoveRequest not being valid. Most probably desired Goal Actor not longer exists. MoveRequest: '%s'"), *MoveRequest.ToString());
        return ResultData;
    }
    if (PathFollowingComponent == nullptr)
    {
        UE_VLOG(this, LogAINavigation, Error, TEXT("MoveTo request failed due missing PathFollowingComponent"));
        return ResultData;
    }
    ensure(MoveRequest.GetNavigationFilter() || !DefaultNavigationFilterClass);
    bool bCanRequestMove = true;
    bool bAlreadyAtGoal = false;
    
    if (!MoveRequest.IsMoveToActorRequest())
    {
        if (MoveRequest.GetGoalLocation().ContainsNaN() || FAISystem::IsValidLocation(MoveRequest.GetGoalLocation()) == false)
        {
            UE_VLOG(this, LogAINavigation, Error, TEXT("AAIController::MoveTo: Destination is not valid! Goal(%s)"), TEXT_AI_LOCATION(MoveRequest.GetGoalLocation()));
            bCanRequestMove = false;
        }
        // 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))
            {
                if (MoveRequest.IsUsingPathfinding())
                {
                    UE_VLOG_LOCATION(this, LogAINavigation, Error, MoveRequest.GetGoalLocation(), 30.f, FColor::Red, TEXT("AAIController::MoveTo failed to project destination location to navmesh"));
                }
                else
                {
                    UE_VLOG_LOCATION(this, LogAINavigation, Error, MoveRequest.GetGoalLocation(), 30.f, FColor::Red, TEXT("AAIController::MoveTo failed to project destination location to navmesh, path finding is disabled perhaps disable goal projection ?"));
                }
                bCanRequestMove = false;
            }
            MoveRequest.UpdateGoalLocation(ProjectedLocation.Location);
        }
        bAlreadyAtGoal = bCanRequestMove && PathFollowingComponent->HasReached(MoveRequest);
    }
    else
    {
        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;
    }
    else if (bCanRequestMove)
    {
        FPathFindingQuery PFQuery;
        const bool bValidQuery = BuildPathfindingQuery(MoveRequest, PFQuery);
        if (bValidQuery)
        {
            FNavPathSharedPtr Path;
            FindPathForMoveRequest(MoveRequest, PFQuery, Path);
            const FAIRequestID RequestID = Path.IsValid() ? RequestMove(MoveRequest, Path) : FAIRequestID::InvalidRequest;
            if (RequestID.IsValid())
            {
                bAllowStrafe = MoveRequest.CanStrafe();
                ResultData.MoveId = RequestID;
                ResultData.Code = EPathFollowingRequestResult::RequestSuccessful;
                if (OutPath)
                {
                    *OutPath = Path;
                }
            }
        }
    }
    if (ResultData.Code == EPathFollowingRequestResult::Failed)
    {
        ResultData.MoveId = PathFollowingComponent->RequestMoveWithImmediateFinish(EPathFollowingResult::Invalid);
    }
    return ResultData;
}


해당 코드가 MoveTo의 전문인데.


 요약하자면 MoveTo는 프로그래머가 설정한 FAIMoveRequest를 넘기면 해당 정보를 바탕으로 현재 월드에 있는 네비메쉬를 찾고, 해당정보를 네비메쉬에게 보내어 네비메쉬는 경로를 찾게 된다.

 

 네비메쉬가 찾은 경로를 바탕으로  RequestMove(MoveRequest, Path)에서 캐릭터에게 이동 명령을 내리고 이동을 수행하며, MoveTo는 해당 경로에대한 정보들 FNavPathSharedPtr에 저장하는 식이다. 

 

 그리고 해당 이동이 완료되면 AAIController의 OnMoveCompleted()가 호출된다.


 이를 이용하여 커스텀 AAIController를 하나 만들어 OnMoveCompleted()를 오버라이드 후


OnMoveCompleted() 호출 시, 다시 Patrol()을 호출하면 굳이 실시간으로 목표와의 거리를 계산하지 않아도 된다.


참고로 Patrol함수는 간단하게 이러하다






음 잘 된다



근데 이 방법이 아름답고 안전한 방법인진 몰?루...?

Behavior tree쓰는 방법도 있는데 것보단 c++로 적 AI를 구현해 보는 게 유연성 있고 공부가 될 것 같아 c++로 작성했다


지적 환영