UE4中资源的引用
强引用(hard reference)
如果A强引用B,那么在加载A时会把B也加载进内存
c++强引用资源
在AMyTest1GameMode的构造函数中加载Blueprint UClass,并赋值给DefaultPawnClass成员变量
那么AMyTest1GameMode类型就强引用着/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter的Blueprint UClass
AMyTest1GameMode::AMyTest1GameMode() { // set default pawn class to our Blueprinted character static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter")); if (PlayerPawnBPClass.Class != NULL) { DefaultPawnClass = PlayerPawnBPClass.Class; } }
注1:由于使用的是字符串,因此在构建版本时,并不会cook并将/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter的Blueprint UClass打进安装包
注2:为了让该资源打进安装包,需要让被cook的地图直接或间接引用该Blueprint UClass;或者在DefaultGame.ini文件的/Script/UnrealEd.ProjectPackagingSettings标签的DirectoriesToAlwaysCook数组中配置包含该资源的目录
[/Script/UnrealEd.ProjectPackagingSettings] +DirectoriesToAlwaysCook=(Path="/Game/ThirdPersonCPP/Blueprints")
资源强引用资源
下图中从MyTest1Character派生的Blueprint中的USkeletalMeshComponent* Mesh组件的USkeletalMesh* SkeletalMesh变量就强引用着SK_Mannequin的Skeletal Mesh资源
UCLASS(hidecategories=Object, config=Engine, editinlinenew, abstract) class ENGINE_API USkinnedMeshComponent : public UMeshComponent { GENERATED_UCLASS_BODY() // ... ... /** The skeletal mesh used by this component. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Mesh") class USkeletalMesh* SkeletalMesh; // ... ... };
注1:由于USkeletalMesh* SkeletalMesh被标为EditAnywhere,因此ThirdPersonCharacter的Blueprint放置到游戏关卡中后,还可以对该Blueprint实例的USkeletalMeshComponent* Mesh组件的USkeletalMesh* SkeletalMesh指向的资源进行再次修改
注2:若被修饰为UPROPERTY(EditDefaultsOnly),就只能在Blueprint中进行修改
软引用(soft reference)
为了控制何时加载资源,可使用TSoftObjectPtr(或TSoftClassPtr)模板类型来软引用资源(或UClass类型)。
软引用的工作方式与硬引用一样,可直接关联对应的资源(或UClass类型),在编辑器中界面表现是一样,可通过拖拽、下拉框或箭头选择要关联的资源。
在编辑器中,仅仅可通过弹出的Tips来区分
另外,被软引用到的资源会被编辑器中的Reference Viewer(引用查看器)工具识别,也会自动cook进安装包
与指定字符串路径相比,当在编辑器中对被软引用到的资源进行移动、改名等操作时,ue4会进行自动调整。
UCLASS(config=Game) class AMyTest1Character : public ACharacter { GENERATED_BODY() // ... ... public: UPROPERTY(Category = MyTest1, EditAnywhere) TSoftObjectPtr<UTexture2D> SourceTexture1; // 软引用 UPROPERTY(Category = MyTest1, EditAnywhere) UTexture2D* SourceTexture2; // 硬引用 };
在Referece Viewer中,粉色的线为弱引用,白色的线为强引用
UTexture2D* AMyTest1Character::GetLazyTexture2D() { if (!SourceTexture1.IsPending()) // 检查资源是否已准备好 { return SourceTexture1.Get(); } return SourceTexture1.LoadSynchronous(); // 手动来同步加载贴图资源 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 } UTexture2D* AMyTest1Character::GetLazyTexture2D_2() { if (!SourceTexture1.IsPending()) // 检查资源是否已准备好 { return SourceTexture1.Get(); } const FSoftObjectPath& SourceTextureRef = SourceTexture1.ToSoftObjectPath(); return Cast<UTexture2D>(UAssetManager::GetStreamableManager().LoadSynchronous(SourceTextureRef)); // 手动来同步加载贴图资源 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 } UTexture2D* AMyTest1Character::GetLazyTexture2D_3() { if (!SourceTexture1.IsPending()) // 检查资源是否已准备好 { return SourceTexture1.Get(); } const FSoftObjectPath& SourceTextureRef = SourceTexture1.ToSoftObjectPath(); UAssetManager::GetStreamableManager().RequestSyncLoad(SourceTextureRef); // 手动来同步加载贴图资源 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 return FindObject<UTexture2D>(nullptr, *SourceTexture1.ToString()); }
如果希望延迟加载UClass,可使用TSoftClassPtr模版类来引用对应的UClass
UCLASS(config=Game) class AMyTest1Character : public ACharacter { GENERATED_BODY() // ... ... public: UPROPERTY(Category = MyTest1, EditAnywhere) TSoftClassPtr<UMyBPObject> MyObjectClass; // 软引用 };
UClass* AMyTest1Character::GetLazyBPObjectClass() { if (MyObjectClass.IsValid()) // 检查类型是否已准备好 { return MyObjectClass.Get(); } return MyObjectClass.LoadSynchronous(); // 手动来同步加载蓝图Class 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 } UClass* AMyTest1Character::GetLazyBPObjectClass_2() { if (MyObjectClass.IsValid()) // 检查类型是否已准备好 { return MyObjectClass.Get(); } const FSoftObjectPath& AssetRef = MyObjectClass.ToSoftObjectPath(); return Cast<UClass>(UAssetManager::GetStreamableManager().LoadSynchronous(AssetRef)); // 手动来同步加载蓝图Class 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 } UClass* AMyTest1Character::GetLazyBPObjectClass_3() { if (MyObjectClass.IsValid()) // 检查类型是否已准备好 { return MyObjectClass.Get(); } const FSoftObjectPath& AssetRef = MyObjectClass.ToSoftObjectPath(); UAssetManager::GetStreamableManager().RequestSyncLoad(AssetRef); // 手动来同步加载蓝图Class 注:同步加载可能会导致游戏线程卡顿,开发者需要评估该行为是否合理 return FindObject<UClass>(nullptr, *MyObjectClass.ToString()); }
使用FSoftClassPath、FSoftObjectPath来软引用UClass和资源
UCLASS(config=Game) class AMyTest1Character : public ACharacter { GENERATED_BODY() // ... ... public: UPROPERTY(Category = MyTest1, EditAnywhere) FSoftClassPath SourceClass1; UPROPERTY(Category = MyTest1, EditAnywhere) FSoftObjectPath SourceObject1; };
注1:FSoftClassPath只能软引用Class、DynamicClass、BlueprintGeneratedClass、WidgetBlueprintGeneratedClass、AnimBluprintGeneratedClass和LinkerPlaceholderClass六大类。这些类的继承关系如下:
Class (632) DynamicClass (760) LinkerPlaceholderClass (1072) BlueprintGeneratedClass (1512) WidgetBlueprintGeneratedClass (1640) AnimBlueprintGeneratedClass (2632)
注2:FSoftObjectPath可以软引用任何资源和UClass
注1:黄色的TPersistentObjectPtr、TSoftObjectPtr、TSoftClassPtr为模板类
注2:FSoftObjectPath类中FThreadSafeCounter CurrentTag、TSet<FName> PIEPackageNames(黄色)是static的
它们之间相互转换关系如下:
// === 定义FSoftObjectPath对象 === FSoftObjectPath ObjectPath1(TEXT("Material'/Engine/EngineDebugMaterials/VertexColorMaterial.VertexColorMaterial'")); FSoftObjectPath ObjectPath2; ObjectPath2.SetPath(TEXT("/Engine/EngineMaterials/DefaultDiffuse_TC_Masks.DefaultDiffuse_TC_Masks")); FSoftObjectPath ObjectPath3(UMyObject::StaticClass()->GetDefaultObject()); // ObjectPath3为:/Script/MyTest1.Default__MyObject // FSoftObjectPtr对象与FSoftObjectPath对象相互转换 FSoftObjectPtr FObjectPtr1(ObjectPath2); const FSoftObjectPath& ObjectPath4 = FObjectPtr1.ToSoftObjectPath(); FSoftObjectPtr FObjectPtr2(UMyObject::StaticClass()->GetDefaultObject()); // FObjectPtr2为:/Script/MyTest1.Default__MyObject const FSoftObjectPath& ObjectPath5 = FObjectPtr2.ToSoftObjectPath(); // === 定义TSoftObjectPtr<T>对象 === TSoftObjectPtr<UMyObject> TObjectPtr1(UMyObject::StaticClass()->GetDefaultObject()); // TObjectPtr1为:/Script/MyTest1.Default__MyObject // TSoftObjectPtr<T>对象与FSoftObjectPath对象相互转换 FSoftObjectPath ObjectPath6 = FSoftObjectPath("/Game/ThirdPerson/Meshes/Bump_StaticMesh.Bump_StaticMesh"); TSoftObjectPtr<UStaticMesh> TObjectPtr2(ObjectPath6); const FSoftObjectPath& ObjectPath7 = TObjectPtr2.ToSoftObjectPath(); // === 定义FSoftClassPath对象 === FSoftClassPath ClassPath1 = FSoftClassPath(TEXT("/Script/MyTest1.MyObject")); FSoftClassPath ClassPath2 = FSoftClassPath(UMyObject::StaticClass()); // ClassPath2为:/Script/MyTest1.MyObject FSoftClassPath ClassPath3; ClassPath3.SetPath(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C")); // === 定义TSoftClassPtr<T>对象 === TSoftClassPtr<UMyObject> TClassPtr1(UMyObject::StaticClass()); // TClassPtr1为:/Script/MyTest1.MyObject // TSoftClassPtr<T>对象与FSoftObjectPath对象相互转换 FSoftObjectPath ObjectPath8(TEXT("/Game/ThirdPersonCPP/Blueprints/MyBPObject.MyBPObject_C")); TSoftClassPtr<UMyBPObject> TClassPtr2(ObjectPath8); const FSoftObjectPath& ObjectPath9 = TClassPtr2.ToSoftObjectPath();
字符串路径
资源(或UClass)通过字符串路径来指定,通过LoadObject(LoadClass)来加载。
该方式不能被Reference Viewer工具识别,资源进行移动、改名等操作时,不会自动调整。也不能自动cook进安装包。
加载资源
// 加载UI蓝图资源 注:不是UI蓝图Class FString MyWidgetBPPath = TEXT("/Game/ThirdPerson/UMG/MyWidgetBlueprint"); UObject* MyWidgetBPObject = LoadObject<UObject>(nullptr, *MyWidgetBPPath); // MyWidgetBPObject的类型为UWidgetBlueprint* // 加载Texture2D资源 FString TexturePath1 = TEXT("/Engine/EngineMaterials/DefaultDiffuse_TC_Masks"); UTexture2D* TextureObj1 = FindObject<UTexture2D>(nullptr, *TexturePath1); // 若之前没有加载,TextureObj1则返回为空 TextureObj1 = LoadObject<UTexture2D>(nullptr, *TexturePath1); // 在LoadObject内部在加载前也会调用FindObject先进行查找,如果不存在,才会从硬盘加载它 // 加载材质资源 FString MaterialPath1 = TEXT("/AnimationSharing/AnimSharingRed.AnimSharingRed"); UMaterialInterface* MaterialObj1 = LoadObject<UMaterialInterface>(nullptr, *MaterialPath1); // 加载StaticMesh资源 FString StaticMeshPath1 = TEXT("/Game/ThirdPerson/Meshes/Bump_StaticMesh.Bump_StaticMesh"); UStaticMesh* MyStaticMesh = Cast<UStaticMesh>(StaticLoadObject(UStaticMesh::StaticClass(), nullptr, *StaticMeshPath1));
如果在UObject类型的构造函数中,可以使用ConstructorHelpers::FObjectFinder来加载资源
static ConstructorHelpers::FObjectFinder<UStaticMesh> MyStaticMeshClass(TEXT("/Game/ThirdPerson/Meshes/Bump_StaticMesh")); UStaticMesh* MyStaticMesh = MyStaticMeshClass.Object;
加载UClass(类型)
// Native UClass FString MyObjectPath = TEXT("/Script/MyTest1.MyObject"); UClass* MyObjectHit = FindObject<UClass>(ANY_PACKAGE, *MyObjectPath); // 在游戏启动初始化时就已创建,因此可以直接Find到 UClass* MyObjectClass = LoadClass<UMyObject>(nullptr, *MyObjectPath); // MyObjectClass为UClass*类型 与MyObjectHit相等 //UClass* MyObjectClass = StaticLoadClass(UMyObject::StaticClass(), nullptr, *MyObjectClassPath); // 与上面语句等价 // UObjcet Blueprint UClass FString MyBPObjectPath = TEXT("/Game/ThirdPersonCPP/Blueprints/MyBlueprintObject.MyBlueprintObject_C"); UClass* MyBPObjectHit = FindObject<UClass>(ANY_PACKAGE, *MyBPObjectPath); UClass* MyBPObjectClass = LoadClass<UObject>(nullptr, *MyBPObjectPath); // MyBPObjectClass为UBlueprintGeneratedClass*类型 //UClass* MyBPObjectClass = StaticLoadClass(UObject::StaticClass(), nullptr, *MyBPObjectPath); // 与上面语句等价 // AActor Blueprint UClass FString PlayerPawnBPPath = TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C"); UClass* PlayerPawnBPClass = StaticLoadClass(APawn::StaticClass(), nullptr, *PlayerPawnBPPath); // PlayerPawnBPClass为UBlueprintGeneratedClass*类型 // 也可使用LoadClass来加载 如:UClass* PlayerPawnBPClass = LoadClass<APawn>(nullptr, *PlayerPawnBPPath); // 也可使用LoadObject来加载 如:UClass* PlayerPawnBPClass = LoadObject<UClass>(nullptr, *PlayerPawnBPPath); // 可以通过FindObject<UClass>函数来查找PlayerPawnBPPath对应的蓝图Class是否已经加载 // 另外,StaticLoadClass、LoadClass、LoadObject函数在加载前也会调用FindObject<UClass>来查找,如果不存在,才会从硬盘加载它 // UClass* PlayerPawnBPClass = FindObject<UClass>(nullptr, *PlayerPawnBPPath); // UUserWidget Blueprint UClass FString MyWidgetBPPath = TEXT("/Game/ThirdPerson/UMG/MyWidgetBlueprint.MyWidgetBlueprint_C"); UClass* MyWidgetBPClass = StaticLoadClass(UObject::StaticClass(), nullptr, *MyWidgetBPPath); // MyWidgetBPClass为UWidgetBlueprintGeneratedClass*类型 // 也可使用LoadClass来加载 如:UClass* MyWidgetBPClass = LoadClass<UObject>(nullptr, *MyWidgetBPPath); // 也可使用LoadObject来加载 如:UClass* MyWidgetBPClass = LoadObject<UClass>(nullptr, *MyWidgetBPPath); UUserWidget* DefaultMyWidgetBPObject = MyWidgetBPClass->GetDefaultObject<UUserWidget>(); UUserWidget* MyWidget = UWidgetBlueprintLibrary::Create(this, MyWidgetBPClass, nullptr); // 基于MyWidgetBPClass类型创建一个UserWidget实例
注1:Native UClass的字符串路径为/Script/【模块名】.【类型名】] // 类型名要去掉U、A等前缀
注2:Blueprint UClass的字符串路径为/【Engine | Game | 插件名】/<XXX>/[名称].[名称] _C
如果在UObject类型的构造函数中,可以使用ConstructorHelpers::FClassFinder来加载UClass(类型)
static ConstructorHelpers::FClassFinder<APawn> PlayerPawnBPClass(TEXT("/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter")); UClass* PawnClass = PlayerPawnBPClass.Class; static ConstructorHelpers::FClassFinder<UObject> MyBPObjectClass(TEXT("/Game/ThirdPersonCPP/Blueprints/MyBlueprintObject")); UClass* MyClass = MyBPObjectClass.Class; static ConstructorHelpers::FClassFinder<UObject> MyWidgetBPClass(TEXT("/Game/ThirdPerson/UMG/MyWidgetBlueprint.MyWidgetBlueprint_C")); UClass* MyWidgetClass = MyWidgetBPClass.Class;
注:传给ConstructorHelpers::FClassFinder的Blueprint UClass的字符串路径不为[名称].[名称] _C时,其内部会进行补全
查找资源API
FindObject,FindObjectFast,FindObjectChecked,FindObjectSafe
FindObject会在内存中查找对象,找到就会返回,找不到会返回nullptr,不会触发加载。
如果传入了Outer,就会在Outer所在的Package下面找对应的资源对象,如果没有Outer就会在全局找这个资源对象。
Fast版本功能和FindObject相同,但是不会检查路径,明确知道完整路径时用这个可以避免检查路径开销,速度会快一些。
Check版本功能也和FindObject相同,但是不会返回nullptr,找不到就会报Fatal,直接停止程序(Shipping和Test版不会)。
Safe版本的函数功能和FindObject相同,但是在正在GC时或者正在保存包时直接返回nullptr。
这些函数最终都会调用到StaticFindObjectFastInternal函数,StaticFindObjectFastInternal内部又会调用StaticFindObjectFastInternalThreadSafe函数
传入的ObjectName(资源路径)会被GetObjectOuterHash转为Hash值,通过Hash值在ThreadHash上取到一个对象列表,之后再根据条件取得要查找的对象
UObject* StaticFindObjectFastInternalThreadSafe(FUObjectHashTables& ThreadHash, const UClass* ObjectClass, const UObject* ObjectPackage, FName ObjectName, bool bExactClass, bool bAnyPackage, EObjectFlags ExcludeFlags, EInternalObjectFlags ExclusiveInternalFlags) { ExclusiveInternalFlags |= EInternalObjectFlags::Unreachable; // If they specified an outer use that during the hashing UObject* Result = nullptr; if (ObjectPackage != nullptr) { int32 Hash = GetObjectOuterHash(ObjectName, (PTRINT)ObjectPackage); FHashTableLock HashLock(ThreadHash); for (TMultiMap<int32, class UObjectBase*>::TConstKeyIterator HashIt(ThreadHash.HashOuter, Hash); HashIt; ++HashIt) { UObject *Object = (UObject *)HashIt.Value(); if /* check that the name matches the name we're searching for */ ((Object->GetFName() == ObjectName) /* Don't return objects that have any of the exclusive flags set */ && !Object->HasAnyFlags(ExcludeFlags) /* check that the object has the correct Outer */ && Object->GetOuter() == ObjectPackage /** If a class was specified, check that the object is of the correct class */ && (ObjectClass == nullptr || (bExactClass ? Object->GetClass() == ObjectClass : Object->IsA(ObjectClass))) /** Include (or not) pending kill objects */ && !Object->HasAnyInternalFlags(ExclusiveInternalFlags)) { checkf(!Object->IsUnreachable(), TEXT("%s"), *Object->GetFullName()); if (Result) { UE_LOG(LogUObjectHash, Warning, TEXT("Ambiguous search, could be %s or %s"), *GetFullNameSafe(Result), *GetFullNameSafe(Object)); } else { Result = Object; } #if (UE_BUILD_SHIPPING || UE_BUILD_TEST) break; #endif } } // ... ... } else { // ... ... } // Not found. return Result; }
FUObjectHashTables ThreadHash类型如下:
class FUObjectHashTables { /** Critical section that guards against concurrent adds from multiple threads */ FCriticalSection CriticalSection; public: /** Hash sets */ TMap<int32, FHashBucket> Hash; TMultiMap<int32, class UObjectBase*> HashOuter; /** Map of object to their outers, used to avoid an object iterator to find such things. **/ TMap<UObjectBase*, FHashBucket> ObjectOuterMap; TMap<UClass*, FHashBucket> ClassToObjectListMap; TMap<UClass*, TSet<UClass*>> ClassToChildListMap; TAtomic<uint64> ClassToChildListMapVersion; /** Map of package to the object their contain. */ TMap<UPackage*, FHashBucket> PackageToObjectListMap; /** Map of object to their external package. */ TMap<UObjectBase*, UPackage*> ObjectToPackageMap; // ... ... };
引擎会把每个对象会根据Hash存到全局的HashOuter里,这是一个TMultiMap,也就是一个hash会对应多个Value
Hash是用GetObjectOuterHash这个函数计算出来的,内部实际是路径FName加Package名字取hash
这个计算结果本身就会冲突(FHashBucket用于处理冲突),多个路径有概率映射到同一个hash值,所以用TMultiMap就可以将多个值存到同一个hash上
这样只要把加载好的对象存到这里,就可以保证即使多次查找也能找到同一个对象
同步加载资源API
LoadObject,LoadClass,LoadPackage
LoadObject、LoadClass内部会先调用FindObject在内存中找,找到了直接返回,没找到就会把路径转化为Package进行LoadPackage同步加载
一个包里如果有多个资源,他们在硬盘上对应的是同一个文件,那么只需要加载这个文件就好了
再深入底层可以看到,会调用到LoadPackageInternal函数,并在该函数中最终调用LoadPackageAsync函数,这就是异步加载的入口,并且最后FlushAsyncLoading,内部阻塞等待,将异步加载转为同步
UPackage* LoadPackageInternal(UPackage* InOuter, const TCHAR* InLongPackageNameOrFilename, uint32 LoadFlags, FLinkerLoad* ImportLinker, FArchive* InReaderOverride, const FLinkerInstancingContext* InstancingContext) { DECLARE_SCOPE_CYCLE_COUNTER(TEXT("LoadPackageInternal"), STAT_LoadPackageInternal, STATGROUP_ObjectVerbose); SCOPED_CUSTOM_LOADTIMER(LoadPackageInternal) ADD_CUSTOM_LOADTIMER_META(LoadPackageInternal, PackageName, InLongPackageNameOrFilename); checkf(IsInGameThread(), TEXT("Unable to load %s. Objects and Packages can only be loaded from the game thread."), InLongPackageNameOrFilename); UPackage* Result = nullptr; if (FPlatformProperties::RequiresCookedData() && GEventDrivenLoaderEnabled && EVENT_DRIVEN_ASYNC_LOAD_ACTIVE_AT_RUNTIME ) { FString InName; FString InPackageName; if (FPackageName::IsPackageFilename(InLongPackageNameOrFilename)) { FPackageName::TryConvertFilenameToLongPackageName(InLongPackageNameOrFilename, InPackageName); } else { InPackageName = InLongPackageNameOrFilename; } if (InOuter) { InName = InOuter->GetPathName(); } else { InName = InPackageName; } FName PackageFName(*InPackageName); { if (FCoreDelegates::OnSyncLoadPackage.IsBound()) { FCoreDelegates::OnSyncLoadPackage.Broadcast(InName); } int32 RequestID = LoadPackageAsync(InName, nullptr, *InPackageName); if (RequestID != INDEX_NONE) { FlushAsyncLoading(RequestID); } } Result = (InOuter ? InOuter : FindObjectFast<UPackage>(nullptr, PackageFName)); return Result; } // ... ... return Result; }
参考