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

13.2 플레이어 데이터와 UI 연동

by ftftgop3 2024. 5. 21.

PlayerState 클래스

게임 점수 및 플레이어 게임 데이터를 별도의 엑터에서 관리하는 클래스

 

ABPlayerState 클래스 생성 

PlayerState 에는 Playername, Score 속성은 이미 설계

프로젝트에 맞게 GameScore, CharacterLevel 변수 추가

ABPlayerState

UCLASS()
class ARENABATTLE_API AABPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	AABPlayerState();

	int32 GetGameScore() const;
	int32 GetCharacterLevel() const;
	//데이터 초기화 함수
	void InitPlayerData();


protected:
	//스코어 점수
	UPROPERTY(Transient)
		int32 GameScore;
	//캐릭터 레벨
	UPROPERTY(Transient)
		int32 CharacterLevel;
};

#include "ABPlayerState.h"
#include "ABGameInstance.h"

AABPlayerState::AABPlayerState()
{
	CharacterLevel = 1;
	GameScore = 0;
}

int32 AABPlayerState::GetGameScore() const
{
	return GameScore;
}

int32 AABPlayerState::GetCharacterLevel() const
{
	return CharacterLevel;
}

void AABPlayerState::InitPlayerData()
{
	SetPlayerName(TEXT("Destiny"));
	CharacterLevel = 5;
	GameScore = 0;
}

 

게임 모드에서 PlayerState 지정 시 

플레이어 컨트롤러 가 초기화 시점에 해당 클래스 인스턴스를 생성 해 

포인터를 플레이어 컨트롤러의 PlayerState 속성에 저장

게임 모드의 PostLogin에서 새로운 플레이어 스테이트 초기화 진행 

AABGameMode::AABGameMode()
{
	DefaultPawnClass = AABCharacter::StaticClass();
	PlayerControllerClass = AABPlayerController::StaticClass();
	//플레이어 스테이트 지정
	PlayerStateClass = AABPlayerState::StaticClass();
}

void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
	ABLOG(Warning, TEXT("PostLogin Begin"));
	Super::PostLogin(NewPlayer);
	ABLOG(Warning, TEXT("PostLogin End"));
	//PostLogin 시점에 PlayerState 초기화 실행
	auto ABPlayerState = Cast<AABPlayerState>(NewPlayer->PlayerState);
	ABCHECK(nullptr != ABPlayerState);
	ABPlayerState->InitPlayerData();
}

 

ABCharacter

플레이어 레벨 정보를 캐릭터에 반영

컨트롤러가 빙의 시점에 ABPlayer State에 접근 해 데이터 갱신

#include "ABPlayerState.h"

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

	switch (CurrentState)
	{
		case ECharacterState::LOADING:
		{
			
			if (bIsPlayer)
			{
				//플레이어 컨트롤러 입력 비활성화
				DisableInput(ABPlayerController);
				//플레이어 스테이트 접근 해 레벨 지정
				auto ABPlayerState = Cast<AABPlayerState>(PlayerState);
				ABCHECK(nullptr != ABPlayerState);
				CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
			}
            //...
         }
    }

 

HUD UI 사용

캐릭터 정보 를 화면에 표시 및 구현

UI_HUD.uasset 파일을 컨트롤 하는 UserWidget 기본 클래스로 ABHUDWidget 생성

UI_HUD 애셋을 열어 그래프 탭에서 ABHUDWidget 클래스를 부모로 설정

 

ABPlayer Controller 

ABHUDWidget 를 생성 해 화면에 띄우는 기능 추가

UCLASS()
class ARENABATTLE_API AABPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	AABPlayerController();
    class UABHUDWidget* GetHUDWidget() const;
    
protected:
	//UI HUD 애셋 레퍼런스 복사한 클래스 정보
	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = UI)
	TSubclassOf<class UABHUDWidget> HUDWidgetClass;
    
private:
	//위젯 인스턴스
	UPROPERTY()
	class UABHUDWidget* HUDWidget;
}

#include "ABHUDWidget.h"
AABPlayerController::AABPlayerController()
{
	//애셋 레퍼런드 로드
	static ConstructorHelpers::FClassFinder<UABHUDWidget> 
    UI_HUD_C(TEXT("/Game/Book/UI/UI_HUD.UI_HUD_C"));
	if (UI_HUD_C.Succeeded())
	{
		HUDWidgetClass = UI_HUD_C.Class;
	}
}

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

	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);
	//인스턴스 생성 후 화면에 부착
	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	HUDWidget->AddToViewport();
}
UABHUDWidget * AABPlayerController::GetHUDWidget() const
{
	return HUDWidget;
}

 

HUD 데이터 연동

플레이어 스테이트 및 캐릭터 스탯 컴포넌트 데이터를 UI 와 연동

 

ABPlayerState 

델리게이트 생성 해 플레이어 데이터가 변경 시 HUD 위젯 업데이트

DECLARE_MULTICAST_DELEGATE(FOnPlayerStateChangedDelegate);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API AABPlayerState : public APlayerState
{
	GENERATED_BODY()
	public:
    
	//스테이트 값 변경 시 델리게이트 호출
	FOnPlayerStateChangedDelegate OnPlayerStateChanged;
}

 

ABHUDWidget 

UI 컨트롤 할 수 있게 각각의 변수에 연동

플레이어 스테이트 변경 하는 델리게이트에 함수 등록

캐릭터 스탯의 HP 변경 시 호출 되는 델리게이트에 함수 등록

갱신 된 데이터를 UI에 적용

 

#pragma once

#include "ArenaBattle.h"
#include "Blueprint/UserWidget.h"
#include "ABHUDWidget.generated.h"

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABHUDWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	void BindCharacterStat(class UABCharacterStatComponent* CharacterStat);
	void BindPlayerState(class AABPlayerState* PlayerState);

protected:
	virtual void NativeConstruct() override;
	void UpdateCharacterStat();
	void UpdatePlayerState();

private:
	TWeakObjectPtr<class UABCharacterStatComponent> CurrentCharacterStat;
	TWeakObjectPtr<class AABPlayerState> CurrentPlayerState;

	UPROPERTY()
		class UProgressBar* HPBar;

	UPROPERTY()
		class UProgressBar* ExpBar;

	UPROPERTY()
		class UTextBlock* PlayerName;

	UPROPERTY()
		class UTextBlock* PlayerLevel;

	UPROPERTY()
		class UTextBlock* CurrentScore;

	UPROPERTY()
		class UTextBlock* HighScore;
};
#include "ABHUDWidget.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "ABCharacterStatComponent.h"
#include "ABPlayerState.h"

void UABHUDWidget::BindCharacterStat(UABCharacterStatComponent * CharacterStat)
{
	//캐릭터 스탯에 있는 HP 체인지 델리게이트에 함수 등록
	ABCHECK(nullptr != CharacterStat);
	CurrentCharacterStat = CharacterStat;
	CharacterStat->OnHPChanged.AddUObject(this, &UABHUDWidget::UpdateCharacterStat);
}

void UABHUDWidget::BindPlayerState(AABPlayerState * PlayerState)
{
	//플레이어 스테이스에 있는 스테이스 변경 델리게이트에 함수 등록
	ABCHECK(nullptr != PlayerState);
	CurrentPlayerState = PlayerState;
	PlayerState->OnPlayerStateChanged.AddUObject(this, &UABHUDWidget::UpdatePlayerState);
}

void UABHUDWidget::NativeConstruct()
{
	Super::NativeConstruct();
	//각각의 UI를 컨트롤 할 수 있게 변수에 저장

	HPBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("pbHP")));
	ABCHECK(nullptr != HPBar);

	ExpBar = Cast<UProgressBar>(GetWidgetFromName(TEXT("pbExp")));
	ABCHECK(nullptr != ExpBar);

	PlayerName = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtPlayerName")));
	ABCHECK(nullptr != PlayerName);

	PlayerLevel = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtLevel")));
	ABCHECK(nullptr != PlayerLevel);

	CurrentScore = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtCurrentScore")));
	ABCHECK(nullptr != CurrentScore);

	HighScore = Cast<UTextBlock>(GetWidgetFromName(TEXT("txtHighScore")));
	ABCHECK(nullptr != HighScore);
}

void UABHUDWidget::UpdateCharacterStat()
{
	//약 포인트 연결 확인 
	ABCHECK(CurrentCharacterStat.IsValid());
	//HP bar 값 갱신
	HPBar->SetPercent(CurrentCharacterStat->GetHPRatio());
}

void UABHUDWidget::UpdatePlayerState()
{
	//약 포인트 연결 확인
	ABCHECK(CurrentPlayerState.IsValid());
	
	//캐릭터 이름, 레벨, 스코어 갱신
	PlayerName->SetText(FText::FromString(CurrentPlayerState->GetPlayerName()));
	PlayerLevel->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetCharacterLevel())));
	CurrentScore->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetGameScore())));
}

 

Player Controller

HUD 위젯과 플레이어 스테이트 연결

#include "ABPlayerState.h"

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

	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);
	//인스턴스 생성 후 화면에 부착
	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	HUDWidget->AddToViewport();

	//HUD widget 과 PlayState 연동 후 실행 
	auto ABPlayerState = Cast<AABPlayerState>(PlayerState);
	ABCHECK(ABPlayerState != nullptr);
	HUDWidget->BindPlayerState(ABPlayerState);
	ABPlayerState->OnPlayerStateChanged.Broadcast();
}

 

ABCharacter

HUD 위젯 과 캐릭터 스탯 컴포넌트 연결

#include "ABHUDWidget.h"

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

	switch (CurrentState)
	{
		case ECharacterState::LOADING:
		{
			
			if (bIsPlayer)
			{
				//플레이어 컨트롤러 입력 비활성화
				DisableInput(ABPlayerController);

				//HUD Widget 과 캐릭터 스탯 연동
				ABPlayerController->GetHUDWidget()->BindCharacterStat(CharacterStat);

				//플레이어 스테이트 접근 해 레벨 지정
				auto ABPlayerState = Cast<AABPlayerState>(PlayerState);
				ABCHECK(nullptr != ABPlayerState);
				CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
			}
			//캐릭터 및 UI 숨김, 데미지 충돌 안함 
			SetActorHiddenInGame(true);
			HPBarWidget->SetHiddenInGame(true);
			bCanBeDamaged = false;
			break;
		}
        //....
   }
   
}

 

HUD 와 캐릭터 데이터 연동 후 테스트

 

플레이어 경험치 정보 표시

플레이어와 NPC를 죽이면 NPC 레벨에 따른 경험치를 받아서 레벨 업 하는 기능 구현

경험치를 프로그래스 바로 표시 하기 떄문에 현재 경험치와 최대 경험치의 비율이 필요

 

경험치 기능 추가

1. 캐릭터 스태에서 NPC를 위한 경험치 값 설정

2. 플레이어 스테이트 에서 플레이어 경험치 보관 

3. NPC 사망 시 플레이어 스테이트 업데이트 

 

PlayeerState

플레이어 경험치에 따라 레벨 업 및 경험치 비율 계산

UCLASS()
class ARENABATTLE_API AABPlayerState : public APlayerState
{
	GENERATED_BODY()
	
public:
	//현재 경험치와 레벨업에 필요한 경험치 비율 계산 후 리턴
	float GetExpRatio() const;
	//현재 경험치에 변수 값 만큼 더하고, 레벨업 판단
	bool AddExp(int32 IncomeExp);

protected:

	//경험치
	UPROPERTY(Transient)
		int32 Exp;

private:
	//새로운 레벨에 맞는 CurrentStatData로 갱신
	void SetCharacterLevel(int32 NewCharacterLevel);
	struct FABCharacterData* CurrentStatData;
};

// Fill out your copyright notice in the Description page of Project Settings.

#include "ABPlayerState.h"
#include "ABGameInstance.h"

AABPlayerState::AABPlayerState()
{
	CharacterLevel = 1;
	GameScore = 0;
	Exp = 0;
}



void AABPlayerState::InitPlayerData()
{
	SetPlayerName(TEXT("Destiny"));
	SetCharacterLevel(5);
	GameScore = 0;
	Exp = 0;
}

float AABPlayerState::GetExpRatio() const
{
	//KINDA_SMALL_NUMBER 작으면 0.0 리턴
	if (CurrentStatData->NextExp <= KINDA_SMALL_NUMBER)
		return 0.0f;
	//비율 계산
	float Result = (float)Exp / (float)CurrentStatData->NextExp;
	ABLOG(Warning, TEXT("Ratio : %f, Current : %d, Next : %d"), Result, Exp, CurrentStatData->NextExp);
	return Result;
}

bool AABPlayerState::AddExp(int32 IncomeExp)
{
	if (CurrentStatData->NextExp == -1)
		return false;
	//레벨업 판단
	bool DidLevelUp = false;
	//현재 경험치에 변수 값을 더하고
	Exp = Exp + IncomeExp;
	if (Exp >= CurrentStatData->NextExp)
	{
		//레벨 업 경험치를 넘어갈 경우 레벨 업 동작
		Exp -= CurrentStatData->NextExp;
		SetCharacterLevel(CharacterLevel + 1);
		DidLevelUp = true;
	}
	//플레이어 스테이트 변경 델리게이트 실행
	OnPlayerStateChanged.Broadcast();
	return DidLevelUp;
}

void AABPlayerState::SetCharacterLevel(int32 NewCharacterLevel)
{

	auto ABGameInstance = Cast<UABGameInstance>(GetGameInstance());
	ABCHECK(nullptr != ABGameInstance);
	//테이블 객체에 전급 후 새로운 레벨에 맞는 데이터 갱신
	CurrentStatData = ABGameInstance->GetABCharacterData(NewCharacterLevel);
	ABCHECK(nullptr != CurrentStatData);

	CharacterLevel = NewCharacterLevel;
}

 

ABHUDWidget

플레이어 스테이스 변경 시 동작하는 함수에 경험치 비율에 따른 exp bar 갱신 기능 추가

void UABHUDWidget::UpdatePlayerState()
{
	//약 포인트 연결 확인
	ABCHECK(CurrentPlayerState.IsValid());
	//경험치 바 갱신
	ExpBar->SetPercent(CurrentPlayerState->GetExpRatio());

	//캐릭터 이름, 레벨, 스코어 갱신
	PlayerName->SetText(FText::FromString(CurrentPlayerState->GetPlayerName()));
	PlayerLevel->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetCharacterLevel())));
	CurrentScore->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetGameScore())));
}

 

ABCharacterStatComponent

NCP가 사망 시 경험치 값 지정

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	GENERATED_BODY()
    public:
    int32 GetDropExp() const; // 현재 경혐치 값 리턴
}

int32 UABCharacterStatComponent::GetDropExp() const
{
	return CurrentStatData->DropExp;
}

 

ABCharacter

현재 캐릭터의 ABCharacterStatComponent 접근 해 경험치 변수 값 를 리턴

NPC 죽음 체크

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()

	public:
	//현재 경험치 값 리턴 
	int32 GetExp() const;
}

//현재 스태에 가지고 있는 경험치 값 리턴
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);
	//대미지를 받고 DEAD 상태
	if (CurrentState == ECharacterState::DEAD)
	{
		//가해자가 플레이어일 경우
		if (EventInstigator->IsPlayerController())
		{
			//NPC 죽음으로 판단 
			auto ABPlayerController = Cast<AABPlayerController>(EventInstigator);
			ABCHECK(nullptr != ABPlayerController, 0.0f);
			ABPlayerController->NPCKill(this);
		}
	}
	return FinalDamage;
}

 

ABPlayerController

 NPC 사망 시 경험치 증가 기능 추가

UCLASS()
class ARENABATTLE_API AABPlayerController : public APlayerController
{
	GENERATED_BODY()
    public:
    //NPC 사망 시 호출 
    void NPCKill(class AABCharacter* KilledNPC) const;
    
    private:
    UPROPERTY()
	class AABPlayerState* ABPlayerState;
}

#include "ABCharacter.h"

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

	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);
	//인스턴스 생성 후 화면에 부착
	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	HUDWidget->AddToViewport();

	//HUD widget 과 PlayState 연동 후 실행
    //ABPlayerState 변수로 지정
	ABPlayerState = Cast<AABPlayerState>(PlayerState);
	ABCHECK(ABPlayerState != nullptr);
	HUDWidget->BindPlayerState(ABPlayerState);
	ABPlayerState->OnPlayerStateChanged.Broadcast();
}

//현재 스테이트의 경험치에 죽은 NPC 경험치 더하기
void AABPlayerController::NPCKill(AABCharacter* KilledNPC) const
{
	ABPlayerState->AddExp(KilledNPC->GetExp());
}