* 플레이어의 데이터를 저장하고 이를 불러들이는 로직을 구현한다

* 언리얼 엔진은 게임의 데이터를 저장하고 불러들이는 기능을 제공한다. SaveGame이라는 언리얼 오브젝트를 상속받은 클래스를 설계하고 이를 언리얼이 제공하는 세이브게임 시스템에 넘겨주면 게임 데이터의 저장과 로딩을 간편하게 구현할 수 있다.

* 언리얼의 세이브게임 시스템을 사용하면 각 플랫폼별로 알맞은 최적의 장소에 데이터가 저장되며, 에디터에서 게임 데이터를 저장하는 ㄴ경우 프로젝트의 Saved 폴더에 있는 SaveGames라는 폴더에 게임데이터가 저장된다.

* SaveGame을 상속하여 클래스를 생성한다.

* 게임세이브 기능에는 각 저장 파일에 접근 \할 수 있는 고유 이름인 슬롯 이름이 필요하다. 슬롯 이름을 다르게 지정해 세이브 데이터를 여러 개 만들 수 있는데, 우리는 Player1이라는 슬롯 이름을 사용해 하나의 세이브 파일만 관리해본다. 처음에는 세이브된 게임 데이터가 없으므로 기본 세이트 데이터를 생성하는 로직을 플레이어 스테이트의 InitPlayerData에 구현된다. 

//ABSaveGame.h
#include "ArenaBattle.h"
#include "GameFramework/SaveGame.h"
#include "ABSaveGame.generated.h"

UCLASS()
class ARENABATTLE_API UABSaveGame : public USaveGame
{
	GENERATED_BODY()
	
public:
	UABSaveGame();

	UPROPERTY()
	int32 Level;

	UPROPERTY()
	int32 Exp;

	UPROPERTY()
	FString PlayerName;

	UPROPERTY()
	int32 HighScore;
};

//ABSaveGame.cpp
#include "ABSaveGame.h"

UABSaveGame::UABSaveGame()
{
	Level = 1;
	Exp = 0;
	PlayerName = TEXT("Guest");
	HighScore = 0;
}

//ABPlayerState.h
class ARENABATTLE_API AABPlayerState : public APlayerState
{


public:
	int32 GetGameHighScore() const;

protected:
	UPROPERTY(Transient)
	int32 GameHighScore;
}

//ABPlayerState.cpp
#include "ABSaveGame.h"

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

	GameHighScore = 0;
	SaveSlotName = TEXT("Player1");

}

int32 AABPlayerState::GetGameHighScore() const
{
	return GameHighScore;
}

void AABPlayerState::InitPlayerData()
{
	auto ABSaveGame = Cast<UABSaveGame>(UGameplayStatics::LoadGameFromSlot(SaveSlotName, 0));
	if (nullptr == ABSaveGame)
	{
		ABSaveGame = GetMutableDefault<UABSaveGame>();
	}


	SetPlayerName(ABSaveGame->PlayerName);
	SetCharacterLevel(ABSaveGame->Level);
	GameScore = 0;
	GameHighScore = ABSaveGame->HighScore;
	Exp = ABSaveGame->Exp;
}

void AABPlayerState::AddGameScore()
{
	GameScore++;
	if (GameScore >= GameHighScore)
	{
		GameHighScore = GameScore;
	}
	OnPlayerStateChanged.Broadcast();
}

 

* 시작하면 UI에 초기화된 플레이어의 데이터가 나타난다.

초기화된 화면


* 이제 플레이어에 관련된 데이터가 변경될 때마다 이를 저장하도록 기능을 구현한다. 최초에 플레이어 데이터를 생성한 후 바로 저장하고, 이후 경험치에 변동이 있을 때마다 저장하는 로직은 다음과 같다

void AABPlayerState::InitPlayerData()
{
......
	SavePlayerData();
}

void AABPlayerState::SavePlayerData()
{
	UABSaveGame* NewPlayerData = NewObject<UABSaveGame>();
	NewPlayerData->PlayerName = GetPlayerName();
	NewPlayerData->Level = CharacterLevel;
	NewPlayerData->Exp = Exp;
	NewPlayerData->HighScore = GameHighScore;

	if (UGameplayStatics::SaveGameToSlot(NewPlayerData, SaveSlotName, 0))
	{
		ABLOG(Error, TEXT("SaveGame Error!"));
	}
}

bool AABPlayerState::AddExp(int32 IncomeExp)
{
.....
	SavePlayerData();
	return DidLevelUp;
}

void AABPlayerState::AddGameScore()
{
......
	SavePlayerData();
}

 

* 언리얼 오브젝트를 생성할 때는 NewObject 명령을 사용하며, Newobject로 생성된 오브젝트를 더 이상 사용하지 않으면 언리얼 실행 환경의 가비지 컬렉터가 이를 탐지해 자동으로 언리얼 오브젝트를 소멸시킨다. 따라서 NewObject로 새성한 언리얼 오브젝트를 삭제하기 위해 delete 키워드를 사용하지 않아도 된다. 

* 월드에 액터를 생성하는 작업도 언리얼 오브젝트를 생성하는 작업이라고 할 수 있다. 하지만 액터는 생성할 떄 고려할 점들이 많으므로 언리얼 엔진은 이를 포괄한 SpawnActor라는 API를 제공하고 있다. SpawnActor의 로직 내부를 살펴보면 결국 newObject를 사용해 액터를 생성한다. 


* 이제 플레이어 스테이트의 하이스코어 값을 HUD UI에 연동시킨다.

//UABHUDWidget.cpp

void UABHUDWidget::UpdatePlayerState()
{
...........
  HighScore->SetText(FText::FromString(FString::FromInt(CurrentPlayerState->GetGameHighScore())));
};

 

* 이제 실행을 하면 SaveGames에 파일이 생성된다. 

 

 

* HUD UI 오른쪽 상당에 위치한 게임의 스코어 부분을 구현해본다

* 게임을 처음 시작하면 입장한 플레이어는 0의 점수를 가진다. 문을 통과해 새롭게 생성된 섹션에서 나타난 NPC를 처치하면 플레이어는 1의 점수를 획득한다. 플레이어는 레벨을 계속 탐험하면서 NPC를 처치하고 점수를 획득하는데, 이 점수는 플레이어의 점수를 의미하면서 동시에 게임스코어라고도 할 수 있다. 

* 멀티플레이 콘텐츠를 기획한다면, 플레이어에 설정된 데이터 외에도 게임의 데이터를 관리하는 기능을 추가로 고려해야 한다. 언리얼 엔진은 이를 관리하도록 게임스테이트라는 클래스를 제공한다. 

* 학습을 위해 게임 스테이트를 사용해 게임데이터만 분리함으로 관리하는 기능을 추가해본다.

* GameStateBase 기반의 클래스를 생성한다.

* 전체 게임 스코어를 저장하는 속성을 추가한다.

 

//class ABGameState
//h
#include "ArenaBattle.h"
#include "GameFramework/GameStateBase.h"
#include "ABGameState.generated.h"

UCLASS()
class ARENABATTLE_API AABGameState : public AGameStateBase
{
	GENERATED_BODY()
	

public:
	AABGameState();

public:
	int32 GetTotalGameScore() const;
	void AddGameScore();

private:
	UPROPERTY(Transient)
	int32 TotalGameScore;
};

//cpp
#include "ABGameState.h"

AABGameState::AABGameState()
{
	TotalGameScore = 0;
}

int32 AABGameState::GetTotalGameScore() const
{
	return TotalGameScore;
}

void AABGameState::AddGameScore()
{
	TotalGameScore++;
}
//AABGameMode.cpp
#include "ABGameMode.h"
#include "ABCharacter.h"
#include "ABPlayerController.h"
#include "ABPlayerState.h"
#include "ABGameState.h"


AABGameMode::AABGameMode()
{
....
	GameStateClass = AABGameState::StaticClass();
}

 

* 이번에는 섹션 액터의 로직으로 이동해 섹션에서 생성한 NPC가 플레이어에 의해 제거되면, 이를 감지해 섹션 액터의 스테이트를 COMPLETE로 변경하는 기능을 추가한다.

* NPC가 제거될 떄 마지막으로 대미지를 입힌 컨트롤러의 기록은 LastHitBy 속성에 저장된다. 이를 사용하면 액터가 제거될 떄 마지막에 피격을 가한 플레이어의 정보를 바로 얻어올 수 있다.

* 이전에 구현한 경험치의 경우 대미지를 입을 떄마다 Instigator를 검사하는 방식보다 소멸될 떄 LastHitBy를 사용해 처리하는 것이 효율적이지만, 학습을 위해 두가지 모두 공부

 

//ABSection.h
UCLASS()
class ARENABATTLE_API AABSection : public AActor
{
private:
UFUNCTION()
	void OnKeyNPCDestroyed(AActor* DestoryedActor);
}
//ABSection.cpp
#include "ABPlayerController.h"

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* DestoryedActor)
{
	auto ABCharacter = Cast<AABCharacter>(DestoryedActor);
	ABCHECK(nullptr != ABCharacter);

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

	SetState(ESectionState::COMPLETE);
}

* NPC를 제거해 섹션을 클리어하면 게임모드에게 스코어를 올리라는 명령을 내린다
* 여기서 마지막 피격을 가한 플레이어 컨트롤러 정보를 함꼐 념겨서 해당 플레이어 스테이트의 스코어를 높이고, 더불어 전체 스코어에 해당하는 게임 스테이트의 스코어도 같이 올린다.

* 현재 게임에 참여 중인 플레이어 컨트롤러의 목록은 월드에서 제공하는 GetPlayerControllerIterator를 사용해 얻어올 수 있다. 이를 사용해 게임 스코어를 구현한 코드는 다음과 같다.  

//AABPlayerState.h
{
public:
    void AddGameScore();
}

//cpp
void AABPlayerState::AddGameScore()
{
	GameScore++;
	OnPlayerStateChanged.Broadcast();
}

//ABPlayerController.h
class ARENABATTLE_API AABPlayerController : public APlayerController
{
public:
...
	void AddGameScore() const;
}

//ABPlayerController.cpp
void AABPlayerController::AddGameScore() const
{
	ABPlayerState->AddGameScore();
}

//ABGameMode.h

UCLASS()
class ARENABATTLE_API AABGameMode : public AGameModeBase
{
	GENERATED_BODY()
public:
	AABGameMode();

	virtual void PostInitializeComponents() override;
	virtual void PostLogin(APlayerController* NewPlayer) override;
	void AddScore(class AABPlayerController * ScoredPlayer);

private:
	UPROPERTY()
	class AABGameState* ABGameState;
};

//cpp
#include "ABGameMode.h"
#include "ABCharacter.h"
#include "ABPlayerController.h"
#include "ABPlayerState.h"
#include "ABGameState.h"


AABGameMode::AABGameMode()
{
	DefaultPawnClass = AABCharacter::StaticClass();
	PlayerControllerClass = AABPlayerController::StaticClass();
	PlayerStateClass = AABPlayerState::StaticClass();
	GameStateClass = AABGameState::StaticClass();
}

void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	auto ABPlayerState = Cast<AABPlayerState>(NewPlayer->PlayerState);
	ABCHECK(nullptr != ABPlayerState);
	ABPlayerState->InitPlayerData();
}

void AABGameMode::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	ABGameState = Cast<AABGameState>(GameState);
}

void AABGameMode::AddScore(class AABPlayerController * ScoredPlayer)
{
	for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
	{
		const auto ABPlayerController = Cast<AABPlayerController>(It->Get());
		if ((nullptr != ABPlayerController) && (ScoredPlayer == ABPlayerController))
		{
			ABPlayerController->AddGameScore();
			break;
		}
	}

	ABGameState->AddGameScore();
}

//ABSection.cpp
#include "ABGameMode.h"

void AABSection::OnKeyNPCDestroyed(AActor* DestoryedActor)
{
.......

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

	SetState(ESectionState::COMPLETE);
}

 

* 컴파일 하고 실행결과 NPC가 제거되면 우측 상단의 스코어가 하나씩 올라간다.


UClass에 대해 자세히 설명시 별도의 문서 작성 수준이기에, 일단 간단히 설명해 보면 다음과 같다.

헤더 파일에 UObject를 적법하게 선언하면 UHT(Unreal Header Tool)가 헤더 파일을 분석하는 과정이 진행되며,
이 과정이 완료되면 Intermediate 폴더에 UObject의 정보를 담은 메타 파일이 생성된다.

이 메타 정보는 UClass 라는 특별한 클래스를 통해 보관되며, UObject의 다음의 정보들을 모두 기록하고 있다.

  • 클래스 계층 구조 정보
  • 멤버 변수
  • 멤버 함수

이를 통해, 언리얼 엔진은 C++ 언어 자체가 지원하지 못하는 Reflection을 지원할 수 있다.

컴파일 단계에서는 개별 UObject의 UClass가 생성(Not instance, just declaration/definition)되고, 
실행 초기의 런타임 과정에서는 UObject마다의 UClass의 인스턴스와 UObject의 인스턴스가 생성된다. 

이 과정에서 생성되는 UObject의 인스턴스는 앞으로 월드에 생성될 UObject 인스턴스의 기본 셋팅을 지정하는데 사용되며, 이를 클래스 기본 객체(CDO : Class Default Object)라 한다.

언리얼 엔진에서 CDO를 만드는 이유는 UObject를 생성할 때마다 매번 초기화하지 않고, 
클래스 기본 객체를 미리 만들어 놓고 복제하는 방식으로 구현되어 있기 때문이다. 

월드에 동일한 Npc 몬스터를 수백마리 스폰시켜야 되는 상황을 생각해 보면, 그리고 해당 Npc 몬스터 하나하나가 그리 작지 않은 크기라면, 모든 Npc 몬스터를 처음부터 생성하는 방법보다 미리 큰 객체 덩어리를 복사한 뒤 속성 값만 변경하는 방법이 훨씬 효과적임을 알 수 있다.

참고로, UObject의 생성자 함수는 CDO를 생성하는데에만 사용된다.
즉, 엔진이 초기화되는 런타임 과정에서 생성자가 호출되는 것이지, 월드에 스폰될 때 생성되는 것이 아님을 헤깔리지 말자.

다시 정리하면, 엔진 초기 구동 런타임에 하나의 UObject가 초기화 될 때 다음 2개의 인스턴스가 함께 생성된다.

  • UClass 인스턴스
  • UObject의 CDO 인스턴스

마지막으로 언리얼 엔진은 모듈 방식으로 구현되는데, 개별 모듈마다 속한 모든 UObject의 초기화(UClass / CDO 인스턴스 생성)를 진행한다.

 

'Unreal > Concept' 카테고리의 다른 글

언리얼 게임실행 프로세스  (0) 2019.05.12
각종 포인터 얻어오기  (0) 2019.05.12
TSubclassOf  (0) 2019.05.08
UFUNCTION 지정 매크로  (0) 2019.05.01
UPROPERTY, UCLASS, category, meta ,Transient 지정 매크로  (0) 2019.05.01

TSubclassOf 는 UClass 유형의 안전성을 보장해 주는 템플릿 클래스입니다.
예를 들어 디자이너가 대미지 유형을 지정하도록 해주는 프로젝타일 클래스를 제작중이라 가정합시다.

그냥 UPROPERTY 유형의 UClass 를 만든 다음 디자이너가 항상 UDamageType 파생 클래스만 할당하기를 바라거나, TSubclassOf 템플릿을 사용하여 선택지를 제한시킬 수도 있습니다. 그 차이점은 아래 코드와 같습니다:

/** type of damage */
UPROPERTY(EditDefaultsOnly, Category=Damage)
UClass* DamageType;
Vs.

/** type of damage */
UPROPERTY(EditDefaultsOnly, Category=Damage)
TSubclassOf<UDamageType> DamageType;

두 번째 선언에서, 템플릿 클래스는 에디터의 프로퍼티 창에 UDamageType 파생 클래스만 선택되도록 합니다.
첫 번째 선언에서는 아무 UClass 나 선택할 수 있습니다. 아래 그림에서 확인됩니다.

StrategyGame 의 프로젝타일 블루프린트 예제

이러한 UPROPERTY 안전성에 추가로, C++ 수준에서의 유형 안전성도 확보할 수 있습니다.
비호환 TSubclassOf 유형을 서로에게 할당하려는 순간, 컴파일 오류가 나게 됩니다.
범용 UClass 를 할당하려는 경우, 할당이 가능한지 검증하는 실행시간 검사를 합니다. 실행시간 검사가 실패하면,
결과값은 nullptr 입니다.

 



*  TSubclassOf 템플릿 클래스를 이용해 특정 멤버 변수나 변수를 특정 UObject/UClass의 하위 클래스로 한정지을 수 있다.

이를 통해, 관련없는 녀석이 들어와 런타임 에러를 낸다거나 하는 실수를 사전에 방지할 수 있다.

특히 프로그래머가 아닌 기획/아트에게 제공되는 블루프린트에 노출되는 변수일수록 중요한 타입 제약이라 할 수 있다.

 

// HUD 화면에 사용할 위젯 클래스
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Power, meta = (BlueprintProtected = "true"))
TSubclassOf<UUserWidget> HUDWidgetClass;

'Unreal > Concept' 카테고리의 다른 글

언리얼 게임실행 프로세스  (0) 2019.05.12
각종 포인터 얻어오기  (0) 2019.05.12
UClass  (0) 2019.05.09
UFUNCTION 지정 매크로  (0) 2019.05.01
UPROPERTY, UCLASS, category, meta ,Transient 지정 매크로  (0) 2019.05.01

* 언리얼 엔진은 플레이어의 정보를 관리하기 위한 용도로  PlayerState라는 클래스를 제동한다. PlayerState를 상속받은 ABPlayerState라는 클래스를 생성한다.

* PlayerState 클래스에는 FString 형의 PlayerName 속성과 float 형의 Score 속성이 이미 설계되 있다.
* 이 속성은 보여질 플레이어의 닉네임과 점수를 관리하기 위한 용도로 사용할 수 있다. 게임의 진행상황을 기록하도록 int32 GameScore라는 속성을 추가하고 , 플레이어의 레벨정보를 표시하기 위한 int32형의 CharaceterLevel이라는 속성을 추가해본다

* 새롭게 생성한 ABPlayerState 클래스를 게임 모드의 PlayerStateClass 속성에 지정하면 엔진에서는 플레이어 컨트롤러가 초기화될 떄 함께 해당 클래스의 인스턴스를 생성하고 그포인터 값을 플레이어 컨트롤러의 PlayerState 속성에 저장한다.
*플레이어 컨트롤러의 구성을 완료하는 시점은 게임 모드의 PostLogin 함수이므로 이떄 함께 ABPlayerState 초기화도 완료해주는 것이 좋다.

#include "ArenaBattle.h"
#include "GameFramework/PlayerState.h"
#include "ABPlayerState.generated.h"

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"

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;
}
//AABGameMode.cpp
#include "ABPlayerState.h"

AABGameMode::AABGameMode()
{
	DefaultPawnClass = AABCharacter::StaticClass();
	PlayerControllerClass = AABPlayerController::StaticClass();
	PlayerStateClass = AABPlayerState::StaticClass();
}

void AABGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	auto ABPlayerState = Cast<AABPlayerState>(NewPlayer->PlayerState);
	ABCHECK(nullptr != ABPlayerState);
	ABPlayerState->InitPlayerData();
}

* 플레이어의 레벨 정보는 실제로 캐릭터에 반영해야 한다. 플레이어 컨트롤러가 캐릭터에 빙의할 때 캐릭터의 PlayerState 속성에 플레이어 스테이트의 포인터를 저장하므로 캐릭터에서도 해당 플레이어 스테이트 정보를 바로 가져올 수 있다.

//ABCharacter.cpp

#include "ABPlayerState.h"

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

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

			auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
			ABCHECK(nullptr != ABPlayerState);
			CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
		}

	}
    ....
 }

 

* 이제 플레이를 누르면 우리가 조종하는 캐릭터는 5레벨로 설정된다.
* 이어서 샘플의 UI 애셋을 이용하여 UserWidger 기본 클래스로 하는  ABHUDWidget 클래스를 새로 생성한다
* 그래프 탭에서 ABHUDWidget 클래스를 부모로 설정한다

반드시 설정해야함

* 플레이어 컨트롤러에서 해당 위젯을 생성한 후 이를 화면에 띄우는 기능을 추가한다. UI 애셋의 레퍼런스를 복사해 클래스 정보를 불러들이고, CreateWidget 함수로 위젯인스턴스를 생성한 후 이를 플레이어의 화면에 씌워준다.

//AABPlayerController.h
#include "ArenaBattle.h"
#include "GameFramework/PlayerController.h"
#include "ABPlayerController.generated.h"

UCLASS()
class ARENABATTLE_API AABPlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	AABPlayerController();
	
	virtual void PostInitializeComponents() override;
	virtual void OnPossess(APawn* aPawn) override;
	
	class UABHUDWidget* GetHUDWidget() const;

protected:
	virtual void BeginPlay() override;

	UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = UI)
		TSubclassOf<class UABHUDWidget> HUDWidgetClass;

private:
	UPROPERTY()
		class UABHUDWidget* HUDWidget;
};
////AABPlayerController.cpp
#include "ABPlayerController.h"
#include "ABHUDWidget.h"


AABPlayerController::AABPlayerController()
{
	//WidgetBlueprint'/Game/Book/UI/UI_HUD.UI_HUD'
	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::PostInitializeComponents()
{
	Super::PostInitializeComponents();
	ABLOG_S(Warning);
}

void AABPlayerController::OnPossess(APawn *aPawn)
{
	ABLOG_S(Warning);
	Super::OnPossess(aPawn);
}

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

	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);


	HUDWidget = CreateWidget<UABHUDWidget>(this, HUDWidgetClass);
	HUDWidget->AddToViewport();
}

UABHUDWidget * AABPlayerController::GetHUDWidget() const
{
	return HUDWidget;
}

 

 


실행화면

 

* 해당  UI에는 플레이어의 데이터와 캐릭터의 HP 데이터 정보가 함께 표시된다. 따라서 플레이어 스테이트와 캐릭터ㅡ 스탯 컴포넌트 정보를 모두 해당 HUD에 연동해야 한다. 먼저 플레이어 스테이트에 새로운 델리게이트를 정의하고 플레이어 데이터가 변경될 떄 HUD 신호를 보내 HUD가 관련 UI 윗젯을 업데이트 하도록 구현한다.

 

//ABPlayerState.h
DECLARE_MULTICAST_DELEGATE(FOnPlayerStateChangeDelegate);

UCLASS()
class ARENABATTLE_API AABPlayerState : public APlayerState
{
public:
....

	FOnPlayerStateChangeDelegate OnPlayerStateChanged;
};

 

//ABHUDWidget.h
#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);
	
	
	//void BindTest(AABPlayerState* point);
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;


};
//ABHUDWidget.cpp
#include "ABHUDWidget.h"
#include "Components/ProgressBar.h"
#include "Components/TextBlock.h"
#include "ABCharacterStatComponent.h"
#include "ABPlayerState.h"


void UABHUDWidget::BindCharacterStat(class UABCharacterStatComponent* CharacterStat)
{
	ABCHECK(nullptr != CharacterStat);
	CurrentCharacterStat = CharacterStat;
	CharacterStat->OnHPChanged.AddUObject(this, &UABHUDWidget::UpdateCharacterStat);
}

void UABHUDWidget::BindPlayerState(class AABPlayerState* PlayerState)
{
	ABCHECK(nullptr != PlayerState);
	CurrentPlayerState = PlayerState;
	PlayerState->OnPlayerStateChanged.AddUObject(this, &UABHUDWidget::UpdatePlayerState);

}



void UABHUDWidget::NativeConstruct()
{
	Super::NativeConstruct();
	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());

	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())));

};

* 코드를 완성하면 플레이어 컨트롤러에서 HUD 위젯과 플레이어 스테이트를 연결하고, 캐릭터에서는 HUD 위젯과 캐릭터 스탯 컴포넌트를 연결한다. 

//AABPlayerController.cpp

#include "ABPlayerState.h"



void AABPlayerController::BeginPlay()
{
....

	auto ABPlayerState = Cast<AABPlayerState>(PlayerState);
	ABCHECK(nullptr != ABPlayerState);
	HUDWidget->BindPlayerState(ABPlayerState);
	ABPlayerState->OnPlayerStateChanged.Broadcast();

}
//AABCharacter.cpp

#include "ABHUDWidget.h"

void AABCharacter::SetCharacterState(ECharacterState NewState)
{
.....
	switch (NewState)
	{
	case ECharacterState::LOADING :
	{
		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);

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

			auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
			ABCHECK(nullptr != ABPlayerState);
			CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
		}
        ....
        }
        

*  이제 플레이어 정보와 캐릭터 스탯 정보가 HUD 위젯에 연동이 되는 것을 확인 할 수 있따.

 


연동된 화면

 

* 이어서 플레이어 데이터에 경험치 정보를 표시하도록 기능을 추가해본다. 플레이어와의 전투에서 NPC가 사망하면 NPC의 레벨이 지정된 경험치를 플레이어에게 전달하고 플레이어가 이를 축적해 레벨업 하는 기능을 구현한다

* UI 에서는 해당 정보를 프로그래스바를 표시하기 떄문에 현재 경험치와 해당 레벨에서의 최대 경험치의 비율 정보를 구하는 작업이 필요하다

* 캐릭터 스탯에 NPC를 위한 경험치 값을 설정하고, 플레이어 스테이트에는 플레이어의 경험치 데이터를 보관하도록 설계를 확장해야 한다. 그리고 캐릭터가 사망할 떄 NPC가 플레이어에게 죽는 지 검사하고, 해당 플레이어 컨트롤러를 통해 플레이어 스테잍르르 업데이트 시키는 로직을 추가해야 한다

* 대미지 프레임워크에서 플레이어 컨트롤러의 정보는 가해자 인자로 전달되므로 이를 사용하면 조금 수월해진다.

//ABPlayerState.h
class ARENABATTLE_API AABPlayerState : public APlayerState
{

public:

	float GetExpRatio() const;
	bool AddExp(int32 IncomeExp);


protected:
	UPROPERTY(Transient)
	int32 Exp;

private:
	void SetCharacterLevel(int32 NewCharacterLevel);
	struct FABCharacterData* CurrentStatData;
};
//AABPlayerState.cpp
#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
{
	if (CurrentStatData->NextExp <= KINDA_SMALL_NUMBER)
		return 0.0f;

	float Result = (float)Exp / (float)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;
}
//UABHUDWidget.cpp
void UABHUDWidget::UpdatePlayerState()
{
	ExpBar->SetPercent(CurrentPlayerState->GetExpRatio());
};
//UABCharacterStatComponent.h
{
public:
.....
	int32 GetDropExp() const;
}

//UABCharacterStatComponent.cpp
int32 UABCharacterStatComponent::GetDropExp() const
{
	return CurrentStatData->DropExp;
}
//AABCharacter.h
{
 public:
    	int32 GetExp() const;
}

//AABCharacter.cpp  
int32 AABCharacter::GetExp() const
{
	return CharacterStat->GetDropExp();
}
//ABPlayerController.h
class ARENABATTLE_API AABPlayerController : public APlayerController
{

public:
......
	void NPCKill(class AABCharacter* KilledNPC) const;

private:
......
	UPROPERTY()
		class AABPlayerState* ABPlayerState;
}


//ABPlayerController.cpp
#include "ABCharacter.h"

void AABPlayerController::BeginPlay()
{
.....
	ABPlayerState = Cast<AABPlayerState>(PlayerState);
	auto ABPlayerState = Cast<AABPlayerState>(PlayerState);
	ABCHECK(nullptr != ABPlayerState);
	HUDWidget->BindPlayerState(ABPlayerState);
	ABPlayerState->OnPlayerStateChanged.Broadcast();

}

void AABPlayerController::NPCKill(class AABCharacter* KilledNPC) const
{
	ABPlayerState->AddExp(KilledNPC->GetExp());
}

//AABCharacter.cpp
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 ABPlayerController = Cast<AABPlayerController>(EventInstigator);
			ABCHECK(nullptr != ABPlayerController);
			ABPlayerController->NPCKill(this);
		}
	}

	return FinalDamage;
}

 

* 이제 NPC를 처치 하면 경험치 관련된 항목이 업데이트 된다.

 


+ Recent posts