| 개념 | 역할 |
|---|---|
PrimaryAsset |
AssetManager가 직접 식별하고 관리할 수 있는 중요 에셋 |
PrimaryAssetId |
PrimaryAsset을 찾기 위한 고유 ID |
PrimaryAsset |
게임 데이터를 코드 밖으로 분리하는 에셋 |
AssetManager
일반적인 Soft Reference는 에셋 경로를 들고 있다가 필요할 때 로드하는 방식이다.AssetManager는 여기에 식별 체계와 관리 규칙을 추가한다.
| 구분 | 의미 |
|---|---|
Soft Reference |
경로 기반으로 에셋을 필요할 때 로드 |
AssetManager |
게임에서 중요한 에셋을 ID 기반으로 관리 |
PrimaryAsset |
AssetManager가 직접 관리하는 대표 에셋 |
Secondary Asset |
Primary Asset에 의해 간접적으로 참조되는 에셋 |
Primary Asset
Primary Asset은 AssetManager가 직접 식별하고 관리할 수 있는 에셋.UPrimaryDataAsset을 상속받고 GetPrimaryAssetId()를 구현해서 AssetManager가 인식할 수 있게 한다.
FPrimaryAssetId는 두 요소로 구성된다.
| 요소 | 의미 |
|---|---|
Type |
에셋 종류. 예: Weapon, PawnData, Experience |
Name |
개별 에셋 이름. 보통 DataAsset 파일 이름 |
| 장점 | 설명 |
|---|---|
| 전역 식별 | 게임 어디서든 PrimaryAssetId로 에셋을 찾을 수 있다 |
| 패키징 제어 | 필요한 에셋만 빌드에 포함하도록 관리할 수 있다 |
| 비동기 로드 | 필요한 시점에 AssetManager를 통해 로드할 수 있다 |
| Bundle 지원 | UI, Gameplay, Visual, Audio처럼 필요한 파트만 나눠 로드할 수 있다 |
| 데이터 기반 구조 | 게임을 코드가 아니라 DataAsset 중심으로 조립할 수 있다 |
Bundle System
Primary Asset 안에 들어 있는 리소스를 용도별로 나누는 방식
namespace EAssetBundle
{
const FName Menu("Menu");
const FName Gameplay("Gameplay");
const FName Visual("Visual");
const FName Audio("Audio");
}
| Bundle | 용도 |
|---|---|
Menu |
UI에 필요한 최소 데이터. 이름, 설명, 썸네일 등 |
Gameplay |
스탯, 능력, 판정 등 게임플레이 데이터 |
Visual |
메시, 머티리얼, 애니메이션 등 시각 리소스 |
Audio |
사운드, 보이스, 효과음 등 오디오 리소스 |
거리 기반 로딩 예시
if (Distance > 1000.0f)
{
LoadPrimaryAsset(EnemyId, { EAssetBundle::Gameplay });
}
else if (Distance > 500.0f)
{
LoadPrimaryAsset(EnemyId, { EAssetBundle::Gameplay, EAssetBundle::Visual });
}
else
{
LoadPrimaryAsset(
EnemyId,
{
EAssetBundle::Gameplay,
EAssetBundle::Visual,
EAssetBundle::Audio
}
);
}
필요한 데이터만 단계적으로 로드
데이터 중심 설계
| 단계 | 목표 | 핵심 |
|---|---|---|
Value Extraction |
숫자와 설정값을 데이터로 이동 | DataTable |
Behavior Abstraction |
행동 자체를 데이터로 선택 | DataAsset + Ability |
Definition as Data |
게임 전체 구성을 데이터화 | Experience, PawnData, GameFeature |
Value Extraction
첫 번째 단계는 코드에 박혀 있던 값을 외부 데이터로 빼내는 것이다.
기존 방식은 무기 수치가 코드 안에 직접 들어간다.
class AWeapon : public AActor
{
float Damage = 100.0f;
float FireRate = 0.5f;
float Range = 1000.0f;
};
이 구조에는 다음 문제가 있다.
| 문제 | 설명 |
|---|---|
| 재컴파일 필요 | 수치 하나를 바꿔도 코드를 다시 빌드해야 한다 |
| 디자이너 접근 어려움 | 디자이너가 직접 밸런스를 수정하기 어렵다 |
| 데이터 관리 어려움 | 무기가 많아질수록 코드가 복잡해진다 |
해결책은 수치를 DataTable로 분리하는 것이다.
USTRUCT(BlueprintType)
struct FWeaponStats : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
float Damage = 10.0f;
UPROPERTY(EditAnywhere)
float FireRate = 1.0f;
UPROPERTY(EditAnywhere)
float Range = 500.0f;
};
CSV 또는 DataTable은 다음처럼 구성할 수 있다.
| Name | Damage | FireRate | Range |
|---|---|---|---|
| Pistol | 25 | 0.3 | 800 |
| Rifle | 40 | 0.1 | 1500 |
| Shotgun | 100 | 1.0 | 300 |
코드에서는 이름으로 행을 찾아 값을 적용한다.
class AWeapon : public AActor
{
UPROPERTY(EditDefaultsOnly)
UDataTable* WeaponStatsTable;
UPROPERTY(EditAnywhere)
FName WeaponType = "Pistol";
float Damage;
float FireRate;
float Range;
void BeginPlay()
{
if (WeaponStatsTable)
{
FWeaponStats* Stats = WeaponStatsTable->FindRow<FWeaponStats>(
WeaponType,
TEXT("WeaponStats")
);
if (Stats)
{
Damage = Stats->Damage;
FireRate = Stats->FireRate;
Range = Stats->Range;
}
}
}
};
이제 디자이너는 CSV나 DataTable만 수정해서 밸런스를 바꿀 수 있다.
Behavior Abstraction
행동 자체를 데이터로 선택GameplayAbility로 분리하고, DataAsset에서 어떤 Ability를 사용할지 선택
이 구조는 기능이 늘어나면 Ability가 늘어나고 코드 분기문에 묶이지 않는다.
UCLASS(BlueprintType)
class UWDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly)
FText WeaponName;
UPROPERTY(EditDefaultsOnly)
FText Description;
UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<UGameplayAbility> PrimaryAbility;
UPROPERTY(EditDefaultsOnly)
TSoftClassPtr<UGameplayAbility> SecondaryAbility;
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> Mesh;
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UAnimMontage> Montage;
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UNiagaraSystem> Niagara;
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(TEXT("Weapon"), GetFName());
}
};
기능 별로 Ability 분리
UCLASS()
class UGA_Func1 : public UGameplayAbility
{
void ActivateAbility(...)
{
SpawnProjectile();
PlayMontage(Animation);
SpawnNiagara();
}
};
UCLASS()
class UGA_Func2 : public UGameplayAbility
{
void ActivateAbility(...)
{
for (int i = 0; i < 3; i++)
{
SpawnProjectile();
Wait(0.1f);
}
}
};
UWDataAsset에 연결된 Ability와 리소스를 로드해서 적용
void ACharacter::EquipData(UWDataAsset* DataAsset)
{
if (!DataAsset)
{
return;
}
// DataAsset에 PrimaryAbility가 설정되어 있는지 확인
// TSoftClassPtr이므로 실제 Ability 클래스가 아직 로드되지 않았을 수 있음
if (!DataAsset->PrimaryAbility.IsNull())
{
// Soft Class Reference를 동기 로드해서 실제 GameplayAbility 클래스로 가져옴
// LoadSynchronous는 로드가 끝날 때까지 현재 흐름이 멈춤
if (TSubclassOf<UGameplayAbility> AbilityClass =
DataAsset->PrimaryAbility.LoadSynchronous())
{
// 이미 장착된 Ability가 있다면 먼저 제거
if (CurrentAbilityHandle.IsValid())
{
AbilitySystemComponent->ClearAbility(CurrentAbilityHandle);
}
// 새 Ability Spec 생성
// AbilityClass : 부여할 Ability 클래스
// 1 : Ability Level
// 0 : Input ID. 프로젝트 구조에 따라 INDEX_NONE 또는 InputTag 방식으로 바꿀 수 있음
// this : SourceObject. 이 Ability가 어떤 오브젝트에서 부여되었는지 추적하는 용도
FGameplayAbilitySpec AbilitySpec(AbilityClass, 1, 0, this);
// ASC에 Ability를 부여하고, 나중에 제거할 수 있도록 Handle 저장
CurrentAbilityHandle = AbilitySystemComponent->GiveAbility(AbilitySpec);
}
}
// DataAsset에 Mesh가 설정되어 있는지 확인
if (!DataAsset->Mesh.IsNull())
{
// Soft Object Reference를 동기 로드해서 실제 StaticMesh 에셋으로 가져옴
UStaticMesh* Mesh = DataAsset->Mesh.LoadSynchronous();
if (Mesh && MeshComponent)
{
// 현재 MeshComponent에 새 Mesh 적용
MeshComponent->SetStaticMesh(Mesh);
}
}
// DataAsset에 Montage가 설정되어 있는지 확인
if (!DataAsset->Montage.IsNull())
{
// Soft Object Reference를 동기 로드해서 실제 AnimMontage로 가져옴
CurrentMontage = DataAsset->Montage.LoadSynchronous();
}
}
Definition as Data
게임 전체 구성을 데이터로 정의
// Lyra에서 서로 다른 게임 구성을 `ParimaryDataAsset`에 넣고 사용하는 코드
UCLASS()
class ULyraExperienceDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Info")
FText ExperienceName; // 게임 모드 이름
UPROPERTY(EditDefaultsOnly, Category = "Gameplay")
TMap<FGameplayTag, UPawnDataAsset*> AvailablePawns; // 선택 가능한 캐릭터 목록
UPROPERTY(EditDefaultsOnly, Category = "Rules")
TSubclassOf<AGameMode> GameModeClass; // 사용할 GameMode
UPROPERTY(EditDefaultsOnly, Category = "Features")
TArray<FString> GameFeaturesToLoad; // 활성화할 GameFeature 목록
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSoftClassPtr<UHUD> HUDClass; // 사용할 HUD
UPROPERTY(EditDefaultsOnly, Category = "Level")
TSoftObjectPtr<UWorld> DefaultMap; // 기본 맵
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(TEXT("Experience"), GetFName());
}
};
적용
// ULyraExperienceDefinition 적용
void LoadExperience(FString ExperienceName)
{
// FPrimaryAssetId
FPrimaryAssetId ExpId(TEXT("Experience"), ExperienceName);
UAssetManager& Manager = UAssetManager::Get();
Manager.LoadPrimaryAsset(
ExpId,
{},
FStreamableDelegate::CreateLambda([this, ExpId]()
{
OnExperienceLoaded(ExpId);
})
);
}
로드가 완료되면 가져와 현재 게임에 적용.
void OnExperienceLoaded(FPrimaryAssetId ExpId)
{
ULyraExperienceDefinition* Experience =
UAssetManager::Get().GetPrimaryAssetObject<ULyraExperienceDefinition>(ExpId);
if (!Experience)
{
return;
}
World->SetGameMode(Experience->GameModeClass);
AvailableCharacters = Experience->AvailablePawns;
for (const FString& Feature : Experience->GameFeaturesToLoad)
{
// GameFeature 활성화
}
if (!Experience->HUDClass.IsNull())
{
TSubclassOf<UHUD> HUDClass = Experience->HUDClass.LoadSynchronous();
// HUD 생성
}
CurrentExperience = Experience;
}
구성
// Component, Ability, InputConfig, Stat을 가질지 DataAsset으로 정의
UCLASS()
class UPawnDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, Category = "Info")
FText CharacterName; // 캐릭터 이름
UPROPERTY(EditDefaultsOnly, Category = "Components")
TArray<TSoftClassPtr<UActorComponent>> Components; // 런타임에 붙일 Component 목록
UPROPERTY(EditDefaultsOnly, Category = "Abilities")
TArray<TSoftClassPtr<UGameplayAbility>> Abilities; // 부여할 GameplayAbility 목록
UPROPERTY(EditDefaultsOnly, Category = "Input")
UInputConfig* InputConfig; // 입력 설정
UPROPERTY(EditDefaultsOnly, Category = "Stats")
float MaxHealth = 100.0f;
UPROPERTY(EditDefaultsOnly, Category = "Stats")
float MoveSpeed = 600.0f;
virtual FPrimaryAssetId GetPrimaryAssetId() const override
{
return FPrimaryAssetId(TEXT("PawnData"), GetFName());
}
};
적용
// UPawnDataAsset 적용
void ApplyPawnData(ALyraCharacter* Character, UPawnDataAsset* PawnData)
{
for (TSoftClassPtr<UActorComponent> CompClass : PawnData->Components)
{
if (!CompClass.IsNull())
{
TSubclassOf<UActorComponent> ComponentClass = CompClass.LoadSynchronous();
if (ComponentClass)
{
UActorComponent* NewComp = NewObject<UActorComponent>(
Character,
ComponentClass
);
// 런타임에 만든 Compoonent는 RegisterComponentWithWorld 호출해야함
// 호출하지 않으면 Component가 정상적으로 활성화되지 않고 `BeginPlay`도 호출되지 않을 수 있음.
NewComp->RegisterComponentWithWorld(GetWorld());
}
}
}
for (TSoftClassPtr<UGameplayAbility> AbilityClass : PawnData->Abilities)
{
if (!AbilityClass.IsNull())
{
TSubclassOf<UGameplayAbility> Ability = AbilityClass.LoadSynchronous();
if (Ability)
{
Character->AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(Ability, 1, INDEX_NONE, Character)
);
}
}
}
if (PawnData->InputConfig)
{
Character->SetInputConfig(PawnData->InputConfig);
}
Character->SetMaxHealth(PawnData->MaxHealth);
Character->SetMoveSpeed(PawnData->MoveSpeed);
}
요약
AssetManager는 게임에서 중요한 에셋을 Primary Asset으로 관리
필요한 시점에 필요한 Bundle만 로드UPrimaryDataAsset을 기반은 FPrimaryAssetId를 통해 전역적으로 식별.
캐릭터, 무기, 게임 모드, UI, 능력 시스템이 복잡해질수록 유지보수성과 확장성에서AssetManager + DataAsset 조합이 큰 장점을 가짐
'Unreal Engine' 카테고리의 다른 글
| 언리얼 모듈, Build.cs, 리플렉션 (0) | 2026.07.02 |
|---|---|
| UE MVVM Plugin (0) | 2026.07.01 |
| 언리얼 레퍼런스 (0) | 2026.06.29 |
| DataTable, DataAsset (0) | 2026.06.26 |
| UE Input 3 (0) | 2026.06.25 |