본문 바로가기

인생언리얼TPS

3.1-9 적AI제어를 위한 FSM 제작하기

TPSPlayer는 WASD키 입력시 애니메이션이 제어된건 ThirdPersonTemplete안에 FSM(유한상태머신)이 구현되어 있기 때문입니다. 이걸 구현해보겠습니다.

 

ActorComponent를 상속받아 EnemyFSM 이라는 C++클래스를 제작합니다. 컴포넌트로 제작하면 다른 컴포넌트에 부착하여 사용이 가능합니다.

 

UEnemyFSM 클래스가 만들어 집니다.

EnemyFSM.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "EnemyFSM.generated.h"

// 사용할 상태 정의
UENUM(BlueprintType)
enum class EEnemyState : uint8
{
	Idle, Move, Attack, Damage, Die,
};

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class TPSPROJECT_API UEnemyFSM : public UActorComponent
{
	GENERATED_BODY()

public:
	UEnemyFSM();

protected:
	virtual void BeginPlay() override;

public:
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

public:
	// 상태변수
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = FSM)
	EEnemyState mState = EEnemyState::Idle;

	// 대기상태
	void IdleState();
	// 이동상태
	void MoveState(float DeltaTime);
	// 공격상태
	void AttackState();
	// 피격상태
	void DamageState();
	// 죽음상태
	void DieState();

	// 대기시간
	UPROPERTY(EditDefaultsOnly, Category = FSM)
	float idleDelayTime = 2;
	// 경과시간
	float currentTime = 0;

	// 타겟
	UPROPERTY(VisibleAnywhere, Category = FSM)
	class ATPSPlayer* target;

	// 소유액터
	UPROPERTY()
	class AEnemy* me;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spawn)
	FString id;
	//이동속도
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spawn)
	float moveSpeed = 1.f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spawn)
	float minSpeed = 1.f;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spawn)
	float maxSpeed = 10.f;
	UFUNCTION()
	void SetMoveSpeed(float value);
	// 공격범위
	UPROPERTY(EditAnywhere, Category = FSM)
	float attackRange = 100.0f;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = FSM)
	FVector destination;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = FSM)
	FVector dir;

	// 공격대기시간
	UPROPERTY(EditAnywhere, Category = FSM)
	float attackDelayTime = 2.0f;

	// 피격 알림 이벤트 함수
	void OnDamageProcess();

	// 체력
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = FSM)
	int32 maxHp = 10;
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = FSM)
	int32 hp = maxHp;
	// 피격대기시간
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = FSM)
	int32 maxMp = 100;
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = FSM)
	int32 mp = maxHp;
	// 피격대기시간
	UPROPERTY(EditAnywhere, Category = FSM)
	float damageDelayTime = 2.0f;
	UPROPERTY(EditAnywhere, Category = FSM)
	float mpDelayTime = 2.0f;
	// 아래로 사라지는 속도
	UPROPERTY(EditAnywhere, Category = FSM)
	float dieSpeed = 50.0f;

	// 사용중인 애니메이션 블루프린트
	UPROPERTY()
	class UEnemyAnim* anim;

	UPROPERTY(EditAnywhere, Category = Animation)
	class UAnimMontage* damageAnim;

	// Enemy 를 소유하고 있는 AIController
	//UPROPERTY()
	//class AAIController* ai;

	// 길찾기 수행시 랜덤위치
	FVector randomPos;
	// 랜덤위치 가져오기
	//bool GetRandomPositionInNavMesh(FVector centerLocation, float radius, FVector& dest);
};

EnemyFSM.cpp

// Fill out your copyright notice in the Description page of Project Settings.

#include "Enemy.h"
#include "EnemyFSM.h"
#include "TPSPlayer.h"
#include <Components/CapsuleComponent.h>
#include <Components/SphereComponent.h>
#include <Components/WidgetComponent.h>
#include "Components/widget.h"
#include <Kismet/GameplayStatics.h>
#include <Kismet/KismetMathLibrary.h>
#include "HPBar.h"

// Sets default values
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;

	// 1. 스켈레탈메쉬 데이터 로드
	//
	ConstructorHelpers::FObjectFinder<USkeletalMesh> tempMesh(TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn.SKM_Quinn"));
	// 1-1. 데이터 로드 성공하면
	if (tempMesh.Succeeded())
	{
		//UE_LOG(LogTemp, Warning, TEXT("Enemy Skeletal Mesh Loading"));
		// 1-2. 데이터 할당
		GetMesh()->SetSkeletalMesh(tempMesh.Object);
		// 1-3. 메쉬 위치 및 회전 설정
		GetMesh()->SetRelativeLocationAndRotation(FVector(0, 0, -88), FRotator(0, -90, 0));
		// 1-4. 메쉬 크기 수정
		GetMesh()->SetRelativeScale3D(FVector(0.84f));
		GetMesh()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	sphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("Collosion"));
	sphereComp->SetRelativeScale3D(FVector(3, 3, 3));
	sphereComp->SetCollisionProfileName(TEXT("EnemyHand"));
	GetCapsuleComponent()->SetCollisionProfileName(TEXT("Enemy"));
	GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));
	// EnemyFSM 컴포넌트 추가
	fsm = CreateDefaultSubobject<UEnemyFSM>(TEXT("FSM"));
	if (fsm) {
		UE_LOG(LogTemp, Warning, TEXT("fsm"));
		//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, FString::Printf( TEXT("FSM: ") );// , fsm->GetFName()));
		//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red,  TEXT("FSM:") );
	// 애니메이션 블루프린트 할당하기
	}
	ConstructorHelpers::FClassFinder<UAnimInstance> tempClass(TEXT("/Game/Anim/ABP_Enemy.ABP_Enemy_C"));
	if (tempClass.Succeeded())
	{
		//UE_LOG(LogTemp, Warning, TEXT("ABP_Enemy Loading"));
		//GetMesh()->SetAnim
		GetMesh()->SetAnimInstanceClass(tempClass.Class);
	}
	widgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("HPWidget"));
	widgetComp->SetupAttachment(GetMesh());
	widgetComp->SetRelativeLocation(FVector(0, 0, 300.0f));
	UClass* WidgetCompClass = LoadClass<UUserWidget>(NULL, TEXT("/Game/UI/WBP_HPEnemy.WBP_HPEnemy_C"));
	if (WidgetCompClass) {
		widgetComp->SetWidgetClass(WidgetCompClass);
		widgetComp->SetWidgetSpace(EWidgetSpace::Screen);
	}
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

// Called when the game starts or when spawned
void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	auto actor = UGameplayStatics::GetActorOfClass(GetWorld(), ATPSPlayer::StaticClass());
	// ATPSPlayer 타입으로 캐스팅
	target = Cast<ATPSPlayer>(actor);
	// 소유객체 가져오기
	//me = Cast<ANPC>(GetOwner());
	me = this;
	widgetComp->InitWidget();
	hpBar = Cast<UHPBar>(widgetComp->GetUserWidgetObject());
	if (IsValid(hpBar)) {
		//FText idText = FText::FromString(FString::FromInt(12));
		FVector _pos = target->GetActorLocation();
		hpBar->ctextId = GetClass()->GetDisplayNameText();
		hpBar->cInfo = FText::FromString(FString::Printf(TEXT("IDLE")));
		//hpBar->ctextPos = FText::FromString(FString::Printf(TEXT("%f %f %f"), _pos.X, _pos.Y, _pos.Z));
		hpBar->cHP = 10;
		hpBar->cMaxHP = 10;
		hpBar->cMP = 100;
		hpBar->cMaxMP = 100;

		/*widget->SetTextID(GetClass()->GetDisplayNameText());
		FVector _pos = target->GetActorLocation();
		widget->SetTextPosition(FText::FromString(FString::Printf(TEXT("%f %f %f"), _pos.X, _pos.Y, _pos.Z)));
		*/
	}
}

// Called every frame
void AEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void AEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}