프로젝트 기간 : 25.02.17 ~ 25.03.05 (약 3주)
1. 개요
오늘은 근거리 적 AI를 구현하는 게 목표였습니다. 근거리는 확실히 원거리에 비해 쉽게 구현할 수 있었어요. 어떻게 구현했는지 천천히 살펴봅시다 !
2. 근거리 적 AI 분석
다행히도 Spawn된 이후에 정찰하는 상태 없이 바로 플레이어에게 돌진하더라구요 ! 덕분에 Patrol 상태를 구현하지 않고 신속한 개발을 진행할 수 있었습니다. Spawn 애니메이션이 끝나면 Move 애니메이션으로 전이되고, 플레이어의 일정 반경 내로 도달하면 Attack 애니메이션으로 전이하면 끝이였습니다.
Attack 애니메이션이 재생되면서 적이 이동하지 않게끔 bool 값만 잘 설정해주면 될 것 같아보입니다.
3. 근거리 적 Behavior Tree
Spawn이 된 이후로 이동, 공격 AI가 동작해야하기 때문에 Decorator를 설정해주었습니다. 공격 상태일 때에는 이동을 하지 않고 공격 로직만 수행해야하기 때문에 이 부분도 Decorator로 막아줬어요. 마지막으로, 적이 사망하면 AI가 가동을 멈춰야 하므로 사망했을 시에 IsDead를 true로 변경해서 AI가 작동하지 않도록 구현했습니다.
4. 근거리 적 Behavior Tree Task
근거리 적은 블루프린트 노드로 구현하는 과정없이 바로 C++ 코드로 구현해줬습니다 ! 원거리 적과 거의 유사한 코드로 구현할 수 있었습니다. 플레이어에게 공격을 가하는 로직은 애니메이션의 특정 프레임에 notify를 걸어서 호출하는 방식으로 구현했기 때문에 일단 근거리 적의 Task는 더 간단하게 구현이 되더라구요.
BTTaskMeleeMove.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "TheFourthDescendant/Monster/Melee/MeleeMonster.h"
#include "BTTaskMeleeMove.generated.h"
UCLASS()
class THEFOURTHDESCENDANT_API UBTTaskMeleeMove : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTaskMeleeMove();
private:
/** AI를 적용할 몬스터 */
AMeleeMonster* Monster;
protected:
/** Task가 실행되기 시작할 때 호출되는 함수 */
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
BTTaskMeleeMove.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTaskMeleeMove.h"
UBTTaskMeleeMove::UBTTaskMeleeMove()
{
Monster = nullptr;
NodeName = "MeleeMove";
}
EBTNodeResult::Type UBTTaskMeleeMove::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
// 몬스터 캐스팅 실패 시 Return
Monster = Cast<AMeleeMonster>(OwnerComp.GetAIOwner()->GetCharacter());
if (Monster == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, "ExecuteTask : Monster Casting Failed !");
return EBTNodeResult::Failed;
}
// 캐스팅에 성공할 경우 Monster의 Move 함수 호출
Monster->Move();
return EBTNodeResult::Succeeded;
}
BTTaskMeleeAttack.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "TheFourthDescendant/Monster/Melee/MeleeMonster.h"
#include "BTTaskMeleeAttack.generated.h"
UCLASS()
class THEFOURTHDESCENDANT_API UBTTaskMeleeAttack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTaskMeleeAttack();
private:
/** AI를 적용할 몬스터 */
AMeleeMonster* Monster;
protected:
/** Task가 실행되기 시작할 때 호출되는 함수 */
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
BTTaskMeleeAttack.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "BTTaskMeleeAttack.h"
UBTTaskMeleeAttack::UBTTaskMeleeAttack()
{
Monster = nullptr;
NodeName = "MeleeAttack";
}
EBTNodeResult::Type UBTTaskMeleeAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
// 몬스터 캐스팅 실패 시 Return
Monster = Cast<AMeleeMonster>(OwnerComp.GetAIOwner()->GetCharacter());
if (Monster == nullptr)
{
GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, "ExecuteTask : Monster Casting Failed !");
return EBTNodeResult::Failed;
}
// 캐스팅에 성공할 경우 Monster의 Attack 함수 호출
// ->Attack();
return EBTNodeResult::Succeeded;
}
근거리 적의 세부적인 이동, 공격 로직 또한 각 해당 클래스에서 구현한 후에 호출하는 방식으로 진행했습니다 !
5. 근거리 적 구현
근거리 적의 세부적인 이동, 공격 로직을 살펴볼까요 !
MeleeMonster.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "TheFourthDescendant/Monster/Monster.h"
#include "MeleeMonster.generated.h"
UCLASS()
class THEFOURTHDESCENDANT_API AMeleeMonster : public AMonster
{
GENERATED_BODY()
protected:
/** 공격 반경 */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
float AttackRange;
private:
/** 공격 로직은 단 한 번 실행하기 위함 */
bool bIsAttacked;
public:
/** 공격 함수 */
UFUNCTION(BlueprintCallable)
virtual void Attack() override;
/** 이동 함수 */
virtual void Move() override;
/** 공격 완료 로직 */
UFUNCTION(BlueprintCallable)
void AttackCompleted();
};
MeleeMonster.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "MeleeMonster.h"
#include "Components/CapsuleComponent.h"
#include "Kismet/GameplayStatics.h"
void AMeleeMonster::Attack()
{
// 공격 로직이 한 번 실행됐을 경우 return
if (bIsAttacked) return;
bIsAttacked = true;
FHitResult HitResult;
FCollisionQueryParams Params;
// 자기 자신은 무시
Params.AddIgnoredActor(this);
const FVector Start = GetActorLocation() + GetActorForwardVector() * GetCapsuleComponent()->GetScaledCapsuleRadius();
const FVector End = Start + GetActorForwardVector() * AttackRange;
bool HitDetected = GetWorld()->SweepSingleByChannel(
HitResult,
Start,
End,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel1,
FCollisionShape::MakeSphere(AttackRange),
Params
);
if (HitDetected)
{
if (HitResult.GetActor() == Player)
{
// 공격
UGameplayStatics::ApplyDamage(Player, AttackPower, EnemyController, this, UDamageType::StaticClass());
}
}
}
void AMeleeMonster::Move()
{
if (EnemyController->bIsArrived)
{
bCanAttack = true;
Blackboard->SetValueAsBool(FName("IsAttacking"), true);
return;
}
EnemyController->MoveToTargetActor(Player);
}
void AMeleeMonster::AttackCompleted()
{
bCanAttack = false;
bIsAttacked = false;
Blackboard->SetValueAsBool(FName("IsAttacking"), false);
EnemyController->bIsArrived = false;
}
위에서 언급했듯, 전방 일정거리에 플레이어가 있는지 판단하고 ApplyDamage로 데미지를 주는 로직은 Attack 함수에 구현되어있고 애니메이션의 특정 프레임에서 호출하는 방식입니다 !
공격 애니메이션은 근거리 적이 플레이어에게 도달했을 때 bCanAttack의 bool 값으로 전이되도록 했습니다 !
6. 근거리 적 애니메이션
원거리 적의 애니메이션 블루프린트에는 Layered blend per bone을 활용했었는데, 근거리 적의 애니메이션 블루프린트는 아주 간단하게 구현할 수 있어서 편했어요 .. ㅠㅠ
결과
근거리도 됐고 원거리도 됐고 ! 뿌듯하네요 ..
'언리얼 엔진 > [팀프로젝트] The Fourth Descendant' 카테고리의 다른 글
[TheFourthDescendant] 05. 적 NavLink로 점프 구현 (0) | 2025.02.25 |
---|---|
[TheFourthDescendant] 04. 적 충돌 무시 구현 (0) | 2025.02.25 |
[TheFourthDescendant] 02. 원거리 적 AI 구현 (0) | 2025.02.20 |
[TheFourthDescendant] 01. 퍼스트 디센던트 모작의 시작 (1) | 2025.02.20 |