강의 목표

  • 언리얼 엔진의 애셋과 이를 포장한 패키지의 개념 이해
  • 언리얼 에디터에서 볼 수 있도록 애셋을 저장하고 불러들이는 방법의 이해
  • 오브젝트 패스를 사용해 다양한 방식으로 애셋을 로딩 하는 방법의 이해

 

 

인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다. 
😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!

 

 

 

목차

     

     


     

     

     

    패키지 (Package)

     

    언리얼 엔진의 패키지와 애셋 개념을 이해하고 언리얼 에디터에 애셋 데이터를 저장하고 불러들이기


     

     

    언리얼 오브젝트 패키지

     

    단일 언리얼 오브젝트가 가진 정보는 저장할 수 있지만, 오브젝트들이 조합되어 있다면?

    • 저장된 언리얼 오브젝트 데이터를 효과적으로 찾고 관리하는 방법은?
    • 복잡한 계층 구조를 가진 언리얼 오브젝트를 효과적으로 저장과 불러들이는 방법을 통일해야 함

     

    언리얼 엔진은 이를 위해 패키지(UPackage) 단위로 언리얼 오브젝트를 관리함

     

    패키지(Package)의 중의적 개념

    • 언리얼 엔진은 다양한 곳에서 단어 패키지를 사용하고 있음
    • 언리얼 오브젝트를 감싼 포장 오브젝트를 의미함. (이번 강의의 주제)
    • 또한 개발된 최종 컨텐츠를 정리해 프로그램으로 만드는 작업을 의미함. ex. 게임 패키징
    • DLC와 같이 향후 확장 콘텐츠에 사용되는 별도의 데이터 묶음을 의미하기도 함 ex. pkg 파일

     

    구분을 위해 언리얼 오브젝트 패키지로 부르는 것도 고려


     

     

    패키지(Package)와 애셋(Asset)

     

    • 언리얼 오브젝트 패키지는 다수의 언리얼 오브젝트를 포장하는데 사용하는 언리얼 오브젝트
      • 모든 언리얼 오브젝트는 패키지에 소속되어 있음.  ex. Transient Package
    • 언리얼 오브젝트 패키지의 서브 오브젝트를 애셋(Asset)이라고 하며 에디터에는 이들이 노출됨.
    • 구조 상 패키지는 다수의 언리얼 오브젝트를 소유할 수 있으나, 일반적으로는 하나의 애셋만 가짐.
    • 애셋은 다시 다수의 서브 오브젝트를 가질 수 있으며, 모두 언리얼 오브젝트 패키지에 포함됨.
      • 하지만 에디터에는 노출되지 않음(= 서브 오브젝트는 에디터에 노출되지 않음). 


     

     

    패키지 생성 코드

     

    const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
    
    ...
    UPackage* StudentPackage = CreatePackage(*PackageName);
    EObjectFlags ObjectFlag = RF_Public | RF_Standalone;
    
    UStudent* TopStudent = NewObject<UStudent>(StudentPackage, 
    																UStudent::StaticClass(), *AssetName, ObjectFlag);
    TopStudent->SetName(TEXT("홍길동"));
    TopStudent->SetOrder(36);

     

    • CreatePackage() :  패키지를 찾고 없는 경우 생성
    • EObjectFlags :  저장 플래그
    • NewObject<T>() :  파라미터가 없는 경우 Transient Package라고 하는 임시 패키지에 저장

     


     

     

    전체코드

     

    GameInstance.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "Engine/StreamableManager.h"
    #include "MyGameInstance.generated.h"
    
    struct FStudentData
    {
    	FStudentData() {}
    	FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {}
    
    	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
    	{
    		Ar << InStudentData.Order;
    		Ar << InStudentData.Name;
    		return Ar;
    	}
    
    	int32 Order = -1;
    	FString Name = TEXT("홍길동");
    };
    
    UCLASS()
    class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
    {
    	GENERATED_BODY()
    	
    public:
    	UMyGameInstance();
    	virtual void Init() override;
    
    	void SaveStudentPackage() const; // 패키지 저장
    	void LoadStudentPackage() const; // 패키지 로드
    
    private:
    
    	static const FString PackageName;
    	static const FString AssetName;
    
    	UPROPERTY()
    	TObjectPtr<class UStudent> StudentSrc;
    };

    패키지 저장

    • void SaveStudentPackage() const

    패키지 로드

    • void LoadStudentPackage() const

     

     

    GameInstance.cpp

    더보기
    #include "MyGameInstance.h"
    #include "Student.h"
    #include "JsonObjectConverter.h"
    #include "UObject/SavePackage.h"
    
    // "/Game"은 Game에서 사용되는 에셋들을 모아놓은 대표 폴더를 의미한다.
    const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
    const FString UMyGameInstance::AssetName = TEXT("TopStudent");
    
    void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)
    {
    	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
    }
    
    UMyGameInstance::UMyGameInstance()
    {
    }
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    
    	FStudentData RawDataSrc(16, TEXT("이득우"));
    
    	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
    	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);
    
    	{
    		const FString RawDataFileName(TEXT("RawData.bin"));
    		FString RawDataAbosolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
    		UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
    		FPaths::MakeStandardFilename(RawDataAbosolutePath);
    		UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
    
    		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
    		if (nullptr != RawFileWriterAr)
    		{
    			*RawFileWriterAr << RawDataSrc;
    			RawFileWriterAr->Close();
    			delete RawFileWriterAr;
    			RawFileWriterAr = nullptr;
    		}
    
    		FStudentData RawDataDest;
    		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath);
    		if (nullptr != RawFileReaderAr)
    		{
    			*RawFileReaderAr << RawDataDest;
    			RawFileReaderAr->Close();
    			delete RawFileReaderAr;
    			RawFileReaderAr = nullptr;
    
    			UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
    		}
    	}
    
    	StudentSrc = NewObject<UStudent>();
    	StudentSrc->SetName(TEXT("이득우"));
    	StudentSrc->SetOrder(59);
    
    	{
    		const FString ObjectDataFileName(TEXT("ObjectData.bin"));
    		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
    		FPaths::MakeStandardFilename(ObjectDataAbsolutePath);
    
    		TArray<uint8> BufferArray;
    		FMemoryWriter MemoryWriterAr(BufferArray);
    		StudentSrc->Serialize(MemoryWriterAr);
    		
    		if (TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
    		{
    			*FileWriterAr << BufferArray;
    			FileWriterAr->Close();
    		}
    
    		TArray<uint8> BufferArrayFromFile;
    		if (TUniquePtr<FArchive> FileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
    		{
    			*FileReaderAr << BufferArrayFromFile;
    			FileReaderAr->Close();
    		}
    
    		FMemoryReader MemoryReaderAr(BufferArrayFromFile);
    		UStudent* StudentDest = NewObject<UStudent>();
    		StudentDest->Serialize(MemoryReaderAr);
    		PrintStudentInfo(StudentDest, TEXT("ObjectData"));
    	}
    
    	{
    		const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
    		FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
    		FPaths::MakeStandardFilename(JsonDataAbsolutePath);
    
    		TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
    		FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);
    
    		FString JsonOutString;
    		TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
    		if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
    		{
    			FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
    		}
    
    		FString JsonInString;
    		FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);
    
    		TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
    
    		TSharedPtr<FJsonObject> JsonObjectDest;
    		if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
    		{
    			UStudent* JsonStudentDest = NewObject<UStudent>();
    			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
    			{
    				PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
    			}
    		}
    	}
    
    	SaveStudentPackage();
    	LoadStudentPackage();
    
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    	Handle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
    		[&]()
    		{
    			if (Handle.IsValid() && Handle->HasLoadCompleted())
    			{
    				UStudent* TopStudent = Cast<UStudent>(Handle->GetLoadedAsset());
    				if (TopStudent)
    				{
    					PrintStudentInfo(TopStudent, TEXT("AsyncLoad"));
    
    					Handle->ReleaseHandle();
    					Handle.Reset();
    				}
    			}
    		}
    	);
    }
    
    void UMyGameInstance::SaveStudentPackage() const
    {
    	// 패키지 로드
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (StudentPackage) // 패키지 있다면 로드
    	{
    		StudentPackage->FullyLoad(); // 안에 가지고 있는 에셋을 모두 다 로딩
    	}
    
    	StudentPackage = CreatePackage(*PackageName);		 // 패키지 생성
    	EObjectFlags ObjectFlag = RF_Public | RF_Standalone; // 패키지 저장 옵션
    
    	// 학생 클래스 생성: 생성한 패키지가 StudentPackage 안으로 들어가도록 설정. 에셋 이름 설정. 저장 플래그 설정.
    	UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
    	TopStudent->SetName(TEXT("이득우"));
    	TopStudent->SetOrder(36);
    
    	// 서브오브젝트 10개 생성
    	const int32 NumofSubs = 10;
    	for (int32 ix = 1; ix <= NumofSubs; ++ix)
    	{
    		FString SubObjectName = FString::Printf(TEXT("Student%d"), ix); // 이름 설정.
    		UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
    		SubStudent->SetName(FString::Printf(TEXT("학생%d"), ix));
    		SubStudent->SetOrder(ix);
    	}
    
    	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    	FSavePackageArgs SaveArgs; // 저장옵션 변수 선언
    	SaveArgs.TopLevelFlags = ObjectFlag; // 저장옵션 설정.
    
    	// 패키지 저장
    	if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
    	{
    		UE_LOG(LogTemp, Log, TEXT("패키지가 성공적으로 저장되었습니다."));
    	}
    }
    
    void UMyGameInstance::LoadStudentPackage() const
    {
    	// 패키지 로드
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (nullptr == StudentPackage)
    	{
    		UE_LOG(LogTemp, Warning, TEXT("패키지를 찾을 수 없습니다."));
    		return;
    	}
    
    	StudentPackage->FullyLoad(); // 안에 가지고 있는 에셋을 모두 다 로딩
    
    	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName); // 원하는 에셋을 찾아서 넣음.
    	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
    }

    패키지 저장

    • void UMyGameInstance::SaveStudentPackage() const

    패키지 로드

    • void UMyGameInstance::LoadStudentPackage() const

     

     


     

     

    실행화면

     

     


     

     

     

     

    에셋 참조와 로딩

     


     

     

    에셋 정보의 저장과 로딩 전략

     

    게임 제작 단계에서 애셋 간의 연결 작업을 위해 직접 패키지를 불러 할당하는 작업은 부하가 큼.

    •  애셋 로딩 대신 패키지와 오브젝트를 지정한 문자열을 대체해 사용. 이를 오브젝트 경로라고 함.
    •  프로젝트 내에 오브젝트 경로 값은 유일함을 보장함.
    •  그렇기에 오브젝트 간의 연결은 오브젝트 경로 값으로 기록될 수 있음.
    •  오브젝트 경로를 사용해 다양한 방법으로 애셋을 로딩할 수 있음.

     

    애셋의 로딩 전략

    •  프로젝트에서 애셋이 반드시 필요한 경우:  생성자 코드에서 미리 로딩
    •  런타임에서 필요한 때 바로 로딩하는 경우:  런타임 로직에서 정적 로딩
    •  런타임에서 비동기적으로 로딩하는 경우:  런타임 로직에서 관리자를 사용 비동기 로딩

     

    에디터에서 애셋을 지정할 때마다 항상 로딩해야 하는가?

     


     

     

    오브젝트 경로 (Object Path)

     

    • 패키지 이름과 애셋 이름을 한 데 묶은 문자열
    • 애셋 클래스 정보는 생략할 수 있음
    • 패키지 내 데이터를 모두 로드하지 않고 오브젝트 경로(Object Path)를 사용해 필요한 애셋만 로드할 수 있음.

     

    { 애셋 클래스 정보 }'{패키지 이름}.{애셋 이름}'

    또는 

    {패키지 이름}.{애셋 이름}


     

     

    언리얼 공식문서 

     

    https://dev.epicgames.com/documentation/ko-kr/unreal-engine/referencing-assets-in-unreal-engine?application_version=5.3


     

     

    강참조:  직접 프로퍼티 참조 (UPROPERTY 매크로),  생성 시간 참조(ConstructorHelpers)

     

    1.  직접 프로퍼티 참조:  UPROPERTY 매크로를 통해 노출

    • 게임플레이 클래스에는 UPROPERTY 를 노출시키면 디자이너가 특정 에셋에 블루프린트 상속을 통해서 또는 월드에 배치한 인스턴스를 통해서 아키타입을 지정한다.
    /** construction start sound stinger */
    
    UPROPERTY(EditDefaultsOnly, Category=Building)
    USoundCue* ConstructionStartStinger;

     

    2.  생성 시간 참조

    • 주어진 프로퍼티에 대해 로드해야 하는 에셋을 프로그래머가 정확히 알고 있어, 그 프로퍼티를 오브젝트의 생성시 일부분으로 설정하는 경우
    • ConstructorHelpers 라는 특수 클래스가 사용되는데, 생성 단계 도중 오브젝트와 오브젝트의 클래스를 찾는 것이다.
    /** gray health bar texture */
    
    UPROPERTY()
    class UTexture2D* BarFillTexture;
    
    AStrategyHUD::AStrategyHUD(const FObjectInitializer& ObjectInitializer) 
    	: Super(ObjectInitializer)
    {
    	static ConstructorHelpers::FObjectFinder<UTexture2D> BarFillObj(TEXT("/Game/UI/HUD/BarFill"));
    	...
    	BarFillTexture = BarFillObj.Object;
    	...
    }

     

     

    약참조.  간접 프로퍼티 참조(TSoftObjectPtr)

     

    • 에셋 로드 시점을 쉽게 제어할 수 있는 방법은 TSoftObjectPtr 을 사용하는 것이다. 
    • 디자이너의 경우, 직접 프로퍼티 레퍼런스인 것처럼 작업할 수 있다. 하지만 직접 포인터 레퍼런스 대신, 에셋의 로드 여부에 대한 안전 검사가 가능한 템플릿 코드와 함께 프로퍼티가 스트링으로 저장된다. 
    •  TSoftObjectPtr 을 사용하려면 에셋을 수동으로 로드해야 한다.
    • 템플릿으로 된 LoadObject<>() 메서드나 StaticLoadObject() 나 FStreamingManager 를 사용하여 오브젝트를 로드할 수 있다 (상세 정보는 비동기 에셋 로딩 문서를 참고). 처음 두 메서드는 에셋을 동기식으로 로드하여 프레임 속도가 출렁일 수 있으니, 게임플레이에 영향을 끼치지 않을 것이 확실한 것에만 사용해야 한다.
    UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Building)
    TSoftObjectPtr<UStaticMesh> BaseMesh;
    
    UStaticMesh* GetLazyLoadedMesh()
    {
    	if (BaseMesh.IsPending())
    	{
            const FSoftObjectPath& AssetRef = BaseMesh.ToStringReference();
            BaseMesh = Cast< UStaticMesh>(Streamable.SynchronousLoad(AssetRef));
    	}
    	
        return BaseMesh.Get();
    }

     

     

    위 코드는 UStaticMesh 의 TSoftObjectPtr 을 사용하여 실행시간에 메시를 지연시켜 로드하고 있다. 에셋은 오브젝트의 로드 여부를 검사합니다. 로드되지 않은 경우, FStreamingManager 를 사용한 동기성 로드가 일어난다. 아니면 TSoftObjectPtr 안의 UStaticMesh 포인터가 호출자에게 반환된다.

    UClass 를 유예식으로 로드하려는 경우, TAssetSubclassOf 템플릿 유형 클래스 전용 버전을 대체하여 TSoftClassPtr 과 같은 접근법을 사용할 수 있다. 이 함수는 지정된 에셋을 참조하는 것과 같은 역할을 하지만, 인스턴스 대신 에셋에 대한 UClass 를 참조한다.


     

     

    오브젝트 검색/로드 

     

    실행시간에 스트링을 만들어 그 오브젝트로의 레퍼런스를 구하는 방식

     

    1.  생성 또는 로드된 UObject 만 사용하려는 경우:  FindObject<>()

    AFunctionalTest* TestToRun = FindObject<AFunctionalTest>(TestsOuter, *TestName);

     

    2.  이미 로드되어 있지 않은 오브젝트를 로드:  LoadObject<>()

    GridTexture = LoadObject<UTexture2D>(NULL, TEXT("/Engine/EngineMaterials/DefaultWhiteGrid.DefaultWhiteGrid"), NULL, LOAD_None, NULL);

     

    3.  UClass 로드:  LoadObject<>()

    DefaultPreviewPawnClass = LoadClass<APawn>(NULL, *PreviewPawnName, NULL, LOAD_None, NULL);
    
    if (!DefaultPreviewPawnClass->IsChildOf(APawn::StaticClass()))
    {
        DefaultPreviewPawnClass = nullptr;
    }

     

     

    애셋 스트리밍 관리자 (Streamable Manager)

     

    • 애셋 비동기 로딩을 지원하는 관리자 객체
    • 콘텐츠 제작과 무관한 싱글턴 클래스에 FStreamableManager를 선언해두면 좋음.
      • GameInstance는 좋은 선택지
    • FStreamableManager를 활용해 애셋의 동기/비동기 로딩을 관리할 수 있음.
    • 다수의 오브젝트 경로를 입력해 다수의 애셋을 로딩하는 것도 가능함.  

     

     

    전체코드

     

    GameInstance.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Engine/GameInstance.h"
    #include "Engine/StreamableManager.h"
    #include "MyGameInstance.generated.h"
    
    struct FStudentData
    {
    	FStudentData() {}
    	FStudentData(int32 InOrder, const FString& InName) : Order(InOrder), Name(InName) {}
    
    	friend FArchive& operator<<(FArchive& Ar, FStudentData& InStudentData)
    	{
    		Ar << InStudentData.Order;
    		Ar << InStudentData.Name;
    		return Ar;
    	}
    
    	int32 Order = -1;
    	FString Name = TEXT("홍길동");
    };
    
    UCLASS()
    class UNREALSERIALIZATION_API UMyGameInstance : public UGameInstance
    {
    	GENERATED_BODY()
    	
    public:
    	UMyGameInstance();
    
    	virtual void Init() override;
    
    	void SaveStudentPackage() const; // 패키지 저장
    	void LoadStudentPackage() const; // 패키지 로드
    	void LoadStudentObject() const;
    
    private:
    
    	static const FString PackageName;
    	static const FString AssetName;
    
    	UPROPERTY()
    	TObjectPtr<class UStudent> StudentSrc;
    
    	FStreamableManager StreamableManager;
    	TSharedPtr<FStreamableHandle> Handle;
    };

    학생 오브젝트 로드

    • void LoadStudentObject() const;

     

     

    GameInstance.cpp

    더보기
    #include "MyGameInstance.h"
    #include "Student.h"
    #include "JsonObjectConverter.h"
    #include "UObject/SavePackage.h"
    
    // "/Game"은 Game에서 사용되는 에셋들을 모아놓은 대표 폴더를 의미한다.
    const FString UMyGameInstance::PackageName = TEXT("/Game/Student");
    const FString UMyGameInstance::AssetName = TEXT("TopStudent");
    
    void PrintStudentInfo(const UStudent* InStudent, const FString& InTag)
    {
    	UE_LOG(LogTemp, Log, TEXT("[%s] 이름 %s 순번 %d"), *InTag, *InStudent->GetName(), InStudent->GetOrder());
    }
    
    UMyGameInstance::UMyGameInstance()
    {
    	// 생성자에서 애셋 로딩(=미리 다 메모리에 올라와 있어야 된다는 것을 의미)
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName);
    	static ConstructorHelpers::FObjectFinder<UStudent> UASSET_TopStudent(*TopSoftObjectPath);
    	if (UASSET_TopStudent.Succeeded())
    	{
    		PrintStudentInfo(UASSET_TopStudent.Object, TEXT("Constructor"));
    	}
    }
    
    void UMyGameInstance::Init()
    {
    	Super::Init();
    
    	FStudentData RawDataSrc(16, TEXT("이득우"));
    
    	const FString SavedDir = FPaths::Combine(FPlatformMisc::ProjectDir(), TEXT("Saved"));
    	UE_LOG(LogTemp, Log, TEXT("저장할 파일 폴더 : %s"), *SavedDir);
    
    	{
    		const FString RawDataFileName(TEXT("RawData.bin"));
    		FString RawDataAbosolutePath = FPaths::Combine(*SavedDir, *RawDataFileName);
    		UE_LOG(LogTemp, Log, TEXT("저장할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
    		FPaths::MakeStandardFilename(RawDataAbosolutePath);
    		UE_LOG(LogTemp, Log, TEXT("변경할 파일 전체 경로 : %s"), *RawDataAbosolutePath);
    
    		FArchive* RawFileWriterAr = IFileManager::Get().CreateFileWriter(*RawDataAbosolutePath);
    		if (nullptr != RawFileWriterAr)
    		{
    			*RawFileWriterAr << RawDataSrc;
    			RawFileWriterAr->Close();
    			delete RawFileWriterAr;
    			RawFileWriterAr = nullptr;
    		}
    
    		FStudentData RawDataDest;
    		FArchive* RawFileReaderAr = IFileManager::Get().CreateFileReader(*RawDataAbosolutePath);
    		if (nullptr != RawFileReaderAr)
    		{
    			*RawFileReaderAr << RawDataDest;
    			RawFileReaderAr->Close();
    			delete RawFileReaderAr;
    			RawFileReaderAr = nullptr;
    
    			UE_LOG(LogTemp, Log, TEXT("[RawData] 이름 %s 순번 %d"), *RawDataDest.Name, RawDataDest.Order);
    		}
    	}
    
    	StudentSrc = NewObject<UStudent>();
    	StudentSrc->SetName(TEXT("이득우"));
    	StudentSrc->SetOrder(59);
    
    	{
    		const FString ObjectDataFileName(TEXT("ObjectData.bin"));
    		FString ObjectDataAbsolutePath = FPaths::Combine(*SavedDir, *ObjectDataFileName);
    		FPaths::MakeStandardFilename(ObjectDataAbsolutePath);
    
    		TArray<uint8> BufferArray;
    		FMemoryWriter MemoryWriterAr(BufferArray);
    		StudentSrc->Serialize(MemoryWriterAr);
    		
    		if (TUniquePtr<FArchive> FileWriterAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*ObjectDataAbsolutePath)))
    		{
    			*FileWriterAr << BufferArray;
    			FileWriterAr->Close();
    		}
    
    		TArray<uint8> BufferArrayFromFile;
    		if (TUniquePtr<FArchive> FileReaderAr = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ObjectDataAbsolutePath)))
    		{
    			*FileReaderAr << BufferArrayFromFile;
    			FileReaderAr->Close();
    		}
    
    		FMemoryReader MemoryReaderAr(BufferArrayFromFile);
    		UStudent* StudentDest = NewObject<UStudent>();
    		StudentDest->Serialize(MemoryReaderAr);
    		PrintStudentInfo(StudentDest, TEXT("ObjectData"));
    	}
    
    	{
    		const FString JsonDataFileName(TEXT("StudentJsonData.txt"));
    		FString JsonDataAbsolutePath = FPaths::Combine(*SavedDir, *JsonDataFileName);
    		FPaths::MakeStandardFilename(JsonDataAbsolutePath);
    
    		TSharedRef<FJsonObject> JsonObjectSrc = MakeShared<FJsonObject>();
    		FJsonObjectConverter::UStructToJsonObject(StudentSrc->GetClass(), StudentSrc, JsonObjectSrc);
    
    		FString JsonOutString;
    		TSharedRef<TJsonWriter<TCHAR>> JsonWriterAr = TJsonWriterFactory<TCHAR>::Create(&JsonOutString);
    		if (FJsonSerializer::Serialize(JsonObjectSrc, JsonWriterAr))
    		{
    			FFileHelper::SaveStringToFile(JsonOutString, *JsonDataAbsolutePath);
    		}
    
    		FString JsonInString;
    		FFileHelper::LoadFileToString(JsonInString, *JsonDataAbsolutePath);
    
    		TSharedRef<TJsonReader<TCHAR>> JsonReaderAr = TJsonReaderFactory<TCHAR>::Create(JsonInString);
    
    		TSharedPtr<FJsonObject> JsonObjectDest;
    		if (FJsonSerializer::Deserialize(JsonReaderAr, JsonObjectDest))
    		{
    			UStudent* JsonStudentDest = NewObject<UStudent>();
    			if (FJsonObjectConverter::JsonObjectToUStruct(JsonObjectDest.ToSharedRef(), JsonStudentDest->GetClass(), JsonStudentDest))
    			{
    				PrintStudentInfo(JsonStudentDest, TEXT("JsonData"));
    			}
    		}
    	}
    
    	SaveStudentPackage();
    	//LoadStudentPackage();
    	LoadStudentObject();
    
    	//*************************************************************************************************
    	//** 비동기 로드
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName); // 파일 경로
    	Handle = StreamableManager.RequestAsyncLoad(TopSoftObjectPath,
    		[&]()
    		{
    			if (Handle.IsValid() && Handle->HasLoadCompleted()) // 유효하고 로딩이 다 끝났다면
    			{
    				UStudent* TopStudent = Cast<UStudent>(Handle->GetLoadedAsset());
    				if (TopStudent)
    				{
    					PrintStudentInfo(TopStudent, TEXT("AsyncLoad"));
    
    					Handle->ReleaseHandle();
    					Handle.Reset();
    				}
    			}
    		}
    	);
        //************************************************************************************************
    }
    
    void UMyGameInstance::SaveStudentPackage() const
    {
    	// 패키지 로드
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (StudentPackage) // 패키지 있다면 로드
    	{
    		StudentPackage->FullyLoad(); // 안에 가지고 있는 에셋을 모두 다 로딩
    	}
    
    	StudentPackage = CreatePackage(*PackageName);		 // 패키지 생성
    	EObjectFlags ObjectFlag = RF_Public | RF_Standalone; // 패키지 저장 옵션
    
    	// 학생 클래스 생성: 생성한 패키지가 StudentPackage 안으로 들어가도록 설정. 에셋 이름 설정. 저장 플래그 설정.
    	UStudent* TopStudent = NewObject<UStudent>(StudentPackage, UStudent::StaticClass(), *AssetName, ObjectFlag);
    	TopStudent->SetName(TEXT("이득우"));
    	TopStudent->SetOrder(36);
    
    	// 서브오브젝트 10개 생성
    	const int32 NumofSubs = 10;
    	for (int32 ix = 1; ix <= NumofSubs; ++ix)
    	{
    		FString SubObjectName = FString::Printf(TEXT("Student%d"), ix); // 이름 설정.
    		UStudent* SubStudent = NewObject<UStudent>(TopStudent, UStudent::StaticClass(), *SubObjectName, ObjectFlag);
    		SubStudent->SetName(FString::Printf(TEXT("학생%d"), ix));
    		SubStudent->SetOrder(ix);
    	}
    
    	const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
    	FSavePackageArgs SaveArgs; // 저장옵션 변수 선언
    	SaveArgs.TopLevelFlags = ObjectFlag; // 저장옵션 설정.
    
    	// 패키지 저장
    	if (UPackage::SavePackage(StudentPackage, nullptr, *PackageFileName, SaveArgs))
    	{
    		UE_LOG(LogTemp, Log, TEXT("패키지가 성공적으로 저장되었습니다."));
    	}
    }
    
    void UMyGameInstance::LoadStudentPackage() const
    {
    	// 패키지 로드
    	UPackage* StudentPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
    	if (nullptr == StudentPackage)
    	{
    		UE_LOG(LogTemp, Warning, TEXT("패키지를 찾을 수 없습니다."));
    		return;
    	}
    
    	StudentPackage->FullyLoad(); // 안에 가지고 있는 에셋을 모두 다 로딩
    
    	UStudent* TopStudent = FindObject<UStudent>(StudentPackage, *AssetName); // 원하는 에셋을 찾아서 넣음.
    	PrintStudentInfo(TopStudent, TEXT("FindObject Asset"));
    }
    
    void UMyGameInstance::LoadStudentObject() const
    {
    	const FString TopSoftObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName); // 애셋의 오브젝트 경로
    
    	// 로딩: 패키지를 로딩하지 않기 때문에 nullptr값을 첫번째 인자 넣음. 두번째 인자로 경로 정보를 넣음.
    	UStudent* TopStudent = LoadObject<UStudent>(nullptr, *TopSoftObjectPath);
    	PrintStudentInfo(TopStudent, TEXT("LoadObject Asset"));
    }

    학생 오브젝트 로드

    • void UMyGameInstance::LoadStudentObject() const;

    비동기 로딩

    • void UMyGameInstance::Init()

     


     

     

    실행화면

     

     

     

    < 생성자에서 에셋 로딩 코드 추가한 버젼 >

     

    만약 위의 파일을 지우고 언리얼 엔진 에디터를 실행하면 아래와 같은 메시지를 띄우며 에디터가 열리지 않는다.

     

    따라서 에셋이 빠지지 않도록 주의해야 한다.

     

     

    < 비동기 로드 추가 버젼 >