본문 바로가기
언리얼 C++ 게임 개발의 정석/13. 게임 플레이 제작

13.1 캐릭터의 스테이스 설정

by ftftgop3 2024. 5. 18.

캐릭터와 AI 캐릭터를 스테이트 머신 모델로 구현

스테이트 종류

PREINIT 스테이트 : 캐릭터 생성 전의 스테이트, 캐릭터와 UI 숨김

 

LOADING 스테이트 : 선택한 캐릭터 애셋 로딩 스테이트

로딩 완료 전까지 플레이어 컨트롤러 입력 부분 비활성화 

 

READY 스테이트 : 캐릭터 애셋 로딩 완료, 캐릭터 및 UI 활성화,

플레이어 컨트롤러 동작, AI 컨트롤러 비헤이비어 트리 로직 구동

 

DEAD 스테이트 : 캐릭터 HP 소진해 사망 스테이트

플레이어 컨트롤러는 입력 중지, UI 비활성화, 충돌 비활성화

AI 컨트롤러는 비헤이비어 트리 로직 중단

일정 시간 후 플레이어의 경우 재시작, AI 는 퇴장

 

 

ArenaBattle.h

캐릭터 스테이스 열거형 정의

C++, 블루 프린트에서 사용 할 수 있게 정의

UENUM(BlueprintType)
enum class ECharacterState : uint8
{
	PREINIT,
	LOADING,
	READY,
	DEAD
};

DECLARE_LOG_CATEGORY_EXTERN(ArenaBattle, Log, All);

ABCharacter.h

캐릭터 상태 변수 추가 및 함수 셋팅

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
    
    public:
	//캐릭터 상태 Set, get
	void SetCharacterState(ECharacterState NewState);
	ECharacterState GetCharacterState() const;
    
    private:
    //캐릭터 생성 에셋 인덱스
	int32 AssetIndex = 0;
	//현재 캐릭터 상태
	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = State, Meta = (AllowPrivateAccess = true))
		ECharacterState CurrentState;
	//플레이어 인지 판단
	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = State, Meta = (AllowPrivateAccess = true))
		bool bIsPlayer;

	UPROPERTY()
		class AABAIController* ABAIController;

	UPROPERTY()
		class AABPlayerController* ABPlayerController;
};

 

 

ABCharacter.cpp

PREINIT 스테이트

캐릭터는 플레이어 인지 판단 하는 부분을 BeginPlay 로 사용

현재 구성은 PossessedBy 함수는 2번 호출 (플레이어에 AI 컨트롤러 자동 부착) 

 

BeginPlay 호출 하면 플레이어 또는 NPC 인지 판단 해 IsPlayer 변수에 지정

캐릭터 애셋 로딩을 시작 하고 LOADING로 변경

 

LOADING 스테이트에서 캐릭터 애셋 로딩이 완료 되면

READY 스테이트로 변경 플레이어 동작 및 게임 진행 

게임 진행 도중 HP 0 되면 DEAD 스테이트로 변경

#include "ABPlayerController.h"

AABCharacter::AABCharacter()
{
    AssetIndex = 4;
	//캐릭터, hp bar 인게임 비활성화 
	SetActorHiddenInGame(true);
	HPBarWidget->SetHiddenInGame(true);
	bCanBeDamaged = false;
}

void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	//플레이어 컨트롤러인지 판단
	bIsPlayer = IsPlayerControlled();
	if (bIsPlayer)
	{
		ABPlayerController = Cast<AABPlayerController>(GetController());
		ABCHECK(nullptr != ABPlayerController);
	}
	else
	{
		ABAIController = Cast<AABAIController>(GetController());
		ABCHECK(nullptr != ABAIController);
	}
	//INI 파일에서 데이터 로드된 UABCharacterSetting 참조
	auto DefaultSetting = GetDefault<UABCharacterSetting>();

	if (bIsPlayer)
	{
		AssetIndex = 4;
	}
	else
	{
		AssetIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num() - 1);
	}
	//AssetIndex 값으로 캐릭터 애셋 지정 
	CharacterAssetToLoad = DefaultSetting->CharacterAssets[AssetIndex];
	//GameInstance 객체 참조
	auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
	ABCHECK(nullptr != ABGameInstance);
	//비동기 로드 실행, 경로, 델리게이트를 사용해 오브젝트 생성후 OnAssetLoadCompleted() 실행
	AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad(CharacterAssetToLoad,
    FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
	//로드로 상태 변경
	SetCharacterState(ECharacterState::LOADING);
}


void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	//현재 상태와 새로운 상태 비교
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (CurrentState)
	{
		case ECharacterState::LOADING:
		{
			//캐릭터 및 UI 숨김, 데미지 충돌 안함 
			SetActorHiddenInGame(true);
			HPBarWidget->SetHiddenInGame(true);
			bCanBeDamaged = false;
			break;
		}
		case ECharacterState::READY:
		{
			//캐릭터 및 UI 활성화, 데미지 충돌 활성화 
			SetActorHiddenInGame(false);
			HPBarWidget->SetHiddenInGame(false);
			bCanBeDamaged = true;
			//HP가 0경우 발생 하는 델리게이트에 DEAD 상태 지정 하게 등록
			CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
				SetCharacterState(ECharacterState::DEAD);
				});
			
			auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
			ABCHECK(nullptr != CharacterWidget);
			//스탯이랑 HP bar 연결
			CharacterWidget->BindCharacterStat(CharacterStat);
			break;
		}
		case ECharacterState::DEAD:
		{
			//충돌 체크 비활성화, 메쉬는 활성화, HP bar 비활성화
			SetActorEnableCollision(false);
			GetMesh()->SetHiddenInGame(false);
			HPBarWidget->SetHiddenInGame(true);
			//죽는 애니메이션 실행, 데미지 충돌 체크 비활성화
			ABAnim->SetDeadAnim();
			bCanBeDamaged = false;
			break;
		}
	}
}

ECharacterState AABCharacter::GetCharacterState() const
{
	return CurrentState;
}

void AABCharacter::OnAssetLoadCompleted()
{
	//로드 된 데이터를 스켈레톤 메쉬로 캐스트 
	USkeletalMesh* AssetLoaded = Cast<USkeletalMesh>(AssetStreamingHandle->GetLoadedAsset());
	//핸들 초기화 
	AssetStreamingHandle.Reset();
	ABCHECK(AssetLoaded != nullptr)

	//현재 메쉬 스켈레톤으로 지정
	GetMesh()->SetSkeletalMesh(AssetLoaded);
	//캐릭터 애셋 로드 완료 했으니 READY 상태 변경
	SetCharacterState(ECharacterState::READY);
}

 

ABAICharacter

스테이트에 맞게 비헤이비어 트리 로직을 수동으로 정지, 시작하게 구조 변경

void AABAIController::Possess(APawn* InPawn)
{
	Super::Possess(InPawn);
}

void AABAIController::RunAI()
{
	//Blackboard 맞는 지 확인
	if (UseBlackboard(BBAsset, Blackboard))
	{
		//블랙 보드의 위치 값 갱신
		Blackboard->SetValueAsVector(HomePosKey, GetPawn()->GetActorLocation());
		//비헤어비어 트리 실행
		if (!RunBehaviorTree(BTAsset))
		{
			ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
		}
	}
}

void AABAIController::StopAI()
{
	//AAIController 의 매개변수를 이용해 UBehaviorTreeComponent 캐스팅
	auto BehaviorTreeComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
	if (nullptr != BehaviorTreeComponent)
	{
		//정지
		BehaviorTreeComponent->StopTree(EBTStopMode::Safe);
	}
}

 

ABCharacter

기존의 PossessedBy 에서 처리하는 부분을 READY 스테이트에서 구현

READY 스테이트에서 플레이어 경우 입력 활성화, AI 경우 비헤이비어 트리 구동

DEAD 스테이트 플레이어 경우 입력 비 활성화, AI 경우 비헤어비어 트리 정지

 

사망한 이후 처리할 로직을 타이머로 구현

5초 후 플레이어 사망 시 레벨 리스타트, NCP 엑터 일 경우 삭제 

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()
 	private:
    //죽은 후 동작 하는 타이머 실행 시간
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = State, Meta = (AllowPrivateAccess = true))
		float DeadTimer;
	//죽은 후 동작 하는 타이머
	FTimerHandle DeadTimerHandle = { };
}

AABCharacter::AABCharacter()
{	
	DeadTimer = 5.0f;
}


void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	//현재 상태와 새로운 상태 비교
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (CurrentState)
	{
		case ECharacterState::LOADING:
		{
			
			if (bIsPlayer)
			{
				//플레이어 컨트롤러 입력 비활성화
				DisableInput(ABPlayerController);
			}
			//캐릭터 및 UI 숨김, 데미지 충돌 안함 
			SetActorHiddenInGame(true);
			HPBarWidget->SetHiddenInGame(true);
			bCanBeDamaged = false;
			break;
		}
		case ECharacterState::READY:
		{
			//캐릭터 및 UI 활성화, 데미지 충돌 활성화 
			SetActorHiddenInGame(false);
			HPBarWidget->SetHiddenInGame(false);
			bCanBeDamaged = true;
			//HP가 0경우 발생 하는 델리게이트에 DEAD 상태 지정 하게 등록
			CharacterStat->OnHPIsZero.AddLambda([this]() -> void {
				SetCharacterState(ECharacterState::DEAD);
				});
			
			auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
			ABCHECK(nullptr != CharacterWidget);
			//스탯이랑 HP bar 연결
			CharacterWidget->BindCharacterStat(CharacterStat);
			
			//플레이어 판단 
			if (bIsPlayer)
			{
				//컨트롤 모드 DIABLO, 속도 설정 후 입력 활성화
				SetControlMode(EControlMode::DIABLO);
				GetCharacterMovement()->MaxWalkSpeed = 600.0f;
				EnableInput(ABPlayerController);
			}
			else
			{
				//컨트롤 모드 NPC, 속도 설정 후 AI 비헤어비어 실행
				SetControlMode(EControlMode::NPC);
				GetCharacterMovement()->MaxWalkSpeed = 400.0f;
				ABAIController->RunAI();
			}
			break;
		}
		case ECharacterState::DEAD:
		{
			//충돌 체크 비활성화, 메쉬는 활성화, HP bar 비활성화
			SetActorEnableCollision(false);
			GetMesh()->SetHiddenInGame(false);
			HPBarWidget->SetHiddenInGame(true);
			//죽는 애니메이션 실행, 데미지 충돌 체크 비활성화
			ABAnim->SetDeadAnim();
			bCanBeDamaged = false;

			if (bIsPlayer)
			{
				//플레이어 입력 비활성화
				DisableInput(ABPlayerController);
			}
			else
			{
				//AI 비헤어비어 정지
				ABAIController->StopAI();
			}
			//죽음 타이머 동작, 
			GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]() -> void {

				if (bIsPlayer)
				{
					//5초 후 레벨 리스타트
					ABPlayerController->RestartLevel();
				}
				else
				{
					//npc 엑터 파괴
					Destroy();
				}

				}), DeadTimer, false);
			break;
		}
	}
}