게임데이터 관리

Unreal 이론|2024. 11. 6. 13:10

[이득우의 언리얼 프로그래밍 Part2 수업의 정리]

 

 

사전 준비

CSV 파일로 게임의 설정 데이터를 저장해 언리얼 엔진에서 Import 한다.

프로젝트 폴더에 GameData 폴더를 만들고, 엑셀 파일을 UTF-8의 csv 파일로 저장

위의 CSV 파일을 사용하기 위해 인덱스 이름과 동일한 이름을 가진 구조체를 생성

-> 소스파일 폴더에서 구조체 헤더파일 작성 -> Generated project file 실행해 헤더파일을 프로젝트에 추가

 

엑셀 데이터의 임포트

  • DataAsset과 유사하게 FTableRowBase를 상속받은 구조체를 선언
  • 엑셀의 Name 컬럼 (얘가 key값이 된다)을 제외한 컬럼과 동일하게 UPROPERTY 속성 선언
  • 엑셀 데이터를 csv로 익스포트 한 후 언리얼 엔진에 임포트

기타->DataTable로 생성 후 Reimport를 클릭해 위에서 만든 Csv 파일을 가져온다

 

데이터를 관리할 싱글톤 클래스의 설정

  • 언리얼 엔진에서 제공하는 싱글톤 클래스
    • 게임 인스턴스
    • 에셋 매니저
    • 게임 플레이 관련 액터 (GameMode, GameState)
    • 프로젝트에 싱글톤으로 등록한 언리얼 오브젝트
  • 언리얼 오브젝트 생성자에서 사용하지 않도록 주의

  • 위의 사진에 싱글톤 클래스를 설정하면 엔진이 초기화 될 때 자동으로 GEngine이라는 전역변수에 싱글톤을 만들어줌
// ABGameSingleton.h

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "ABCharacterStat.h"
#include "ABGameSingleton.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogABGameSingleton, Error, All);

/**
 * 
 */
UCLASS()
class ARENABATTLE_API UABGameSingleton : public UObject
{
	GENERATED_BODY()

public:
	UABGameSingleton();
	static UABGameSingleton& Get();

	// Character Stat Data Section
public:
	FORCEINLINE FABCharacterStat GetCharacterStat(int32 InLevel) const { return CharacterStatTable.IsValidIndex(InLevel) ? CharacterStatTable[InLevel] : FABCharacterStat(); }

	UPROPERTY()
	int32 CharacterMaxLevel;


private:
	// Stat Data 내부 보관용 배열 - 추후 필요한 객체들에게 이 배열로 뿌림
	TArray<FABCharacterStat> CharacterStatTable;
	
};


// ABGameSingleton.cpp

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


#include "GameData/ABGameSingleton.h"

DEFINE_LOG_CATEGORY(LogABGameSingleton);

UABGameSingleton::UABGameSingleton()
{
	// UDataTable -> (Key, Value)의 한쌍으로 이루어짐
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (nullptr != DataTableRef.Object)
	{
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		// Algo의 Transform 알고리즘으로 value값만 가져오도록 설계
		Algo::Transform(ValueArray, CharacterStatTable,
			[](uint8* Value)
			{
				return *reinterpret_cast<FABCharacterStat*>(Value);
			}
		);
	}

	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

UABGameSingleton& UABGameSingleton::Get()
{
	// 엔진 초기화시 바로 활성화 되기 때문에 Get 함수로 받을 수 없다면 잘못된것이므로 CastCheck로 형변환 됬는지 강력히 검사
	UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton);
	if (Singleton) {
		return *Singleton;
	}
	UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));

	// 코드 흐름을 위해 작성한것일 뿐 실제 여기까지 가는 일이 일어나지 않도록 앞에서 철저히 검사
	return *NewObject<UABGameSingleton>();
}
  • 싱글톤 클래스의 로직. 싱글톤 클래스가 엔진 초기화시 제대로 생성되었는지 검사하며, 요청한 객체가 잘 쓸수 있도록 생성자에서 DataTable의 value들을 가져온다.

 

프로젝트의 주요 레이어

  • 게임 레이어: 기믹과 NPC
  • 미들웨어 레이어: 캐릭터의 스탯 컴포넌트, 아이템 박스
  • 데이터 레이어: 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스

위에서 아래로만 참조하도록

 

캐릭터 스탯 시스템

  • 위의 구조대로 Stat Component가 Base Stat과 무기의 능력치에 따른 modifier Stat을 합산하여 반영하도록 ABCharacterStatComponent의 구조를 변경, 아래의 함수와 변수를 추가
void SetLevelStat(int32 InNewLevel);
FORCEINLINE float GetCurrentLevel() const { return CurrentLevel; }
FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat)
float CurrentLevel;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat BaseStat;

UPROPERTY(Transient, VisibleInstanceOnly, Category = Stat, Meta = (AllowPrivateAccess = "true"))
FABCharacterStat ModifierStat;
  • 위의 구조 변경에 따라 CharacterBase 파일도 Stat의 값을 참조하도록 적절하게 변경

  • 기믹 클래스에서 스테이지가 상승할때 마다 NPC 레벨이 증가해 더 강해진 NPC를 스폰하도록 설정
  • 이를 위해 스테이지 번호를 지정하고, 이 번호의 증가에 NPC의 레벨을 연동. 이를 위해 CharacterBase에 SetLevel 함수 선언
  • 근데 이렇게 해서 테스트 하다보면 2스테이지부터 MaxHp와 CurrentHp가 안맞는 현상 발생

액터의 생성과 지연 생성의 프로세스

  • 액터의 BeginPlay에서 CurrentHp값과 MaxHp값이 세팅이 되지만, 기믹 함수에서는 이미 SpawnActor를 실행하고 SetLevel 함수가 실행되기 때문에 CurrentHp값이 반영안되는 문제 발생
  • SpawnActor 대신 SpawnActorDeferred 함수를 사용하면 FinishSpawning 함수를 마지막에 호출해 액터의 BeginPlay가 실행된 후 최종적으로 스폰처리가 완료된다
  • 스테이지 스폰과 NPC 스폰, 상자 스폰을 모두 Deferred로 변경해준다.

  • 캐릭터가 무기를 습득했을 때 무기스탯에 따라 최종 스탯을 얻는 구조 추가
  • Weapon 클래스에 ModifierStat 변수를 추가해서, CharacterBase의 equip weapon 함수에서 Stat이 WeaponItemData의 ModifierStat을 얻도록 로직 추가

  • NPC 캐릭터가 스폰될 때 각기 다른 캐릭터로 스폰되도록 설정 추가
  • 프로젝트 폴더의 Config->DefaultArenaBattle.ini 파일 추가.

NPCMeshes라는 TArray가 ABCharacterNonPlayer에 선언되어 있다면 이것의 값을 지정하는 ini 파일

  • AABCharacterNonPlayer에 Config파일을 가져와 메쉬를 로딩하도록 로직 추가
// ABCharacterNonPlayer.h

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

#pragma once

#include "CoreMinimal.h"
#include "Character/ABCharacterBase.h"
#include "Engine/StreamableManager.h"
#include "ABCharacterNonPlayer.generated.h"

/**
 * 
 */
UCLASS(config = ArenaBattle)
class ARENABATTLE_API AABCharacterNonPlayer : public AABCharacterBase
{
	GENERATED_BODY()

public:
	AABCharacterNonPlayer();

protected:
	virtual void PostInitializeComponents() override;

protected:
	void SetDead() override;
	void NPCMeshLoadCompleted();
	
	// 비동기 방식으로 로드
	UPROPERTY(config)
	TArray<FSoftObjectPath> NPCMeshes;

	TSharedPtr<FStreamableHandle> NPCMeshHandle;
};

// ABCharacterNonPlayer.cpp

#include "Character/ABCharacterNonPlayer.h"
#include "Engine/AssetManager.h"

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);
}

void AABCharacterNonPlayer::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	ensure(NPCMeshes.Num() > 0);
	int32 RandIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
	NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}

void AABCharacterNonPlayer::NPCMeshLoadCompleted()
{
	if (NPCMeshHandle.IsValid())
	{
		USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset());
		if (NPCMesh)
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	NPCMeshHandle->ReleaseHandle();
}
  • 이후 실행해보면 메쉬 로딩까지 NPC가 숨겨져있다가 랜덤한 캐릭터 메쉬로 잘 로딩되는것 확인 가능

'Unreal 이론' 카테고리의 다른 글

인공지능 - 행동트리 모델의 구현  (0) 2024.11.11
인공지능 - 행동트리 모델의 이해  (2) 2024.11.10
무한 맵의 제작  (0) 2024.10.31
아이템 시스템  (1) 2024.10.30
캐릭터 스탯과 위젯  (1) 2024.10.27

댓글()