지금까지 만든 프로젝트를 토대로 이제는 구현에 중점이 아닌 게임스러움을 추구하며 바꾸려 한다
오늘은 캐릭터 스테이트를 나누고, 이에 따라 게임의 진행 흐름을 만들고자 한다
캐릭터를 체계적으로 관리하기 위해 스테이트 머신을 통해 구현하고자 한다
PREINIT
캐릭터 생성 전의 스테이트, 에셋을 설정하고 캐릭터와 UI를 숨겨둔다LOADING
게임이 시작되는 단계, 조종하는 컨트롤러가 유저인지 AI 인지 설정한다READY
숨겨둔 캐릭터와 UI 를 보여주고, 조종하며 전투가 진행된다DEAD
캐릭터의 HP 가 0 이 되는 스테이트, AI는 행동 트리가 비활성되며 유저는 레벨이 재시작된다ArenaBattle.h 파일에 캐릭터 상태에 대한 enum class 를 선언해준다
ArenaBattle.h
UENUM(BlueprintType)
enum class ECharacterState : uint8
{
PREINIT,
LOADING,
READY,
DEAD
};
...
먼저 AI 컨트롤러가 상태에 맞게 수정이 될 수 있게 구현되어있는 부분들을 바꿔주도록 한다
ABAIController.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "AIController.h"
#include "ABAIController.generated.h"
/**
*
*/
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
GENERATED_BODY()
public:
AABAIController();
virtual void OnPossess(APawn* InPawn) override;
static const FName HomePosKey;
static const FName PatrolPosKey;
static const FName TargetKey;
void RunAI();
void StopAI();
...
}
ABAIController.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
const FName AABAIController::HomePosKey(TEXT("HomePos"));
const FName AABAIController::PatrolPosKey(TEXT("PatrolPos"));
const FName AABAIController::TargetKey(TEXT("Target"));
AABAIController::AABAIController()
{
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject
(TEXT("/Game/Book/AI/BB_ABCharacter.BB_ABCharacter"));
if (BBObject.Succeeded())
BBAsset = BBObject.Object;
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject
(TEXT("/Game/Book/AI/BT_ABCharacter.BT_ABCharacter"));
if (BTObject.Succeeded())
BTAsset = BTObject.Object;
}
void AABAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
}
void AABAIController::RunAI()
{
UBlackboardComponent* BlackboardComponent = Blackboard;
if (UseBlackboard(BBAsset, BlackboardComponent))
{
Blackboard->SetValueAsVector(HomePosKey, GetPawn()->GetActorLocation());
if (!RunBehaviorTree(BTAsset))
ABLOG(Error, TEXT("AIController couldn't run behavior tree!"));
}
}
void AABAIController::StopAI()
{
auto BehaviorTreeComponent = Cast<UBehaviorTreeComponent>(BrainComponent);
if (nullptr != BehaviorTreeComponent)
BehaviorTreeComponent->StopTree();
}
이때 원래 OnPossess에 있던 내용을 ABCharacter에서 대신 해줄것이기 때문에 지워주도록 한다
캐릭터 클래스에 만들어둔 열거형을 선언해주고, 각 스테이트 마다 캐릭터의 상태를 설정해준다
ABCharacter.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "ArenaBattle.h"
#include "GameFramework/Character.h"
#include "ABCharacter.generated.h"
DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AABCharacter();
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;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = State, Meta = (AllowprivateAccess = true))
float DeadTimer;
FTimerHandle DeadTimerHandle = {};
};
ABCharacter.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "ABCharacter.h"
#include "ABAnimInstance.h"
#include "DrawDebugHelpers.h"
#include "ABWeapon.h"
#include "ABCharacterStatComponent.h"
#include "Components/WidgetComponent.h"
#include "ABCharacterWidget.h"
#include "ABAIController.h"
#include "ABGameInstance.h"
#include "ABCharacterSetting.h"
#include "Engine/AssetManager.h"
#include "ABPlayerController.h"
#include "ABSection.h"
// Sets default values
AABCharacter::AABCharacter()
{
// 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;
...
AssetIndex = 4;
SetActorHiddenInGame(true);
HPBarWidget->SetHiddenInGame(true);
SetCanBeDamaged(false);
DeadTimer = 5.0f;
}
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
CurrentState = NewState;
switch (CurrentState)
{
case ECharacterState::LOADING:
{
if (bIsPlayer)
DisableInput(ABPlayerController);
SetActorHiddenInGame(true);
HPBarWidget->SetHiddenInGame(true);
SetCanBeDamaged(false);
break;
}
case ECharacterState::READY:
{
SetActorHiddenInGame(false);
HPBarWidget->SetHiddenInGame(false);
SetCanBeDamaged(true);
CharacterStat->OnHPIsZero.AddLambda([this]() -> void { SetCharacterState(ECharacterState::DEAD); });
auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
CharacterWidget->BindCharacterStat(CharacterStat);
if (bIsPlayer)
{
SetControlMode(EControlMode::QUARTERVIEW);
GetCharacterMovement()->MaxWalkSpeed = 600.0f;
EnableInput(ABPlayerController);
}
else
{
SetControlMode(EControlMode::NPC);
GetCharacterMovement()->MaxWalkSpeed = 300.0f;
ABAIController->RunAI();
}
break;
}
case ECharacterState::DEAD:
{
SetActorEnableCollision(false);
GetMesh()->SetHiddenInGame(false);
HPBarWidget->SetHiddenInGame(true);
ABAnim->SetDeadAnim();
SetCanBeDamaged(false);
if (bIsPlayer)
DisableInput(ABPlayerController);
else
ABAIController->StopAI();
GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]() ->void
{
if (bIsPlayer)
ABPlayerController->RestartLevel();
else
Destroy();
}), DeadTimer, false);
break;
}
}
}
ECharacterState AABCharacter::GetCharacterState() const
{
return CurrentState;
}
// Called when the game starts or when spawned
void AABCharacter::BeginPlay()
{
Super::BeginPlay();
bIsPlayer = IsPlayerControlled();
if (bIsPlayer)
ABPlayerController = Cast<AABPlayerController>(GetController());
else
ABAIController = Cast<AABAIController>(GetController());
auto DefaultSetting = GetDefault<UABCharacterSetting>();
if (bIsPlayer)
AssetIndex = 4;
else
AssetIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num() - 1);
CharacterAssetToLoad = DefaultSetting->CharacterAssets[AssetIndex];
AssetStreamingHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad
(CharacterAssetToLoad, FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
SetCharacterState(ECharacterState::LOADING);
}
...
void AABCharacter::OnAssetLoadCompleted()
{
USkeletalMesh* AssetLoaded = Cast<USkeletalMesh>(AssetStreamingHandle->GetLoadedAsset());
AssetStreamingHandle.Reset();
GetMesh()->SetSkeletalMesh(AssetLoaded);
SetCharacterState(ECharacterState::READY);
}
...
이때 OnPossessed 의 함수는 이제 사용하지 않기 때문에 지워주도록 한다
설정한대로 초기화가 잘 되며 죽은 이후도 잘 적용이 된다