#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;
}
#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);
}
데이터 테이블과 데이터 에셋
데이터 테이블로도 데이터 에셋이 할수있는 일을 대체해서 사용할 수 있다.
데이터테이블에서 TSoftObjectPtr같이 경로만 들고있을수 있음 문자열로
굳이 치면 단점은 데이터 테이블은 한번에 메모리에 다 올려야 한다. 그래서 데이터 테이블 자체는 하드로 사용
magic number
매직 넘버(Magic Number)란 코드 안에서 의미 설명 없이 그냥 하드코딩된 숫자(또는 문자열) 상수