• 먼저 분기를 위해 로직을 확장한다.
  • 데코레이터 클래스는 CalculateRawConditionValue 함수를 상속받아 원하는 조건이 달성됐는지 파악하도록 설계됐다. 이 함수는 const로 선언돼 데코레이터 클래스의 멤버 변수 값을 변경할 수 없다.

 

//h
UCLASS()
class ARENABATTLE_API UBTDecorator_IsInAttackRange : public UBTDecorator
{
    GENERATED_BODY()
public:
    UBTDecorator_IsInAttackRange();

protected:
    virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};

//cpp
#include "BTDecorator_IsInAttackRange.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTDecorator_IsInAttackRange::UBTDecorator_IsInAttackRange()
{
    NodeName = TEXT("CanAttack");
}

bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
    bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

    auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
    if (nullptr == ControllingPawn)
        return false;

    auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));

    if (nullptr == Target)
        return false;

    bResult = (Target->GetDistanceTo(ControllingPawn) <= 200.0f);
    return bResult;
}
  • 그리고 완성된 데코를 가장 왼쪾에 위치한 시퀸스 컴포짓에 부착한다
  • 우측은 반대 조건으로 부착한다(밑 사진에는 안들어 있음)

  • Wait 대신에 실레조 플레이어를 공격할 태스크를 생성한다
  • 공격 태스크는 공격 애니메이션이 끝날 떄까지 대기해야 하는 지연 태스크 이므로 ExecuteTask의 결과 값을 InProgress로 반환하고, 공격이 끝났을 떄 태스크가 끝났다고 알려줘야 한다.
  • 이를 알려주는 함수가 FinistLatentTask 이다. 이 함수를 호출하지 않으면 행동트리 시스템은 현재 태스크에 계속 머물어 있는다. 그래서 호출 할수 있도록 노드의 Tick 기능을 활성화하고 조건을 파악한 후 태스크 종료 명령을 내려줘야 한다
//h
UCLASS()
class ARENABATTLE_API UBTTask_Attack : public UBTTaskNode
{
    GENERATED_BODY()
public:
    UBTTask_Attack();

protected:

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

//cpp
#include "BTTask_Attack.h"

UBTTask_Attack::UBTTask_Attack()
{
    bNotifyTick = true; //틱 기능 활성화?

}

EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
    Super::ExecuteTask(OwnerComp, NodeMemory);
    return EBTNodeResult::InProgress;
}

void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
    FinishLatentTask(OwnerComp,EBTNodeResult::Succeeded);
}
  • 실제로 캐릭터에 공격 명령을 내리고, 공격이 끝난 시점을 파악해 태스크를 종료하도록 기능을 구현한다
  • AI 컨트롤러에서도 공격명령을 내릴 수 있도록 ABCharacter 클래스의 Attack 함수의 접근 권한을 public으로 변경한다 .
  • 플레이어의 공격이 종료되면 공격 태스크에서 해당 알림을 받을 수 있도록 델리게이트를 새로 선언하고 공격이 종료될 떄 이를 호출하는 로직을 캐릭터에 구현한다
  • 캐릭터의 델리게이트 설정이 완료되면 태스크에서 람다 함수를 해당 델리게이트에 등록하고 Tick 함수 로직에서 이를 파악해 FinishLatentTask 함수를 호출함으로써 태스크를 종료하도록 구현한다

 

//h
DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);

{
public:
    void Attack();
    FOnAttackEndDelegate OnAttackEnd;
}

//cpp
void AABCharacter::OnAttackMontageEnded(UAnimMontage * Montage, bool bInterrupted)
{
.....

    OnAttackEnd.Broadcast();
}
//h
UCLASS()
class ARENABATTLE_API UBTTask_Attack : public UBTTaskNode
{
    GENERATED_BODY()
public:
    UBTTask_Attack();

protected:

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
private:
    bool IsAttacking = false;
};


//cpp
#include "BTTask_Attack.h"
#include "ABAIController.h"
#include "ABCharacter.h"

UBTTask_Attack::UBTTask_Attack()
{
    bNotifyTick = true; //틱 기능 활성화?
    IsAttacking = false;
}

EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
    Super::ExecuteTask(OwnerComp, NodeMemory);

    auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
    if (nullptr == ABCharacter)
        return EBTNodeResult::Failed;

    ABCharacter->Attack();
    IsAttacking = true;

    ABCharacter->OnAttackEnd.AddLambda([this]() ->void {IsAttacking = false; });

    return EBTNodeResult::InProgress;
}


void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
    if (!IsAttacking)
    {
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
    }
}
  • 위 내용을 하고 기존 제일 왼쪽에 있던 태스크를 새로 만든 Attack로 교체한다
  • NPC가 플레이어를 공격할 떄 제자리에 정지하기 떄문에 플레이어가 NPC 뒤로 돌아가도 계속 같은 곳을 공격한다
  • 그래서 공격하면시 동시에 플레이러를 향해 회전하는 기능을 추가한다
  • 블랙보드의 Target으로 회전하는 태스크를 추가한다. BTTask_TurnToTarget을 만들고 일정한 속도로 회전하도록 FMath::RInterpTo 함수를 사용해 회전시키는 기능을 구현한다

 

//h
UCLASS()
class ARENABATTLE_API UBTTask_TurnToTarget : public UBTTaskNode
{
    GENERATED_BODY()

public:
    UBTTask_TurnToTarget();
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory) override;
};

//cpp
#include "BTTask_TurnToTarget.h"
#include "ABAIController.h"
#include "ABCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"


UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
    NodeName = TEXT("Turn");
}

EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
    Super::ExecuteTask(OwnerComp, NodeMemory);

    auto ABCharacter = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
    if (nullptr == ABCharacter)
    {
        return EBTNodeResult::Failed;
    }
    auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
    if (nullptr == Target)
        return EBTNodeResult::Failed;

    FVector LookVector = Target->GetActorLocation() - ABCharacter->GetActorLocation();
    LookVector.Z = 0.0f;

    FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
    ABCharacter->SetActorRotation(FMath::RInterpTo(
        ABCharacter->GetActorRotation(),
        TargetRot,GetWorld()->GetDeltaSeconds(),
        2.0f));

    return EBTNodeResult::Succeeded;
}
  • 회전 태스크를 완성하면 공격로직에서 사용한 시퀸스 컴포짓을 심플 패러럴 컴포짓으로 대처 한다

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로 변경한다.

행동트리 시스템

  • 행동트리는 NPC가 해야 할 행동을 분석하고 우선순위가 높은 행동부터 NPC가 실행할 수 있도록 트리 구조로 설계하는 기법
  • 블랙보드 애샛, 비헤이비어트리 에셋 을 생성
  • 위 2개를 C++ 코드를 사용할라면 'AIModule' 모듈을 추가해야한다
//////////////h
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
    GENERATED_BODY()

public:
    AABAIController();
    virtual void OnPossess(APawn* InPawn) override;

private:
    UPROPERTY()
    class UBehaviorTree* BTAsset;

    UPROPERTY()
    class UBlackboardData* BBAsset;
};

/////////////////cpp
#include "ABAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"

AABAIController::AABAIController()
{
    static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("/Game/Book/AI/BB_ABCharacter.BB_ABCharacter"));
    if (BBObject.Succeeded())
    {
        BBAsset = BBObject.Object;
    }

    static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("/Game/Book/AI/BT_ABCharacter.BT_ABCharacter"));
    if (BTObject.Succeeded())
    {
        BTAsset = BTObject.Object;
    }
}

void AABAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    if (UseBlackboard(BBAsset, Blackboard))
    {
        Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
        if (!RunBehaviorTree(BTAsset))
        {
            ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
        }
    }
}
  • NPC 순찰기능을 구현하려면 2가지 데이터가 필요하다
  • NPC가 생성됐을 떄의 위치값, Vector 타입으로 키를 생성하고 HomePos
  • NPC가 순찰할 위치정보를 보관할 키, Vector 타입으로 PatroPos
  • 일단 블랙보드에 추가
  • 앞으로 관련 키 이름이 절대 변하지 않는다근 가정하에 static const 를 사용해 변수 초기 값을 지정함. 이렇게 선언하면 참조하기 편하지만, 하드코딩으로 값을 변경해야 하는 단점이 있다.
//h
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
...........
    static const FName HomePosKey;
    static const FName PatrolPosKey;
 }
///////////cpp
#include "BehaviorTree/BlackboardComponent.h"

const FName AABAIController::HomePosKey(TEXT("HomePos"));
const FName AABAIController::PatrolPosKey(TEXT("PatrolPos"));


void AABAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    if (UseBlackboard(BBAsset, Blackboard))
    {
        Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
        if (!RunBehaviorTree(BTAsset))
        {
            ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
        }
    }
}
  • 사용하기 위해 모듈에 "GameplayTasks" 추가한다.
  • 다음으로 NPC가 이동할 위치인 PatrolPos 데이터를 생성해야 한다. 이는 순찰할 때마다 바뀌므로 테스트를 제작해 행동트리에서 블랙보드에 값을 쓰도록 설계하는 것이 좋다
  • BTTaskNode를 부모 클래스로 하는 BTTask_FindPatrolPos클래스를 생성한다
  • UI 에서 표현할 떄는 BTTask_ 접두사 부분이 자동으로 걸러진다

행동트리는 태스크를 실행할때, ExecuteTask라는 멤버 함수를 실행하고, 함수는 다음의 셋중 하나의 값을 반환한다
Aborted : 태스크 실행 중에 준단됐다. 결과적으로 실패 
Failed : 태스크를 수행했지만 실패 
Succeeded : 태스크를 성공적으로 수행 
InProgress : 대스크를 계속 수행하고 있다. 태크스의 실행 결과는 향후 얄려줌

* 그래서 ExeCuteTask 함수에서 다음 정찰 지점을 찾는 로직을 구현하고 바로 실행 결과를 반환라도록 구현한다. 태스크의 이름을 다른 이름으로 표시하고 싶다면 NodeName속성을 다른값으로 지정하면 된다

//h

//h
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_FindPatrolPos.generated.h"

UCLASS()
class ARENABATTLE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
    GENERATED_BODY()
public:
    UBTTask_FindPatrolPos();

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};


////cpp
#include "BTTask_FindPatrolPos.h"
#include "ABAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NavigationSystem.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
	NodeName = TEXT("KKB_FindPatrolPos");
}



EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
		return EBTNodeResult::Failed;

	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
	if (nullptr == NavSystem)
		return EBTNodeResult::Failed;

	FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(AABAIController::HomePosKey);
	FNavLocation NextPatrol;

	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextPatrol))
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(AABAIController::PatrolPosKey, NextPatrol.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}

 

Secter 가 아닌 Sequence (뻘짓 5시간)

AIContriller와 네비게이션 시스템

  • 레벨에 배치한 캐릭터가 스스로 움직일 수 있도록 AI 컨트롤러를 생성해 캐릭터에게 부여한다
  • AIAController를 부모로 하는 ABAIAController 클래스를 생성
  • AI 생성 옵션을 PlaceInWorldSpawned로 설정
#include "ABAIController.h"

AABCharacter::AABCharacter()
{
...................
    AIControllerClass(pawn에 있음) = AABAIController::StaticClass();  
    AutoPossessAI(pawn에 있음) = EAutoPossessAI::PlacedInWorldOrSpawned;

}

AIController 로직을 부여

  • NPC는 스스로 움직여야 하기 떄문에 이를 보조할 내비게이션 메시를 이요ㅕㅇ한다
  • 로직을 구현하기 전에 먼제 레벨에 내비게이션 메시를 배치해 NPC가 스스로 움직일 수 있는 환경을 구축한다

  • 생성된 내비 메시를 이용하여, ABAIController에 빙의한 폰에게 목적지를 알려줘 폰이 목적지까지 스스로 움직이도록 명령을 추가한다.
  • 그리고 AI 컨트롤러에 타이머를 설치해 3초마다 폰에게 목적지로 이동하는 명령을 내린다.
  • 언리얼 엔진의 내비게이션 시스템은 이동가능한 목적지를 ㅐㄴ덤으로 가져오는 GetRandomPointInavigabkeRadius 함수와 목적지로 폰을 이동시키는 SimpleMoveToLocation 함수를 제공한다.
  • 4.20 부터는 UNavigationSystem -> UNavigationSystemV1으로 변경됨
//h
class KGAME_API AABAIController : public AAIController
{
    GENERATED_BODY()

public:
    AABAIController();
    virtual void OnPossess(APawn* InPawn) override;
    virtual void OnUnPossess() override;


private:
    void OnRepeatTimer();
    FTimerHandle RepeatTimerHandle;
    float RepeatInterval;
};

//cpp
#include "ABAIController.h"
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"

AABAIController::AABAIController()
{
    RepeatInterval = 3.0f;
}


void AABAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);
    GetWorld()->GetTimerManager().SetTimer(RepeatTimerHandle,this,
        &AABAIController::OnRepeatTimer,RepeatInterval,true);
}

void AABAIController::OnUnPossess()
{
    Super::OnUnPossess();
    GetWorld()->GetTimerManager().ClearTimer(RepeatTimerHandle);
}


void AABAIController::OnRepeatTimer()
{
    auto CurrentPawn = GetPawn();
    ABCHECK(nullptr != CurrentPawn);

    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (nullptr == NavSystem) return;

    FNavLocation NextLocation;

    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextLocation))
    {
        UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, NextLocation);
    }

}

행동트리 시스템

  • 행동트리는 NPC가 해야 할 행동을 분석하고 우선순위가 높은 행동부터 NPC가 실행할 수 있도록 트리 구조로 설계하는 기법
  • 블랙보드 애샛, 비헤이비어트리 에셋 을 생성
  • 위 2개를 C++ 코드를 사용할라면 'AIModule' 모듈을 추가해야한다

BP로 만든 UI -> 캐릭터에서 위젯클래스+위젯 멤버변수 생성 -> 캐릭터정보 및 상태 데이터 - 캐릭터 위젯바인딩
캐릭터 위젯 클래스, 캐릭터 클래스, 캐릭터 데이터 클래스
캐릭터 클래스에서는 위젯클래스를 만들어서 랜더까지
캐릭터 위젯 클래스은 캐릭터데이터에서 HP 데이터를 받고, HP 위젯을 갱신랜더
캐릭터데이터 클래슨는 정보 갱신
캐릭터 위젯 클래스 - 캐릭터 데이터 클래스는 밀접한 관계?

UserWidget을 상속 받아 클래스를 생성한다. 
캐릭터 컴포넌트의 델리게이트 로직을 완성하면, UI에서 캐릭터 컴포넌트에 연결해 HP가 변할 때 마다 프로그레스바를 업데이트하도록 추가
언리얼은 무기 가능한 오차를 측정할떄 KINDA_SMALL_NUMBER라는 매크로를 제공한다

.h
class KGAME_API UABCharacterStatComponent : public UActorComponent
{
public:
    void SetNewLevel(int32 NewLevel);
    void SetDamage(float NewDamage);
    float GetAttack();
    void SetHP(float NewHP);
    float GetHPRatio();

    FOnHPIsZeroDelegete OnHPIsZero;
    FOnHPChangeDelegate OnHpChanged;

  //.cpp
  void UABCharacterStatComponent::SetNewLevel(int32 NewLevel)
{
    ABCHECK(nullptr != ABGameInstance);
    CurrentStatData = ABGameInstance->GetABCharacterData(NewLevel);
    if (nullptr != CurrentStatData)
    {
        Level = NewLevel;
        SetHP(CurrentStatData->MaxHP);
    }
    else
    {
        ABLOG(Error, TEXT("Level (%d) data doesn't exist"), NewLevel);
    }
}


void UABCharacterStatComponent::SetDamage(float NewDamage)
{
    ABCHECK(nullptr != CurrentStatData);
    SetHP(FMath::Clamp<float>(CurrentHP - NewDamage, 0.0f, CurrentStatData->MaxHP));
}

void UABCharacterStatComponent::SetHP(float NewHP)
{
    CurrentHP = NewHP;
    OnHpChanged.Broadcast();

    if (CurrentHP <=KINDA_SMALL_NUMBER)
    {
        CurrentHP = 0.0f;
        OnHPIsZero.Broadcast();
    }
}
float UABCharacterStatComponent::GetHPRatio()
{
    ABCHECK(nullptr != CurrentStatData,0.0f);
    return (CurrentStatData->MaxHP) < KINDA_SMALL_NUMBER ? 0.0f : (CurrentHP / CurrentStatData->MaxHP);
}

언리얼 오브젝트의 약포인터 선언은 TWeakObjectPtr을 사용한다.
캐릭터 위젯은 캐릭터와 생사를 같이 하기 때문에 약포인터의 사용을 필요 없지만, 학습용도로 사용
만약 UI와 캐릭터가 서로 다른 액터라면 약 포인터를 사용하는 것이 좋다

 

//.h
class KGAME_API UABCharacterWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    void BindCharacterStat(class UABCharacterStatComponent* NewCharacterStat);

private:
    TWeakObjectPtr<class UABCharacterStatCompoent> CurrentCharacterStat;
};

//cpp
#include "ABCharacterWidget.h"
#include "ABCharacterStatComponent.h"
void UABCharacterWidget::BindCharacterStat(UABCharacterStatComponent* NewCharacterStat)
{
    ABCHECK(nullptr != NewCharacterStat);
    NewCharacterStat->OnHPChanged.AddLambda([this]()->void {
        if (CurrentCharacterStat.IsValid())
        {
            //ABLOG(Error, TEXT("HPRATIO : %f"), CurrentCharacterStat->GetHpRation());
        }
    });
}

캐릭터의 PostInitializeComponents 함수에서 캐릭터 컴포넌트와 UI 위켓을 연결한다.
이제 캐릭터의 beginplay에서 캐릭터 컴포넌트와 UI를 연결한다. (4.21이후부터)

void AABCharacter::BeginPlay()
{
    Super::BeginPlay();


    auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
    if (nullptr != CharacterWidget)
    {
        CharacterWidget->BindCharacterStat(CharacterStat);
    }

}

완성후 그래프 탭에서 클래스 세팅 버튼을 누른 후 부모클래스가 ABCharacterWidget으로 변경한다.

UI 시스템이 준비되면 NativeConstruct 함수가 호출되는데, UI 생성은 플레이어 컨트롤러의 BeginPlay 전에 호출된 PostinitializeComponents 함수에서 발생한 멸령은 UI에 반영 되지 않는다. 따라서 NativeConstruct 함수에서 위젯 내용을 업데이트하는 로직을 구현하는 것이 필요하다

//h
class KGAME_API UABCharacterWidget : public UUserWidget
{
    GENERATED_BODY()

public:
    void BindCharacterStat(class UABCharacterStatComponent* NewCharacterStat);

protected:
    virtual void NativeConstruct() override;
    void UpdateHPWidget();

private:
    TWeakObjectPtr<class UABCharacterStatComponent> CurrentCharacterStat;

    UPROPERTY()
    class UProgressBar* HPProgressBar;

};


//cpp
#include "ABCharacterWidget.h"
#include "ABCharacterStatComponent.h"
#include "Components/ProgressBar.h"

void UABCharacterWidget::BindCharacterStat(UABCharacterStatComponent* NewCharacterStat)
{
    ABCHECK(nullptr != NewCharacterStat);
    CurrentCharacterStat = NewCharacterStat;
    NewCharacterStat->OnHPChanged.AddUObject(this, &UABCharacterWidget::UpdateHPWidget);

}

void UABCharacterWidget::NativeConstruct()
{
    Super::NativeConstruct();
    HPProgressBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("PB_HPBar")));
    ABCHECK(nullptr != HPProgressBar);
    UpdateHPWidget();
}


void UABCharacterWidget::UpdateHPWidget()
{
    if (CurrentCharacterStat.IsValid())
    {
        if (nullptr != HPProgressBar)
        {

            HPProgressBar->SetPercent(CurrentCharacterStat->GetHPRatio());
        }
    }

}

+ Recent posts