리얼 개발

[UE5] 적 AI 공격 구현하기 본문

Unreal Engine5

[UE5] 적 AI 공격 구현하기

econo-my 2024. 7. 29. 18:10

 

전 포스팅에서 AI 적을 만드는 것에 대한 간단한 이야기를 했는데, 이번에는 이를 바탕으로 몬스터의 공격과 스킬 등을 구현해보겠다.

 

 

BT 클래스

Behavior Tree 클래스마다 구현해야 하는 가상 함수들이 있다.

 

UBTTask

class UBTTaskNode
{
	GENERATED_BODY()

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

 

UBTService

class UBTService
{
	GENERATED_BODY()

protected:
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
	
};

 

UBTDecorator

class UBTDecorator
{
	GENERATED_BODY()

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

 

GIF에서 보이는 공을 날리는 모션은 UBTService에서 관리하고 있다. Service 노드는 Composite의 추가 기능 노드로 일정 간격(Tick) 으로 실행되는 노드이다. 

 

 

몬스터 스킬 구현

Service 노드에서 Player가 몬스터의 감지 범위 내에 들어왔는지 확인하고 들어왔다면 일정 확률로 BossSkill_2() 가상 함수를 호출해 스킬을 실행한다.

TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParams(SCENE_QUERY_STAT(Detect), false, ControllingPawn);
bool HitResult = World->OverlapMultiByChannel(OverlapResults, Center, FQuat::Identity, ECC_GameTraceChannel1, FCollisionShape::MakeSphere(DetectRadius), CollisionQueryParams);

if (HitResult)
{
	for (auto const& OverlapResult : OverlapResults)
	{
		APawn* Pawn = Cast<APawn>(OverlapResult.GetActor());
		if (Pawn && Pawn->GetController()->IsPlayerController())
		{
			OwnerComp.GetBlackboardComponent()->SetValueAsObject(BBKEY_TARGET, Pawn);

			/* 디버그 */
			/*DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 1.0f);
			DrawDebugPoint(World, Pawn->GetActorLocation(), 10.0f, FColor::Green, false, 1.0f);
			DrawDebugLine(World, ControllingPawn->GetActorLocation(), Pawn->GetActorLocation(), FColor::Green, false, 1.0f);*/

			//달려가기
			AIPawn->RunToCharacter();
			double Distance = FVector::Distance(ControllingPawn->GetActorLocation(), Pawn->GetActorLocation());
			//UE_LOG(LogTemp, Display, TEXT("Distance : %f"), Distance);
			if (FMath::RandRange(1.0, 10.0) <= 3.5 && Distance >= 300.0f)
			{
				AIPawn->BossSkill_2(Pawn);
			}
			return;
		}
	}
}
DECLARE_DELEGATE(FAIMonsterAttackFinished);
/**
 * 
 */
class ROOMRPG_API IRMonsterAIInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void SetAIAttackDelegate(const FAIMonsterAttackFinished& InOnAttackFinished) = 0;
	virtual void AttackByAI() = 0;

	virtual void RunToCharacter() = 0;
	virtual void WalkToCharacter() = 0;

	virtual void BossSkill_2(APawn* Player) = 0;
};

해당 인터페이스를 통해 위 Service 노드와 몬스터 클래스를 연결할 수 있었다.

 

BossSkill_2가 호출되며 아래의 코드들이 실행된다.

void ARMonsterBoss::BossSkill_2(APawn* Player)
{
	BeginSkill_2(Player);
}

void ARMonsterBoss::BeginSkill_2(APawn* Player)
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance)
	{
		ARAIController* AIController = Cast<ARAIController>(GetController());
		if (AIController)
		{
			AIController->StopAI();

			AnimInstance->StopAllMontages(0.0f);
			AnimInstance->Montage_Play(Skill2Montage, 1.5f);

			FVector Location = GetMesh()->GetSocketLocation(TEXT("RightHand"));
			ARBomb* BombActor = GetWorld()->SpawnActor<ARBomb>(Bomb, Location, GetActorRotation());
			BombActor->SetDirection(Player->GetActorLocation());
			BombActor->SetOwner(this);
		}
	}

	FOnMontageEnded EndDelegate;
	EndDelegate.BindUObject(this, &ARMonsterBoss::EndSkill_2);
	AnimInstance->Montage_SetEndDelegate(EndDelegate, Skill2Montage);
}

void ARMonsterBoss::EndSkill_2(UAnimMontage* TargetMontage, bool IsProperlyEnded)
{
	ARAIController* AIController = Cast<ARAIController>(GetController());
	if (AIController)
	{
		AIController->RunAI();
	}
}

 

 

 

몬스터 스킬 투사체

SpawnActor를 통해 생성되는 ARBomb 이 몬스터 스킬의 투사체이다. 해당 클래스는 나이아가라 파티클과 피격 판정 및 대미지를 관리하고 있다.

#include "Prop/RBomb.h"
#include "GameFramework/ProjectileMovementComponent.h"
#include "NiagaraSystem.h"
#include "NiagaraComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Components/SphereComponent.h"
#include "Interface/RBossBombInterface.h"


ARBomb::ARBomb()
{
	PrimaryActorTick.bCanEverTick = true;

	Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
	RootComponent = Mesh;

	Trigger = CreateDefaultSubobject<USphereComponent>(TEXT("Trigger"));
	Trigger->SetupAttachment(RootComponent);
	Trigger->SetCollisionProfileName(TEXT("RTrigger"));
	Trigger->SetActive(false);

	ProjectileComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileMovement"));
	ProjectileComponent->UpdatedComponent = Mesh;
	ProjectileComponent->InitialSpeed = 1000.f;
	ProjectileComponent->MaxSpeed = 1000.f;
	ProjectileComponent->bRotationFollowsVelocity = true;

	NiagaraComponent = CreateDefaultSubobject<UNiagaraComponent>(TEXT("Niagara"));

	static ConstructorHelpers::FObjectFinder<UNiagaraSystem> FirstFireRef(TEXT("/Script/Niagara.NiagaraSystem'/Game/M5VFXVOL2/Niagara/Fire_for_BP/NFire_BP_00.NFire_BP_00'"));
	if (FirstFireRef.Object)
	{
		FirstFire = FirstFireRef.Object;
	}
	static ConstructorHelpers::FObjectFinder<UNiagaraSystem> ExplosionRef(TEXT("/Script/Niagara.NiagaraSystem'/Game/M5VFXVOL2/Niagara/Explosion/NFire_Exp_00.NFire_Exp_00'"));
	if (ExplosionRef.Object)
	{
		Explosion = ExplosionRef.Object;
	}
	static ConstructorHelpers::FObjectFinder<UNiagaraSystem> NiagaraRef(TEXT("/Script/Niagara.NiagaraSystem'/Game/M5VFXVOL2/Niagara/Fire/NFire_08.NFire_08'"));
	if (NiagaraRef.Object)
	{
		GroundFire = NiagaraRef.Object;
	}

	NiagaraComponent->SetupAttachment(RootComponent);
	NiagaraComponent->SetAsset(FirstFire);
	NiagaraComponent->bAutoActivate = true;
}

void ARBomb::BeginPlay()
{
	Super::BeginPlay();
	
	Mesh->OnComponentHit.AddDynamic(this, &ARBomb::OnHit);
	Trigger->OnComponentBeginOverlap.AddDynamic(this, &ARBomb::OnBombBeginOverlap);
	FTimerHandle TimerHandle;
	GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]()
		{
			Destroy();
		}, 8.0f, false);
}

void ARBomb::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

void ARBomb::SetDirection(const FVector& InDirection)
{
	ProjectileComponent->Velocity = InDirection * ProjectileComponent->InitialSpeed;
}

void ARBomb::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
	if (bFlag)
	{
		bFlag = false;
		Mesh->SetSimulatePhysics(false);
		Mesh->SetHiddenInGame(true);
		Trigger->SetActive(true);
		NiagaraComponent->SetAsset(Explosion);

		const float Damage = 75.0f;
		IRBossBombInterface* Boss = Cast<IRBossBombInterface>(GetOwner());
		if (Boss)
		{
			Boss->TakeDamageByBomb(OtherActor, Damage);
		}

		FTimerHandle TimerHandle;
		GetWorld()->GetTimerManager().SetTimer(TimerHandle, [this]()
			{
				NiagaraComponent->SetAsset(GroundFire);
			}, 2.5f, false);		
	}
}

void ARBomb::OnBombBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	float Distance = FVector::Distance(GetActorLocation(), OtherActor->GetActorLocation());
	float Damage = (1 / Distance) * 7000;
	UE_LOG(LogTemp, Display, TEXT("Overlap Damage : %f"), Damage);
		
	IRBossBombInterface* Boss = Cast<IRBossBombInterface>(GetOwner());
	if (Boss)
	{
		Boss->TakeDamageByBomb(OtherActor, Damage);
	}
}

 

OnComponentBeginOverlap
OnComponentHit

 

해당 델리게이트들을 이용해서 플레이어가 투사체에 맞았을 때, 혹은 투사체가 떨어진 곳의 불길에 플레이어가 들어갈 때의 상황에 대미지를 주고자 했다.

 

 

플레이어에게 대미지 전달

위의 Bomb 클래스에서 플레이어에게 대미지를 전달하려고 했지만, TakeDamage 함수에 필요한 인자인 Controller를 가져올 수 없었다. 그리고 대미지를 전달하는 객체가 몬스터인건 맞으니 Bomb 클래스에서 플레이어에게 대미지를 바로 전달하는 것도 어색했다. 

 

그래서 인터페이스를 하나 만들어 몬스터 클래스에 피격 당한 객체와 대미지를 전달해 몬스터가 플레이어의 체력을 깍도록 설계했다.

class ROOMRPG_API IRBossBombInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual void TakeDamageByBomb(AActor* InOtherActor, float Damage) = 0;
};

 

 

이를 구현한 몬스터 클래스에서 해당 함수를 재정의 해주면 플레이어에게 스킬로 대미지를 입힐 수 있다.

void ARMonsterBoss::TakeDamageByBomb(AActor* InOtherActor, float Damage)
{
	FDamageEvent DamageEvent;
	InOtherActor->TakeDamage(Damage, DamageEvent, GetController(), this);
}

 

 

'Unreal Engine5' 카테고리의 다른 글

[UE5] 보간 함수를 이용한 회전  (1) 2024.09.02
[UE5] 용어 정리  (4) 2024.09.02
[UE5] AI 적 조종하기  (0) 2024.07.29
[UE5] 아이템 획득하기  (0) 2024.07.11
[UE5] Enhanced Input을 활용한 이동  (1) 2024.07.01