에셋 로딩 구조를 만들면서 처음에는 레벨이나 런타임에서 필요한 에셋을 모아 Bundle 단위로 관리하고, 필요할 때 그 Bundle을 로드하는 구조를 생각했다.
하지만 실제 프로젝트 구조를 계속 맞춰 보니, 이 이름과 개념이 우리 프로젝트에는 조금 과했다.
최종적으로는 Level Preload Generator에 가까운 구조가 되었고, 지금은 레벨별 DA_LevelPreload_* DataAsset을 만들고 그 안에 미리 로드할 에셋을 저장하는 방식으로 정리했다.
이 글은 그 과정에서 겪은 시행착오를 정리한 기록이다.
처음 생각한 방향
현재 레벨에 필요한 에셋을 자동으로 찾는다.
찾은 에셋을 DataAsset에 저장한다.
로딩 레벨에서 DataAsset을 읽어 에셋을 미리 로드한다.
로드가 끝나면 실제 플레이 레벨로 넘어간다.
처음의 Asset Bundle Generator는 이름 그대로 Bundle을 중심으로 생각했다.
하지만 우리 프로젝트는 싱글플레이 였다.
Bundle이 과했던 이유
멀티플레이 게임처럼 서버 권한, 클라이언트 권한, 관전 상태, 전투 지역별 스트리밍 정책을 세밀하게 나눠야 하는 상황이 아니다.
게다가 레벨에 들어가면 그 레벨의 주요 요소는 대부분 필요하다.
이 상황에서 Bundle을 여러 개로 나누면 장점보다 관리 비용이 먼저 커졌다.
Runtime Record에서 막힌 지점
중간에는 Runtime Record 기능도 생각했다.
플레이 중 생성된 액터나 드롭된 아이템을 기록해서 DataAsset에 다시 넣는 방식이다.
하지만 실제 구조를 보면 드롭 아이템은 레벨에서 직접 분석해야 할 대상이 아니었다.
아이템은 Id와 DataAsset을 기준으로 움직이고, 필드 Actor도 결국 DataAsset이 가진 Mesh나 ActorClass를 통해 생성된다.
따라서 런타임에 생긴 Actor를 매번 기록하기보다, 레벨에서 드롭 가능성이 있는 Item DataAsset들을 로딩 대상에 넣는 쪽이 더 안정적이었다.
DataAsset 구조로 수렴한 이유
현재 구조의 중심은 UFTLevelPreloadDataAsset이다.
UCLASS(BlueprintType)
class PROJECTFT_API UFTLevelPreloadDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FName LevelId = NAME_None;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TSoftObjectPtr Level;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray> GeneratedEnvironmentAssets;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray> GeneratedInventoryItemAssets;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
bool bPreloadAllInventoryItemDataAssets = true;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray> AdditionalPreloadAssets;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
TArray> ExcludedAssets;
};
Bundle이 사라졌다.
대신 레벨 하나가 로딩 단위가 된다.
DA_LevelPreload_Lvl_Main
DA_LevelPreload_Lvl_Hub
DA_LevelPreload_Market_Test
현재 로딩 흐름
현재 정규 흐름은 LoadingGameMode를 거친다.
flowchart TD
A["GameFlowSubsystem: 다음 FlowState 결정"] --> B{"해당 State가 Loading Level 사용?"}
B -->|"Yes"| C["Lvl_Loading 진입"]
B -->|"No"| H["대상 레벨 직접 OpenLevel"]
C --> D["LoadingGameMode StartPlay"]
D --> E["State에 맞는 DataAsset 선택"]
E --> F["Preload Level AssetsAsync"]
F --> G["로딩 에셋 이름과 진행률 표시"]
G --> I["로드 완료 후 버튼 활성화"]
I --> J["CompleteLoadingAndOpenCurrentStateLevel"]
J --> K["실제 플레이 레벨 OpenLevel"]
Flow Route DataTable과 연결
레벨 이동도 하드코딩에서 DataTable로 옮겼다.
이전에는 코드 안에 다음처럼 박혀 있었다.
const FName FallbackLoadingLevelName(TEXT("Lvl_Loading"));
const FName FallbackMainMenuLevelName(TEXT("Lvl_MainMenu"));
const FName FallbackBaseLevelName(TEXT("Lvl_Hub"));
const FName FallbackRaidLevelName(TEXT("Market_Test"));
지금은 DataTable Row를 사용한다.
USTRUCT(BlueprintType)
struct PROJECTFT_API FFTFlowLevelRouteStruct : public FTableRowBase
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadOnly)
EFTFlowStateType State = EFTFlowStateType::MainMenu;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSoftObjectPtr Level;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
bool bUseLoadingLevel = true;
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSoftObjectPtr LevelPreloadDataAsset;
};
Generator의 최종 역할
그래서 플러그인의 역할도 바뀌었다.
현재 레벨을 분석한다.
레벨에 배치된 환경 에셋을 수집한다.
프로젝트의 Inventory Item DataAsset을 수집한다.
DA_LevelPreload_*에 저장한다.
필요하면 Additional / Excluded 목록으로 보정한다.시행착오로 얻은 기준
이번 작업에서 가장 크게 얻은 기준은 이것이다.
범용 구조를 먼저 만들지 말고,
현재 프로젝트의 실제 로딩 단위를 먼저 확인해야 한다.처음의 Bundle 구조는 재사용 가능해 보였지만, 우리 프로젝트의 현재 요구와는 거리가 있었다.
멀티플레이 권한 분리도 없고, 같은 레벨 안에서 일부 시스템만 선택적으로 로드하지도 않는다.
반대로 Level Preload 구조는 단순하다.
- 레벨 하나
- DataAsset 하나
- 로딩 화면 하나
- AssetManager의 비동기 로드 하나
'Unreal Engine' 카테고리의 다른 글
| MyaCat 정리 2 (0) | 2026.07.28 |
|---|---|
| MyaCat 정리 1 (0) | 2026.07.27 |
| AssetManager ⇄ Bundle System(Data Asset) (0) | 2026.07.09 |
| Unreal Engine GC (0) | 2026.07.08 |
| Unreal Engine Singleton (0) | 2026.07.07 |