캐릭터 스테이드 설정

  • 플레이어 캐릭터와 AI 캐릭터의 기능을 체계적으로 관리하기 위해 캐릭터에도 스테이트 머신 모델을 구현한다
  • PRINT 스테이트 : 캐릭터 생성 전의 스테이트, 기본 캐릭터 애셋이 설정돼 있지만, 캐리겉와 UI를 숨겨둔다. 해당 스테이트는 대미지를 입지 않음
  • LOADING 스테이트 : 선택한 캐릭터 애셋을 로딩하는 스테이트, 이때는 게임이 시작된 시점이므로 현재 조종하는 컨트롤러가 AI인지 플레이어인지 구분할 수 있다. 플레이어 컨트롤러인 경우 애셋 로딩이 완료될 떄까지 캐릭터를 조종하지 못하도록 입력을 비활성화 한다
  • READY 스테이트 : 캐릭터 애셋 로딩이 완료된 스테이트. 숨겨둔 캐릭터와 UI를 보여주며, 이떄부터는 공격을 받으면 대미지를 입는다. 플레이어 컨트롤러는 비로소 캐릭터를 조종할 수 있으며, AI 컨트롤러는 행도트리 로직을 구동해 캐릭터를 동작 시킨다
  • DEAD 스테이트 : 캐릭터가 HP를 소진해 사망할 떄 발생하는 스테이트, 죽는 애니메이션을 재생하고 UI 끄는 한편, 충돌 기능을 없애고, 대미지를 입지 않도록 설정한다. 컨트롤러가 플레이어인 경우 입력을 비활성화하고 AI인 경우 행동트리 로직을 중지한다. 일정시간이 지난 후에는 플레이어의 경우 레벨을 재시작하고 AI는 레벨에서 퇴장한다.
  • 학습을 위해 블루프린트와 호환되는 열거형 선언
/////////ArenaBattle.h
UENUM(BlueprintType)
{
    PREINIT,
    LOADING,
    READY,
    DEAD
};

////////ABCharacter.h

class ARENABATTLE_API AABCharacter : public ACharacter
{
    GENERATED_BODY()
public:
    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;
}
///AABCharacter.cpp

AABCharacter::AABCharacter()
{
    AssetIndex =4;
    SetActorHiddenInGame(true);
    HPBarWidget->SetHiddenInGame(true);
    bCanBeDamaged = false;
}

void AABCharacter::BeginPlay()
{
    Super::BeginPlay();


    // Changed in 4.21. copied from PostInitializeComponents()
    auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
    if (nullptr != CharacterWidget)
    {
        CharacterWidget->BindCharacterStat(CharacterStat);
    }

    bIsPlayer = IsPlayerControlled();

    if (bIsPlayer)
    {
        ABPlayerController = Cast<AABPlayerController>(GetController());
        ABCHECK(nullptr != ABPlayerController);
    }
    else
    {
        ABAIController = Cast<AABAIController>(GetController());
        ABCHECK(nullptr != ABAIController);
    }


    auto DefaultSetting = GetDefault<UABCharacterSetting>();

    if (bIsPlayer)
    {
        AssetIndex = 4;
    }
    else
    {
        AssetIndex = FMath::RandRange(0, DefaultSetting->CharacterAssets.Num()-1);
    }

    CharacterAssetToLoad = DefaultSetting->CharacterAssets[AssetIndex];
    auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
    ABCHECK(nullptr != ABGameInstance)

    AssetStreamingHandle = ABGameInstance->StreamableManager.RequestAsyncLoad(CharacterAssetToLoad, FStreamableDelegate::CreateUObject(this, &AABCharacter::OnAssetLoadCompleted));
    SetCharacterState(ECharacterState::LOADING);
}


void AABCharacter::SetCharacterState(ECharacterState NewState)
{
    ABCHECK(CurrentState != NewState);
    CurrentState = NewState;

    switch (NewState)
    {
    case ECharacterState::LOADING :
        SetActorHiddenInGame(true);
        HPBarWidget->SetHiddenInGame(true);
        bCanBeDamaged = false;
        break;

    case ECharacterState::READY:
        SetActorHiddenInGame(false);
        HPBarWidget->SetHiddenInGame(false);
        bCanBeDamaged = true;

        CharacterStat->OnHPIsZero.AddLambda([this]()->void {SetCharacterState(ECharacterState::DEAD);});


        auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
        ABCHECK(nullptr != CharacterWidget);
        CharacterWidget->BindCharacterStat(CharacterStat);
        break;

    case ECharacterState::DEAD:

        SetActorEnableCollision(false);
        GetMesh()->SetHiddenInGame(false);
        HPBarWidget->SetHiddenInGame(true);
        ABAnim->SetDeadAnim();
        bCanBeDamaged = false;

        break;
    }
}

ECharacterState AABCharacter::GetCharacterState() const
{
    return CurrentState;
}
void AABCharacter::OnAssetLoadCompleted()
{
    AssetStreamingHandle->ReleaseHandle();
    TSoftObjectPtr<USkeletalMesh> LoadedAssetPath(CharacterAssetToLoad);
    ABCHECK(LoadedAssetPath.IsValid());

    GetMesh()->SetSkeletalMesh(LoadedAssetPath.Get());
    SetCharacterState(ECharacterState::READY);
}
  • 스테이트 구성이 끝나면, 스테이트에 맞게 행동트리 로직을 수동으로 구동하고 중지할 수 있게, AI 컨트롤러의 구조를 변경한다.
//h
UCLASS()
class ARENABATTLE_API AABAIController : public AAIController
{
public:
    void RunAI();
    void StopAI();
}

//cpp
void AABAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);

}

void AABAIController::RunAI()
{
    if (UseBlackboard(BBAsset, Blackboard))
    {
        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(EBTStopMode::Safe);
    }
}

플레이어가 빙의할 떄 발생하는 캐릭터의 PossessedBy 함수는 제거하고 대신 캐릭터의 READY 스테이트에서 이를 구현한다.

플레이어인 경우 입력을 활성화하고 AI의 경우 행동트리 시스템을 구동하는 로직을 추가한다. 그리고 DEAD 스테이트에는 플레이어의 경우 입력 처리를 중단하고, AI 경우에느 행동트리 시스템을 종료한다.

그리고 타이머를 발동시켜 사망한 이후에 처리할 로직도 구현한다.

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{

private:


	UPROPERTY(EditAnyWhere, BlueprintReadWrite, Category = State, Meta = (AllowPrivateAccess = true))
		float DeadTimer;

	FTimerHandle DeadTimerHandle = {};

};
void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	ABCHECK(CurrentState != NewState);
	CurrentState = NewState;

	switch (NewState)
	{
	case ECharacterState::LOADING :
	{
		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);
		}

		SetActorHiddenInGame(true);
		HPBarWidget->SetHiddenInGame(true);
		bCanBeDamaged = false;
		break;
	}
	case ECharacterState::READY:
	{
		SetActorHiddenInGame(false);
		HPBarWidget->SetHiddenInGame(false);
		bCanBeDamaged = true;

		CharacterStat->OnHPIsZero.AddLambda([this]()->void {SetCharacterState(ECharacterState::DEAD); });


		auto CharacterWidget = Cast<UABCharacterWidget>(HPBarWidget->GetUserWidgetObject());
		ABCHECK(nullptr != CharacterWidget);
		CharacterWidget->BindCharacterStat(CharacterStat);

		if (bIsPlayer)
		{
			SetControlMode(EControlMode::DIABLO);
			GetCharacterMovement()->MaxWalkSpeed = 600.0f;
			EnableInput(ABPlayerController);
		}
		else
		{
			SetControlMode(EControlMode::NPC);
			GetCharacterMovement()->MaxWalkSpeed = 400.0f;
			ABAIController->RunAI();
		}


		break;
	}
	case ECharacterState::DEAD:
	{
		SetActorEnableCollision(false);
		GetMesh()->SetHiddenInGame(false);
		HPBarWidget->SetHiddenInGame(true);
		ABAnim->SetDeadAnim();
		bCanBeDamaged = 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;
	}
	}
}

 

 

+ Recent posts