구조체와 데이터 테이블 활용
언리얼 엔진에서 구조체와 데이터 테이블은 게임 데이터를 효율적으로 관리하고 사용하는 데 중요한 역할을 합니다.
이 절에서는 C++를 사용하여 구조체를 정의하고 데이터 테이블을 활용하는 방법을 살펴보겠습니다.
USTRUCT 매크로를 사용한 구조체 정의
USTRUCT 매크로를 사용하여 언리얼 엔진의 리플렉션 시스템에 구조체를 등록할 수 있습니다.
USTRUCT(BlueprintType)
struct FItemData
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString Name;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Value;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Weight;
};
이 구조체는 이제 블루프린트에서도 사용할 수 있습니다.
블루프린트에서 사용 가능한 구조체 생성
블루프린트에서 더 쉽게 사용할 수 있도록 구조체를 확장할 수 있습니다.
USTRUCT(BlueprintType)
struct FCharacterStats : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Mana;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float MovementSpeed;
};
FTableRowBase
를 상속받음으로써 이 구조체를 데이터 테이블의 행으로 사용할 수 있습니다.
데이터 테이블 생성 및 로드
데이터 테이블을 C++에서 생성하고 로드하는 방법은 다음과 같습니다.
// 데이터 테이블 에셋 생성
UDataTable* CreateCharacterStatsTable()
{
UDataTable* CharacterStatsTable = NewObject<UDataTable>(GetTransientPackage(), FName("CharacterStatsTable"));
CharacterStatsTable->RowStruct = FCharacterStats::StaticStruct();
// 데이터 추가
FCharacterStats* NewRow = (FCharacterStats*)CharacterStatsTable->AddRow(FName("Character1"));
NewRow->Health = 100;
NewRow->Mana = 50;
NewRow->MovementSpeed = 300.0f;
return CharacterStatsTable;
}
// 데이터 테이블 로드
UDataTable* LoadCharacterStatsTable()
{
static const FString ContextString(TEXT("Character Stats Table Context"));
return Cast<UDataTable>(StaticLoadObject(UDataTable::StaticClass(), NULL, TEXT("/Game/Data/CharacterStatsTable")));
}
데이터 테이블의 효과적인 활용
게임 내에서 데이터 테이블을 효과적으로 활용하는 예시
class AMyGameMode : public AGameModeBase
{
GENERATED_BODY()
public:
AMyGameMode();
UFUNCTION(BlueprintCallable)
FCharacterStats GetCharacterStats(FName CharacterID);
private:
UPROPERTY()
UDataTable* CharacterStatsTable;
};
AMyGameMode::AMyGameMode()
{
CharacterStatsTable = LoadCharacterStatsTable();
}
FCharacterStats AMyGameMode::GetCharacterStats(FName CharacterID)
{
static const FString ContextString(TEXT("Get Character Stats"));
FCharacterStats* FoundRow = CharacterStatsTable->FindRow<FCharacterStats>(CharacterID, ContextString, true);
if (FoundRow)
{
return *FoundRow;
}
// 기본값 반환
return FCharacterStats();
}
이 예시에서는 캐릭터 ID를 기반으로 캐릭터의 스탯을 검색합니다.
런타임 중 데이터 테이블 수정 및 저장
런타임 중 데이터 테이블을 수정하고 저장하는 방법
void ModifyAndSaveCharacterStats(FName CharacterID, int32 NewHealth)
{
static const FString ContextString(TEXT("Modify Character Stats"));
FCharacterStats* FoundRow = CharacterStatsTable->FindRow<FCharacterStats>(CharacterID, ContextString, true);
if (FoundRow)
{
FoundRow->Health = NewHealth;
// 변경사항 저장
FString SavedFilePath = FPaths::ProjectContentDir() + "Data/CharacterStatsTable.json";
UDataTableSaveUtilities::SaveDataTableAsJSON(CharacterStatsTable, SavedFilePath);
}
}
이 함수는 특정 캐릭터의 체력을 수정하고 변경사항을 JSON 파일로 저장합니다.
대량의 데이터 효율적 관리 전략
- 데이터 분할 : 관련 데이터를 여러 테이블로 분할하여 관리합니다.
- 인덱싱 : 자주 접근하는 데이터에 대해 인덱스를 사용합니다.
- 캐싱 : 자주 사용되는 데이터를 메모리에 캐시합니다.
class UDataManager : public UObject
{
GENERATED_BODY()
public:
FCharacterStats GetCachedCharacterStats(FName CharacterID);
private:
UPROPERTY()
UDataTable* CharacterStatsTable;
TMap<FName, FCharacterStats> CachedCharacterStats;
void CacheFrequentlyUsedData();
};
void UDataManager::CacheFrequentlyUsedData()
{
for (auto It = CharacterStatsTable->GetRowMap().CreateConstIterator(); It; ++It)
{
FName RowName = It.Key();
FCharacterStats* RowData = (FCharacterStats*)It.Value();
CachedCharacterStats.Add(RowName, *RowData);
}
}
FCharacterStats UDataManager::GetCachedCharacterStats(FName CharacterID)
{
if (FCharacterStats* FoundStats = CachedCharacterStats.Find(CharacterID))
{
return *FoundStats;
}
// 캐시에 없으면 테이블에서 검색
return GetCharacterStats(CharacterID);
}
성능 고려사항
- 데이터 접근 최적화 : 자주 접근하는 데이터는 캐시하여 사용합니다.
- 메모리 사용량 : 큰 데이터 세트의 경우 스트리밍 방식을 고려합니다.
- 직렬화 성능 : 대규모 데이터 테이블의 경우 바이너리 형식 사용을 고려합니다.
메모리 관리 전략
- 참조 카운팅 :
UPROPERTY()
매크로를 사용하여 가비지 컬렉션을 활용합니다. - 수동 메모리 관리 : 필요한 경우
NewObject<>()
와MarkPendingKill()
을 사용합니다.
class UDataTableManager : public UObject
{
GENERATED_BODY()
public:
void LoadDataTable(FString TablePath);
void UnloadDataTable(FString TableName);
private:
UPROPERTY()
TMap<FString, UDataTable*> LoadedDataTables;
};
void UDataTableManager::LoadDataTable(FString TablePath)
{
UDataTable* NewTable = Cast<UDataTable>(StaticLoadObject(UDataTable::StaticClass(), NULL, *TablePath));
if (NewTable)
{
FString TableName = FPaths::GetBaseFilename(TablePath);
LoadedDataTables.Add(TableName, NewTable);
}
}
void UDataTableManager::UnloadDataTable(FString TableName)
{
if (UDataTable** TablePtr = LoadedDataTables.Find(TableName))
{
(*TablePtr)->MarkPendingKill();
LoadedDataTables.Remove(TableName);
}
}
데이터 유효성 검사
데이터의 무결성을 보장하기 위해 유효성 검사를 수행할 수 있습니다.
bool ValidateCharacterStats(const FCharacterStats& Stats)
{
if (Stats.Health < 0 || Stats.Mana < 0 || Stats.MovementSpeed < 0)
{
UE_LOG(LogTemp, Error, TEXT("Invalid character stats: Health, Mana, and MovementSpeed must be non-negative"));
return false;
}
if (Stats.MovementSpeed > 1000.0f)
{
UE_LOG(LogTemp, Warning, TEXT("Unusually high movement speed detected: %f"), Stats.MovementSpeed);
}
return true;
}
// 데이터 테이블 로드 시 유효성 검사
void ValidateCharacterStatsTable(UDataTable* Table)
{
for (auto It = Table->GetRowMap().CreateConstIterator(); It; ++It)
{
FName RowName = It.Key();
FCharacterStats* RowData = (FCharacterStats*)It.Value();
if (!ValidateCharacterStats(*RowData))
{
UE_LOG(LogTemp, Error, TEXT("Invalid data in row %s"), *RowName.ToString());
}
}
}
구조체와 데이터 테이블을 효과적으로 활용하면 게임 데이터를 체계적으로 관리하고 쉽게 접근할 수 있습니다.
성능과 메모리 사용을 최적화하기 위해 캐싱, 인덱싱, 그리고 적절한 메모리 관리 전략을 사용해야 합니다.
또한 데이터 유효성 검사를 통해 게임의 안정성을 높일 수 있습니다.