Unreal Engine/GCC Class (5.5.4)

Unreal Engine 5 배워보기 - 35일차

보별 2025. 5. 13. 23:12
반응형

컴퓨터 및 게임 관련 전공인이 아닌 문외한 일반인이 언리얼 엔진을 배우면서 정리한 내용입니다.

그날그날 배웠던 내용을 제가 나중에 보기 위해 정리한 것으로, 지금 당장 보기에 부족한 점이 아주아주 많고 추후에 수정이 될 수도 있습니다.

오탈자 및 잘못 기재된 내용 지적, 부족한 내용 설명은 언제든지 환영입니다.

 

본글은 언리얼 엔진 5.5.4 버전 영문판을 기준으로 합니다.

 

----------------------------------------------------------------------------------------------------------------

 

35일차 학습 내용

- 적 전투 상태 부여 및 전환

- 삼항연산자

- 적의 공격 확률 계산

 

 

적 전투 상태 부여 및 전환

BT에서 Selector 노드는 조건에 따라 자식 노드를 실행하는 노드이다.

이번에는 Selector 노드를 활용하면 전투 상태와 비전투 상태를 나눠보도록 하겠다.

Selector 노드에서 Service를 활용하면 매 틱마다 실행하여 AI 행동에 필요한 판단 정보를 업데이트해줄 수있다.

전투 상태와 비전투 상태를 나눠주는 Service를 C++ 코드로 작성해보도록 하겠다.

우선은 "BTService" 를 검색하여 "BTSevice_BlackboardBase" 를 부모로 하는 C++ 클래스를 생성해준다.

Task와 마찬가지로 뒤에 'BlackboardBase' 가 들어가있으면 BB의 키 값이 들어가 있다.

"BTService" C++ 클래스는 플레이어와 적 간의 거리를 계산하여 거리에 따라 전투 상태와 비전투 상태를 나눠주는 역할을 해주도록 할 것이기에, "BTService_PlayerDirection" 으로 명명해준다.

그리고 BB에서 전투 상태와 비전투 상태를 구분해줄 수 있도록 bool 변수를 "CombatState" 라는 이름으로 새로 생성해주었다.

아래와 같이 코드를 입력해준다.

BTService_PlayerDirection.h

1
2
3
4
5
6
7
8
9
10
11
UCLASS()
class MYPROJECT_API UBTService_PlayerDetection : public UBTService_BlackboardBase
{
    GENERATED_BODY()
 
public:
    UBTService_PlayerDetection();
 
protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
cs

BTService_PlayerDirection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "BTService_PlayerDetection.h"
#include "NormalZombie.h" // 추가
#include "Kismet/GameplayStatics.h" // 추가
#include "GameFramework/CharacterMovementComponent.h" // 추가
#include "ZombieAIController.h" // 추가
 
UBTService_PlayerDetection::UBTService_PlayerDetection()
{
    NodeName = TEXT("Player Detection Service");
    Interval = 0.5f;  // 검사 간
}
 
void UBTService_PlayerDetection::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); // 0번째 pawn(플레이어)의 위치를 가져옴
    AZombieAIController* AIController = Cast<AZombieAIController>(OwnerComp.GetAIOwner());
    ANormalZombie* Zombie = Cast<ANormalZombie>(AIController ? AIController->GetPawn() : nullptr);
 
    if (!PlayerPawn || !AIController) return;
 
    const float DistancetoPlayer = FVector::Dist(Zombie->GetActorLocation(), PlayerPawn->GetActorLocation());
    UBlackboardComponent* BB = AIController->GetBlackboardComponent();
    
    if (DistancetoPlayer <= 1200)
    {
        if (AIController)
        {
            if (BB)
            {
                BB->SetValueAsBool(TEXT("CombatState"), true);
                BB->SetValueAsVector(TEXT("PlayerActor"), PlayerPawn->GetActorLocation());
            }
        }
    }
    else
    {
        if (AIController)
        {
            if (BB)
            {
                BB->SetValueAsBool(TEXT("CombatState"), false);
            }
        }
    }
}
cs

적과 플레이어 간의 거리가 1200보다 작거나 같으면 적이 공격 상태로 되며 플레이를 추적하도록 하였고, 1200보다 클 경우는 비전투 상태가 되도록 설정해주었다.

매 틱마다 "BTService_PlayerDirection" 가 실행되기에, BB에 업데이트 되는 플레이어의 위치를 지속적으로 업데이트 해줄 수 있다.

ZombieAIController.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "NormalZombie.h" // 추가
 
.
.
.
 
void AZombieAIController::BeginPlay()
{
    Super::BeginPlay();
 
    if (UseBlackboard(BlackboardData, BlackboardComponent))
    {
        BlackboardComponent = GetBlackboardComponent();
        RunBehaviorTree(BehaviorTree);
    }
 
    UBlackboardComponent* BB = GetBlackboardComponent(); // 추가
    if (BB)
    {
        ANormalZombie* Zombie = Cast<ANormalZombie>(GetPawn());
        BB->SetValueAsVector(TEXT("DefaultVector"), Zombie->GetActorLocation()); // 게임 시작할 때 좀비의 위치를 
    }
}
cs

적의 초기 위치의 경우에는 매 틱마다 갱신되면 안되기에 "BTService_PlayerDirection" 에 삽입해주지 않았고, 관리하기 편하도록 "ZombieAIController" 에 넣어주면서 게임이 처음 실행될 때 생성된 그 위치를 기억해야하기에 BeginPlay에 넣어주었다.

*"ZombieAIController" 가 아닌 "NormalZombie" 에 넣어도 상관은 없음; 본인이 관리하기 편하도록 해주기만 하면 됨

NormalZombie.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void ANormalZombie::BeginPlay()
{
    Super::BeginPlay();
 
    AZombieAIController* AIController = Cast<AZombieAIController>(GetController());
    //if (AIController)
    //{
    //    UBlackboardComponent* BB = AIController->GetBlackboardComponent(); AIContorlle로 옮김
    //    if (BB)
    //    {
    //        FVector WorldMoveVector = GetActorTransform().TransformPosition(ZombieMoveVector); 
    //        //FVector LocalMoveVector = GetActorTransform().TransformPosition(ZombieMoveVector);
    //        //FVector WorldMoveVector = ZombieMoveVector;
    //        APawn* playerActor = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
    //        BB->SetValueAsVector(TEXT("PlayerActor"), playerActor->GetActorLocation());
    //        BB->SetValueAsVector(TEXT("DefaultVector"), GetActorLocation());
    //        BB->SetValueAsVector(TEXT("RandomVector"), LocalMoveVector);
    //    }
    //}
}
cs

위에서 플레이어의 위치는 "BTService_PlayerDirection" 에서 설정해주었고, 적의 초기 위치를 "ZombieAIController" 에서 설정해주었기에, 기존에 "NormalZombie" 에 존재하던 위치값들을 주석처리해주었다.

코드를 작성하고 컴파일을 한 뒤, 추가로 해줘야할 것이 있기에 BT로 다시 들어간다.

Seqeunce를 우클릭하여 "Add Decorator" 를 누른 뒤 "Blackboard" 를 추가해준다.

그러면 위 그림과 같이 Seqeunce 노드 위에 파란색의 메뉴가 생기는데, 좌우 모두 Seqeunce 노드의 "Blackboard Key" 를 'Combat State' 로 설정해준다.

그리고 좌측의 Seqeunce 노드는 전투 상태를 실행시키고 싶기에 "Key Query" 를 'Is Set' 으로 설정해주고, 우측은 'Is Not Set' 으로 설정하여 비전투 상태에 실행되도록 해준다.

이번엔 Selector 노드를 우클릭하고 "Add Service" 를 누른 뒤 'Player Direction' 을 검색하여 우리가 위에서 생성해준 "BTService_PlayerDirection" 을 넣어준다.

마지막으로 위 그림과 같이 Sequence 밑에 자식 노드들을 연결해준다.

적과 플레이어 간의 거리가 지정한 거리보다 작거나 같으면 적이 공격 상태로 되며 플레이를 추적한 뒤에 잠시 대기하고, 지정한 거리보다 클 경우는 적이 비전투 상태로 되며 생성된 위치로 이동한 뒤 잠시 대기하도록 하였다.

게임을 실행해보면 위 그림과 같이 플레이어가 설정한 거리 내에 있으면 추적하고, 플레이어가 설정한 거리보다 멀어지면 다시 제자리로 돌아가는 것을 확인할 수 있다.

위 그림을 보면 적이 플레이어를 추적했을 때 너무 가깝게 붙어있는 것을 확인할 수 있다.

적이 추적을 완료했을 때 적과 플레이어의 거리를 좀 더 자연스럽게 거리를 두도록 설정을 추가해주도록 하겠다.

적이 플레이어를 추적하고 난 뒤의 적과 플레이어 간 거리를 좀 띄워주고 싶다면, BT로 가서 "Acceptable Radius" 를 위 그림과 같이 적당히 크게 설정해주면 된다.

그러면 적이 플레이어를 추적했을 때 위 그림과 같이 적당히 거리가 떨어진 채로 있는 것을 확인할 수 있다.

이번엔 전투 상태일 때의 Sequence에 위 그림과 같이 34일 차에서 생성한 Attack Task를 연결해준다.

그러면 적이 플레이어와의 거리가 일정 거리 이하가 되면, 적이 전투 상태로 전환하며 플레이어를 추적하고 플레이어를 공격한 뒤 잠시 대기하는 과정을 반복하게 된다.

게임을 실행해보면 위 그림과 같이 정상적으로 작동하는 것을 확인할 수 있다.

 

삼항연산자

"BTService_PlayerDirection.cpp" 에서 입력한 코드 중 오늘 사용한 삼항연산자에 대해 잠시 살펴보도록 하겠다.

ANormalZombie* Zombie = Cast<ANormalZombie>(AIController ? AIController->GetPawn() : nullptr);

이 부분에서 사용된 형식이 삼항연산자이다.

삼항연산자는 "조건 ? 참 : 거짓" 이런 식의 구조를 나타내고 있으며, 조건이 참일 경우 참에 입력한 것이 실행되고, 조건이 거짓일 경우 거짓에 입력한 것이 실행된다.

조건문과 비슷한 원리이다.

위의 코드는 AIController가 있을 경우 AIController를 Zombie에 대입하고, AIController가 없을 경우 Zombie에 아무것도 대입하지 않는 것이다.

한 가지 간단한 예를 들어보도록 하겠다.

int num1;

int num2 = num1 > 10 ? num1 : 0;

위의 코드는 정수형의 num1이 10보다 클 경우 정수형의 num2에 num1의 값을 대입하고, 아닐 경우 '0' 의 값을 대입하는 것이다.

 

적의 공격 확률 계산

코드에서 행동에 확률을 부여할 때는 우리가 흔히 생각하는 %로 계산하고 생각하는 것이 아닌, 확률의 기본인 전체 경우의 수에서 시행되기를 원하는 경우의 수로 생각해야한다.

우리가 34일 차에도 했었지만 애니메이션이나 소리를 넣어줄 때 배열로 만들어서 추가해주었고, 삽입된 애니메이션이나 소리의 배열 중에서 임의로 선택해 실행해주었다.

개발에서 사용하는 확률은 위와 같이 배열로 생각하는 것이 좋다.

예를 하나 들어보도록 하겠다.

적이 행동을 할 배열의 수를 아래와 같이 정해주었다.

공격 상태를 실행하는 배열 3 / 방어 상태를 실행하는 배열  4 / 대기 상태를 실행하는 배열 3

위와 같이 설정해줄 경우, 배열의 index 값의 0~9 중 하나를 선택하여 실행하는 것이다.

체력이 낮아졌을 때 호전성을 높여서 패턴의 변화를 주고 싶을 경우, 체력이 낮아졌을 경우 행동할 배열의 수를 따로 설정해주고 아래와 같이 공격 상태를 실행하는 배열을 늘리면 된다.

공격 상태를 실행하는 배열 6 / 방어 상태를 실행하는 배열  2 / 대기 상태를 실행하는 배열 2 

이렇게 설정해줄 경우 체력이 많을 때는 30%의 확률로 적이 공격을 하게 되고, 체력이 적을 경우에는 60%의 확률로 적이 공격하게 되니 호전성이 증가하게 되는 것이다.

이런 식으로 배열을 통해 행동의 수를 정해줌으로서 적의 패턴에 다양한 변화를 줄 수 있다.

 

 

*오늘까지 작성한 코드 정리

NormalZombie.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// Fill out your copyright notice in the Description page of Project Settings.
 
 
#include "NormalZombie.h"
#include "ZombieAnimInstance.h"
#include "AIController.h"
#include "ZombieAIController.h"
#include "Kismet/GameplayStatics.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
 
ANormalZombie::ANormalZombie()
{
    InitAI();
}
 
void ANormalZombie::BeginPlay()
{
    Super::BeginPlay();
 
    AZombieAIController* AIController = Cast<AZombieAIController>(GetController());
}
 
void ANormalZombie::InitAI()
{
    AIControllerClass = AZombieAIController::StaticClass();
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
 
void ANormalZombie::PlayAttackSound()
{
    if (zombieType == EZombieType::HumanZombie && HumanZombieAttackSounds.Num() > 0// ZombieAttackSound가 있을 경우 실행
    {
        int randomInt = FMath::RandRange(0, HumanZombieAttackSounds.Num() - 1);
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), HumanZombieAttackSounds[randomInt], GetActorLocation());
    }
    else if (zombieType == EZombieType::DogZombie && DogZombieAttackSounds.Num() > 0)
    {
        int randomInt = FMath::RandRange(0, DogZombieAttackSounds.Num() - 1);
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), DogZombieAttackSounds[randomInt], GetActorLocation());        
    }
 
    else if (zombieType == EZombieType::WingZombie && WingZombieAttackSounds.Num() > 0)
    {
        int randomInt = FMath::RandRange(0, WingZombieAttackSounds.Num() - 1);
        UGameplayStatics::PlaySoundAtLocation(GetWorld(), WingZombieAttackSounds[randomInt], GetActorLocation());
    }    
}
 
void ANormalZombie::PlayAttackMontage()
{
    UAnimInstance* animInstance = GetMesh()->GetAnimInstance();
 
    if (zombieType == EZombieType::HumanZombie && HumanZombieAttackMontages.Num() > 0)
    {
        int randomInt = FMath::RandRange(0, HumanZombieAttackMontages.Num() - 1);
        if (animInstance)
        {
            animInstance->Montage_Play(HumanZombieAttackMontages[randomInt]);
        }
    }
    else if (zombieType == EZombieType::DogZombie && DogZombieAttackMontages.Num() > 0)
    {
        int randomInt = FMath::RandRange(0, DogZombieAttackMontages.Num() - 1);
        if (animInstance)
        {
            animInstance->Montage_Play(DogZombieAttackMontages[randomInt]);
        }
    }
 
    else if (zombieType == EZombieType::WingZombie && WingZombieAttackMontages.Num() > 0)
    {
        int randomInt = FMath::RandRange(0, WingZombieAttackMontages.Num() - 1);
        if (animInstance)
        {
            animInstance->Montage_Play(WingZombieAttackMontages[randomInt]);
        }
    }
}
 
void ANormalZombie::SetCurrentTask(UBTTaskNode* Task)
{
    CurrentTask = Task;
}
 
UBTTaskNode* ANormalZombie::GetCurrentTask() const
{
    return CurrentTask;
}
 
cs

BTService_PlayerDirection.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Fill out your copyright notice in the Description page of Project Settings.
 
#pragma once
 
#include "CoreMinimal.h"
#include "BehaviorTree/Services/BTService_BlackboardBase.h"
#include "BTService_PlayerDetection.generated.h"
 
/**
 * 
 */
UCLASS()
class MYPROJECT_API UBTService_PlayerDetection : public UBTService_BlackboardBase
{
    GENERATED_BODY()
 
public:
    UBTService_PlayerDetection();
 
protected:
    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
 
cs

BTService_PlayerDirection.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Fill out your copyright notice in the Description page of Project Settings.
 
 
#include "BTService_PlayerDetection.h"
#include "NormalZombie.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ZombieAIController.h"
 
UBTService_PlayerDetection::UBTService_PlayerDetection()
{
    NodeName = TEXT("Player Detection Service");
    Interval = 0.5f;
}
 
void UBTService_PlayerDetection::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); // 0번째 pawn(플레이어)의 위치를 가져옴
    AZombieAIController* AIController = Cast<AZombieAIController>(OwnerComp.GetAIOwner());
    ANormalZombie* Zombie = Cast<ANormalZombie>(AIController ? AIController->GetPawn() : nullptr);
 
    if (!PlayerPawn || !AIController) return;
 
    const float DistancetoPlayer = FVector::Dist(Zombie->GetActorLocation(), PlayerPawn->GetActorLocation());
    UBlackboardComponent* BB = AIController->GetBlackboardComponent();
    
    if (DistancetoPlayer <= 1200)
    {
        if (AIController)
        {
            if (BB)
            {
                BB->SetValueAsBool(TEXT("CombatState"), true);
                BB->SetValueAsVector(TEXT("PlayerActor"), PlayerPawn->GetActorLocation());
            }
        }
    }
    else
    {
        if (AIController)
        {
            if (BB)
            {
                BB->SetValueAsBool(TEXT("CombatState"), false);
            }
        }
    }
}
cs

ZombieAIController.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Fill out your copyright notice in the Description page of Project Settings.
 
 
#include "ZombieAIController.h"
#include "Kismet/GameplayStatics.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NormalZombie.h" // 추가
 
AZombieAIController::AZombieAIController()
{
    BlackboardComponent = CreateDefaultSubobject<UBlackboardComponent>(TEXT("BlackboardComponent"));
    BehaviorTreeComponent = CreateDefaultSubobject<UBehaviorTreeComponent>(TEXT("BehaviorTreeComponent"));
 
    static ConstructorHelpers::FObjectFinder<UBlackboardData> BB(TEXT("/Script/AIModule.BlackboardData'/Game/AI/BB_Zombie.BB_Zombie'"));
    static ConstructorHelpers::FObjectFinder<UBehaviorTree> BT(TEXT("/Script/AIModule.BehaviorTree'/Game/AI/BT_Zombie.BT_Zombie'"));
    
    if (BB.Succeeded())
    {
        BlackboardData = BB.Object;
    }
 
    if (BT.Succeeded())
    {
        BehaviorTree = BT.Object;
    }
}
 
void AZombieAIController::BeginPlay()
{
    Super::BeginPlay();
 
    if (UseBlackboard(BlackboardData, BlackboardComponent))
    {
        BlackboardComponent = GetBlackboardComponent();
        RunBehaviorTree(BehaviorTree);
    }
 
    UBlackboardComponent* BB = GetBlackboardComponent();
    if (BB)
    {
        ANormalZombie* Zombie = Cast<ANormalZombie>(GetPawn());
        BB->SetValueAsVector(TEXT("DefaultVector"), Zombie->GetActorLocation());
    }
}
cs