남은 총알의 개수를 UI로 띄워보자.

 

테스트로 만들어볼 것이기 때문에 비율 같은것은 따로 계산하지 않고 Text를 하나 적당히 배치한다.

 

// MyHUD.h
class FIRSTPLAYER_API UMyHUD : public UUserWidget
{
public:
    UPROPERTY(meta = (BindWidget))
    class UTextBlock* AmmoText;
};

 

그리고 UserWidget을 상속받는 C++ 클래스를 하나 만들어주고 블루프린트 위젯의 텍스트와 직접 바인딩 시키기 위해 속성도 지정한다. 블루프린트 위젯의 부모 클래스도 MyHUD로 바꿔준다.

 

 

// FirstPlayerGameMode.h
public:
    UPROPERTY()
    TSubclassOf<UUserWidget> HUD_Class; // UI 리소스

    UPROPERTY()
    UUserWidget* CurrentWidget; // 현재 띄우고 있는 UI
    
// FirstPlayerGameMode.cpp
AFirstPlayerGameMode::AFirstPlayerGameMode() : Super()
{
    static ConstructorHelpers::FClassFinder<UMyHUD> UI_HUD(TEXT("WidgetBlueprint'/Game/WBP_HUD.WBP_HUD_C'"));
    if (UI_HUD.Succeeded())
    {
        HUD_Class = UI_HUD.Class;

        CurrentWidget = CreateWidget(GetWorld(), HUD_Class);
        if (CurrentWidget)
        {
            CurrentWidget->AddToViewport(); // Z-order를 정하고 뷰포트에 추가함
            // CurrentWidget->RemoveFromViewport(); // 뷰포트에서 제거
        }
    }
}

 

블루프린트로 만든 WBP_HUD를 게임모드 C++ 클래스에서 생성해서 멤버 변수(HUD_Class)로써 가지고 있고 현재 월드에 멤버 변수에 저장한 WBP_HUD를 생성한다.

CurrentWidget은 현재 띄워진 UI를 가지게 되는데 실습에서는 하나의 UI만 사용하므로 별다른 차이는 없다.

 

다만 생성만 하고 뷰포트에 띄운것이 아니므로 AddToViewport 함수를 통해 실제로 출력한다.

반대로 RemoveFromViewport 함수를 통해 뷰포트에서 제거할 수도 있다.

 

 

현재까지 WBP_HUD에서 만들어둔 텍스트를 MyHUD 클래스에서 바인딩 하고, 게임모드 클래스의 생성자에서 UI 리소스(WBP_HUD)를 로드하고 뷰포트에 출력하는 것 까지 구현했다.

 

이제 실제로 총알 개수가 변해야 한다.

어떻게 구현할지 간단하게 생각해보면 해당 UI에서 멤버 변수를 만들고 데이터를 관리해서 직접 UI에 반영시키는 방법이 있을 것이다.

하지만 UI와 실제 데이터와 뒤섞으면 관리하기가 굉장히 어려워진다. 구조가 복잡해지면 해당 데이터가 UI 용도였는지 인게임 로직 용도였는지 알기가 어려워진다.

그렇기 때문에 UI에서 실제 데이터를 관리하지 않도록 주의해야한다.

 

지금은 총알 개수 하나만 테스트를 해보는 것이기 때문에 따로 데이터를 관리하지 않고 캐릭터 클래스에서 총알 관리를 해 볼 것이다.

 

// FirstPlayerCharacter.h
public:
    void RefreshUI();
    
public:
    int32 AmmoCount = 5;
    int32 MaxAmmoCount = 5;
    
// FirstPlayerCharacter.cpp
void AFirstPlayerCharacter::RefreshUI()
{
	// 월드의 게임모드를 가져와서 캐스팅 한다
    AFirstPlayerGameMode* GameMode = Cast<AFirstPlayerGameMode>(UGameplayStatics::GetGameMode(GetWorld()));
    if (GameMode)
    {
        UMyHUD* MyHUD = Cast<UMyHUD>(GameMode->CurrentWidget);
        if (MyHUD)
        {
            const FString AmmoStr = FString::Printf(TEXT("Ammo %01d/%01d"), AmmoCount, MaxAmmoCount);
            MyHUD->AmmoText->SetText(FText::FromString(AmmoStr));
        }
    }
}

void AFirstPlayerCharacter::OnFire()
{
    if (AmmoCount <= 0)
        return;

    AmmoCount -= 1;

    RefreshUI();
    
    // ..생략
}

 

BeginPlay에서도 UIRefresh를 추가해주면 시작 즉시 5/5로 초기화 된다.

위에서 설명했듯이 실제 캐릭터의 데이터는 다른 클래스나 구조체에서 관리하는 것이 좋을 것이다.

 

 

총알 하나만 관리하는데도 생각할 것들이 생긴다. 구조가 더 복잡해질때를 고려해야한다.

게임 컨텐츠와 UI가 맞물리다 보면 어디서 데이터를 관리해야 하는지가 생각보다 복잡해 질 수 있다.

따로 UI매니저를 만들어서 공통으로 접근할 수 있는것을 만들어서 관리 한다던가 하는 방법을 사용할 수 있다.

C++에서 사용하던 표준 컨테이너들(vector, map 등등)은 언리얼 프로젝트에서는 사용하지 않는 것이 좋다.

대신 언리얼에서 제공하는 컨테이너들을 사용하면 된다.

 

https://docs.unrealengine.com/4.26/ko/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/TArrays/

위의 문서에서 컨테이너들에 대한 정보들을 볼 수 있다.

 

vector ≒ TArray

unordered_map TMap

unordered_set TSet

string ≒ FString

 

TArray에서 주의해야 할 점은 vector의 clear와 TArray의 Empty가 동일하다.

vector의 empty를 기대하고 Empty를 사용했다가는 컨테이너가 다 비워져버린다.

 

STL 컨테이너의 개념과 크게 다르지는 않지만 활용 방법이 약간씩 다르므로 문서를 참고해서 사용하면 될 것이다.

 

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 핵심 5개 클래스 -完-  (0) 2022.09.12
[UE4 입문] UMG 실습  (0) 2022.09.12
[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12

언리얼에서 기본적으로 제공해주는 샘플 코드를 분석하는 것만으로도 기본적인 작동 방식이나 구현 방법을 알 수 있어서 상당히 도움이 된다.

 

보통 다수의 인원이 작업하는 프로젝트들은 svn이나 git을 이용해 버전 관리를 해서 공동작업을 어렵지 않게 하는데, 블루프린트로 작업을 하게되면 공동작업에 있어서 충돌이 많이 일어나기 때문에 버전 관리가 어렵다.

 

UI와 관련된 위젯을 제외하고는 웬만해서 C++로 작업하는게 더 나을 수 있다.

UI는 보통 UI/UX팀에서 만들어서 보내주는 경우가 많다.

 

C++로 오랫동안 작업하고 익숙한 사람들은 블루프린트를 모를 수도 있다.

블루프린트 노드들을 보고 흐름을 읽을 줄만 알면 된다.

 

UI의 공통적인 부분(ex:스킬. 스킬은 제각기 다르지만 사용시 쿨타임이 돈다거나 하는 부분은 공통적)은 위젯 블루프린트로 만들어서 기능을 재사용 하면 좋다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] UMG 실습  (0) 2022.09.12
[UE4 입문] 언리얼 컨테이너  (0) 2022.09.12
[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11

AI 주변에 플레이어가 없으면 주변을 탐색하는 행동은 RPG에서는 매우 흔한일이다.

하지만 현재 구현한 것 만으로는 오로지 탐색만 하기 때문에 플레이어가 접근 시 다른 행동을 하게끔 해야한다.

그것을 할 수 있도록 하는것이 셀렉터이다.

타겟을 찾았으면 왼쪽으로 분기하고 못찾았으면 오른쪽으로 분기하게끔 하면 된다.

 

그렇지만 매 틱마다 계속 서치를 하는것은 부담이 되기 때문에 주기적으로(1~2초) 한번씩 탐색하는 것이 일반적이다.

물론 언리얼은 그것에 대한 것도 준비가 되어있다. 서비스라는 개념이다.

 

우선 서비스부터 만들어 보도록 한다.

 

 

// BTService_SearchTarget.h
public:
    UBTService_SearchTarget();

    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    
// BTService_SearchTarget.cpp
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_SearchTarget::UBTService_SearchTarget()
{
    NodeName = TEXT("SearchTarget");
    Interval = 1.0f; // Tick의 주기 설정
}

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

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

    UWorld* World = CurrentPawn->GetWorld();
    FVector Center = CurrentPawn->GetActorLocation();
    float SearchRadius = 500.f;

    if (World == nullptr)
        return;

    TArray<FOverlapResult> OverlapResults;
    // 3번째 인자는 무시할 액터. 즉, 자기 자신은 무시하겠다는 뜻이다
    FCollisionQueryParams QueryParams(NAME_None, false, CurrentPawn);

    // 월드에 충돌체를 생성해서 충돌된 오브젝트들을 첫번째 인자에 넣어준다
    bool bResult = World->OverlapMultiByChannel(
        OverlapResults,
        Center,
        FQuat::Identity,
        ECollisionChannel::ECC_GameTraceChannel2,
        FCollisionShape::MakeSphere(SearchRadius),
        QueryParams);

    if (bResult)
    {
        for (auto& OverlapResult : OverlapResults)
        {
            // MyCharacter로 캐스팅해서 유효한 경우만 블랙보드에 값을 넘겨준다
            AMyCharacter* MyCharacter = Cast<AMyCharacter>(OverlapResult.GetActor());
            if (MyCharacter && MyCharacter->GetController()->IsPlayerController())
            {
            OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), MyCharacter);

            DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Green, false, 0.2f);

            return;
            }
        }
        DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
    }
    else
    {
        // 서치 실패시 nullptr을 넘겨준다
        OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), nullptr);
        DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
    }
}

 

생성자에서 Interval값을 조절하여 TickNode의 호출 간격을 조절할 수 있다.

 

이제 블랙보드에서 키를 추가해주고 셀렉터에 서비스를 추가해주자.

 

 

 

범위 안에 없을 때

 

범위 안에 있을 때

매 틱마다 실행되는게 아닌 1초마다 실행되고 충돌(서치) 여부를 DrawDebugSphere를 통해 시각적으로 알 수 있다.

 

아직 갈길이 멀다. 범위 안에서 탐지시 타겟을 추적하는 기능이 필요하다.

 

데코레이터를 추가해서 컴포짓 노드의 분기 조건을 만들어준다.

왼쪽은 범위 안에 들어와서 타겟이 있는경우, 오른쪽은 타겟이 없는 경우에 실행된다.

처음에는 범위 밖에 있으므로 오른쪽으로 분기하여 5초 대기-랜덤한 목적지 이동을 실행하지만 이동이 끝난시점에 범위 안에 있다면 왼쪽으로 분기하여 타겟을 추적한다.

 

데코레이터는 if-else문과 유사하다고 생각하면 된다.

 

추적까지 성공했다면 이제 일정 범위 안에 접근시 공격하는것까지 추가해보자.

'공격이 가능한가?' 를 따져야하는 일종의 조건식이므로 데코레이터를 만들어준다.

 

// BTDecorator_CanAttack.h
public:
    UBTDecorator_CanAttack();

    virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemoty) const override;
    
// BTDecorator_CanAttack.cpp
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

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

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

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

    auto Target = Cast<AMyCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(FName(TEXT("Target"))));

    if (Target == nullptr)
        return false;

    // 타겟과의 거리가 200 미만이면 공격 실행을 위해 true반환
    return bResult && Target->GetDistanceTo(CurrentPawn) <= 200.f;
}

 

공격이 불가능한 경우에는 추적을 해야하므로 오른쪽 분기는 inversed(반대조건)을 체크해준다.

이제 조건과 분기는 만들었으므로 태스크를 만들어서 실제로 실행해 볼 때이다.

 

 

// BTTask_Attack.h
public:
    UBTTask_Attack();

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

private:
    bool bIsAttacking = false;

// BTTask_Attack.cpp
#include "MyAIController.h"
#include "MyCharacter.h"

UBTTask_Attack::UBTTask_Attack()
{
    NodeName = TEXT("Attack");
    
    // 기본은 false이고 true 설정시 TickTask가 실행된다
    bNotifyTick = true;
    bIsAttacking = false;
}

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

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

    MyCharacter->Attack();
    bIsAttacking = true;

    return Result;
}

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

    // 공격이 끝났으면 작업의 끝을 알린다
    if (bIsAttacking == false)
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

 

타겟을 탐지하고(서비스:SearchTarget) -> 200만큼 범위안에 들어왔는지 체크하고(데코레이터:CanAttack) -> 공격

이 순서로 작동하게 될 것이다.

다만 현재는 태스크 실행시 Attack 함수를 호출하여 애니메이션 몽타주를 재생하고  bIsAttacking을 true로 변경하는 것은 있지만 다시 false로 되돌리는 부분이 없다.

 

그럼 공격이 끝난 시점은 어떻게 구분하면 되는가?

일전에 MyCharacter에서도 공격 시작시 애니메이션 몽타주가 재생되면서 IsAttacking이 true로 설정되었다가 몽타주 재생이 끝나면 OnAttackMontageEnded 함수에 의해 false로 되돌리는 부분이 있었다. 그것을 OnMontageEnded 델리게이트에 등록함으로써 몽타주 종료시 호출되게끔 했었다.

 

그것과 마찬가지로 델리게이트에 등록해서 몽타주 재생이 끝날 시 호출하면 될 것이다. 다만 직접 호출은 아니고 공격 모션이 끝나면 bIsAttacking을 false로 변경하는 함수를 구독시키고 Broadcast로 쏴줄 것이다.

 

 

// MyCharacter.h
DECLARE_MULTICAST_DELEGATE(FOnAttackEnd);

public:
    FOnAttackEnd OnAttackEnd;
    
// MyCharacter.cpp
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
    IsAttacking = false;
    // 구독자가 누군지 몰라도 등록된 콜백을 호출하라고 신호를 보냄
    OnAttackEnd.Broadcast();
}

 

// BTTask_Attack.cpp
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    MyCharacter->OnAttackEnd.AddLambda([this]()
        {
            bIsAttacking = false;
        });
}

 

이제 공격 시작시 true로 변경하고 몽타주 재생이 끝나면 콜백이 호출되어 false로 변경되고 TickTask에 의해 계속 검사되고 있었으므로 작업의 끝을 알리게 된다.

 

마지막으로 만들어준 Attack 노드를 붙여주면 범위 내에 감지 후 일정거리내에 도달시 공격 모션을 실행하게 된다.

 

 

[정리]

 

  • 태스크 : 보라색 노드. AI의 행동이나 블랙보드의 값 조정같은 작업을 한다. 반드시 컴포짓 노드를 거쳐 실행되어야 한다.
  • 데코레이터 : 파란색 노드. 특정 노드의 실행 여부를 결정짓는 노드이다.
  • 서비스 : 초록색 노드. 노드가 실행되는 동안 같이 실행된다.
  • 비헤이비어 트리가 복잡해질수록 이점은 있지만 초기에 세팅하는게 어렵다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 언리얼 컨테이너  (0) 2022.09.12
[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03

블랙보드와 비헤이비어 트리 둘다 필요하므로 만들어준다.

 

블랙보드는 애님 인스턴스를 먼저 만들고 애니메이션 재생과 관련된 데이터를 몰아넣고 매 프레임마다 해당 데이터들을 이용해서 애니메이션을 재생하는데 사용하던 개념과 비슷하다.

 

저번까지는 빙의 시 타이머를 등록하여 랜덤한 이동을 테스트 해봤는데 비헤이비어 트리를 이용하면 이런 부분이 필요가 없어진다.

 

// MyAIController.h
private:
    UPROPERTY()
    class UBehaviorTree* BehaviorTree;

    UPROPERTY()
    class UBlackboardData* BlackboardData;
    
// MyAIController.cpp
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"

AMyAIController::AMyAIController()
{
    static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("BehaviorTree'/Game/AI/BT_MyCharacter.BT_MyCharacter'"));
    if (BT.Succeeded())
    {
        BehaviorTree = BT.Object;
    }

    static ConstructorHelpers::FObjectFinder<UBlackboardData> BD(TEXT("BlackboardData'/Game/AI/BB_MyCharacter.BB_MyCharacter'"));
    if (BD.Succeeded())
    {
        BlackboardData = BD.Object;
    }
}

void AMyAIController::OnPossess(APawn* InPawn)
{
    // 빙의 시
    Super::OnPossess(InPawn);

    // 블랙보드 사용
    if (UseBlackboard(BlackboardData, Blackboard))
    {
        // 비헤이비어 트리를 실행
        if (RunBehaviorTree(BehaviorTree))
        {
            // Log
        }
    }
}

 

아직까지는 비헤이비어 트리를 작성한게 딱히 없기때문에 아무런 행동도 하지 않지만 실제로는 적용되고 있다.

 

 

Task는 저것들이 전부가 아니라 기본적으로 제공되는 것들이고 당연히 커스텀으로 만들어서 사용할 수도 있다.

최상단의 Find Patrol Pos는 커스텀으로 만든 것이다.

 

BTTaskNode를 상속받은 클래스를 만들어서 필요한 기능을 안에 구현하면 된다.

 

// BTTask_FindPatrolPos.h
UCLASS()
class TESTUNREALENGINE_API UBTTask_FindPatrolPos : public UBTTaskNode
{
    GENERATED_BODY()

public:
    UBTTask_FindPatrolPos();

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

// BTTask_FindPatrolPos.cpp
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
    // 해당 이름으로 노드가 표시된다
    NodeName = TEXT("FindPatrolPos");
}

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

    auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();

    if (CurrentPawn == nullptr)
        return EBTNodeResult::Failed;

    // 월드의 내비게이션 시스템을 가져온다
    UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());

    if (NavSystem == nullptr)
        return EBTNodeResult::Failed;

    FNavLocation RandomLocation;

    // 월드의 내비게이션 범위중 원점(첫번째 매개변수)을 기준으로 반지름 만큼의 범위를 한정지어
    // 랜덤한 좌표를 가져온다(RandomLocation에 저장됨)
    if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, OUT RandomLocation))
    {
        // 해당 액터를 RandomLocation으로 이동
        OwnerComp.GetBlackboardComponent()->SetValueAsVector(FName(TEXT("PatrolPos")), RandomLocation.Location);
        return EBTNodeResult::Succeeded;
    }

    return EBTNodeResult::Failed;
}

 

ExecuteTask는 딱 보기에도 태스크의 실행이다.

 

3초간 랜덤한 위치로 이동하는 구현부를 이쪽으로 옮겨오면서 코드 수정이 이루어졌다.

원래 직접 이동시키는 함수가 있었으나 시퀀스를 이용할 것이기 때문에 랜덤한 좌표를 계산하는것만 가져오도록 한다.

 

비헤이비어 트리에서 시퀀스에 등록된 노드들은 각 노드별로 성공시 순차적으로 실행되지만 중간에 실패하면 이후의 노드들의 실행이 중단된다.

그렇기 때문에 매번 중요한 곳에서 체크할 때마다 Failed를 반환해 주어야 한다.

 

블랙보드에 등록된 키에 값을 넣어주려면 SetValue~~ 함수를 이용해서 블랙보드에 만들어 준 키 이름과 동일한 것을 입력하고 어떤 값을 넘겨줄지를 두번째 인자로 넘겨주면 된다.

 

블랙보드에 키를 만들어주고

 

트리를 위와 같이 구성하면 5초 대기 후 좌표를 구해서 해당 좌표로 이동하는 것을 반복하게 된다.

 

[정리]

 

  • 컴포짓 : 셀렉터, 시퀀스, 심플 페러렐 노드가 있다.
  • 셀렉터 : 자식 노드중 하나라도 성공하면 나머지 자식 노드의 실행을 중단한다. 실패하면 다음 자식 노드를 실행한다.
  • 시퀀스 : 자식 노드중 하나라도 실패하면 나머지 자식 노드의 실행을 중단한다. 성공하면 다음 자식 노드를 실행한다.
  • 심플 페러렐 : 전체 노드 트리와 동시에 하나의 태스크를 실행할 수 있다.
  • BTTaskNode : 비헤이비어 트리에 커스텀 노드를 추가할 수 있다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #2  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03
[UE4 입문] 스탯 매니저  (0) 2022.09.02

+ Recent posts