* 언리얼 엔진은 게임의 데이터를 저장하고 불러들이는 기능을 제공한다. SaveGame이라는 언리얼 오브젝트를 상속받은 클래스를 설계하고 이를 언리얼이 제공하는 세이브게임 시스템에 넘겨주면 게임 데이터의 저장과 로딩을 간편하게 구현할 수 있다.
* 언리얼의 세이브게임 시스템을 사용하면 각 플랫폼별로 알맞은 최적의 장소에 데이터가 저장되며, 에디터에서 게임 데이터를 저장하는 ㄴ경우 프로젝트의 Saved 폴더에 있는 SaveGames라는 폴더에 게임데이터가 저장된다.
* SaveGame을 상속하여 클래스를 생성한다.
* 게임세이브 기능에는 각 저장 파일에 접근 \할 수 있는 고유 이름인 슬롯 이름이 필요하다. 슬롯 이름을 다르게 지정해 세이브 데이터를 여러 개 만들 수 있는데, 우리는 Player1이라는 슬롯 이름을 사용해 하나의 세이브 파일만 관리해본다. 처음에는 세이브된 게임 데이터가 없으므로 기본 세이트 데이터를 생성하는 로직을 플레이어 스테이트의 InitPlayerData에 구현된다.
* 언리얼 오브젝트를 생성할 때는 NewObject 명령을 사용하며, Newobject로 생성된 오브젝트를 더 이상 사용하지 않으면 언리얼 실행 환경의 가비지 컬렉터가 이를 탐지해 자동으로 언리얼 오브젝트를 소멸시킨다. 따라서 NewObject로 새성한 언리얼 오브젝트를 삭제하기 위해 delete 키워드를 사용하지 않아도 된다.
* 월드에 액터를 생성하는 작업도 언리얼 오브젝트를 생성하는 작업이라고 할 수 있다. 하지만 액터는 생성할 떄 고려할 점들이 많으므로 언리얼 엔진은 이를 포괄한 SpawnActor라는 API를 제공하고 있다. SpawnActor의 로직 내부를 살펴보면 결국 newObject를 사용해 액터를 생성한다.
* 게임을 처음 시작하면 입장한 플레이어는 0의 점수를 가진다. 문을 통과해 새롭게 생성된 섹션에서 나타난 NPC를 처치하면 플레이어는 1의 점수를 획득한다. 플레이어는 레벨을 계속 탐험하면서 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 인스턴스 생성)를 진행한다.
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 나 선택할 수 있습니다. 아래 그림에서 확인됩니다.
이러한 UPROPERTY 안전성에 추가로, C++ 수준에서의 유형 안전성도 확보할 수 있습니다. 비호환 TSubclassOf 유형을 서로에게 할당하려는 순간, 컴파일 오류가 나게 됩니다. 범용 UClass 를 할당하려는 경우, 할당이 가능한지 검증하는 실행시간 검사를 합니다. 실행시간 검사가 실패하면, 결과값은 nullptr 입니다.
* TSubclassOf 템플릿 클래스를 이용해 특정 멤버 변수나 변수를특정 UObject/UClass의 하위 클래스로 한정지을 수 있다.
이를 통해, 관련없는 녀석이 들어와 런타임 에러를 낸다거나 하는 실수를 사전에 방지할 수 있다.
특히 프로그래머가 아닌 기획/아트에게 제공되는 블루프린트에 노출되는 변수일수록 중요한 타입 제약이라 할 수 있다.
// HUD 화면에 사용할 위젯 클래스
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = Power, meta = (BlueprintProtected = "true"))
TSubclassOf<UUserWidget> HUDWidgetClass;
* 언리얼 엔진은 플레이어의 정보를 관리하기 위한 용도로 PlayerState라는 클래스를 제동한다. PlayerState를 상속받은 ABPlayerState라는 클래스를 생성한다.
* PlayerState 클래스에는 FString 형의 PlayerName 속성과 float 형의 Score 속성이 이미 설계되 있다. * 이 속성은 보여질 플레이어의 닉네임과 점수를 관리하기 위한 용도로 사용할 수 있다. 게임의 진행상황을 기록하도록 int32 GameScore라는 속성을 추가하고 , 플레이어의 레벨정보를 표시하기 위한 int32형의 CharaceterLevel이라는 속성을 추가해본다
* 새롭게 생성한 ABPlayerState 클래스를 게임 모드의 PlayerStateClass 속성에 지정하면 엔진에서는 플레이어 컨트롤러가 초기화될 떄 함께 해당 클래스의 인스턴스를 생성하고 그포인터 값을 플레이어 컨트롤러의 PlayerState 속성에 저장한다. *플레이어 컨트롤러의 구성을 완료하는 시점은 게임 모드의 PostLogin 함수이므로 이떄 함께 ABPlayerState 초기화도 완료해주는 것이 좋다.
*해당 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;
};
*이제 플레이어 정보와 캐릭터 스탯 정보가 HUD 위젯에 연동이 되는 것을 확인 할 수 있따.
* 이어서 플레이어 데이터에 경험치 정보를 표시하도록 기능을 추가해본다. 플레이어와의 전투에서 NPC가 사망하면 NPC의 레벨이 지정된 경험치를 플레이어에게 전달하고 플레이어가 이를 축적해 레벨업 하는 기능을 구현한다
* UI 에서는 해당 정보를 프로그래스바를 표시하기 떄문에 현재 경험치와 해당 레벨에서의 최대 경험치의 비율 정보를 구하는 작업이 필요하다
* 캐릭터 스탯에 NPC를 위한 경험치 값을 설정하고, 플레이어 스테이트에는 플레이어의 경험치 데이터를 보관하도록 설계를 확장해야 한다. 그리고 캐릭터가 사망할 떄 NPC가 플레이어에게 죽는 지 검사하고, 해당 플레이어 컨트롤러를 통해 플레이어 스테잍르르 업데이트 시키는 로직을 추가해야 한다
* 대미지 프레임워크에서 플레이어 컨트롤러의 정보는 가해자 인자로 전달되므로 이를 사용하면 조금 수월해진다.