๐ก C++๋ก ์ ์ ๋ง๋ค์ด๋ณด์. ๋ํ ์ ํ์ํ๊ธฐ๊ณ๋ฅผ ํฌํจํ์ฌ ๋ง๋ค์ด ๋ณด๊ฒ ๋ค.
์ด๋ฒ์๋ C++๋ฅผ ์ฌ์ฉํด์ ์ ์ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
์ ์ด ํ๋ ์ด์ด๋ฅผ ์ซ์ ์ค๊ณ ,
ํ๋ ์ด์ด๋ฅผ ๊ณต๊ฒฉํ๊ณ ,
์ฒด๋ ฅ์ด 0์ด ๋๋ฉด ์ฃฝ๋,
์ AI์๊ฒ ์ง๋ฅ์ ๋ถ์ฌํด์ฃผ๋ ํ๋์ ๋ชจ๋ธ์
๋๋ค.
์ด๋ฌํ ๋ชจ๋ธ์ State ํจํด์ผ๋ก ์ฌ๋ฌ ์ ํ๋ ์ํ๊ฐ ์กด์ฌํ๊ณ , ํน์ ์กฐ๊ฑด์ ๋ฌผ๋ ค ์๋ก ์ฐ๊ฒฐ๋์ด ๋์ํฉ๋๋ค.
์ฐ์ ์ ์ ์ค์ฒด๋ถํฐ ๋ง๋ค์ด ๋ณด๊ฒ ์ต๋๋ค.
C++ํด๋์ค ์์ฑํด์ฃผ์๊ณ ,
Character ๊ธฐ๋ฐ์ผ๋ก ๋ง๋ค์ด ์ฃผ์ธ์.
์ด๋ฆ์ Enemy๋ก ์ง์ด ์ฃผ๊ฒ ์ต๋๋ค.
์์๋ก SKM_Manny๋ฅผ ์
ํ ์ฃผ๊ฒ ์ต๋๋ค.
๋ ํผ๋ฐ์ค ๋ณต์ฌํด์ฃผ์๊ณ ,
Enemy์ ์์ฑ์์์ ์๋ ์ฝ๋๋ฅผ ์ฌ์ฉํด ์ ์ฉํด์ฃผ์ธ์.
AEnemy::AEnemy()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
ConstructorHelpers::FObjectFinder<USkeletalMesh>mannyMesh(TEXT("/Script/Engine.SkeletalMesh'/Game/Characters/Mannequins/Meshes/SKM_Manny.SKM_Manny'"));
if (mannyMesh.Succeeded())
{
GetMesh()->SetSkeletalMesh(mannyMesh.Object);
GetMesh()->SetRelativeLocationAndRotation(FVector(0, 0, -90), FRotator(0, -90, 0));
}
}
โ์ปดํ์ผ ์ ์ฅโํ ์ธ๋ฆฌ์ผ ์๋ํฐ๋ก ๋์์ Enemy c++ Class๋ฅผ ์ฐํด๋ฆญํด์ BP๋ฅผ ํ๋ ๋ง๋ค์ด ์ค๋๋ค.
Blueprint ํด๋์ BP_Enemy๋ผ๊ณ ์ง์ด ์ฃผ๊ฒ ์ต๋๋ค.
์ด์ ์ ์๊ฒ ๋ค์ด๊ฐ ์ง๋ฅ์ ๋ถ์ฌํด์ฃผ๊ฒ ์ต๋๋ค.
Blueprint์์ Behavior Tree๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ์๋๋ฐ,
์ด๋ฒ์๋ FSM์ ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค.
๊ทธ ์ ์ Actor Component๋ ์๋์ ์กํฐ์ ์ฒจ๊ฐํ ์ ์๋ ์ค๋ธ์ ํธ์ ๋๋ค. Camera, Spring Arm์ฒ๋ผ ์ปดํฌ๋ํธ๋ก ๋ถ์ฐฉํด์ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ฃผ๋ ๊ฒ์ด์ฃ .
์์ธํ ๋ด์ฉ์ ๋ฐ๋ก ํฌ์คํ ํ๊ฒ ์ต๋๋ค.
๊ทธ๋์ FSM์ Actor Component๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑํด์ฃผ๊ฒ ์ต๋๋ค.
ํด๋์ค๋ฅผ All Classes๋ก ํ์ฌ Actor Component๋ก ๊ฒ์ํด์ฃผ์ธ์.
EnemyFSM์ด๋ผ๊ณ ์ด๋ฆ ์ง์ด ์ฃผ๊ฒ ์ต๋๋ค.
์ด์ ์ฝ๋๋ฅผ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
์ฐ์ ๊ฐ ์ํ์ ๋ํ ์ก์ Enum Class๋ก ๋ง๋ค์ด ์ฃผ๊ฒ ์ต๋๋ค.
IDLE, MOVE, ATTACK, DAMAGE, DIE 5๊ฐ์ง ์ํ๋ฅผ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "EnemyFSM.generated.h"
UENUM(BlueprintType)
enum class EEnemyState : uint8
{
IDLE,
MOVE,
ATTAK,
DAMAGE,
DIE,
};
ํ์ฌ ์ํ๋ฅผ ์ ์ฅํ๋ mState๋ณ์๋ฅผ ๋ง๋ค๊ณ IDLE๋ก ์ด๊ธฐํํด์ฃผ์ธ์.
๋ํ ๊ฐ ์ํ์ผ ๋ ์คํํ ํจ์๋ฅผ ์ ์ธํด์ฃผ์ธ์.
public:
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category=FSM)
EEnemyState mState = EEnemyState::IDLE;
void IdleState();
void MoveState();
void AttackState();
void DamageState();
void DieState();
Tick()ํจ์์์ Switch๋ฌธ์ ์ฌ์ฉํ์ฌ mState ๊ฐ์ ๋ฐ๋ผ ํน์ ํจ์๊ฐ ์คํ๋๋๋ก ํด์ฃผ๊ฒ ์ต๋๋ค.
// Called every frame
void UEnemyFSM::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// ...
switch (mState)
{
case EEnemyState::IDLE:
IdleState();
break;
case EEnemyState::MOVE:
MoveState();
break;
case EEnemyState::ATTAK:
AttackState();
break;
case EEnemyState::DAMAGE:
DamageState();
break;
case EEnemyState::DIE:
DieState();
break;
default:
break;
}
}
FSM์ ํ๋์ ์ปดํฌ๋ํธ์
๋๋ค.
Enemy Class๋ก ์ด๋ํด Camera์ฒ๋ผ ๋ถ์ฐฉํด์ฃผ๋ฉด ๋ฉ๋๋ค.
์ปดํฌ๋ํธ์ธ UEnemyFSM์ ์ ์ธํด์ฃผ๊ฒ ์ต๋๋ค.
public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = FSMComponent)
class UEnemyFSM* fsm;
๊ทธ๋ฆฌ๊ณ CreateDefaultSubobject๋ฅผ ์ฌ์ฉํด์ ํ ๋นํด์ฃผ์ธ์.
#include "EnemyFSM.h"
// Sets default values
AEnemy::AEnemy()
{
// ์๋ต
fsm = CreateDefaultSubobject<UEnemyFSM>(TEXT("FSM"));
}
โ์ปดํ์ผ ์ ์ฅโํ๊ณ ์๋ํฐ์์ BP_Enemy๋ฅผ ํ์ธํด์ฃผ์ธ์.
๊ทธ๋ผ Components์ Fsm์ด ๋ค์ด๊ฐ ์๋ ๊ฒ์ ํ์ธํ์ค ์ ์์ต๋๋ค.
๋ค์ EnemyFSM Class๋ก ๊ฐ์ ๊ฐ ์ํ ๋ณ ๊ธฐ๋ฅ์ ๊ตฌํํด์ฃผ๊ฒ ์ต๋๋ค.
Idle ์ํ๋ถํฐ ์งํํ๊ฒ ์ต๋๋ค.
์ผ๋จ IdleDelayTime๊ณผ currentTime์ ๊ฐ๊ฐ ์ด๊ธฐํํด์ ๋ง๋ค์ด ์ฃผ์ธ์.
public:
UPROPERTY(VisibleAnywhere,BlueprintReadOnly,Category=FSM)
EEnemyState mState = EEnemyState::IDLE;
// ์ถ๊ฐ
UPROPERTY(EditDefaultsOnly,Category=FSM)
float idleDelayTime = 2;
float currentTime = 0;
๊ทธ๋ฆฌ๊ณ Tick์์ currentTime์ DeltaTime์ ๋์ ํฉํ์ฌ ํ์ฌ ์๊ฐ์ ๊ฒฝ๊ณผ๋ฅผ ์ฒดํฌํด์ฃผ๊ฒ ์ต๋๋ค.
void UEnemyFSM::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// ์ถ๊ฐ
currentTime += DeltaTime;
// ์๋ต
}
๋ํ currentTime์ด ๊ฐ ์ํ ๋ณ ์๊ฐ๋ณด๋ค ์ปค์ง๋ค๋ฉด ๋ค๋ฅธ ์ํ๋ก ์ ํํ๊ณ , 0์ผ๋ก ๋ค์ ์ด๊ธฐํํ๋ฉด ๋๊ฒ ์ฃ ?
์ฌ๊ธฐ์๋ Move์ํ๋ก ์ ํํ๊ณ 0์ผ๋ก ์ด๊ธฐํ ํด์ฃผ๊ฒ ์ต๋๋ค.
void UEnemyFSM::IdleState()
{
if (currentTime > idleDelayTime)
{
mState = EEnemyState::MOVE;
currentTime = 0;
}
}
Move์ํ๋ ํ๋ ์ด์ด๋ฅผ ํฅํด ๋ค๊ฐ์ค๋๋ก ํด์ฃผ๊ฒ ์ต๋๋ค.
EnemyFSM Class๋ก ์ด๋ํด์ฃผ์ธ์.
๊ทธ๋ฆฌ๊ณ ์์ ๋์๊ณผ ํ๊ฒ์ ์ ์ฅํ ๋ณ์๋ฅผ ๊ฐ๊ฐ ์ ์ธํด์ฃผ์ธ์.
์ฐพ์๊ฐ AMyPlayerํด๋์ค์ target๊ณผ,
์์ ์์ธ AEnemyํด๋์ค์ me๋ฅผ ์ ์ธํด์ฃผ๊ฒ ์ต๋๋ค.
๋ํ ์ด๋ ์ํ์ ๋ํ ์๊ฐ๋ณ์๋ 1๋ก ์ด๊ธฐํํ์ฌ ๋ง๋ค์ด ์ฃผ์ธ์.
public:
UPROPERTY(VisibleAnywhere,Category=FSM)
class AMyPlayer* target;
UPROPERTY(VisibleAnywhere, Category = FSM)
class AEnemy* me;
UPROPERTY(EditDefaultsOnly, Category = FSM)
float moveDelayTime = 1;
๊ฐ ํด๋์ค์ ํค๋๋ฅผ ์ถ๊ฐํด์ฃผ์๊ณ , BeginPlay์์ ์ด๊ธฐํํด์ฃผ๊ฒ ์ต๋๋ค.
GetActorOfClass๋ฅผ ์ฌ์ฉํด์ ์๋์ AMyPlayer๋ฅผ ์ฐพ์ target์ ์ด๊ธฐํํด์ฃผ์์ต๋๋ค.
me๋ GetOwner()๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ๋จํ๊ฒ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
#include "MyPlayer.h"
#include "Enemy.h"
#include <Kismet/GameplayStatics.h>
// Called when the game starts
void UEnemyFSM::BeginPlay()
{
Super::BeginPlay();
// ์ถ๊ฐ
auto actor = UGameplayStatics::GetActorOfClass(GetWorld(), AMyPlayer::StaticClass());
target = Cast<AMyPlayer>(actor);
me = Cast<AEnemy>(GetOwner());
}
target๊ณผ me์ ๋ํด์ ์ด๊ธฐํ๊ฐ ์ ์์ ์ผ๋ก ์ด๋ฃจ์ด์ง์ง ์์์ ๊ฒฝ์ฐ์ ๋ํ ์์ธ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ๊ฒ ์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ target์์ me์ ์์น๊ฐ์ ์ฌ์ฉํด์ ๊ฑฐ๋ฆฌ์, ๋ฐฉํฅ์ ์ ์ ์์ต๋๋ค.
GetActorLocation()์ ์ฌ์ฉํด์ Vector๊ฐ์ ์ป๊ฒ ์ต๋๋ค.
void UEnemyFSM::MoveState()
{
if (target != nullptr && me != nullptr)
{
FVector Direction = target->GetActorLocation() - me->GetActorLocation();
}
FVector Direction์ ๊ฑฐ๋ฆฌ, ๋ฐฉํฅ์ ์ ์ ์๋ ์ ๋ณด๊ฐ ์๊ฒผ์ต๋๋ค.
Direction.GetSafeNormal()์ ํตํด ์ ๊ทํ๋ฅผ ํ์ฌ ๊ฐ์ 1๋ก ๋ง๋ค์ด ์ฃผ์ด ๋ฐฉํฅ์ ๋ํ ๋ฐ์ดํฐ๋ง ๋ง๋ค ์ ์์ต๋๋ค.
์ด ๊ฐ์ AddMovement Input์ ๋ฃ์ด ํด๋น ๋ฐฉํฅ์ผ๋ก ์ด๋ํ๋๋ก ํด์ฃผ๊ฒ ์ต๋๋ค.
๊ทธ๋ฅ Normalize()๋ฅผ ์ฌ์ฉํ์
๋ ๋ฌด๋ฐฉํ ๊ฒ ๊ฐ์ต๋๋ค.
void UEnemyFSM::MoveState()
{
if (target != nullptr && me != nullptr)
{
FVector Direction = target->GetActorLocation() - me->GetActorLocation();
// ์ถ๊ฐ
me->AddMovementInput(Direction.GetSafeNormal());
}
}
๊ทธ๋ฆฌ๊ณ moveDelayTime๋ณด๋ค CurrentTime์ด ์ปค์ง๋ค๋ฉด ๋ค์ IDLE์ํ๋ก ๋ณํํด์ฃผ๊ฒ ์ต๋๋ค.
void UEnemyFSM::MoveState()
{
// ์๋ต
// ์ถ๊ฐ
if (currentTime > moveDelayTime)
{
mState = EEnemyState::IDLE;
currentTime = 0;
}
}
โ์ปดํ์ผ ์ ์ฅโํ ์คํํด๋ณด๊ฒ ์ต๋๋ค.
๊ทธ๋ผ ์ด์ 2์ด ๋๊ธฐ ํ, 1์ด ์์ง์ด๋ ์ ์ด ์์ฑ ๋์์ต๋๋ค.
์ด๋ ์ํ์ ๋ํ ์๊ฐ๋ณ์์, ๊ณต๊ฒฉ ์ฌ๊ฑฐ๋ฆฌ์ ๋ํ ๋ณ์๋ฅผ ๋ง๋ค์ด ์ฃผ์ธ์.
UPROPERTY(EditAnywhere,BlueprintReadOnly,Category=FSM)
float attackRange = 150.0f;
UPROPERTY(EditDefaultsOnly, Category = FSM)
float attackDelayTime = 2;
attackRange๋ณด๋ค ๊ฑฐ๋ฆฌ๊ฐ ๊ฐ๊น์ฐ๋ฉด mState๋ฅผ ATTACK์ผ๋ก,
์๋๋ผ๋ฉด IDLE์ํ ๊ทธ๋๋ก ๋ณํํ๋๋ก ํด์ฃผ๊ฒ ์ต๋๋ค.
๊ณต๊ฒฉ ์ฌ๊ฑฐ๋ฆฌ ๋ด๋ผ๋ฉด ATTACK ์ํ๋ก ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
๋ํ currentTime์ 0์ผ๋ก ์ด๊ธฐํํ์ง ์์ต๋๋ค.
void UEnemyFSM::MoveState()
{
FVector Direction = target->GetActorLocation() - me->GetActorLocation();
if (Direction.Size() <= attackRange)
{
mState = EEnemyState::ATTACK;
}
else
{
if (target != nullptr && me != nullptr)
{
me->AddMovementInput(Direction.GetSafeNormal());
}
if (currentTime > moveDelayTime)
{
mState = EEnemyState::IDLE;
currentTime = 0;
}
}
}
AttackState()๋ ์ ์ฌํฉ๋๋ค.
์ฌ๊ฑฐ๋ฆฌ ์ด๋ด์ธ์ง ๋จผ์ ํ๋จํฉ๋๋ค.
์ฌ๊ฑฐ๋ฆฌ๋ณด๋ค ๊ธธ๋ค๋ฉด ๋ค์ MOVE ์ํ๋ก ์ ํํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ณต๊ฒฉ ๋๊ธฐ ์๊ฐ์ ํ๋จ ํ ๊ณต๊ฒฉํฉ๋๋ค.
void UEnemyFSM::AttackState()
{
FVector Direction = target->GetActorLocation() - me->GetActorLocation();
if (Direction.Size() <= attackRange)
{
if (currentTime > attackDelayTime)
{
UE_LOG(LogTemp, Warning, TEXT("Attack State!!"));
currentTime = 0;
}
}
else
{
mState = EEnemyState::MOVE;
currentTime = 0;
}
}
โ์ปดํ์ผ ์ ์ฅโ ํ ์คํํด๋ณด๊ฒ ์ต๋๋ค.
์ ๋ ํ์คํ๊ฒ ๋ณด์ฌ ๋๋ฆฌ๊ธฐ ์ํด attackRange์ 350๊น์ง ๋๋ ธ์ต๋๋ค.
moveDelayTime๊ณผ attackDelayTime์ 1์ด, 2์ด๋ก ๊ฐญ์ด ์์ด์ ์ฌ๊ฑฐ๋ฆฌ ๋ด๋ก ๋ค์ด์๋ ๋ฐ๋ก ๊ณต๊ฒฉ์ ํ์ง ์๋ ๋ชจ์ต์ ๋๋ค.
์ด๋ ์ ๋นํ ์์ ํด์ฃผ์ธ์.
์ ๋ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํด์ ์ ์ด ์์ง์ด์ง๋ง ์ฌ๋ฌ๋ถ์ ์ ๋๋ฉ์ด์ ์ด ๋์ค์ง ์๊ณ T์๋ก ๋ค๊ฐ์ฌ ๊ฒ๋๋ค.
์ ๋๋ฉ์ด์ ์ ์ฉ๊ณผ ์ถ๊ฐ ๊ธฐ๋ฅ์ ๋ค์ ํฌ์คํ ์ ์งํํ๊ฒ ์ต๋๋ค.