PackageAsset Create |
PackageAsset Reference |
PackageAsset Load |
||||||||||
|
|
|
/* UObject 생성 */
{
UMyClass* MyObject = NewObject<UMyClass>();
}
/* 이미 메모리 상에 할당된 경우 */
{
UMyClass* MyObject = FindObject<UMyClass>(MyPackageName, MyAssetName);
}
/* 메모리 상에 할당되지 않은 경우
내부적으로 FindObject 과 같은 역할을 하므로, 이것만 써도 문제없다.
*/
{
UMyClass* MyObject = LoadObject<UMyClass>(NULL,TEXT("PackageName.AssetName")/*, NULL, LOAD_None, NULL*/);
}
/* 생성자단계 로드
생성자에서 에셋을 로딩하는 경우, 게임이 시작되기전에 미리 다 메모리에 올라와야 있어야 함을 의미한다.
반드시 있다는 전제하에 수행되어야 한다. 없다면, 에디터 초기화시 강력한 에러가 발생하게 된다. ("Error : CDO Constructor")
해당 생성자코드는 에디터 초기화시와, 게임시작시 총 2번 호출된다.
*/
{
const FString MyObjectPath = FString::Printf(TEXT("%s,%s"), TEXT("PackageName"), TEXT("AssetName"));
static ConstructorHelpers::FObjectFinder<UMyClass> MyObject_Loaded(*MyObjectPath);
if(MyObject_Loaded.Succeeded())
{
UMyClass* MyObject = MyObject_Loaded.Object;
}
}
/* 비동기 로드 */
{
// .h
#include "Engine/StreamableManager.h"
FStreamableManager* StreamableManager;
TSharedPtr<FStreamableHandle> Handle; // 스트리밍된 에셋을 관리할 수 있는 핸들
//.cpp
const FString MyObjectPath = FString::Printf(TEXT("%s,%s"), TEXT("PackageName"), TEXT("AssetName"));
Handle = StreamableManager.RequestAsyncLoad(MyObjectPath,
[&](){
if(Handle.IsValid() && Handle->HasLoadCompleted())
{
UMyClass* MyObject = Cast<UMyClass>(Handle->GetLoadedAsset());
if(MyObject)
{
// 비동기 로드 완료
Handle->ReleaseHandle(); //사용한 핸들 닫기
Handle.Reset();
}
}
}
)
}
UObject 생성 | NewObject | to Package |
UObject 로드 | LoadObject | in Package |
UObject 찾기 | FindObject | in Package
in Asset |
하위 언리얼 오브젝트가 있는, 언리얼 오브젝트의 관리
패키지를 만들기 위해서는 패키지와, 패키지가 담을 대표 에셋을 설정해 주어야 한다. 그리고 이것들의 이름을 지정해주어야 한다.
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
{
FString PackageName = TEXT("/Game/AssetPackage_2");
FString AssetName = TEXT("Asset_A");
FString FilePath = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension()); // FilePath : ../../../../../UnrealProject/JumpGame/CPPTEST/CPPTEST/Content/AssetPackage_2.uasset
EObjectFlags ObjectDefaultFlag = RF_Public | RF_Standalone; // ObjectFlag 는 Package 저장시, UObject 생성시에 쓰인다.
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = ObjectDefaultFlag;
// 패키지 이미 있다면
UPackage* SavePack = LoadPackage(nullptr, *FilePath, LOAD_None); //LoadPackage()
// 기존 패키지 로드
if (SavePack)
{
SavePack->FullyLoad();
}
// 패키지 생성
SavePack = CreatePackage(*PackageName);
// 오브젝트 설정
{
UMyObject* ObjForSave = NewObject<UMyObject>(SavePack, *AssetName, ObjectDefaultFlag);
ObjForSave->intParam = 25;
ObjForSave->StrParam = TEXT("#he231lldoekdll3142lde21jnkskk");
}
// 패키지 저장
UPackage::SavePackage(SavePack, nullptr, *FilePath, SaveArgs);
UE_LOG(LogTemp, Warning, TEXT("FilePath : %s"), *FilePath);
// 패키지 로드 ( FilePath 를 사용해도 된다. )
UPackage* LoadPack = LoadPackage(nullptr, *FilePath, LOAD_None);
if (LoadPack)
{
LoadPack->FullyLoad();
// 오브젝트 찾기
{
UMyObject* FoundObj = FindObject<UMyObject>(LoadPack, *AssetName);
if (FoundObj)
{
UE_LOG(LogTemp, Warning, TEXT("Obj : {%d , %s}"), FoundObj->intParam, *FoundObj->StrParam);
}
else {
UE_LOG(LogTemp, Warning, TEXT("not Found Object in Package"));
}
}
}
}
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
/*
/Temp : Saved 폴더에 매핑
/Game : Content 폴더에 매핑
*/
const FString PackageName = TEXT("/Game/MyPackage"); // 패키지 이름 설정 (폴더에 저장될 패키지의 이름이 된다. /Game/MyPackage.uasset )
const FString PackageAssetName = TEXT("PackageTargetClass_AssetName"); // 패키지 대표 에셋 이름 설정 ( 에디터에 표시될 에셋의 이름이 된다 . /Script/UECPP_Tutorial.PackageTargetClass'/Game/MyPackage.PackageTargetClass_AssetName' )
void UMyGameInstance::SavePackage()
{
// 0. 패키지를 미리 로드하기 : 이미 존재한다면, 로드먼저 하고 추가해 주는것이 안전하다.
// 로드를 먼저 안해주면 에러발생 할 수 있다 : cannot be saved as it has only been partially loaded
UPackage* MyPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (MyPackage)
{
MyPackage->FullyLoad();
}
// 1. Package 생성
MyPackage = ::CreatePackage(*PackageName);
EObjectFlags ObjectDefaultFlag = RF_Public | RF_Standalone; // UPackage 저장 기본 옵션
// 2. Package 의 대표 에셋 생성및 설정 ( Package 에 새로 성성한 UObject 를 Asset으로 넣어준다. )
UPackageTargetClass* UObject_ForMainAsset = NewObject<UPackageTargetClass>(/*패키지*/MyPackage, /*메타클래스*/UPackageTargetClass::StaticClass(), /*에셋이름*/*PackageAssetName, /*옵션플래그*/ObjectDefaultFlag);
UObject_ForMainAsset->SetStrParam(TEXT("Main Asset"));
UObject_ForMainAsset->SetNumParam(0);
// 3. Subobject 를 Asset 에다가 추가
for (int i = 0; i < 10; i++)
{
// MainAsset 의 하위로 들어갈 Subobject 의 이름 생성, 추가
FString SubobjectName = FString::Printf(TEXT("Subobject[%d]"), i);
UPackageTargetClass* UObject_ForSubobject = NewObject<UPackageTargetClass>(/*상위에셋*/UObject_ForMainAsset, /*메타클래스*/UPackageTargetClass::StaticClass(), /*에셋이름*/*SubobjectName, /*옵션플래그*/ObjectDefaultFlag);
UObject_ForSubobject->SetStrParam(SubobjectName);
UObject_ForSubobject->SetNumParam(i);
}
// 4. Package 저장 : 저장하기 위해서는 패키지의 경로와, 패키지의 확장자를 지정해주어야 한다.
const FString PackageFileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = ObjectDefaultFlag;
if (UPackage::SavePackage(MyPackage, nullptr, *PackageFileName, SaveArgs))
{
UE_LOG(LogTemp, Log, TEXT("Package(%s) 가 성공적으로 저장되었습니다."), *PackageFileName);
}
}
void UMyGameInstance::LoadPackage()
{
// 1. Package 로드
UPackage* MyPackage = ::LoadPackage(nullptr, *PackageName, LOAD_None);
if (nullptr == MyPackage)
{
UE_LOG(LogTemp, Warning, TEXT("Package(%s) 를 읽는데 실패하였습니다."), *PackageName);
}
// 2. 내부 모든 Asset을 로드해줍니다.
MyPackage->FullyLoad();
// 3. 에셋 찾기
UPackageTargetClass* FoundAsset = FindObject<UPackageTargetClass>(MyPackage, *PackageAssetName);
if (nullptr == FoundAsset) {
UE_LOG(LogTemp, Warning, TEXT("Package(%s) 에는 Asset (%s) 이 없습니다."), *PackageName, *PackageAssetName);
}
UE_LOG(LogTemp, Log, TEXT("Package(%s) 로부터 찾은 Asset. UPackageTargetClass { NumParam : %d, StrParam : %s}"),*PackageName, FoundAsset->GetNumParam(), *FoundAsset->GetStrParam());
// 4. 서브오브젝트 찾기
/*
서브오브젝트는
Asset 에서 이름으로 FindObject 로 찾거나
Asset 에서 GetObjectsWithOuter 로 모든 Subobject 를 찾아서, 걸러서 찾거나 해야한다.
DefaultSubobject 로는 찾을수 없다.
*/
{
// 4-1 : FindObject 로 찾기
{
for (int i = 0; i < 10; i++)
{
FString SubobjectNameForFind = FString::Printf(TEXT("Subobject[%d]"), i);
UPackageTargetClass* FoundSubobject = FindObject<UPackageTargetClass>(/*상위오브젝트(에셋)*/FoundAsset, *SubobjectNameForFind); // 서브오브젝트는 Package 안이 아니라, Asset 안에서 찾아야 한다.
if (nullptr != FoundSubobject)
{
UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject->GetNumParam(), *FoundSubobject->GetStrParam());
}
else {
UE_LOG(LogTemp, Warning, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass %s 을 찾을 수 없었습니다."), *FoundAsset->GetName(), *SubobjectNameForFind);
}
}
}
// 4-2 : Asset 에서 이름으로 찾기 -> 실패 : DefaultSubobject 로는 찾을수가 없다.
{
for (int i = 0; i < 10; i++)
{
FString SubobjectNameForFind = FString::Printf(TEXT("Subobject[%d]"), i);
UObject* FoundSubobject = FoundAsset->GetDefaultSubobjectByName(*SubobjectNameForFind);
if (nullptr != FoundSubobject)
{
UPackageTargetClass* FoundSubobject_PackageTarget = Cast<UPackageTargetClass>(FoundSubobject);
UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject_PackageTarget->GetNumParam(), *FoundSubobject_PackageTarget->GetStrParam());
}
else {
UE_LOG(LogTemp, Warning, TEXT("DefaultSubobject 를 MainAsset 에서 Asset (%s) 를 찾지 못했습니다."), *SubobjectNameForFind);
}
}
}
// 4-3 : GetDefaultSubobjects 로 모든 DefaultSubobject 찾기 -> 실패 : DefaultSubobject 로는 찾을수가 없다.
{
TArray<UObject*> FoundSubobjects;
FoundAsset->GetDefaultSubobjects(FoundSubobjects); // 이게 개수가 0 이라면 , Subobject 는 DefaultSubobject 와는 다르다는 이야기 이다.
if (FoundSubobjects.Num() > 0)
{
UE_LOG(LogTemp, Log, TEXT("찾은 서브오브젝트 개수 : %d, FoundSubobjects : %s"), FoundSubobjects.Num(), *FoundSubobjects[0]->GetName());
}
else {
UE_LOG(LogTemp, Warning, TEXT("Asset 안에는 DefaultSubobject 가 없습니다."));
}
}
// 4-4 : GetObjectsWithOuter 로 모든 Subobject 찾기
{
TArray<UObject*> FoundSubobjects;
GetObjectsWithOuter(FoundAsset, FoundSubobjects);
UE_LOG(LogTemp, Log, TEXT("찾은 서브오브젝트 개수 : %d"), FoundSubobjects.Num());
for (int i = 0; i < 10; i++)
{
UPackageTargetClass* FoundSubobject_Casted = Cast<UPackageTargetClass>(FoundSubobjects[i]);
if (nullptr != FoundSubobject_Casted)
{
UE_LOG(LogTemp, Log, TEXT("Asset (%s) 안의 Subobject UPackageTargetClass : {NumParam : %d, StrParam : %s} 을 찾았습니다 "), *FoundAsset->GetName(), FoundSubobject_Casted->GetNumParam(), *FoundSubobject_Casted->GetStrParam());
}
}
}
}
}
|
|
Asset 참조에는 Strong Reference 와, Soft Reference 가 있다. Strong Reference 는 Object A 가 Object B 를 참조하여 Object A 로드시 Object B 를 로드되는경우. Soft Reerence 는 경로 같은 문자열 형태의 간접 매커니즘을 통해, Object A 가 Object B 를 참조하게 만드는 경우.
헤더파일에서 직접 멤버변수로 참조하고 있는 경우
class UCLASS_B;
UCLASS()
class PROJECT_API UCLASS_A{
GENERATED_BODY()
public:
UPROPERTY()
UCLASS_B* Object_B; // 강참조 :: 직접 프로퍼티 참조
}
생성자 코드는 엔진이 초기화 될때 실행된다. 즉 게임이 실행되기 전에 해당 에셋이 로딩이 된다. ConstructorHelpers 라는 특수 클래스가 사용되는데, 생성 단계 도중 오브젝트와 , 오브젝트의 클래스를 찾을 수 있다.
class UCLASS_B;
UCLASS()
class PROJECT_API UCLASS_A{
GENERATED_BODY()
public:
// 생성자 참조
CLASS_A()
{
static ConstructorHelpers::FObjectFinder<UCLASS_B> Found_Object_B(TEXT("/Game/PackageName/AssetName"));
UCLASS_B* Object_B = Found_Object_B.Object;
}
}
TSoftObjectPtr 을 사용하는 경우. 직접 프로퍼티 레퍼런스 인 것처럼 작업할 수 있다. 이때는 유효한지 검사를 통해 사용해야 합니다. TSoftObjectPtr 을 사용하려면, 에셋을 수동으로 로드 해야 합니다. 템플림함수 LoadObject<>() StaticLoadObject() FStreamingManager 를 사용해서 오브젝트를 로드할 수 있습니다.
class UCLASS_B;
UCLASS()
class PROJECT_API UCLASS_A{
GENERATED_BODY()
public:
UPROPERTY()
TSoftObjectPtr<UCLASS_B> Object_B; // 약참조 :: 오브젝트 경로를 사용한 참조
// 오브젝트를 Load 하고 가져옵니다.
UCLASS_B* GetObjectWithLoad();
}
// in cpp
void UCLASS_A::GetObjectWithLoad()
{
if(Object_B.IsPending())
{
const TSoftObjectPath& AssetRef = Object_B.ToStringReference(); // Object Path 가져오기
FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager;
Object_B = Cast<UCLASS_B>(Streamable.SynchronousLoad(AssetRef));
}
return Object_B.Get();
}
/* Persistent : 지속성 있는 */
template<class TObjectID>
struct TPersistentObjectPtr
{
/**
* Dereference the pointer, which may cause it to become valid again. Will not try to load pending outside of game thread
* @return nullptr if this object is gone or the pointer was null, otherwise a valid UObject pointer
*/
FORCEINLINE UObject* Get() const
{
UObject* Object = WeakPtr.Get();
// Do a full resolve if the returned object is null and either we think we've loaded new objects, or the weak ptr may be stale
if (!Object && ObjectID.IsValid() && (TObjectID::GetCurrentTag() != TagAtLastTest || !WeakPtr.IsExplicitlyNull()))
{
Object = ObjectID.ResolveObject();
WeakPtr = Object;
// Not safe to update tag during save as ResolveObject may have failed accidentally
if (Object || !GIsSavingPackage)
{
TagAtLastTest = TObjectID::GetCurrentTag();
}
// If this object is pending kill or otherwise invalid, this will return nullptr as expected
Object = WeakPtr.Get();
}
return Object;
}
/**
* Test if this does not point to a live UObject, but may in the future
* @return true if this does not point to a real object, but could possibly
*/
FORCEINLINE bool IsPending() const
{
return Get() == nullptr && ObjectID.IsValid();
}
/**
* Test if this points to a live UObject
* @return true if Get() would return a valid non-null pointer
*/
FORCEINLINE bool IsValid() const
{
return !!Get();
}
/**
* Test if this can never point to a live UObject
* @return true if this is explicitly pointing to no object
*/
FORCEINLINE bool IsNull() const
{
return !ObjectID.IsValid();
}
private:
/** Once the object has been noticed to be loaded, this is set to the object weak pointer **/
mutable FWeakObjectPtr WeakPtr;
/** Compared to CurrentAnnotationTag and if they are not equal, a guid search will be performed **/
mutable int32 TagAtLastTest;
/** Guid for the object this pointer points to or will point to. **/
TObjectID ObjectID;
}
struct FSoftObjectPtr : public TPersistentObjectPtr<FSoftObjectPath>
{
// .... 생략
}
struct TSoftObjectPtr
{
template <class U>
friend struct TSoftObjectPtr;
public:
// .... 생략
FORCEINLINE bool IsValid() const
{
// This does the runtime type check
return Get() != nullptr;
}
/**
* Dereference the soft pointer.
* @return nullptr if this object is gone or the lazy pointer was null, otherwise a valid UObject pointer
*/
FORCEINLINE T* Get() const
{
return dynamic_cast<T*>(SoftObjectPtr.Get());
}
/**
* Test if this does not point to a live UObject, but may in the future
* @return true if this does not point to a real object, but could possibly
*/
FORCEINLINE bool IsPending() const
{
return SoftObjectPtr.IsPending();
}
/**
* Test if this can never point to a live UObject
* @return true if this is explicitly pointing to no object
*/
FORCEINLINE bool IsNull() const
{
return SoftObjectPtr.IsNull();
}
// .... 생략
private:
FSoftObjectPtr SoftObjectPtr;
};
UAsset 에 대한 Synchronous Load (동기 로드)와 ASynchronous Load (비동기 로드)기능을 제공하는 관리자 객체 다수의 오브젝트 경로를 입력해 다수의 에셋을 로딩하는것도 과능하다. 콘텐츠 제작과 무관한 싱글톤 클래스에 FStreamableManager 를 선언해 두면 좋다. (예 - GameInstance)
에셋 비동기 로드는 어떻게 할까? 가장 쉬운 방법은 FStreamableManager를 사용하는 것입니다. 우선 FStreamableManager 를 만들어 줘야 하는데, Singleton Object, 이를테면 DefaultEngine.ini 에서 GameSingletonClassName에 지정된 오브젝트에 넣는 것이 좋습니다. 그리고 StreamableManager에 FSoftObjectPath 를 전달한 다음 로드를 시작합니다. SynchronousLoad는 메인 스레드를 너무 오래 붙잡아 둘 가능성이 있습니다. RequestAsyncLoad를 사용하면 애셋을 비동기 로드한 다음 완료되면 델리게이트를 호출합니다. 방법은 아래와 같습니다.
void UGameCheatManager::GrantItems()
{
TArray<FSoftObjectPath> ItemsToStream;
FStreamableManager& Streamable = UGameGlobals::Get().StreamableManager;
for(int32 i = 0; i < ItemList.Num(); ++i)
{
// ItemList 는 TArray<TSoftObjectPtr<UGameItem>> 이며, 에디터에서 디자이너에 의해 수정된 것.
ItemsToStream.AddUnique(ItemList[i].ToStringReference());
}
Streamable.RequestAsyncLoad(ItemsToStream, FStreamableDelegate::CreateUObject(this, &UGameCheatManager::GrantItemsDeferred));
}
void UGameCheatManager::GrantItemsDeferred()
{
for(int32 i = 0; i < ItemList.Num(); ++i)
{
UGameItemData* ItemData = ItemList[i].Get();
if(ItemData)
{
MyPC->GrantItem(ItemData);
}
}
}
StreamableManager 는 델리게이트가 호출될 때까지 로드하는 애셋에 대한 하드 레퍼런스를 유지시켜, 비동기 로드 요청한 오브젝트의 델리게이트가 호출되기도 전에 가비지 컬렉팅되는 일이 없도록 합니다. 델리게이트가 호출된 이후에는 그 레퍼런스가 해제되므로, 계속해서 남아있도록 하려면 어딘가에 하드 레퍼런스를 해 줘야 합니다. 같은 메서드를 사용해서 FAssetData 를 비동기 로드할 수도 있는데, 그냥 ToStringReference() 를 호출한 다음 배열에 추가시키고 델리게이트를 붙여 RequestAsyncLoad 를 호출해 주면 됩니다. 위에 언급한 메서드를 조합하면 게임 내 어느 애셋에 대해서도 효율적인 로드가 가능한 시스템을 구축할 수 있을 것입니다. 메모리에 직접 접근하는 게임플레이 코드가 비동기 로드를 처리하도록 변환해 주는 작업에 시간이 조금 걸리겠지만, 그 이후에는 게임에서 발생하는 멈춤 현상이나 차지하는 메모리 양이 훨씬 줄어들 것입니다.