TIL | 로딩 파이프라인, 비동기로드, PrimaryDataAsset, DataTable

Kyu_·2026년 4월 9일

20주차

목록 보기
3/4

팀프로젝트

#pragma once

#include "CoreMinimal.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Engine/StreamableManager.h"
#include "EDGameDataSubsystem.generated.h"

UENUM(BlueprintType)
enum class EDataLoadPhase : uint8
{
	NotStarted      UMETA(DisplayName = "시작 전"),
	LoadingLobby    UMETA(DisplayName = "로비 데이터 로딩"),
	LobbyReady      UMETA(DisplayName = "로비 준비 완료"),

	LoadingItem     UMETA(DisplayName = "아이템 데이터 로딩"),
	LoadingMonster  UMETA(DisplayName = "몬스터 데이터 로딩"),

	Completed       UMETA(DisplayName = "완료")
};

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAllDataLoaded);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAllLobbyDataLoaded);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnDataPhaseChanged, EDataLoadPhase, NewPhase);

/**
 *
 */
UCLASS()
class ETERNALDREAMS_API UEDGameDataSubsystem : public UGameInstanceSubsystem
{
	GENERATED_BODY()
public:
	template<typename T>
	T* GetData(const FPrimaryAssetId& Id) const;

	template<typename T>
	TArray<T*> GetAllDataOfType(const FPrimaryAssetType& AssetType) const;

public:
	// ================================================================
    // UGameInstanceSubsystem 인터페이스
    // ================================================================

	// GameInstanceSubsystem 수명주기 진입시점 자동 호출
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
	// GameInstanceSubsystem 수명주기 종료시점 자동 호출
    virtual void Deinitialize() override;

	UFUNCTION(BlueprintCallable, Category = "ED|EDGameDataSubsystem", meta = (WorldContext = "WorldContextObject"))
    static UEDGameDataSubsystem* Get(const UObject* WorldContextObject);

	/**
	 * 게임 데이터 전체를 단계별로 비동기 로드하는 진입점 함수
	 */
	UFUNCTION(BlueprintCallable, Category = "ED|EDGameDataSubsystem")
    void InitializeGameData();

    // 로딩 상태 조회
    UFUNCTION(BlueprintPure, Category = "ED|EDGameDataSubsystem")
    EDataLoadPhase GetCurrentPhase() const { return CurrentPhase; }

    UFUNCTION(BlueprintPure, Category = "ED|EDGameDataSubsystem")
    bool IsDataReady() const { return CurrentPhase == EDataLoadPhase::Completed; }
public:
    // ================================================================
    // 델리게이트
    // ================================================================

	UPROPERTY(BlueprintAssignable, Category = "ED|EDGameDataSubsystem")
	FOnAllLobbyDataLoaded OnLobbyDataReady;

    /**
     * 모든 데이터 로드 완료 시 방송
     * 게임 시작 신호
	 */
    UPROPERTY(BlueprintAssignable, Category = "ED|EDGameDataSubsystem")
    FOnAllDataLoaded OnAllDataLoaded;

    /**
     * 단계가 바뀔 때마다 방송
     * 로딩 화면 텍스트 업데이트용
     */
    UPROPERTY(BlueprintAssignable, Category = "ED|EDGameDataSubsystem")
    FOnDataPhaseChanged OnPhaseChanged;

    // ================================================================
    // Asset Type 상수 (Project Settings > Asset Manager 등록 이름과 일치)
    // ================================================================
    static const FPrimaryAssetType LobbyAssetType;
    static const FPrimaryAssetType ItemAssetType;
    static const FPrimaryAssetType MonsterAssetType;
private:
	// ================================================================
	// 단계별 로드 함수
	// ================================================================
	void LoadPhase_Lobby();
	void LoadPhase_Item();
	void LoadPhase_Monster();

	// 각 단계 완료 콜백
	void OnLobbyDataLoaded();
	void OnItemDataLoaded();
	void OnMonsterDataLoaded();

	// 공통 캐싱 헬퍼 -> 특정 타입의 로드된 에셋들을 DataCache에 저장
	void CacheLoadedAssets(const FPrimaryAssetType& AssetType);

	// 상태 전이 헬퍼
	void SetPhase(EDataLoadPhase NewPhase);

	/**
	 * 에셋 캐시
	 * Key: FPrimaryAssetId
	 * Value: 로드된 UObject* 포인터
	 */
	UPROPERTY()
	TMap<FPrimaryAssetId, TObjectPtr<UObject>> DataCache;

	/**
	 * 핸들 컨테이너 -> 멤버변수로 관리 소멸 방지
	 * Key: 단계 이름
	 */
	TMap<FName, TSharedPtr<FStreamableHandle>> PhaseHandles;

	// ================================================================
	// 내부 상태
	// ================================================================
	EDataLoadPhase CurrentPhase = EDataLoadPhase::NotStarted;
};

// ================================================================
// 템플릿 구현부
// ================================================================

template<typename T>
T* UEDGameDataSubsystem::GetData(const FPrimaryAssetId& Id) const
{
	const TObjectPtr<UObject>* Found = DataCache.Find(Id);
	return Found ? Cast<T>(*Found) : nullptr;
}

template<typename T>
TArray<T*> UEDGameDataSubsystem::GetAllDataOfType(const FPrimaryAssetType& AssetType) const
{
	TArray<T*> Result;

	for (const auto& Pair : DataCache)
	{
		if (Pair.Key.PrimaryAssetType == AssetType)
		{
			if (T* Casted = Cast<T>(Pair.Value.Get()))
			{
				Result.Add(Casted);
			}
		}
	}
	return Result;
}
  • 로딩 파이프라인 구성
  • PrimaryDataAsset의 텍스트데이터? 들을 미리 캐싱해둠
  • TSoftObjectPtr, TSoftClassPtr 즉 소프트 레퍼런스로 등록해놓은 에셋들은 따로 해당 클래스에서 AM.LoadAssetAsync로 비동기 로드하면 될듯하다.
  • 로비 게임모드에서 AllDataLoaded 델리게이트를 구독시키고 콜백을 만들어서 게임씬으로 넘어가게끔 하면 될듯함
#include "Core/EDGameDataSubsystem.h"

#include "Core/EDAssetManager.h"
#include "Kismet/GameplayStatics.h"

const FPrimaryAssetType UEDGameDataSubsystem::LobbyAssetType = FPrimaryAssetType(TEXT("LobbyData"));
const FPrimaryAssetType UEDGameDataSubsystem::ItemAssetType = FPrimaryAssetType(TEXT("ItemData"));
const FPrimaryAssetType UEDGameDataSubsystem::MonsterAssetType = FPrimaryAssetType(TEXT("MonsterData"));

void UEDGameDataSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);
	LoadPhase_Lobby();
}

void UEDGameDataSubsystem::Deinitialize()
{
	Super::Deinitialize();
}

UEDGameDataSubsystem* UEDGameDataSubsystem::Get(const UObject* WorldContextObject)
{
	if (!WorldContextObject) return nullptr;

	UGameInstance* GI = UGameplayStatics::GetGameInstance(WorldContextObject);
	if (!GI) return nullptr;

	return GI->GetSubsystem<UEDGameDataSubsystem>();
}

// 외부에서 호출
void UEDGameDataSubsystem::InitializeGameData()
{
	if (CurrentPhase != EDataLoadPhase::LobbyReady)
	{
		UE_LOG(LogTemp, Warning, TEXT("[EDGameDataSubsystem - InitializeGameData] 로비 데이터가 아직 준비되지 않았습니다."));
		return;
	}
	LoadPhase_Item();
}

// ================================================================
// 단계별 로드 구현
// ================================================================

void UEDGameDataSubsystem::LoadPhase_Lobby()
{
	SetPhase(EDataLoadPhase::LoadingLobby);
	UEDAssetManager& AM = UEDAssetManager::Get();
	TArray<FPrimaryAssetId> Ids;
	AM.GetPrimaryAssetIdList(LobbyAssetType, Ids);
	if (Ids.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("[EDGameDataSubsystem - LoadPhase_Lobby] Data가 없습니다. 다음 단계로 건너뜁니다."));
		OnLobbyDataLoaded();
		return;
	}

	TSharedPtr<FStreamableHandle> Handle = AM.LoadPrimaryAssetsAsync(
		Ids,
		{TEXT("Lobby")},
		FStreamableDelegate::CreateUObject(this, &UEDGameDataSubsystem::OnLobbyDataLoaded)
	);

	PhaseHandles.Add(TEXT("Lobby"), Handle);
}

void UEDGameDataSubsystem::LoadPhase_Item()
{
	SetPhase(EDataLoadPhase::LoadingItem);
	UEDAssetManager& AM = UEDAssetManager::Get();
	TArray<FPrimaryAssetId> Ids;
	AM.GetPrimaryAssetIdList(ItemAssetType, Ids);
	if (Ids.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("[EDGameDataSubsystem - LoadPhase_Item] Data가 없습니다. 다음 단계로 건너뜁니다."));
		OnItemDataLoaded();
		return;
	}

	TSharedPtr<FStreamableHandle> Handle = AM.LoadPrimaryAssetsAsync(
		Ids,
		{TEXT("ITEM")},
		FStreamableDelegate::CreateUObject(this, &UEDGameDataSubsystem::OnItemDataLoaded)
	);
	PhaseHandles.Add(TEXT("Item"), Handle);
}

void UEDGameDataSubsystem::LoadPhase_Monster()
{
	SetPhase(EDataLoadPhase::LoadingMonster);
	UEDAssetManager& AM = UEDAssetManager::Get();
	TArray<FPrimaryAssetId> Ids;
	AM.GetPrimaryAssetIdList(MonsterAssetType, Ids);
	if (Ids.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("[EDGameDataSubsystem - LoadPhase_Monster] Data가 없습니다. 다음 단계로 건너뜁니다."));
		OnMonsterDataLoaded();
		return;
	}

	TSharedPtr<FStreamableHandle> Handle = AM.LoadPrimaryAssetsAsync(
		Ids,
		{TEXT("Monster")},
		FStreamableDelegate::CreateUObject(this, &UEDGameDataSubsystem::OnMonsterDataLoaded)
	);
	PhaseHandles.Add(TEXT("Monster"), Handle);
}

// ================================================================
// 단계별 완료 콜백 함수
// ================================================================

/**
 * 로비에서 필요한 데이터 다 로드하고 나서 오는 곳
 * 여기서는 다음 페이즈로 넘기지 않고, 외부에서 (게임씬같은곳에서 게임 화면으로 넘어갈때)
 * InitializeGameData를 실행해서 다음 로드를 시작
 */
void UEDGameDataSubsystem::OnLobbyDataLoaded()
{
	CacheLoadedAssets(LobbyAssetType);
	SetPhase(EDataLoadPhase::LobbyReady);
	OnLobbyDataReady.Broadcast();
	UE_LOG(LogTemp, Log, TEXT("[EDGameDataSubsystem - OnLobbyDataLoaded] Lobby 데이터 로드 완료"));
}

void UEDGameDataSubsystem::OnItemDataLoaded()
{
	CacheLoadedAssets(ItemAssetType);
	UE_LOG(LogTemp, Log, TEXT("[EDGameDataSubsystem - OnItemDataLoaded] Item 데이터 로드 완료"));
	LoadPhase_Monster();
}

void UEDGameDataSubsystem::OnMonsterDataLoaded()
{
	CacheLoadedAssets(MonsterAssetType);
	UE_LOG(LogTemp, Log, TEXT("[EDGameDataSubsystem - OnMonsterDataLoaded]  Monster 데이터 로드 완료"));
	SetPhase(EDataLoadPhase::Completed);

	// 완료 신호
	OnAllDataLoaded.Broadcast();
}

// ================================================================
// 캐싱 함수
// ================================================================

void UEDGameDataSubsystem::CacheLoadedAssets(const FPrimaryAssetType& AssetType)
{
	UEDAssetManager& AM = UEDAssetManager::Get();
	TArray<FPrimaryAssetId> Ids;
	AM.GetPrimaryAssetIdList(AssetType, Ids);
	for (const FPrimaryAssetId& Id : Ids)
	{
		UObject* Obj = AM.GetPrimaryAssetObject(Id);
		if (!Obj)
		{
			UE_LOG(LogTemp, Warning, TEXT("[EDGameDataSubsystem - CacheLoadedAssets] 로드 실패 : %s"), *Id.ToString());
			continue;
		}
		DataCache.Add(Id, Obj);
	}
}

// ================================================================
// 상태 전이
// ================================================================

void UEDGameDataSubsystem::SetPhase(EDataLoadPhase NewPhase)
{
	CurrentPhase = NewPhase;
	OnPhaseChanged.Broadcast(NewPhase);
}
  • cpp에서는 LoadPhase와 OnLoad콜백으로 나누어져 있다.
  • LoadPhase를 통해서 다음에 어떤 데이터를 로드해야하는지 나눴고, 콜백으로 다음 페이즈로 넘어가는식으로 설계 되어있다.
  • 사이사이에 SetPhase로 페이즈를 갱신해주는 부분도 있다.

해야할거

  • 주석 줄이기
  • 언로드 로직 만들기
  • 팀원들이 보기 편하게 비동기로드 정리하기
  • EDGameDataSubsystem에서 데이터 로드 파이프라인 조금 더 구체화 하기
  • 인벤토리 프라이머리 데이터 에셋으로 실험해보기
  • 인벤토리열때 데이터를 메모리로 들고있는걸로 ㄱㄱ, 그리고 ?같은 이미지도 좋을것 같다.
  • 메모리에서 언로드할때 지연시간 가지기

배운거

  • 데이터 테이블과 데이터 에셋
    데이터 테이블로도 데이터 에셋이 할수있는 일을 대체해서 사용할 수 있다.
    데이터테이블에서 TSoftObjectPtr같이 경로만 들고있을수 있음 문자열로
    굳이 치면 단점은 데이터 테이블은 한번에 메모리에 다 올려야 한다. 그래서 데이터 테이블 자체는 하드로 사용

  • magic number

매직 넘버(Magic Number)란 코드 안에서 의미 설명 없이 그냥 하드코딩된 숫자(또는 문자열) 상수

0개의 댓글