NPC가 정찰 중에 플레이어를 발견하면 플레이러를 추격하도록 구현

  • 플레이어 정보를 블랙보드에 저장하도록 Object 타입으로 Target 변수를 생성
  • Object 타입에서는 기반 클래스를 ABCharacter로 지정

  • 행동패턴은 플레이어를 발견했는지, 못했는지에 따라 추격과 정찰로 구분한다.
  • 그래서 셀렉터로 확장한다.
  • 추격과 정찰 중 추격에 더 우선권을 주고, 추격로직은 Target을 행헤 이동하도록 설계한다


  • 그리고 BTService를 부모로하는 BTService_Detect 클래스를 생성
  • 행동트리 서비스노드는 자신이 속한 컴포짓 노드가 활성화 될 경우 주기적으로 TickNOde 함수를 호출한다. 호출주기는 노드 내부에 설정된 Interval 속성 값으로 지정 할 수 있다.
  • TickNode 함수에는 NPC의 위치를 기준으로 반경 6미터 내에 캐릭터가 있는지 감지하는 기능을 넣는다. 반경 내에 모든 캐릭터를 감지하는 OverlapMultiByChannel 함수를 사용한다.
  • 감지된 모든 캐릭터 정보는 목록을 관리하는 데 적압한 TArry로 전달된다.
  • 만든 후에 Selecter 에 Detect를 선택해 컴포짓에 부착한다
//h
class ARENABATTLE_API UBTService_Detect : public UBTService
{
    GENERATED_BODY()
public:
    UBTService_Detect();

protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

//cpp
#include "BTService_Detect.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_Detect::UBTService_Detect()
{
    NodeName = TEXT("Detect");
    Interval = 1.0f;
}

void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

    APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();   //폰 얻어오기

    if (nullptr == ControllingPawn) return;

    UWorld* World = ControllingPawn->GetWorld();
    FVector Center = ControllingPawn->GetActorLocation();
    float DetectRadius = 600.0f;

    if (nullptr == World) return;

    TArray<FOverlapResult> OverlapResults;
    FCollisionQueryParams CollisionQueryParam(NAME_None, false, ControllingPawn);

    bool bResult = World->OverlapMultiByChannel(
        OverlapResults,
        Center,
        FQuat::Identity,
        ECollisionChannel::ECC_EngineTraceChannel2,
        FCollisionShape::MakeSphere(DetectRadius),
        CollisionQueryParam
    );

    DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}


  • NPC가 탐지 영역 내의 캐릭터를 감지한다면, 그중에서 우리가 조종하는 캐릭터를 추려내야 한다.
  • 캐릭터를 조종하는 컨트롤러가 플레이어 컨트롤러인지 파악할 수 있도록 IsPlayerController 함수를 사용한다
  • 플레이어가 감지되면 Target 값을 플레이러로 지정하고 아니면 nullptr로 지정한다
  • 감지하면 녹색으로 구체를 그리고 NPC와 연결된 선을 추가로 그린다
ABAIController 
//h
static const FName TargetKey;
//cpp
const FName AABAIController::TargetKey(TEXT("Target"));

//UBTService_Detect.cpp
void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

    bool bResult = World->OverlapMultiByChannel(
        OverlapResults,
        Center,
        FQuat::Identity,
        ECollisionChannel::ECC_GameTraceChannel2,
        FCollisionShape::MakeSphere(DetectRadius),
        CollisionQueryParam
    );
    if (bResult)
    {
        for (auto OverlapResult : OverlapResults)
        {
            AABCharacter* ABCharacter = Cast<AABCharacter>(OverlapResult.GetActor());
            if (ABCharacter && ABCharacter->GetController()->IsPlayerController())
            {
                OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, ABCharacter);
                DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);

                DrawDebugPoint(World, ABCharacter->GetActorLocation(), 10.0f, FColor::Blue, false, 0.2f);
                DrawDebugLine(World, ControllingPawn->GetActorLocation(), ABCharacter->GetActorLocation(), FColor::Blue, false, 0.27f);
                return;
            }
        }
    }
    else
    {
        OwnerComp.GetBlackboardComponent()->SetValueAsObject(AABAIController::TargetKey, nullptr);
    }

    DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
  • 그런데 이동할 때 회전이 부자연 스럽게 꺽인다. 이를 보강하기 위해 NPC를 위한 ControlMode를 추가 * NPC 이동 방향에 따라 회전하도록 캐릭터 무브먼트 설정을 변경해본다
//h
class ARENABATTLE_API AABCharacter : public ACharacter
{
public:
  virtual void PossessedBy(AController* NewController) override;
}

//cpp


void AABCharacter::PossessedBy(AController * NewController)
{
    Super::PossessedBy(NewController);

    if (IsPlayerControlled())
    {
        SetControlMode(EControlMode::DIABLO);
        GetCharacterMovement()->MaxWalkSpeed = 600.0f;
    }
    else
    {
        SetControlMode(EControlMode::NPC);
        GetCharacterMovement()->MaxWalkSpeed = 300.0f;
    }
}

void AABCharacter::SetControlMode(EControlMode NewControlMode)
{
    CurrentControlMode = NewControlMode;

    switch (CurrentControlMode)
    {
    case EControlMode::NPC:
        bUseControllerRotationYaw = false;
        GetCharacterMovement()->bUseControllerDesiredRotation = false;
        GetCharacterMovement()->bOrientRotationToMovement = true;
        GetCharacterMovement()->RotationRate = FRotator(0.0f, 480.0f, 0.0f);
        break;
    }
}
  • 이제 서비스가 실행된 결과에 따라 셀렉터 데코레이더 왼쪽의 추격과 셀렉터 데코레이터 오른쪽의 정찰 로직이 나눠지도록 행동트리 로직을 구성한다
  • 서비스 결과는 블랙보드의 Target 키에 값이 있는지, 없는지로 구분할 수 있다.
  • 그래서 데코레이터 노드를 사용한다.
  • 해당 키값의 변경이 감지되면 현재 컴포짓 노드의 실행을 곧바로 취소하고 노티파이 옵저버 값알 OnValue Change로 변경한다.

+ Recent posts