게임플레이의 제작
‘이득우의 언리얼 c++ 게임 개발의 정석’ 책을 참고하여 작성한 포스트입니다.
언리얼 엔진이 제공하는 플레이어 스테이트와 게임 스테이트를 알아보고,
이들을 바탕으로 게임에 사용할 데이터를 체계적으로 관리하는 기능을 구현해 본다.
캐릭터 스테이트의 설정
- 구현할 캐릭터의 스테이트는 다음과 같다.
- PREINIT state
- 캐릭터 생성전의 스테이트.
- 기본 캐릭터 에셋이 설정돼 있지만 캐릭터와 UI를 숨겨둔다.
- 데미지를 입지 않는 상태
- LOADING state
- 캐릭터 에셋을 로딩하는 스테이트
- 게임이 시작된 시점이다.
- READY state
- 에셋 로딩이 완료된 스테이트.
- 숨겨둔 캐릭터와 UI를 보여준다.
- 플레이어는 이때부터 조작이 가능해지며, AI 컨트롤러는 비헤이비어 트리 로직을 구동한다.
- 데미지를 입을 수 있는 상태
- DEAD state
- 죽는 애니메이션을 재생하고 UI 및 충돌 기능을 끈다.
- 플레이어인 경우 입력을 비활성화 하고, AI 의 경우 비헤이비어 트리 로직을 중지한다.
- PREINIT state
- 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