icon안동민 개발노트

구조체와 데이터 테이블 활용


 언리얼 엔진에서 구조체와 데이터 테이블은 게임 데이터를 효율적으로 관리하고 사용하는 데 중요한 역할을 합니다.

 이 절에서는 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 파일로 저장합니다.

대량의 데이터 효율적 관리 전략

  1. 데이터 분할 : 관련 데이터를 여러 테이블로 분할하여 관리합니다.
  2. 인덱싱 : 자주 접근하는 데이터에 대해 인덱스를 사용합니다.
  3. 캐싱 : 자주 사용되는 데이터를 메모리에 캐시합니다.
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);
}

성능 고려사항

  1. 데이터 접근 최적화 : 자주 접근하는 데이터는 캐시하여 사용합니다.
  2. 메모리 사용량 : 큰 데이터 세트의 경우 스트리밍 방식을 고려합니다.
  3. 직렬화 성능 : 대규모 데이터 테이블의 경우 바이너리 형식 사용을 고려합니다.

메모리 관리 전략

  1. 참조 카운팅 : UPROPERTY() 매크로를 사용하여 가비지 컬렉션을 활용합니다.
  2. 수동 메모리 관리 : 필요한 경우 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());
        }
    }
}

 구조체와 데이터 테이블을 효과적으로 활용하면 게임 데이터를 체계적으로 관리하고 쉽게 접근할 수 있습니다.

 성능과 메모리 사용을 최적화하기 위해 캐싱, 인덱싱, 그리고 적절한 메모리 관리 전략을 사용해야 합니다. 또한, 데이터 유효성 검사를 통해 게임의 안정성을 높일 수 있습니다.

 이러한 기법들을 적절히 조합하여 사용하면, 대규모 게임 프로젝트에서도 효율적인 데이터 관리 시스템을 구축할 수 있습니다.