이제 Enemy의 AI가 기본적인 공격모션까지 완성했으니 추가적인 모션을 넣고자 한다.
이는 Attack Montage에 여러 모션을 추가하고, Section을 나누어 랜덤한 값을 받아서 이에 따른 모션을 출력하도록 할 것이다.


먼저 Animation Montage에 원하는 모션을 넣고 이를 Section별로 나누어 주어야 한다. Anim Instance에서 Enemy의 Animation을 출력할 때, Section별로 출력할 것이기에 2번째 사진처럼 모든 Section의 Link 또한 끊어놓아야 연속적으로 재생되지 않는다.

각각의 Animation에는 다음과 같이 Notify를 추가하였다. Enemy의 무기나 공격범위에 달려있는 Box Collision을 기본적으로 No Collision 상태로 해놓았으며 해당 Notify가 시작되고 끊나기 전까지만 Enabled Collision 상태로 구현하였다.
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "NotifyState_Enemy_RightAttack.generated.h"
UCLASS()
class THELASTSURVIVOR_API UNotifyState_Enemy_RightAttack : public UAnimNotifyState
{
GENERATED_BODY()
public:
virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration);
virtual void NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation);
bool bNotifyCalled = false;
};
AnimNotifyState를 부모클래스로 지정하여 생성한 CPP 파일이다. AnimNotify와는 다르게 AnimNotifyState는 시작과 끝나는 지점에서 각각 호출이 가능하기에 특정 구간에서 Notify를 실행시키고 싶다면 이처럼 생성하는 것이 편하다.
#include "NotifyState_Enemy_RightAttack.h"
#include "MainEnemy.h"
#include "Components/BoxComponent.h"
void UNotifyState_Enemy_RightAttack::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
Super::NotifyBegin(MeshComp, Animation, TotalDuration);
if (!bNotifyCalled) {
bNotifyCalled = true;
if (MeshComp && MeshComp->GetOwner()) {
AMainEnemy* Enemy = Cast<AMainEnemy>(MeshComp->GetOwner());
if (Enemy) {
UE_LOG(LogTemp, Warning, TEXT("right"));
Enemy->OnCollStart(1);
}
}
}
}
void UNotifyState_Enemy_RightAttack::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
if (bNotifyCalled) {
bNotifyCalled = false;
if (MeshComp && MeshComp->GetOwner()) {
AMainEnemy* Enemy = Cast<AMainEnemy>(MeshComp->GetOwner());
if (Enemy) {
Enemy->OnCollEnd();
}
}
}
}
시작과 끝나는 지점에서 호출되는 각각의 함수는 Collision의 상태를 바꿔주는 함수를 호출하는 작업만 수행한다. 본문에서는 RightAttack에 관련한 코드만 있으나 LeftAttack 또한 위처럼 Collision의 상태를 바꿔주는 코드만 작성하면 된다.
void AMainEnemy_Grunt::Attack()
{
Super::Attack();
auto AnimInstance = Cast<UGruntAnim>(GetMesh()->GetAnimInstance());
if (AnimInstance == nullptr) return;
randomPattern = FMath::RandRange(1, 80);
if (randomPattern <= 25) {
damage = 20;
AnimInstance->Attack("Attack_A");
}
else if (randomPattern <= 50) {
damage = 20;
AnimInstance->Attack("Attack_B");
}
else if (randomPattern <= 70) {
damage = 40;
AnimInstance->Attack("Attack_C");
}
else {
damage = 40;
AnimInstance->Attack("Attack_Dropdown");
}
AnimInstance->OnMontageEnded.RemoveDynamic(this, &AMainEnemy_Grunt::OnAttackMontageEnded);
AnimInstance->OnMontageEnded.AddDynamic(this, &AMainEnemy_Grunt::OnAttackMontageEnded);
}
Enemy의 Attack 함수이다. RandRange() 함수를 통해 Random값을 받아와서 AnimInstance에 이에 따라 수행할 공격패턴을 매개변수로 전달하였다.
void UGruntAnim::Attack(FString pattern)
{
Montage_Play(AttackMontage, 1.0f);
Montage_JumpToSection(*pattern);
}
Enemy의 AnimInstance에서는 전달 받은 매개변수를 Section값으로만 할당하여 Montage를 호출하면 된다.
이와 같이 BehaviorTree를 따로 건들지 않고, Attack함수 하나만으로 다양한 패턴의 Enemy를 구현해보았다.