콘텐츠로 이동

UE5 C++ DataAsset & DataTable

개요 — 데이터를 코드에서 분리해야 하는 이유

섹션 제목: “개요 — 데이터를 코드에서 분리해야 하는 이유”

게임 밸런스 수치(캐릭터 스탯, 아이템 가격, 레벨별 경험치)를 C++ 소스코드에 하드코딩하면 기획자가 값을 변경할 때마다 엔지니어가 개입해야 합니다. UDataAssetUDataTable을 사용하면 데이터를 에셋으로 분리해 기획자가 에디터에서 직접 수정할 수 있습니다.

비교 항목UDataAssetUDataTable
구조단일 객체 (트리형)행(Row) 기반 테이블
적합한 데이터아이템 정의, 캐릭터 설정레벨 경험치표, 가격표
CSV 임포트불가가능
Blueprint 편집가능가능
계층 상속C++ 상속으로 확장구조체 정의로 확장

WeaponDataAsset.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "GameplayTagContainer.h"
#include "WeaponDataAsset.generated.h"
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
Melee,
Ranged,
Magic
};
UCLASS(BlueprintType)
class MYGAME_API UWeaponDataAsset : public UDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Info")
FName WeaponID;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Info")
FText DisplayName;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Info")
EWeaponType WeaponType = EWeaponType::Melee;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Stats")
float BaseDamage = 10.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Stats")
float AttackSpeed = 1.0f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Tags")
FGameplayTagContainer AbilityTags;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Visuals")
TSoftObjectPtr<USkeletalMesh> WeaponMesh;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Weapon|Visuals")
TSoftObjectPtr<UParticleSystem> HitEffect;
};

1.2 Primary Data Asset — 에셋 관리자 연동

섹션 제목: “1.2 Primary Data Asset — 에셋 관리자 연동”

UPrimaryDataAsset을 사용하면 Asset Manager와 연동해 비동기 로드가 가능합니다.

CharacterDataAsset.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CharacterDataAsset.generated.h"
UCLASS(BlueprintType)
class MYGAME_API UCharacterDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// PrimaryDataAsset 식별자 (Asset Manager에서 사용)
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(TEXT("CharacterData"), GetFName());
}
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character")
FText CharacterName;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character|Stats")
float MaxHealth = 100.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character|Stats")
float MoveSpeed = 400.f;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character|Mesh")
TSoftObjectPtr<USkeletalMesh> CharacterMesh;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Character|Abilities")
TArray<TSoftClassPtr<class UGameplayAbility>> DefaultAbilities;
};

1.3 DefaultEngine.ini에 Primary Asset Type 등록

섹션 제목: “1.3 DefaultEngine.ini에 Primary Asset Type 등록”
[/Script/Engine.AssetManagerSettings]
+PrimaryAssetTypesToScan=(PrimaryAssetType="CharacterData",AssetBaseClass=/Script/MyGame.CharacterDataAsset,bHasBlueprintClasses=False,bIsEditorOnly=False,Directories=((Path="/Game/Data/Characters")),Rules=(Priority=0,bApplyRecursively=True))
WeaponComponent.cpp
void UWeaponComponent::EquipWeapon(TSoftObjectPtr<UWeaponDataAsset> WeaponDataRef)
{
// 동기 로드 (소용량 에셋, 게임 시작 시)
UWeaponDataAsset* WeaponData = WeaponDataRef.LoadSynchronous();
if (!WeaponData) return;
BaseDamage = WeaponData->BaseDamage;
AttackSpeed = WeaponData->AttackSpeed;
// 비동기 스켈레탈 메시 로드
StreamableManager.RequestAsyncLoad(WeaponData->WeaponMesh.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &UWeaponComponent::OnMeshLoaded, WeaponData));
}
void UWeaponComponent::OnMeshLoaded(UWeaponDataAsset* WeaponData)
{
if (USkeletalMesh* Mesh = WeaponData->WeaponMesh.Get())
{
WeaponMeshComponent->SetSkeletalMesh(Mesh);
}
}

FTableRowBase를 상속한 구조체를 행(Row) 타입으로 사용합니다.

ItemTableRow.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "ItemTableRow.generated.h"
USTRUCT(BlueprintType)
struct FItemTableRow : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FText DisplayName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
FText Description;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 BuyPrice = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
int32 SellPrice = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
float Weight = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
TSoftObjectPtr<UTexture2D> Icon;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item")
bool bIsStackable = false;
};
ItemDatabase.h
UCLASS()
class MYGAME_API AItemDatabase : public AActor
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Data")
TObjectPtr<UDataTable> ItemDataTable;
// FName 키(Row Name)로 아이템 데이터 조회
UFUNCTION(BlueprintCallable, Category = "Data")
const FItemTableRow* GetItemData(FName ItemID) const;
UFUNCTION(BlueprintCallable, Category = "Data")
TArray<FName> GetAllItemIDs() const;
};
ItemDatabase.cpp
const FItemTableRow* AItemDatabase::GetItemData(FName ItemID) const
{
if (!ItemDataTable) return nullptr;
// FindRow<T>: 템플릿으로 타입 안전 조회, 없으면 nullptr
return ItemDataTable->FindRow<FItemTableRow>(ItemID, TEXT("GetItemData"));
}
TArray<FName> AItemDatabase::GetAllItemIDs() const
{
if (!ItemDataTable) return {};
return ItemDataTable->GetRowNames();
}
void AShopManager::PopulateShopInventory()
{
if (!ItemDataTable) return;
// GetAllRows: 모든 행을 TArray로 획득
TArray<FItemTableRow*> AllRows;
ItemDataTable->GetAllRows<FItemTableRow>(TEXT("PopulateShop"), AllRows);
for (FItemTableRow* Row : AllRows)
{
if (Row && Row->BuyPrice > 0)
{
ShopInventory.Add(Row);
}
}
}

DataTable은 CSV 파일로 데이터를 임포트할 수 있습니다.

---,DisplayName,Description,BuyPrice,SellPrice,Weight,bIsStackable
Sword_Iron,철제 검,기본 철제 검,100,50,3.5,false
Potion_HP_Small,소형 HP 포션,HP 50 회복,30,15,0.5,true
Shield_Wood,나무 방패,간단한 나무 방패,80,40,4.0,false

에디터에서 DataTable 에셋을 생성할 때 Row Structure로 FItemTableRow를 지정한 뒤 CSV를 임포트합니다.


4. 비동기 로드 — Asset Manager 활용

섹션 제목: “4. 비동기 로드 — Asset Manager 활용”
MyGameInstance.cpp
void UMyGameInstance::PreloadCharacterData(FName CharacterID)
{
UAssetManager& Manager = UAssetManager::Get();
FPrimaryAssetId AssetId(TEXT("CharacterData"), CharacterID);
TArray<FName> BundlesToLoad = { TEXT("UI") }; // Bundle 단위 로드
Manager.LoadPrimaryAsset(AssetId, BundlesToLoad,
FStreamableDelegate::CreateUObject(this, &UMyGameInstance::OnCharacterDataLoaded, AssetId));
}
void UMyGameInstance::OnCharacterDataLoaded(FPrimaryAssetId AssetId)
{
UAssetManager& Manager = UAssetManager::Get();
UCharacterDataAsset* Data = Cast<UCharacterDataAsset>(
Manager.GetPrimaryAssetObject(AssetId));
if (Data)
{
UE_LOG(LogTemp, Log, TEXT("Loaded: %s (HP: %.0f)"),
*Data->CharacterName.ToString(), Data->MaxHealth);
}
}

상황권장
단일 캐릭터·아이템 설정UDataAsset (또는 UPrimaryDataAsset)
테이블 형태의 게임 데이터UDataTable + CSV 임포트
비동기 로드 + 메모리 관리UPrimaryDataAsset + UAssetManager
런타임 행 추가/삭제TMap<FName, FMyStruct> + DataAsset

핵심 규칙:

  • DataAsset의 메시·텍스처 참조는 TSoftObjectPtr로 선언해 필요할 때 로드 (Soft Reference)
  • DataTable의 FindRow<T> 두 번째 파라미터는 디버그 컨텍스트 문자열 — 비워두면 안 됨
  • Primary Asset 등록은 DefaultEngine.iniPrimaryAssetTypesToScan 섹션에서 경로를 지정

6. UCurveTable — 레벨별 스탯 곡선

섹션 제목: “6. UCurveTable — 레벨별 스탯 곡선”
// UCurveTable: 레벨(X) → 스탯(Y) 매핑 테이블
// 에디터에서 Content Browser > Miscellaneous > Curve Table 생성
UCLASS()
class AStatScalingManager : public AActor
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly, Category = "Data")
TObjectPtr<UCurveTable> StatCurveTable;
public:
float GetStatAtLevel(FName StatName, int32 Level) const
{
if (!StatCurveTable) return 0.f;
// FindCurve: 행 이름으로 곡선 검색
const FRealCurve* Curve = StatCurveTable->FindCurve(StatName, TEXT("GetStatAtLevel"));
if (!Curve) return 0.f;
return Curve->Eval(static_cast<float>(Level));
}
};
// 사용 예시
float MaxHP = StatManager->GetStatAtLevel(TEXT("MaxHealth"), PlayerLevel);
float AttackDamage = StatManager->GetStatAtLevel(TEXT("Attack"), PlayerLevel);

// 런타임에 DataTable 행 추가 (주의: 영구 저장은 직접 파일 I/O 필요)
void AItemManager::AddRuntimeItem(FName ItemID, FItemTableRow NewRow)
{
if (!ItemDataTable) return;
// 기존 행 덮어쓰기 또는 신규 추가
ItemDataTable->AddRow(ItemID, NewRow);
UE_LOG(LogTemp, Log, TEXT("Added runtime item: %s"), *ItemID.ToString());
}
// 행 제거
void AItemManager::RemoveItem(FName ItemID)
{
if (!ItemDataTable) return;
ItemDataTable->RemoveRow(ItemID);
}
// 조건 필터링 — 특정 가격 범위의 아이템 조회
TArray<FItemTableRow*> AItemManager::GetAffordableItems(int32 PlayerGold) const
{
TArray<FItemTableRow*> AllRows;
ItemDataTable->GetAllRows<FItemTableRow>(TEXT("Filter"), AllRows);
return AllRows.FilterByPredicate([PlayerGold](const FItemTableRow* Row)
{
return Row && Row->BuyPrice <= PlayerGold;
});
}