UE5 C++ DataAsset & DataTable
개요 — 데이터를 코드에서 분리해야 하는 이유
Section titled “개요 — 데이터를 코드에서 분리해야 하는 이유”게임 밸런스 수치(캐릭터 스탯, 아이템 가격, 레벨별 경험치)를 C++ 소스코드에 하드코딩하면 기획자가 값을 변경할 때마다 엔지니어가 개입해야 합니다. UDataAsset과 UDataTable을 사용하면 데이터를 에셋으로 분리해 기획자가 에디터에서 직접 수정할 수 있습니다.
| 비교 항목 | UDataAsset | UDataTable |
|---|---|---|
| 구조 | 단일 객체 (트리형) | 행(Row) 기반 테이블 |
| 적합한 데이터 | 아이템 정의, 캐릭터 설정 | 레벨 경험치표, 가격표 |
| CSV 임포트 | 불가 | 가능 |
| Blueprint 편집 | 가능 | 가능 |
| 계층 상속 | C++ 상속으로 확장 | 구조체 정의로 확장 |
1. UDataAsset
Section titled “1. UDataAsset”1.1 기본 DataAsset 정의
Section titled “1.1 기본 DataAsset 정의”#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 — 에셋 관리자 연동
Section titled “1.2 Primary Data Asset — 에셋 관리자 연동”UPrimaryDataAsset을 사용하면 Asset Manager와 연동해 비동기 로드가 가능합니다.
#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 등록
Section titled “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))1.4 DataAsset 동기 로드
Section titled “1.4 DataAsset 동기 로드”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); }}2. UDataTable
Section titled “2. UDataTable”2.1 행 구조체 정의
Section titled “2.1 행 구조체 정의”FTableRowBase를 상속한 구조체를 행(Row) 타입으로 사용합니다.
#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;};2.2 DataTable 선언 및 조회
Section titled “2.2 DataTable 선언 및 조회”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;};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();}2.3 DataTable 전체 순회
Section titled “2.3 DataTable 전체 순회”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); } }}3. CSV 임포트 (DataTable)
Section titled “3. CSV 임포트 (DataTable)”DataTable은 CSV 파일로 데이터를 임포트할 수 있습니다.
---,DisplayName,Description,BuyPrice,SellPrice,Weight,bIsStackableSword_Iron,철제 검,기본 철제 검,100,50,3.5,falsePotion_HP_Small,소형 HP 포션,HP 50 회복,30,15,0.5,trueShield_Wood,나무 방패,간단한 나무 방패,80,40,4.0,false에디터에서 DataTable 에셋을 생성할 때 Row Structure로 FItemTableRow를 지정한 뒤 CSV를 임포트합니다.
4. 비동기 로드 — Asset Manager 활용
Section titled “4. 비동기 로드 — Asset Manager 활용”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.ini의PrimaryAssetTypesToScan섹션에서 경로를 지정