6 minute read

‘이득우의 언리얼 c++ 게임 개발의 정석’ 책을 참고하여 작성한 포스트입니다.


언리얼 엔진이 제공하는 플레이어 스테이트와 게임 스테이트를 알아보고,
이들을 바탕으로 게임에 사용할 데이터를 체계적으로 관리하는 기능을 구현해 본다.


캐릭터 스테이트의 설정

  • 구현할 캐릭터의 스테이트는 다음과 같다.
    • PREINIT state
      • 캐릭터 생성전의 스테이트.
      • 기본 캐릭터 에셋이 설정돼 있지만 캐릭터와 UI를 숨겨둔다.
      • 데미지를 입지 않는 상태
    • LOADING state
      • 캐릭터 에셋을 로딩하는 스테이트
      • 게임이 시작된 시점이다.
    • READY state
      • 에셋 로딩이 완료된 스테이트.
      • 숨겨둔 캐릭터와 UI를 보여준다.
      • 플레이어는 이때부터 조작이 가능해지며, AI 컨트롤러는 비헤이비어 트리 로직을 구동한다.
      • 데미지를 입을 수 있는 상태
    • DEAD state
      • 죽는 애니메이션을 재생하고 UI 및 충돌 기능을 끈다.
      • 플레이어인 경우 입력을 비활성화 하고, AI 의 경우 비헤이비어 트리 로직을 중지한다.
  • UENUM(BlueprintType)으로 선언하고 uint8로 기반 유형(underlying type)을 지정해 준다.
  • 현재 설정 상 하나의 캐릭터를 주인공과 NPC가 공유해서 사용하고 있고, 플레이어도 전용 AI 컨트롤러가 자동으로 부착되어 PossessedBy 함수가 두 번 호출 된다.
  • 그러므로 NPC와 플레이어를 구분을 확실히 할 수 있는 시점은 BeginPlay다.
  • BeginPlay에서 bIsPlayer 변수를 이용해 구분하고, 이어서 선택된 캐릭터 에셋의 로딩을 시작 후 스테이트를 LOADING으로 변경한다.
  • 로딩이 완료되면 READY 상태로 변경해준다.

BeginPlay 코드

// character.h
private:
	FSoftObjectPath CharacterAssetToLoad = FSoftObjectPath(nullptr);
	TSharedPtr<struct FStreamableHandle> AssetStreamingHandle;

// character.cpp
void AABCharacter::BeginPlay()
{
	Super::BeginPlay();
	bIsPlayer = IsPlayerControlled();
	if (bIsPlayer)
	{
		ABPlayerController = Cast<AABPlayerController>(GetController());
		LSCHECK(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::OnAssetLoadCompleted()
{
	USkeletalMesh* AssetLoaded = Cast<USkeletalMesh>(AssetStreamingHandle->GetLoadedAsset());
	AssetStreamingHandle.Reset(); // ?? 더 로딩 하는 걸 멈춰주나?

	ABCHECK(nullptr != AssetLoaded);
	GetMesh()->SetSkeletalMesh(AssetLoaded);
	SetCharacterState(ECharacterState::READY);
}

FSoftObjectPath

  • A struct that contains a string reference to an object, either a top level asset or a subobject.

FStreamableHandle

  • A handle to a synchronous or async load.

FStreamableManager

  • A native class for managing streaming assets in and keeping them in memory.
  • AssetManager is the global singleton version of this with blueprint access.

FStreamableManager::RequestAsyncLoad

  • This is the primary streamable operation.
  • FStreamableDelegate이 매개변수로 들어온 오버로딩
TSharedPtr< FStreamableHandle > RequestAsyncLoad
(	
	// Assets to load off disk
    TArray< FSoftObjectPath > TargetsToStream,
	// Delegate to call when load finishes.
	// Will be called on the next tick if asset is already loaded, or many seconds later
    FStreamableDelegate DelegateToCall,
	// Priority to pass to the streaming system, higher priority will be loaded first
    TAsyncLoadPriority Priority,
    bool bManageActiveHandle,
    bool bStartStalled,
    FString DebugName
)

FStreamableHandle::GetLoadedAsset

  • Returns first asset in requested asset list, if it’s been successfully loaded.

SetCharacterState 코드

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

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

			// UABHUDWidget
			ABPlayerController->GetHUDWidget()->BindCharacterStat(CharacterStat);

			auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
			ABCHECK(nullptr != ABPlayerState);
			CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
		}
		else
		{
			auto ABGameMode = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());
			ABCHECK(nullptr != ABGameMode);
			int32 TargetLevel = FMath::CeilToInt(((float)ABGameMode->GetScore() * 0.8f));
			int32 FinalLevel = FMath::Clamp<int32>(TargetLevel, 1, 20);
			ABLOG(Warning, TEXT("New NPC Level : %d"), FinalLevel);
			CharacterStat->SetNewLevel(FinalLevel);
		}

		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());
		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();
		SetCanBeDamaged(false);
		
		if(bIsPlayer)
		{
			DisableInput(ABPlayerController);
		}
		else
		{
			ABAIController->StopAI();
		}

		GetWorld()->GetTimerManager().SetTimer(DeadTimerHandle, FTimerDelegate::CreateLambda([this]()->void {
			if (bIsPlayer)
			{
				//ABPlayerController->RestartLevel();
				ABPlayerController->ShowResultUI();
			}
			else
			{
				Destroy();
			}
		}), DeadTimer, false);
	}
	}
}

AActor::DisableInput

  • Removes this actor from the stack of input being handled by a PlayerController.
virtual void DisableInput
(
    class APlayerController * PlayerController
)

UWorld::GetAuthGameMode

  • Returns the current Game Mode instance, which is always valid during gameplay on the server.

USceneComponent::SetHiddenInGame

  • Changes the value of bHiddenInGame, if false this will disable Visibility during gameplay

AActor::SetCanBeDamaged

  • Sets the value of bCanBeDamaged without causing other side effects to this instance.

AI의 비헤이비어 트리 구동 활성 비활성화

  • OnPossessed 함수에서 아래 코드를 밖으로 빼준다.
void AABAIController::RunAI()
{
    UBlackboardComponent* BlackboardComp = Blackboard.Get();
    if (UseBlackboard(BBAsset, BlackboardComp))
    {
        this->Blackboard = BlackboardComp;
        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);
    }
}

UBehaviorTreeComponent::StopTree

  • Stops execution
void StopTree
(
    EBTStopMode::Type StopMode
)

AAIController.BrainComponent

  • Component responsible for behaviors.

EBTStopMode::Type

namespace EBTStopMode
{
    enum Type
    {
        Safe,
        Forced,
    }
}

플레이어 데이터와 UI 연동

  • 플레이어 정보를 관리하기 위한 용도로 PlayerState 클래스가 제공된다.
  • 이를 상속받는 클래스를 하나 생성하자.
  • 이 클래스에는 FString 타입의 PlayerName 속성과 float 타입의 Score 속성이 정의되어 있다.
  • 게임 모드의 PlayerStateClass 속성에 지정해주면 플레이어 컨트롤러가 초기화될 때 이 클래스의 인스턴스를 생성하고 그 포인터 값을 플레이어 컨트롤러의 PlayerState 속성에 저장한다.
  • 플레이어 컨트롤러 구성 완료 시점은 게임 모드의 PostLogin 함수이다.
  • 이때 새로 만든 PlayerState 초기화를 완료해 주는 것이 좋다.
  • 플레이어 컨트롤러가 캐릭터에 빙의할 때 캐릭터의 PlayerState 속성에 플레이어 state 속성을 저장하므로 캐릭터에서도 해당 플레이어 스테이트 정보를 바로 가져올 수 있다.
// gamemode.cpp
// constructor
// gamemode의 player state 클래스를 변경
PlayerStateClass = ALSPlayerState::StaticClass();

// GameMode::PostLogin(APlayerController* NewPlayer)
Super:: ....
auto LSPlayerState = Cast<ALSPlayerState>(NewPlayer->PlayerState);
LSPlayerState->InitPlayerData(); // 이 함수로 초기화

// character.cpp
// SetCharacterState
...
case LOADING
if (bIsPlayer)
{
	auto LSPlayerState = Cast<ALSPlayerState>(GetPlayerState());
	CharacterStat->SetNewLevel(LSPlayerState->GetCharacterLevel());
}

AController::GetPlayerState

  • this controller’s PlayerState cast to the template type, or NULL if there is not one. May return null if the cast fails.
/**
* @return this controller's PlayerState cast to the template type, or NULL if there is not one.
* May return null if the cast fails.
*/
template < class T >
T* GetPlayerState() const
{
	return Cast<T>(PlayerState);
}

HUD(Head Up Display) 위젯과 스탯 연동

  • UI 위젯 관리를 위해 UserWidget 클래스를 상속받는 클래스를 하나 생성하자.
  • UI 에셋의 부모 클래스를 생성한 클래스로 설정하면 이 c++ 클래스를 통해 위젯을 관리할 수 있다.
  • 플레이어 컨트롤러 클래스에서 CreateWidget을 통해 위젯 인스턴스를 생성 후 AddToViewport를 이용해 화면에 띄워 준다.
  • PlayerState와 연동하기 위해 PlayerState 클래스에 델리게이트를 생성하고, HUDWidget 클래스에서 HUD 업데이트 함수를 PlayerState의 델리게이트에 연결해 준다.
  • 플레이어 컨트롤러에서 PlayerState 선언 시 PlayerState와 HUDWidget을 연결해 주고, 델리게이트를 한번 실행해 초기화 해준다.
  • 캐릭터 클래스에서는 HUDWidget과 스탯 부분을 연결해 준다.

HUDWidget = CreateWidget(this, HUDWidgetClass);

template<typename WidgetT, typename OwnerT>
WidgetT * CreateWidget
(
    OwnerT * OwningObject,
    TSubclassOf< UUserWidget > UserWidgetClass,
    FName WidgetName
)

UUserWidget::AddToViewport

  • Adds it to the game’s viewport and fills the entire screen, unless SetDesiredSizeInViewport is called to explicitly set the size.
void AddToViewport
(
    // The higher the number,
    // the more on top this widget will be.
    int32 ZOrder
)

HPBar = Cast(GetWidgetFromName(TEXT("pbHP")));

UUserWidget::GetWidgetFromName

  • The uobject widget corresponding to a given name
UWidget* GetWidgetFromName
(
    const FName& Name
) const

UTextRenderComponent::SetText

  • Change the text value and signal the primitives to be rebuilt
  • Fstring 버전은 deprecated 되었음
    void SetText
    (
      const FText& Value
    )
    

경험치 연동

  • 데미지 프레임워크에서 플레이어 컨트롤러의 정보가 Instigator(가해자) 인자로 전달되므로 이를 이용해 적이 죽었을 때 경험치 획득 로직을 구현해 본다.
  • PlayerController 클래스에 NPCKill 함수를 정의하고, 이 함수가 PlayerState의 AddExp 함수를 호출하도록 한다.
  • 캐릭터 클래스의 TakeDamage 함수에서 죽을 때 Isntigator 플레이어 컨트롤러의 NPCKill 함수를 호출하도록 해준다.
  • 경험치는 CharacterStatComponent의 GetDropExp()로 현재 캐릭터 스탯 데이터의 DropExp를 얻을 수 있게 하고,
  • 캐릭터 클래스에서 이를 가져오게 한다.
// PlayerController.cpp
void AABPlayerController::NPCKill(class AABCharacter* KilledNPC) const
{
    ABPlayerState->AddExp(KilledNPC->GetExp());
}

// character.cpp
int32 AABCharacter::GetExp() const
{
	return CharacterStat->GetDropExp();
}

float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const & DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
	ABLOG(Warning, TEXT("Actor : %s took damage : %f"), *GetName(), FinalDamage);

	CharacterStat->SetDamage(FinalDamage);

	if (CurrentState == ECharacterState::DEAD)
	{
		if (EventInstigator->IsPlayerController())
		{
			auto Ikk = Cast<AABPlayerController>(EventInstigator);
			ABCHECK(nullptr != Ikk, 0.0f);
			Ikk->NPCKill(this);
		}
	}

	return FinalDamage;
}

게임 데이터의 관리

  • 플레이어에 설정된 데이터 외에 게임의 데이터를 관리해야 할 때는 GameStateBase 클래스를 상속받은 클래스를 사용해주면 된다.
  • 게임 전역의 상태를 관리하는 클래스이다.
  • 적이 죽으면 스코어를 증가시키는 로직을 구현할 것인데, 이번에는 적이 죽었을 때 LastHitBy 속성을 이용해 본다.
  • NPC가 제거될 대 마지막으로 데미지를 입힌 컨트롤러의 기록이 LastHitBy 속성에 저장된다.
  • OnDestroyed
    • 액터가 Destroy 될 때 호출되는 …
  • 멀티플레이 상황이라면 Section 클래스에서 점수를 관리할 것이다.
void AABSection::OnNPCSpawn()
{
	GetWorld()->GetTimerManager().ClearTimer(SpawnNPCTimerHandle);
	auto KeyNPC = GetWorld()->SpawnActor<AABCharacter>(GetActorLocation() + FVector::UpVector * 88.0f, FRotator::ZeroRotator);
	if (nullptr != KeyNPC)
	{
		KeyNPC->OnDestroyed.AddDynamic(this, &AABSection::OnKeyNPCDestroyed);
	}
}

void AABSection::OnKeyNPCDestroyed(AActor* DestroyedActor)
{
	auto ABCharacter = Cast<AABCharacter>(DestroyedActor);
	ABCHECK(nullptr != ABCharacter);

	auto ABPlayerController = Cast<AABPlayerController>(ABCharacter->LastHitBy);
	ABCHECK(nullptr != ABPlayerController);

	auto ABGameMode = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());
	ABCHECK(nullptr != ABGameMode);
	ABGameMode->AddScore(ABPlayerController);

	SetState(ESectionState::COMPLETE);
}

APawn.LastHitBy

  • Controller of the last Actor that caused us damage.
AController * LastHitBy

Leave a comment