컴퓨터 및 게임 관련 전공인이 아닌 문외한 일반인이 언리얼 엔진을 배우면서 정리한 내용입니다.
그날그날 배웠던 내용을 제가 나중에 보기 위해 정리한 것으로, 지금 당장 보기에 부족한 점이 아주아주 많고 추후에 수정이 될 수도 있습니다.
오탈자 및 잘못 기재된 내용 지적, 부족한 내용 설명은 언제든지 환영입니다.
본글은 언리얼 엔진 5.5.4 버전 영문판을 기준으로 합니다.
----------------------------------------------------------------------------------------------------------------
34일차 학습 내용
- 적 공격 AI 추가
공격 Task 생성
공격 소리 및 애니메이션 추가
공격 판정 추가
적 공격 AI 추가
공격을 하는 Task를 BT에 넣어주면 공격을 수행하게 되는데, 공격을 명령하는 Task는 언리얼 엔진에서 기본적으로 제공해주지 않기에 직접 만들어서 써야한다.
공격 Task를 C++로 제작하고 BT에 삽입해보도록 하겠다.
공격 Task 생성
우선 Visual Studio에서 프로젝트의 "Build.cs" 를 열고 아래 그림과 같이 "Gameplay Tasks" 를 추가해준 뒤, 프로젝트를 닫고 프로젝트 파일을 우클릭한 뒤 "Generate" 를 해준다.

이렇게 하면 Task를 C++ 코드로 작성한 뒤 BT에 추가할 수 있게 된다.

새로운 C++ 클래스를 위 그림과 같이 "BTTask_BlackboardBase" 형식으로 생성해준다.
공격과 관련된 Task를 생성해줄 것이기에, 이름은 "BTTask_Attack" 으로 명명해줬다.
참고로 "BTTask_BBBase" 형식 같은 경우에는 BB의 키 값을 보유한 Task고, "BTTaskNode" 같은 경우는 BB의 키 값을 보유하고 있지 않은 Task다.
"BTTask_BlackboardBase" 이하에 있는 형식들은 전부 BT에서 기본적으로 생성할 수 있는 Task들이다.
그리고 새로 생성한 "BTTask_Attack" 에 아래와 같이 코드를 작성해준다.
BTTask_Attack.h
|
1
2
3
4
5
6
7
8
9
10
11
|
UCLASS()
class MYPROJECT_API UBTTask_Attack : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
UBTTask_Attack();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; // Task 함수 기본 형식
};
|
cs |
BTTask_Attack.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
|
#include "BTTask_Attack.h"
#include "BehaviorTree/BTTaskNode.h" // 추가
#include "BehaviorTree/BlackboardComponent.h" // 추가
#include "ZombieAIController.h" // 추가
#include "AIController.h" // 추가
#include "Kismet/GameplayStatics.h" // 추가
#include "NormalZombie.h" // 추가
#include "Engine/GameEngine.h" // 추가
UBTTask_Attack::UBTTask_Attack()
{
NodeName = "ZombieAttackTask";
bNotifyTick = false;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
ANormalZombie* zombie = Cast<ANormalZombie>(OwnerComp.GetAIOwner()->GetCharacter());
if (zombie == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Failed")); // 적에게 부여된 값이 없을 경우 Attack Failed로 출력
return EBTNodeResult::Failed;
}
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Succeeded")); // 적에게 부여된 값이 있을 경우 Attack Succeeded로 출력
return EBTNodeResult::Succeeded;
}
|
cs |

위와 같이 코드를 작성하고 나면 BT에서 "Attack" 을 검색했을 시, 위에서 생성한 Attack Task가 검색이 가능하게 되고, BT 상에 배치도 가능하게 된다.

위 그림과 같이 BT 상에 Task들을 연결해준다.
위 BT는 적이 공격을 하고 있다는 메세지를 출력하고 RandomVector로 지정된 곳으로 이동한 뒤, 일정 시간동안 대기 후 원래 위치로 돌아가고 다시 일정 시간을 대기하는 것을 반복하는 BT이다.

게임을 실행해보면 위 그림과 같이 "Attack Succeeded" 라는 메세지가 뜨며 정상적으로 작동하는 것을 확인할 수 있다.
공격 소리 및 애니메이션 추가
이번엔 적이 공격할 때 소리를 재생하면서 공격 애니메이션을 재생하도록 해보겠다.
적의 소리, 공격 애니메이션 등을 한 종류의 적에 하나씩만 넣는게 아닌, 여러개를 넣은 뒤에 그 중에서 무작위로 재생시키는 방식으로 할 것이다.
이 경우 적 종류 별 TMap에서 해당 적을 불러온 뒤에, 해당 적의 소리, 애니메이션을 지정된 배열 내에 있는 것들 중 무작위로 재생시키면 된다.
아래와 같이 코드를 입력해준다.
NormalZombie.h
|
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
|
UCLASS()
class MYPROJECT_API ANormalZombie : public AZombieBase
{
GENERATED_BODY()
.
.
.
public:
void InitAI();
void PlayAttackSound(); // 소리 재생 함수 추가
void PlayAttackMontage(); // 애니메이션 재생 함수 추가
.
.
.
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TMap<EZombieAttackSoundType, USoundBase*> ZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound") // 좀비 별 무작위 소리 재생을 위해 배열로 설정
TArray<USoundBase*>HumanZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TArray<USoundBase*>DogZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TArray<USoundBase*>WingZombieAttackSounds;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim") // 좀비 별 무작위 애니메이션 재생을 위해 배열로 설정
TArray<UAnimMontage*>HumanZombieAttackMontages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim")
TArray<UAnimMontage*>DogZombieAttackMontages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim")
TArray<UAnimMontage*>WingZombieAttackMontages;
};
|
cs |
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
|
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
.
.
.
void ANormalZombie::PlayAttackSound()
{
if (zombieType == EZombieType::HumanZombie && HumanZombieAttackSounds.Num() > 0) // ZombieType이 HumanZombie 면서 ZombieAttackSound가 있을 경우 실행
{
int randomInt = FMath::RandRange(0, HumanZombieAttackSounds.Num() - 1); // ZombieAttackSound에 있는 것들 중 무작위로 실행; 배열의 갯수는 '1'부터 시작이지만 index는 '0'부터 시작이기에 '-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) // ZombieType이 HumanZombie 면서 ZombieAttackMontage 있을 경우 실행
{
int randomInt = FMath::RandRange(0, HumanZombieAttackMontages.Num() - 1); // ZombieAttackMontage에 있는 것들 중 무작위로 실행
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]);
}
}
}
|
cs |
BTTask_Attack.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
ANormalZombie* zombie = Cast<ANormalZombie>(OwnerComp.GetAIOwner()->GetCharacter());
if (zombie == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Failed"));
return EBTNodeResult::Failed;
}
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Succeeded"));
// 위에서 NormalZombie를 캐스팅 했으므로 함수 호출 가능
zombie->PlayAttackSound(); // 공격 소리 재생 추가
zombie->PlayAttackMontage(); // 공격 애니메이션 재생 추가
return EBTNodeResult::Succeeded;
}
|
cs |
위 코드는 소리 및 공격 애니메이션을 적의 BP에 추가할 수 있도록 해준 것이고 재생할 것을 정해준 것은 아니기에, 아래 그림과 같이 BP에서 따로 추가해줘야한다.

지난 33일차에서 생성한 각 적 개체 별 BP에 들어가서 각 개체에 해당하는 소리를 넣어주면 된다.

혹은 위 그림과 같이 "NormalZombie" 를 부모로 하는 BP를 생성한 뒤, 위 그림과 같이 모든 개체 별로 소리 및 애니메이션을 지정해준다.
그리고 레벨에 BP_NormalZombie를 배치한 뒤, 위 그림과 같이 레벨에 배치한 각 BP_NormalZombie을 클릭하여 각각 위 사진과 같이 개체를 정해주면 된다.

그러면 위 그림과 같이 공격 메세지가 출력되면서 공격 소리 및 애니메이션을 재생하는 것을 확인할 수 있다.
공격 판정 추가
여기에 Sphere Trace 및 Anim Notify를 사용하여 공격 애니메이션이 재생되는 특정 시간 중에 공격 판정을 주도록 해보겠다.

우선은 AnimNotify를 사용하기 위해 위 그림과 같이 AnimNotify를 부모로 하는 C++ 클래스를 하나 생성해주고, 해당 AnimNotify는 적이 공격할 때 사용할 것이기에 이름은 "ZombieAttackAnimNotify" 로 명명해주었다.
그리고 아래와 같이 코드를 작성해준다.
NormalZombie.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include "BehaviorTree/BTTaskNode.h" // Task 노드에 대한 정보를 전달을 위해 추가
.
.
.
public:
void InitAI();
void PlayAttackSound();
void PlayAttackMontage();
void SetCurrentTask(UBTTaskNode* Task); // 현재 Task 상태
UBTTaskNode* GetCurrentTask() const; // 외부로 현재 실행 중인 Task를 보내는 함수
private:
UBTTaskNode* CurrentTask; // 현재 실행 중인 Task 정보를 저장
|
cs |
NormalZombie.cpp
|
1
2
3
4
5
6
7
8
9
|
void ANormalZombie::SetCurrentTask(UBTTaskNode* Task)
{
CurrentTask = Task;
}
UBTTaskNode* ANormalZombie::GetCurrentTask() const
{
return CurrentTask;
}
|
cs |
BTTask_Attack.h
|
1
2
3
4
|
public:
UBTTask_Attack();
void AttackTrace(class ANormalZombie* NormalZombie); // 추가
|
cs |
BTTask_Attack.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
|
#include "PlayerCharacterControl.h" // 공격했을 때 플레이어를 인식하고 접근하기 위해 추가
#include "Engine/DamageEvents.h" // 플레이어에게 대미지를 입히기 위해 추가
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
ANormalZombie* zombie = Cast<ANormalZombie>(OwnerComp.GetAIOwner()->GetCharacter());
if (zombie == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Failed"));
return EBTNodeResult::Failed;
}
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Succeeded"));
zombie->SetCurrentTask(this); // 추가
zombie->PlayAttackSound();
zombie->PlayAttackMontage();
return EBTNodeResult::Succeeded;
}
void UBTTask_Attack::AttackTrace(ANormalZombie* NormalZombie) // 공격 Collision 생성
{
if (!NormalZombie) return;
FVector RightHand = NormalZombie->GetMesh()->GetSocketLocation("hand_r"); // 오른손에 Collision Sphere 생성
FVector LeftHand = NormalZombie->GetMesh()->GetSocketLocation("hand_l"); // 왼손에 Collision Sphere 생성
float SphereRadius = 50.0f;
float Damage = 15.0f;
if (NormalZombie->zombieType == EZombieType::HumanZombie) // 각 좀비 별 Collision Sphere 크기 생성 및 대미지 부
{
SphereRadius = 50.0f;
Damage = 20;
}
else if (NormalZombie->zombieType == EZombieType::DogZombie)
{
SphereRadius = 20.0f;
Damage = 10;
}
else if (NormalZombie->zombieType == EZombieType::WingZombie)
{
SphereRadius = 30.0f;
Damage = 35;
}
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(NormalZombie);
bool isHit = GetWorld()->SweepSingleByChannel(Hit, RightHand, LeftHand, FQuat::Identity, ECC_GameTraceChannel4, FCollisionShape::MakeSphere(SphereRadius), Params);
DrawDebugSphere(GetWorld(), RightHand, SphereRadius, 12, FColor::Red, false, 1.0f);
DrawDebugSphere(GetWorld(), LeftHand, SphereRadius, 12, FColor::Red, false, 1.0f);
if (isHit)
{
APlayerCharacterControl* Player = Cast<APlayerCharacterControl>(Hit.GetActor());
if (Player)
{
Player->TakeDamage(Damage, FDamageEvent(), NormalZombie->GetController(), NormalZombie);
}
}
}
|
cs |
ZombieAttackAnimNotify.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h" // 추가
#include "ZombieAttackAnimNotify.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API UZombieAttackAnimNotify : public UAnimNotify
{
GENERATED_BODY()
public:
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override; // Anim Notify
};
|
cs |
ZombieAttackAnimNotify.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#include "ZombieAttackAnimNotify.h"
#include "BTTask_Attack.h" // 추가
#include "NormalZombie.h" // 추가
#include "BehaviorTree/BTTaskNode.h" //
void UZombieAttackAnimNotify::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
ANormalZombie* zombie = Cast<ANormalZombie>(MeshComp->GetOwner());
if (!zombie) return;
UBTTaskNode* CurrentTask = zombie->GetCurrentTask(); // 때릴 때 Trace가 생성되는 것이 Task에 들어있기에, 현재 Task가 어떤 Task인지(Move To 인지 Attack 인지) 확인한 뒤에 현재 Task가 Attack이면 실행
if (!CurrentTask) return;
if (UBTTask_Attack* AttackTask = Cast<UBTTask_Attack>(CurrentTask)) // 캐스팅이 되면 true 값이 되고 캐스팅 실패 시 자동으로 false 값이 되기에, false 값일 경우 따로 설정해주지 않아도 됨
{
AttackTask->AttackTrace(zombie);
}
}
|
cs |
위 그림과 같이 공격 애니메이션으로 지정한 Montage에 들어가서 타임라인을 우클릭 하고 "Zombie" 를 검색하면, 위에서 생성했던 "ZombieAttackAnimNotify" 가 뜨는 것을 확인할 수 있다.
해당 Notify를 원하는 시간 대에 배치해주면, 아래 그림과 같이 애니메이션 중 Notify 지점에 도달했을 때 공격 판정의 Sphere Trace가 생성되는 것을 확인할 수 있다.
오늘 작성한 코드의 알고리즘을 정리하면 아래와 같다.

공격을 실행시키는 Attack Task를 생성하여 BT에 연결하였다.
적이 공격할 시에는 일단은 공격할 때 나는 소리, 공격 애니메이션, 공격 판정이 있는데, 공격할 때 나는 소리와 공격 애니메이션은 'Normal Zombie' 에서 실행할 수 있기에, 우리는 공격할 때 공격 소리와 공격 애니메이션은 'Normal Zombie' 에서 호출하여 가져오고 공격 판정을 주는 것만 따로 추가해주면 된다.
공격 판정을 주는 것을 우리는 Attack Task에서 실행하도록 하였다.
그리고 공격 판정은 공격 애니메이션이 재생되는 모든 시간 대에 주는 것이 아닌, 특정 동작을 할 때 이후로 주고 싶기에 Anim Notify를 사용하였다.
'Normal Zombie' 현재 상태를 get 하고 set한 뒤에 현재 적의 공격 상태를 감지하고, 공격 상태가 됐을 때 공격 애니메이션이 진행되면 Anim Notify 설정한 시간이 되었을 때 공격 판정이 나오게 된다.
*오늘까지 작성한 코드 정리
NormalZombie.h
|
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
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "ZombieBase.h"
#include "BehaviorTree/BTTaskNode.h" // Task 노드에 대한 정보를 전달을 위해 추가
#include "NormalZombie.generated.h"
UENUM()
enum class EZombieAttackSoundType :uint8
{
HumanZombieAttackSound1, HumanZombieAttackSound2, DogZombieAttackSound1, DogZombieAttackSound2, WingZombieAttackSound1, WingZombieAttackSound2
};
UCLASS()
class MYPROJECT_API ANormalZombie : public AZombieBase
{
GENERATED_BODY()
public:
ANormalZombie();
private:
virtual void BeginPlay() override;
public:
void InitAI();
void PlayAttackSound();
void PlayAttackMontage();
void SetCurrentTask(UBTTaskNode* Task);
UBTTaskNode* GetCurrentTask() const; // 외부로 현재 실행 중인 Task를 보내는 함수
private:
UBTTaskNode* CurrentTask; // 현재 실행 중인 Task 정보를 저장
public:
// AllowPrivateAccess: C++ 변수가 private로 선언되어도 접근 가능하게 허용 / MakeEditWidget: 에디터에서 이 변수를 3D 공간에 시각적으로 표시
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI", meta= (AllowPrivateAccess = "true", MakeEditWidget = "true"))
FVector ZombieMoveVector = FVector(0, 0, 0);
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TMap<EZombieAttackSoundType, USoundBase*> ZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TArray<USoundBase*>HumanZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TArray<USoundBase*>DogZombieAttackSounds;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAttackSound")
TArray<USoundBase*>WingZombieAttackSounds;
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim")
TArray<UAnimMontage*>HumanZombieAttackMontages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim")
TArray<UAnimMontage*>DogZombieAttackMontages;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ZombieAnim")
TArray<UAnimMontage*>WingZombieAttackMontages;
};
|
cs |
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
93
94
95
96
97
98
99
100
101
102
103
104
105
|
// 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());
if (AIController)
{
UBlackboardComponent* BB = AIController->GetBlackboardComponent();
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);
}
}
}
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 |
BTTask_Attack.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTTask_Attack.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API UBTTask_Attack : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
UBTTask_Attack();
void AttackTrace(class ANormalZombie* NormalZombie);
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; // Task 함수 기본 형식
};
|
cs |
BTTask_Attack.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
|
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTask_Attack.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "ZombieAIController.h"
#include "AIController.h"
#include "Kismet/GameplayStatics.h"
#include "NormalZombie.h"
#include "Engine/GameEngine.h"
#include "PlayerCharacterControl.h"
#include "Engine/DamageEvents.h"
UBTTask_Attack::UBTTask_Attack()
{
NodeName = "ZombieAttackTask";
bNotifyTick = false;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
ANormalZombie* zombie = Cast<ANormalZombie>(OwnerComp.GetAIOwner()->GetCharacter());
if (zombie == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Failed"));
return EBTNodeResult::Failed;
}
GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("Attack Succeeded"));
zombie->SetCurrentTask(this);
zombie->PlayAttackSound(); // 공격 소리 재생
zombie->PlayAttackMontage(); // 공격 애니메이션 재생
return EBTNodeResult::Succeeded;
}
void UBTTask_Attack::AttackTrace(ANormalZombie* NormalZombie) // 공격 Collision 생성
{
if (!NormalZombie) return;
FVector RightHand = NormalZombie->GetMesh()->GetSocketLocation("hand_r");
FVector LeftHand = NormalZombie->GetMesh()->GetSocketLocation("hand_l");
float SphereRadius = 50.0f;
float Damage = 15.0f;
if (NormalZombie->zombieType == EZombieType::HumanZombie)
{
SphereRadius = 50.0f;
Damage = 20;
}
else if (NormalZombie->zombieType == EZombieType::DogZombie)
{
SphereRadius = 20.0f;
Damage = 10;
}
else if (NormalZombie->zombieType == EZombieType::WingZombie)
{
SphereRadius = 30.0f;
Damage = 35;
}
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(NormalZombie);
bool isHit = GetWorld()->SweepSingleByChannel(Hit, RightHand, LeftHand, FQuat::Identity, ECC_GameTraceChannel4, FCollisionShape::MakeSphere(SphereRadius), Params);
DrawDebugSphere(GetWorld(), RightHand, SphereRadius, 12, FColor::Red, false, 1.0f);
DrawDebugSphere(GetWorld(), LeftHand, SphereRadius, 12, FColor::Red, false, 1.0f);
if (isHit)
{
APlayerCharacterControl* Player = Cast<APlayerCharacterControl>(Hit.GetActor());
if (Player)
{
Player->TakeDamage(Damage, FDamageEvent(), NormalZombie->GetController(), NormalZombie);
}
}
}
|
cs |
ZombieAttackAnimNotify.h
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "ZombieAttackAnimNotify.generated.h"
/**
*
*/
UCLASS()
class MYPROJECT_API UZombieAttackAnimNotify : public UAnimNotify
{
GENERATED_BODY()
public:
virtual void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
};
|
cs |
ZombieAttackAnimNotify.cpp
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// Fill out your copyright notice in the Description page of Project Settings.
#include "ZombieAttackAnimNotify.h"
#include "BTTask_Attack.h"
#include "NormalZombie.h"
#include "BehaviorTree/BTTaskNode.h"
void UZombieAttackAnimNotify::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
ANormalZombie* zombie = Cast<ANormalZombie>(MeshComp->GetOwner());
if (!zombie) return;
UBTTaskNode* CurrentTask = zombie->GetCurrentTask(); // 때릴 때 Trace가 생성되는 것이 Task에 들어있기에, 현재 Task가 어떤 Task인지(Move To 인지 Attack 인지) 확인한 뒤에 현재 Task가 Attack이면 실행
if (!CurrentTask) return;
if (UBTTask_Attack* AttackTask = Cast<UBTTask_Attack>(CurrentTask)) // 캐스팅이 되면 true 값이 되고 캐스팅 실패 시 자동으로 false 값이 되기에, false 값일 경우 따로 설정해주지 않아도 됨
{
AttackTask->AttackTrace(zombie);
}
}
|
cs |
'Unreal Engine > GCC Class (5.5.4)' 카테고리의 다른 글
| Unreal Engine 5 배워보기 - 36일차 (0) | 2025.05.14 |
|---|---|
| Unreal Engine 5 배워보기 - 35일차 (0) | 2025.05.13 |
| Unreal Engine 5 배워보기 - 33일차 (0) | 2025.05.09 |
| Unreal Engine 5 배워보기 - 32일차 (0) | 2025.05.08 |
| Unreal Engine 5 배워보기 - 31일차 (0) | 2025.05.07 |