Init
This commit is contained in:
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/AssetBundles/AssetBundles.jpg
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/AssetBundles/AssetBundles.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,166 @@
|
||||
# AssetBundles
|
||||
|
||||
- **功能描述:** 标明该属性其引用的资产属于哪一些AssetBundles。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** strings="a,b,c"
|
||||
- **限制类型:** UPrimaryDataAsset内部的FSoftObjectPtr,FSoftObjectPath
|
||||
- **关联项:** [IncludeAssetBundles](../IncludeAssetBundles/IncludeAssetBundles.md)
|
||||
- **常用程度:** ★★★
|
||||
|
||||
用于UPrimaryDataAsset内部的 SoftObjectPtr 或 SoftObjectPath 属性,标明其引用的资产属于哪一些AssetBundles。
|
||||
|
||||
要理解这个的作用,需要先理解一些基本概念:
|
||||
|
||||
- PrimaryAsset指的是在游戏中可以进行手动载入/释放的东西。包括关卡文件(.umap)以及一些游戏相关的物件,例如角色或者背包里的物品。顾名思义,主要资产指的是游戏里的主要根部资产,其引用树下有一大堆其他资产。另一方面,我们往往会主动加载或释放这些主要资产,比如加载关卡,加载怪物角色,加载掉落道具。但我们一般不太会直接去加载材质贴图声音这种资产,因为它们绝大多数是被主要资产引用着。我们在加载主要资产的时候,就会自带的加载这些次要资产了。
|
||||
- SecondaryAsset指的是其他的那些Assets了,例如贴图和声音等。这一类型的assets是根据PrimaryAsset来自动进行载入的。我们一般来说不太需要对次要资产进行管理,其会被主要资产根据引用关系来自动的加载。
|
||||
- AssetBundle可以叫做资产包,其实就是一个Asset的列表,我们对每个资产包起个名字来区分,比如UI,Game,这样其实也是对一些资产进行标签分类。这里的Asset我们不区分是PrimaryAsset还是SecondaryAsset,因为这是从用途上进行区分的,而不是加载方式。AssetBundle的作用是当我们加载PrimaryAsset的时候,这个PrimaryAsset本身可能引用着另外一些SecondaryAsset资产,各自有不同的用途。我们就可以把这些SecondaryAsset资产划分到不同的AssetBundle里,这样我们在加载PrimaryAsset的时候,可以通过额外提供AssetBundleName来更加精细化的控制SecondaryAsset资产的加载。
|
||||
- PrimaryAsset里的指定AssetBundle的Asset属性必须是软引用,否则是硬引用的话无论如何也会被加载进来。软引用的Asset在默认时候需要我们手动的进行加载,通过附加AssetBundle,就可以在加载PrimaryAsset的时候,附带的加载该软引用资产。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_Asset_Item :public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite,EditAnywhere)
|
||||
FString Name;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere,meta=(AssetBundles="UI,Game"))
|
||||
TSoftObjectPtr<UTexture2D> Icon;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere,meta=(AssetBundles="Game"))
|
||||
TSoftObjectPtr<UStaticMesh> Mesh;
|
||||
public:
|
||||
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
|
||||
};
|
||||
```
|
||||
|
||||
## 测试效果:
|
||||
|
||||
- 首先我们在BP里定义了UMyProperty_Asset_Item 的资产,并相应的配置上了引用的对象。如图所示,有一个图标是给UI和Game用的,有一个Mesh是专门给Game用的。大概设想一下在一些界面我们只需要道具的图标就可以了。
|
||||
- 然后在LoadPrimaryAsset的时候可以指定LoadBundles的名字,从而只加载特定的Bundle。如下图所示。
|
||||
- 当指定Bundle为UI的时候,可以看见Mesh并没有加载进来。
|
||||
- 当指定Bundle为Game的时候,可以看见Icon和Mesh都加载了进来。
|
||||
- 要注意在编辑器下测试时候,如果之前已经加载了Mesh,因为还常驻在编辑器内存里。因此即使是使用名字UI,也仍然会发现Mesh可以被引用到。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
首先UPrimaryDataAsset里有一个AssetBundleData保存着当前引用的AssetBundle的信息,这个信息是在编辑器环境下PreSave的时候保存的,会在UAssetManager::InitializeAssetBundlesFromMetadata里进行meta的分析和映射。之后在UAssetManager的LoadPrimaryAsset时内部调用 ChangeBundleStateForPrimaryAssets,然后检查AssetBundle把其他额外要一并加载的Asset添加到PathsToLoad,从而最终完成一并加载的这个逻辑。
|
||||
|
||||
```cpp
|
||||
void UAssetManager::InitializeAssetBundlesFromMetadata_Recursive(const UStruct* Struct, const void* StructValue, FAssetBundleData& AssetBundle, FName DebugName, TSet<const void*>& AllVisitedStructValues) const
|
||||
{
|
||||
static FName AssetBundlesName = TEXT("AssetBundles");
|
||||
static FName IncludeAssetBundlesName = TEXT("IncludeAssetBundles");
|
||||
|
||||
//根据当前对象的值,搜索拥有AssetBundles的属性的值,最后AddBundleAsset,BundleName就是设置的值,而FoundRef是引用的对象的资产路径
|
||||
TSet<FName> BundleSet;
|
||||
TArray<const FProperty*> PropertyChain;
|
||||
It.GetPropertyChain(PropertyChain);
|
||||
|
||||
for (const FProperty* PropertyToSearch : PropertyChain)
|
||||
{
|
||||
if (PropertyToSearch->HasMetaData(AssetBundlesName))
|
||||
{
|
||||
TSet<FName> LocalBundleSet;
|
||||
TArray<FString> BundleList;
|
||||
const FString& BundleString = PropertyToSearch->GetMetaData(AssetBundlesName);
|
||||
BundleString.ParseIntoArrayWS(BundleList, TEXT(","));
|
||||
|
||||
for (const FString& BundleNameString : BundleList)
|
||||
{
|
||||
LocalBundleSet.Add(FName(*BundleNameString));
|
||||
}
|
||||
|
||||
// If Set is empty, initialize. Otherwise intersect
|
||||
if (BundleSet.Num() == 0)
|
||||
{
|
||||
BundleSet = LocalBundleSet;
|
||||
}
|
||||
else
|
||||
{
|
||||
BundleSet = BundleSet.Intersect(LocalBundleSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const FName& BundleName : BundleSet)
|
||||
{
|
||||
AssetBundle.AddBundleAsset(BundleName, FoundRef.GetAssetPath());
|
||||
}
|
||||
}
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
void UPrimaryDataAsset::UpdateAssetBundleData()
|
||||
{
|
||||
// By default parse the metadata
|
||||
if (UAssetManager::IsInitialized())
|
||||
{
|
||||
AssetBundleData.Reset();
|
||||
UAssetManager::Get().InitializeAssetBundlesFromMetadata(this, AssetBundleData);
|
||||
}
|
||||
}
|
||||
|
||||
void UPrimaryDataAsset::PreSave(FObjectPreSaveContext ObjectSaveContext)
|
||||
{
|
||||
Super::PreSave(ObjectSaveContext);
|
||||
|
||||
UpdateAssetBundleData();
|
||||
|
||||
if (UAssetManager::IsInitialized())
|
||||
{
|
||||
// Bundles may have changed, refresh
|
||||
UAssetManager::Get().RefreshAssetData(this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void UPrimaryDataAsset::PostLoad()
|
||||
{
|
||||
Super::PostLoad();
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
FAssetBundleData OldData = AssetBundleData;
|
||||
|
||||
UpdateAssetBundleData();
|
||||
|
||||
if (UAssetManager::IsInitialized() && OldData != AssetBundleData)
|
||||
{
|
||||
// Bundles changed, refresh
|
||||
UAssetManager::Get().RefreshAssetData(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//加载asset的时候,如果有FAssetBundleEntry,则一起加到PathsToLoad里
|
||||
TSharedPtr<FStreamableHandle> UAssetManager::ChangeBundleStateForPrimaryAssets(const TArray<FPrimaryAssetId>& AssetsToChange, const TArray<FName>& AddBundles, const TArray<FName>& RemoveBundles, bool bRemoveAllBundles, FStreamableDelegate DelegateToCall, TAsyncLoadPriority Priority)
|
||||
{
|
||||
if (!AssetPath.IsNull())
|
||||
{
|
||||
// Dynamic types can have no base asset path
|
||||
PathsToLoad.Add(AssetPath);
|
||||
}
|
||||
|
||||
for (const FName& BundleName : NewBundleState)
|
||||
{
|
||||
FAssetBundleEntry Entry = GetAssetBundleEntry(PrimaryAssetId, BundleName);
|
||||
|
||||
if (Entry.IsValid())
|
||||
{
|
||||
for (const FTopLevelAssetPath & Path : Entry.AssetPaths)
|
||||
{
|
||||
PathsToLoad.AddUnique(FSoftObjectPath(Path));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogAssetManager, Verbose, TEXT("ChangeBundleStateForPrimaryAssets: No assets for bundle %s::%s"), *PrimaryAssetId.ToString(), *BundleName.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
参考文档:[https://dev.epicgames.com/documentation/en-us/unreal-engine/asset-management-in-unreal-engine?application_version=5.4](https://dev.epicgames.com/documentation/en-us/unreal-engine/asset-management-in-unreal-engine?application_version=5.4)
|
@@ -0,0 +1,29 @@
|
||||
# CollapsableChildProperties
|
||||
|
||||
- **功能描述:** 在TextureGraph模块中新增加的meta。用于折叠一个结构的内部属性。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **元数据类型:** bool
|
||||
- **限制类型:** TextureGraph插件内使用
|
||||
- **关联项:** [ShowInnerProperties](ShowInnerProperties/ShowInnerProperties.md)
|
||||
- **常用程度:** 0
|
||||
|
||||
在TextureGraph模块中新增加的meta。用于折叠一个结构的内部属性。
|
||||
|
||||
## 源码:
|
||||
|
||||
```cpp
|
||||
bool STG_GraphPinOutputSettings::CollapsibleChildProperties() const
|
||||
{
|
||||
FProperty* Property = GetPinProperty();
|
||||
bool Collapsible = false;
|
||||
// check if there is a display name defined for the property, we use that as the Pin Name
|
||||
if (Property && Property->HasMetaData("CollapsableChildProperties"))
|
||||
{
|
||||
Collapsible = true;
|
||||
}
|
||||
return Collapsible;
|
||||
}
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = NoCategory, meta = (TGType = "TG_Input", CollapsableChildProperties,ShowOnlyInnerProperties, FullyExpand, NoResetToDefault, PinDisplayName = "Settings") )
|
||||
FTG_OutputSettings OutputSettings;
|
||||
```
|
@@ -0,0 +1,80 @@
|
||||
# DisplayThumbnail
|
||||
|
||||
- **功能描述:** 指定是否在该属性左侧显示一个缩略图。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** bool
|
||||
- **限制类型:** UObject*
|
||||
- **关联项:** [ThumbnailSize](../ThumbnailSize.md)
|
||||
- **常用程度:** ★★★
|
||||
|
||||
指定是否在该属性左侧显示一个缩略图。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class INSIDER_API AMyActor_Thumbnail_Test :public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayThumbnail = "false"))
|
||||
UObject* MyObject;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayThumbnail = "true"))
|
||||
UObject* MyObject_DisplayThumbnail;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
AActor* MyActor;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (DisplayThumbnail = "true"))
|
||||
AActor* MyActor_DisplayThumbnail;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## 测试效果:
|
||||
|
||||
可见MyObject_DisplayThumbnail的左侧显示出了所选择资产的缩略图,而MyObject因为设置了false因此是没有的。如果不设置DisplayThumbnail =false,则默认也是会显示缩略图的。
|
||||
|
||||
MyActor_DisplayThumbnail出现了缩略图的图标,但是发现并没有显示出正确的说了图。AActor在默认情况下是不显示缩略图的。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
判断是否要显示缩略图就在这个函数。
|
||||
|
||||
默认情况下非Actor类型才会显示。另外SPropertyEditorAsset是用在资产类型属性上的,其实就是Object属性。
|
||||
|
||||
```cpp
|
||||
bool SPropertyEditorAsset::ShouldDisplayThumbnail(const FArguments& InArgs, const UClass* InObjectClass) const
|
||||
{
|
||||
if (!InArgs._DisplayThumbnail || !InArgs._ThumbnailPool.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool bShowThumbnail = InObjectClass == nullptr || !InObjectClass->IsChildOf(AActor::StaticClass());
|
||||
|
||||
// also check metadata for thumbnail & text display
|
||||
const FProperty* PropertyToCheck = nullptr;
|
||||
if (PropertyEditor.IsValid())
|
||||
{
|
||||
PropertyToCheck = PropertyEditor->GetProperty();
|
||||
}
|
||||
else if (PropertyHandle.IsValid())
|
||||
{
|
||||
PropertyToCheck = PropertyHandle->GetProperty();
|
||||
}
|
||||
|
||||
if (PropertyToCheck != nullptr)
|
||||
{
|
||||
PropertyToCheck = GetActualMetadataProperty(PropertyToCheck);
|
||||
|
||||
return GetTagOrBoolMetadata(PropertyToCheck, TEXT("DisplayThumbnail"), bShowThumbnail);
|
||||
}
|
||||
|
||||
return bShowThumbnail;
|
||||
}
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/DisplayThumbnail/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/DisplayThumbnail/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
# ExposeFunctionCategories
|
||||
|
||||
- **功能描述:** 指定该Object属性所属于的类里的某些目录下的函数可以直接在本类上暴露出来。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** strings="a,b,c"
|
||||
- **限制类型:** UObject*
|
||||
- **常用程度:** ★★★
|
||||
|
||||
指定该Object属性所属于的类里的某些目录下的函数可以直接在本类上暴露出来。
|
||||
|
||||
一开始直接还挺难理解的其含义和作用的,但这其实是一个便利性的功能而已。比如有类A里面定义一些函数,然后类B里有个A的对象。这个时候如果想在B对象身上去调用A的函数,就得手动先拖拉出B.ObjA然后再拖拉出其内部的函数。我们希望把当前B的应用上下文场景下,可以把A里的某些函数直接比较方便的暴露到B里来调用。
|
||||
|
||||
其实就是引擎帮我们自动的拖拉出B.ObjA这一步操作而已。你如果想要调用A里的更多的函数,也可以自己手动在B.ObjA身上拖拉右键出更多的函数。
|
||||
|
||||
源码里这种应用也比较多,比较方便的例子是以下源码例子,这样当前在ASkeletalMeshActor 身上就可以直接拖拉出USkeletalMeshComponent里ExposeFunctionCategories 所定义的目录的函数。
|
||||
|
||||
```cpp
|
||||
UCLASS(ClassGroup=ISkeletalMeshes, Blueprintable, ComponentWrapperClass, ConversionRoot, meta=(ChildCanTick), MinimalAPI)
|
||||
class ASkeletalMeshActor : public AActor
|
||||
{
|
||||
private:
|
||||
UPROPERTY(Category = SkeletalMeshActor, VisibleAnywhere, BlueprintReadOnly, meta = (ExposeFunctionCategories = "Mesh,Components|SkeletalMesh,Animation,Physics", AllowPrivateAccess = "true"))
|
||||
TObjectPtr<class USkeletalMeshComponent> SkeletalMeshComponent;
|
||||
}
|
||||
```
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_ExposeFunctionCategories :public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "FirstFunc")
|
||||
void MyExposeFunc1() {}
|
||||
UFUNCTION(BlueprintCallable, Category = "SecondFunc")
|
||||
void MyExposeFunc2() {}
|
||||
UFUNCTION(BlueprintCallable, Category = "ThirdFunc")
|
||||
void MyExposeFunc3() {}
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_ExposeFunctionCategories_Test :public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadOnly, meta = (ExposeFunctionCategories = "FirstFunc,ThirdFunc"))
|
||||
UMyProperty_ExposeFunctionCategories* MyObject_Expose;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
## 测试效果:
|
||||
|
||||
可以见到在UMyProperty_ExposeFunctionCategories_Test 类型的Object身上,我直接输入MyExposeFunc就可以弹出“FirstFunc,ThirdFunc”这两个目录里的这两个函数,而不会直接弹出MyExposeFunc2,因为其没有直接被暴露出来。
|
||||
|
||||
而如果在MyObject_Expose这种内部对象上直接拖拉右键,则可以见到所有内部定义的函数。注意这里虽然有两个条目的MyExposeFunc1,但其实调用出来的是同一个函数,实际并没有影响。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
在蓝图右键菜单的构建过程中,会判断某个操作是否要过滤掉。这里的IsUnexposedMemberAction就是判断这个函数是否应该被过滤掉。大致的逻辑是取得其函数对应的属性,比如在UMyProperty_ExposeFunctionCategories_Test 这个Object身上,递归到子对象,其实有3个函数会进来参加测试。这3个函数(MyExposeFunc 1 2 3)各自有自己的Category,但都对应MyObject_Expose这个Property,因此其AllExposedCategories的值是我们定义的“FirstFunc,ThirdFunc”数组,最终只有两个函数通过测试,因此最后显示1,3两个函数。
|
||||
|
||||
```cpp
|
||||
static bool BlueprintActionMenuUtilsImpl::IsUnexposedMemberAction(FBlueprintActionFilter const& Filter, FBlueprintActionInfo& BlueprintAction)
|
||||
{
|
||||
bool bIsFilteredOut = false;
|
||||
|
||||
if (UFunction const* Function = BlueprintAction.GetAssociatedFunction())
|
||||
{
|
||||
TArray<FString> AllExposedCategories;
|
||||
for (FBindingObject Binding : BlueprintAction.GetBindings())
|
||||
{
|
||||
if (FProperty* Property = Binding.Get<FProperty>())
|
||||
{
|
||||
const FString& ExposedCategoryMetadata = Property->GetMetaData(FBlueprintMetadata::MD_ExposeFunctionCategories);
|
||||
if (ExposedCategoryMetadata.IsEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TArray<FString> PropertyExposedCategories;
|
||||
ExposedCategoryMetadata.ParseIntoArray(PropertyExposedCategories, TEXT(","), true);
|
||||
AllExposedCategories.Append(PropertyExposedCategories);
|
||||
}
|
||||
}
|
||||
|
||||
const FString& FunctionCategory = Function->GetMetaData(FBlueprintMetadata::MD_FunctionCategory);
|
||||
bIsFilteredOut = !AllExposedCategories.Contains(FunctionCategory);
|
||||
}
|
||||
return bIsFilteredOut;
|
||||
}
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ExposeFunctionCategories/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ExposeFunctionCategories/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
# FullyExpand
|
||||
|
||||
- **使用位置:** UPROPERTY
|
||||
- **元数据类型:** bool
|
||||
- **关联项:** [ShowInnerProperties](ShowInnerProperties/ShowInnerProperties.md)
|
||||
|
||||
但是没有发现该Meta被使用的原理代码。
|
||||
|
||||
在源码中搜索发现有多处应用,但实际上没有原理代码。
|
||||
|
||||
```cpp
|
||||
/** The options that are available on the node. */
|
||||
UPROPERTY(EditAnywhere, Instanced, Category = "Options", meta=(ShowInnerProperties, FullyExpand="true"))
|
||||
TObjectPtr<UMovieGraphValueContainer> SelectOptions;
|
||||
|
||||
/** The currently selected option. */
|
||||
UPROPERTY(EditAnywhere, Instanced, Category = "Options", meta=(ShowInnerProperties, FullyExpand="true"))
|
||||
TObjectPtr<UMovieGraphValueContainer> SelectedOption;
|
||||
```
|
@@ -0,0 +1,96 @@
|
||||
# HideAssetPicker
|
||||
|
||||
- **功能描述:** 隐藏Object类型引脚上的AssetPicker的选择列表
|
||||
- **使用位置:** UFUNCTION
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** strings="a,b,c"
|
||||
- **限制类型:** UObject*
|
||||
- **常用程度:** ★★
|
||||
|
||||
隐藏Object类型引脚上的AssetPicker的选择列表。这在有时我们只是想要自己传递Object引用,不希望用户选择到引擎里别的资产的时候会比较有用。因为Asset类型其实也是Object,因此对于Object引用类型的参数叫做HideAssetPicker。
|
||||
|
||||
在源码里并没有找到有使用的地方,但是这个功能是可用的。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UFUNCTION(BlueprintCallable)
|
||||
static void MyFunc_NoHideAssetPicker(UObject* ObjectClass) {}
|
||||
|
||||
UFUNCTION(BlueprintCallable, meta = (HideAssetPicker = "ObjectClass"))
|
||||
static void MyFunc_HideAssetPicker(UObject* ObjectClass) {}
|
||||
```
|
||||
|
||||
## 蓝图效果:
|
||||
|
||||
默认的情况MyFunc_NoHideAssetPicker是可以弹出选择列表的。而MyFunc_HideAssetPicker则就隐藏了起来。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
判断一个函数引脚是否允许打开AssetPicker的逻辑是:
|
||||
|
||||
- 必须是个object 类型
|
||||
- 如果是UActorComponent则不显示
|
||||
- 如果是Actor类型,那么得在关卡蓝图中,且该Actor是placeable的才显示。
|
||||
- 如果用HideAssetPicker显式指定了该参数,则该参数也不显示。
|
||||
|
||||
```cpp
|
||||
bool UEdGraphSchema_K2::ShouldShowAssetPickerForPin(UEdGraphPin* Pin) const
|
||||
{
|
||||
bool bShow = true;
|
||||
if (Pin->PinType.PinCategory == PC_Object)
|
||||
{
|
||||
UClass* ObjectClass = Cast<UClass>(Pin->PinType.PinSubCategoryObject.Get());
|
||||
if (ObjectClass)
|
||||
{
|
||||
// Don't show literal buttons for component type objects
|
||||
bShow = !ObjectClass->IsChildOf(UActorComponent::StaticClass());
|
||||
|
||||
if (bShow && ObjectClass->IsChildOf(AActor::StaticClass()))
|
||||
{
|
||||
// Only show the picker for Actor classes if the class is placeable and we are in the level script
|
||||
bShow = !ObjectClass->HasAllClassFlags(CLASS_NotPlaceable)
|
||||
&& FBlueprintEditorUtils::IsLevelScriptBlueprint(FBlueprintEditorUtils::FindBlueprintForNode(Pin->GetOwningNode()));
|
||||
}
|
||||
|
||||
if (bShow)
|
||||
{
|
||||
if (UK2Node_CallFunction* CallFunctionNode = Cast<UK2Node_CallFunction>(Pin->GetOwningNode()))
|
||||
{
|
||||
if (UFunction* FunctionRef = CallFunctionNode->GetTargetFunction())
|
||||
{
|
||||
const UEdGraphPin* WorldContextPin = CallFunctionNode->FindPin(FunctionRef->GetMetaData(FBlueprintMetadata::MD_WorldContext));
|
||||
bShow = ( WorldContextPin != Pin );
|
||||
|
||||
// Check if we have explictly marked this pin as hiding the asset picker
|
||||
const FString& HideAssetPickerMetaData = FunctionRef->GetMetaData(FBlueprintMetadata::MD_HideAssetPicker);
|
||||
if(!HideAssetPickerMetaData.IsEmpty())
|
||||
{
|
||||
TArray<FString> PinNames;
|
||||
HideAssetPickerMetaData.ParseIntoArray(PinNames, TEXT(","), true);
|
||||
const FString PinName = Pin->GetName();
|
||||
for(FString& ParamNameToHide : PinNames)
|
||||
{
|
||||
ParamNameToHide.TrimStartAndEndInline();
|
||||
if(ParamNameToHide == PinName)
|
||||
{
|
||||
bShow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Cast<UK2Node_CreateDelegate>( Pin->GetOwningNode()))
|
||||
{
|
||||
bShow = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bShow;
|
||||
}
|
||||
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/HideAssetPicker/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/HideAssetPicker/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/IncludeAssetBundles/IncludeAssetBundles.jpg
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/IncludeAssetBundles/IncludeAssetBundles.jpg
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,109 @@
|
||||
# IncludeAssetBundles
|
||||
|
||||
- **功能描述:** 用于UPrimaryDataAsset的子对象属性,指定应该继续递归到该子对象里去探测AssetBundle数据。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** string="abc"
|
||||
- **限制类型:** UPrimaryDataAsset内部的ObjectPtr属性
|
||||
- **关联项:** [AssetBundles](../AssetBundles/AssetBundles.md)
|
||||
- **常用程度:** ★★
|
||||
|
||||
用于UPrimaryDataAsset的子对象属性,指定应该继续递归到该子对象里去探测AssetBundle数据。
|
||||
|
||||
这样这些指对象内部的 FSoftObjectPtr 或 FSoftObjectPath 属性,其上面标明的AssetBundle的数据才会被解析添加到UPrimaryDataAsset的AssetBundleData里。
|
||||
|
||||
- 默认情况下,InitializeAssetBundlesFromMetadata_Recursive只会分析到UPrimaryDataAsset的本身这一层级的属性,比如下面的Icon和Mesh属性。
|
||||
- 而如果再嵌套了一层,就是UPrimaryDataAsset下面拥有只对象,UMyProperty_Asset_ChildObject,而UMyProperty_Asset_ChildObject 里面又包含FSoftObjectPath ,希望它被属于AssetBundles 的一部分,在加载UPrimaryDataAsset的时候同时一并加载。这个时候就需要告诉引擎需要继续分析这个子对象。
|
||||
- 注意到UMyProperty_Asset_ChildObject我都是用TObjectPtr,是个硬引用,该对象在UMyProperty_Asset_Item 被加载的时候也会自然被加载进来。因此无论如何,UMyProperty_Asset_ChildObject 都会被加载进来。但是UMyProperty_Asset_ChildObject 内部的ChildIcon是用TSoftObjectPtr,是软引用,因此必须依赖AssetBundle的机制才会被加载。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_Asset_ChildObject :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (AssetBundles = "Client"))
|
||||
TSoftObjectPtr<UTexture2D> ChildIcon;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_Asset_Item :public UPrimaryDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
FString Name;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (AssetBundles = "UI,Game"))
|
||||
TSoftObjectPtr<UTexture2D> Icon;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (AssetBundles = "Game"))
|
||||
TSoftObjectPtr<UStaticMesh> Mesh;
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
TObjectPtr<UMyProperty_Asset_ChildObject> MyChildObject_NotIncludeAssetBundles;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (IncludeAssetBundles))
|
||||
TObjectPtr<UMyProperty_Asset_ChildObject> MyChildObject_IncludeAssetBundles;
|
||||
public:
|
||||
virtual FPrimaryAssetId GetPrimaryAssetId() const override;
|
||||
};
|
||||
```
|
||||
|
||||
## 测试效果:
|
||||
|
||||
配置的数据图的下部分,分别配置了两张图片。但在LoadPrimaryAsset后,只有MyChildObject_IncludeAssetBundles内部的ChildIcon才被加载进来。
|
||||
|
||||

|
||||
|
||||
如果分析UMyProperty_Asset_Item 的AssetBunbleData数据,会发现其Client只包含第二张Stone图片的路径。这是因为只有第二张图片才被分析到并包含进来。
|
||||
|
||||
```cpp
|
||||
{
|
||||
BundleName = "Client";
|
||||
BundleAssets =
|
||||
{
|
||||
{
|
||||
AssetPath =
|
||||
{
|
||||
PackageName = "/Game/Asset/Image/T_Shop_Stone";
|
||||
AssetName = "T_Shop_Stone";
|
||||
};
|
||||
SubPathString = "";
|
||||
};
|
||||
},
|
||||
AssetPaths =
|
||||
{
|
||||
{
|
||||
PackageName = "/Game/Asset/Image/T_Shop_Stone";
|
||||
AssetName = "T_Shop_Stone";
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 原理:
|
||||
|
||||
UPrimaryDataAsset下的属性如果是个Object属性,只当有IncludeAssetBundles的时候,才会进一步递归向下探测。
|
||||
|
||||
```cpp
|
||||
void UAssetManager::InitializeAssetBundlesFromMetadata_Recursive(const UStruct* Struct, const void* StructValue, FAssetBundleData& AssetBundle, FName DebugName, TSet<const void*>& AllVisitedStructValues) const
|
||||
{
|
||||
static FName AssetBundlesName = TEXT("AssetBundles");
|
||||
static FName IncludeAssetBundlesName = TEXT("IncludeAssetBundles");
|
||||
|
||||
//根据当前对象的值,搜索拥有AssetBundles的属性的值,最后AddBundleAsset,BundleName就是设置的值,而FoundRef是引用的对象的资产路径
|
||||
else if (const FObjectProperty* ObjectProperty = CastField<FObjectProperty>(Property))
|
||||
{
|
||||
if (ObjectProperty->PropertyFlags & CPF_InstancedReference || ObjectProperty->GetOwnerProperty()->HasMetaData(IncludeAssetBundlesName))
|
||||
{
|
||||
const UObject* Object = ObjectProperty->GetObjectPropertyValue(PropertyValue);
|
||||
if (Object != nullptr)
|
||||
{
|
||||
InitializeAssetBundlesFromMetadata_Recursive(Object->GetClass(), Object, AssetBundle, Object->GetFName(), AllVisitedStructValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@@ -0,0 +1,156 @@
|
||||
# LoadBehavior
|
||||
|
||||
- **功能描述:** 用在UCLASS上标记这个类的加载行为,使得相应的TObjectPtr属性支持延迟加载。可选的加载行为默认为Eager,可改为LazyOnDemand。
|
||||
- **使用位置:** UCLASS
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** string="abc"
|
||||
- **限制类型:** TObjectPtr
|
||||
- **常用程度:** ★
|
||||
|
||||
用在UCLASS上标记这个类的加载行为,使得相应的TObjectPtr属性支持延迟加载。可选的加载行为默认为Eager,可改为LazyOnDemand。
|
||||
|
||||
- 默认Eager的逻辑和我们常见的资源硬引用的逻辑相同,就是如果A硬引用了B,在加载A的时候就会递归把B也加载进来。
|
||||
- 改为LazyOnDemand的逻辑是只有在这个资源真正被需要(触发Get)的时候才会去加载该资源。这也是种硬引用,但是是延迟加载的。同样A硬引用了B,在加载A的时候,不会直接立马就加载B,而是先记录下来这个引用关系信息(B的ObjectPath)。在A里真正需要访问B的时候,这个时候因为已经事先记录知道了B在哪里,因此就可以在这个时候再去把B加载进来。如果加载的够快,对用户是无感的。LazyOnDemand只在编辑器下生效,这么做的好处是可以尽快的打开编辑器,而不是等待所有资源都加载进来。因为其实并不是所有资源都要第一时间加载解析进来访问。
|
||||
- 同FSoftObjectPtr的区别是后者是软引用,需要用户手动的自己判断时机加载。而LazyOnDemand是自动的延迟加载,用户是无感的,不需要做额外的操作。
|
||||
- LoadBehavior只作用于TObjectPtr属性,UObject*属性总是直接都加载的。因为只有TObjectPtr里实现了UObject*的引用路径信息编码,才可以支持延迟加载。
|
||||
- LoadBehavior也只支持在编辑器环境。因为在Runtime,TObjectPtr会退化成UObject*,也就全部都是直接加载了。
|
||||
- LoadBehavior一般标记在资产类型的类上。源码里标记的类有:DataAsset,DataTable,CurveTable,SoundCue,SoundWave,DialogueWave,AnimMontage。因此假如你自己自定义了资产类,也包含了许多数据,就可以用LazyOnDemand来优化在编辑器下的加载速度。
|
||||
- 要测试LoadBehavior,要打开引擎的的LazyLoadImports功能,默认情况下是关闭的。打开的方式可在DefaultEngine.ini下增加Core.System.Experimental节下LazyLoadImports=True的设置。源码可参考IsImportLazyLoadEnabled这个方法。
|
||||
- 在测试的时候,要小心如果是双击打开DataAsset资产,因为在属性细节面板里要展示属性的值,在属性上会调用GetObjectPropertyValue_InContainer,因此会触发ObjectHandleResolve,导致TObjectPtr的Resolve。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
如下专门定义了UMyDataAsset_Eager 和UMyDataAsset_LazyOnDemand 两种DataAsset,并标注了不同的LoadBehavior 以做对比。
|
||||
|
||||
```cpp
|
||||
//(BlueprintType = true, IncludePath = Property/MyProperty_Asset.h, IsBlueprintBase = true, LoadBehavior = Eager, ModuleRelativePath = Property/MyProperty_Asset.h)
|
||||
UCLASS(Blueprintable, Blueprintable, meta = (LoadBehavior = "Eager"))
|
||||
class INSIDER_API UMyDataAsset_Eager :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
float Score;
|
||||
};
|
||||
|
||||
//(BlueprintType = true, IncludePath = Property/MyProperty_Asset.h, IsBlueprintBase = true, LoadBehavior = LazyOnDemand, ModuleRelativePath = Property/MyProperty_Asset.h)
|
||||
UCLASS(Blueprintable, Blueprintable, meta = (LoadBehavior = "LazyOnDemand"))
|
||||
class INSIDER_API UMyDataAsset_LazyOnDemand :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
float Score;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyClass_LoadBehaviorTest :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
TObjectPtr<UMyDataAsset_LazyOnDemand> MyLazyOnDemand_AssetPtr;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
TObjectPtr<UMyDataAsset_Eager> MyEager_AssetPtr;
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere,meta = (LoadBehavior = "Eager"))
|
||||
TObjectPtr<UMyDataAsset_LazyOnDemand> MyLazyOnDemand_AssetPtr_EagerOnProperty;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere,meta = (LoadBehavior = "LazyOnDemand"))
|
||||
TObjectPtr<UMyDataAsset_Eager> MyEager_AssetPtr_LazyOnDemandOnProperty;
|
||||
|
||||
public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
UMyDataAsset_LazyOnDemand* MyLazyOnDemand_Asset;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere)
|
||||
UMyDataAsset_Eager* MyEager_Asset;
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable)
|
||||
static void LoadBehaviorTest();
|
||||
};
|
||||
|
||||
void UMyClass_LoadBehaviorTest::LoadBehaviorTest()
|
||||
{
|
||||
UPackage* pk = LoadPackage(nullptr, TEXT("/Game/Class/Behavior/LoadBehavior/DA_LoadBehaviorTest"), 0);
|
||||
UMyClass_LoadBehaviorTest* obj = LoadObject<UMyClass_LoadBehaviorTest>(pk, TEXT("DA_LoadBehaviorTest"));
|
||||
}
|
||||
|
||||
//开启功能
|
||||
DefaultEngine.ini
|
||||
[Core.System.Experimental]
|
||||
LazyLoadImports=True
|
||||
```
|
||||
|
||||
## 测试结果:
|
||||
|
||||
在编辑器运行起来之后,手动调用LoadBehaviorTest来加载这个UMyClass_LoadBehaviorTest 的DataAsset。查看不同类型属性的对象值。可以发现:
|
||||
|
||||
- 其中MyLazyOnDemand_AssetPtr和MyLazyOnDemand_AssetPtr_EagerOnProperty的ObjectPtr的值是还没有Resolved的,其他的都可以查看到直接对象的值。
|
||||
- 可以得出的结论有,只有在UCLASS上标记LazyOnDemand才可以使得延迟加载生效。在属性上标记LoadBehavior 并不会起作用。直接UObject*的属性统统都会直接加载。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
在LinkerLoadImportBehavior.cpp里可看见判断LoadBehavior的FindLoadBehavior方法,因此发现其只作用在UCLASS上。
|
||||
|
||||
另外也可在TObjectPtr的Get函数里发现ResolveObjectHandle的调用。这是触发Resolve的地方。
|
||||
|
||||
也注意到UE_WITH_OBJECT_HANDLE_LATE_RESOLVE 的定义是WITH_EDITORONLY_DATA,因此是在编辑器环境下生效。
|
||||
|
||||
```cpp
|
||||
//D:\github\UnrealEngine\Engine\Source\Runtime\CoreUObject\Private\UObject\LinkerLoadImportBehavior.cpp
|
||||
enum class EImportBehavior : uint8
|
||||
{
|
||||
Eager = 0,
|
||||
// @TODO: OBJPTR: we want to permit lazy background loading in the future
|
||||
//LazyBackground,
|
||||
LazyOnDemand,
|
||||
};
|
||||
|
||||
EImportBehavior FindLoadBehavior(const UClass& Class)
|
||||
{
|
||||
//Package class can't have meta data because of UHT
|
||||
if (&Class == UPackage::StaticClass())
|
||||
{
|
||||
return EImportBehavior::LazyOnDemand;
|
||||
}
|
||||
|
||||
static const FName Name_LoadBehavior(TEXT("LoadBehavior"));
|
||||
if (const FString* LoadBehaviorMeta = Class.FindMetaData(Name_LoadBehavior))
|
||||
{
|
||||
if (*LoadBehaviorMeta == TEXT("LazyOnDemand"))
|
||||
{
|
||||
return EImportBehavior::LazyOnDemand;
|
||||
}
|
||||
return EImportBehavior::Eager;
|
||||
}
|
||||
else
|
||||
{
|
||||
//look in super class to see if it has lazy load on
|
||||
const UClass* Super = Class.GetSuperClass();
|
||||
if (Super != nullptr)
|
||||
{
|
||||
return FindLoadBehavior(*Super);
|
||||
}
|
||||
return EImportBehavior::Eager;
|
||||
}
|
||||
}
|
||||
|
||||
#define UE_WITH_OBJECT_HANDLE_LATE_RESOLVE WITH_EDITORONLY_DATA
|
||||
|
||||
inline UObject* ResolveObjectHandle(FObjectHandle& Handle)
|
||||
{
|
||||
#if UE_WITH_OBJECT_HANDLE_LATE_RESOLVE || UE_WITH_OBJECT_HANDLE_TRACKING
|
||||
UObject* ResolvedObject = ResolveObjectHandleNoRead(Handle);
|
||||
UE::CoreUObject::Private::OnHandleRead(ResolvedObject);
|
||||
return ResolvedObject;
|
||||
#else
|
||||
return ReadObjectHandlePointerNoCheck(Handle);
|
||||
#endif
|
||||
}
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/LoadBehavior/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/LoadBehavior/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
# MustBeLevelActor
|
||||
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** bool
|
||||
|
||||
意思是这个必须是场景里的Actor,而不是LevelScriptActor的意思。
|
||||
|
||||
触发时机在用箭头选择的当前选中actor的时候。
|
||||
|
||||
## 源码中遇见:
|
||||
|
||||
```cpp
|
||||
if (FObjectPropertyBase* ObjectProperty = CastField<FObjectPropertyBase>(Property))
|
||||
{
|
||||
ObjectClass = ObjectProperty->PropertyClass;
|
||||
bMustBeLevelActor = ObjectProperty->GetOwnerProperty()->GetBoolMetaData(TEXT("MustBeLevelActor"));
|
||||
RequiredInterface = ObjectProperty->GetOwnerProperty()->GetClassMetaData(TEXT("MustImplement"));
|
||||
}
|
||||
```
|
@@ -0,0 +1,246 @@
|
||||
# ShowInnerProperties
|
||||
|
||||
- **功能描述:** 在属性细节面板中显示对象引用的内部属性
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** bool
|
||||
- **限制类型:** UObject*
|
||||
- **关联项:** [ShowOnlyInnerProperties](../ShowOnlyInnerProperties/ShowOnlyInnerProperties.md), [FullyExpand](../FullyExpand.md), [CollapsableChildProperties](../CollapsableChildProperties.md)
|
||||
- **常用程度:** ★★★★★
|
||||
|
||||
在属性细节面板中显示对象引用的内部属性。
|
||||
|
||||
默认情况下,对象引用属性的内部属性在细节面板里是不会显示出来的,只是孤零零的显示一个对象名字。但你如果想直接显示出其内部属性然后可以编辑的话,就需要ShowInnerProperties这个meta的作用。
|
||||
|
||||
但ShowInnerProperties作用有两个限定条件,一是这个属性得是UObject*,二是这个属性不是个容器。
|
||||
|
||||
同时也注意到,Struct属性是默认就会显示内部属性的,因此也不需要再设置ShowInnerProperties。
|
||||
|
||||
**和EditInineNew的区别是什么?**
|
||||
|
||||
这种效果,和在UCLASS上设置EditInineNew配合其对象引用属性上设置Instanced,达成的效果很相似。区别是UCLASS上设置EditInineNew会使得一个类的对象属性引用可以在属性面板里创建对象, 而UPROPERTY上的Instanced,会使得这个属性自动的增加EditInline的meta,因此也会产生显示内部属性的同样效果。因此结论上来说,和ShowInnerProperties像的是本质是EditInline这个meta。但EditInline的效果多了一层是它支持对象容器,而ShowInnerProperties只支持单个对象引用属性。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FMyPropertyInner
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
int32 StructInnerInt = 123;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FString StructInnerString;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class INSIDER_API UMyProperty_InnerSub :public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
int32 ObjectInnerInt = 123;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FString ObjectInnerString;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType, EditInlineNew)
|
||||
class INSIDER_API UMyProperty_InnerSub_EditInlineNew :public UObject
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
int32 ObjectInnerInt = 123;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FString ObjectInnerString;
|
||||
};
|
||||
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class INSIDER_API UMyProperty_Inner :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FMyPropertyInner InnerStruct;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ShowInnerProperties))
|
||||
FMyPropertyInner InnerStruct_ShowInnerProperties;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
UMyProperty_InnerSub* InnerObject;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ShowInnerProperties))
|
||||
UMyProperty_InnerSub* InnerObject_ShowInnerProperties;
|
||||
|
||||
//(Category = MyProperty_Inner, EditInline = , ModuleRelativePath = Property/MyProperty_Inner.h)
|
||||
//CPF_Edit | CPF_BlueprintVisible | CPF_ZeroConstructor | CPF_NoDestructor | CPF_HasGetValueTypeHash | CPF_NativeAccessSpecifierPublic
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditInline))
|
||||
UMyProperty_InnerSub* InnerObject_EditInline;
|
||||
|
||||
//(Category = MyProperty_Inner, EditInline = true, ModuleRelativePath = Property/MyProperty_Inner.h)
|
||||
//CPF_Edit | CPF_BlueprintVisible | CPF_ExportObject | CPF_ZeroConstructor | CPF_InstancedReference | CPF_NoDestructor | CPF_PersistentInstance | CPF_HasGetValueTypeHash | CPF_NativeAccessSpecifierPublic
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Instanced)
|
||||
UMyProperty_InnerSub* InnerObject_Instanced;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
UMyProperty_InnerSub_EditInlineNew* InnerObject_EditInlineNewClass;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditInline))
|
||||
UMyProperty_InnerSub_EditInlineNew* InnerObject_EditInlineNewClass_EditInline;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Instanced)
|
||||
UMyProperty_InnerSub_EditInlineNew* InnerObject_EditInlineNewClass_Instanced;
|
||||
|
||||
public:
|
||||
UFUNCTION(CallInEditor)
|
||||
void ClearInnerObject();
|
||||
UFUNCTION(CallInEditor)
|
||||
void InitInnerObject();
|
||||
};
|
||||
|
||||
void UMyProperty_Inner::ClearInnerObject()
|
||||
{
|
||||
InnerObject = nullptr;
|
||||
InnerObject_ShowInnerProperties = nullptr;
|
||||
InnerObject_EditInline = nullptr;
|
||||
InnerObject_Instanced = nullptr;
|
||||
|
||||
InnerObject_EditInlineNewClass = nullptr;
|
||||
InnerObject_EditInlineNewClass_EditInline = nullptr;
|
||||
InnerObject_EditInlineNewClass_Instanced = nullptr;
|
||||
|
||||
Modify();
|
||||
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
PropertyEditorModule.NotifyCustomizationModuleChanged();
|
||||
}
|
||||
|
||||
void UMyProperty_Inner::InitInnerObject()
|
||||
{
|
||||
InnerObject = NewObject<UMyProperty_InnerSub>(this);
|
||||
InnerObject_ShowInnerProperties = NewObject<UMyProperty_InnerSub>(this);
|
||||
InnerObject_EditInline = NewObject<UMyProperty_InnerSub>(this);
|
||||
InnerObject_Instanced = NewObject<UMyProperty_InnerSub>(this);
|
||||
|
||||
InnerObject_EditInlineNewClass = NewObject<UMyProperty_InnerSub_EditInlineNew>(this);
|
||||
InnerObject_EditInlineNewClass_EditInline = NewObject<UMyProperty_InnerSub_EditInlineNew>(this);
|
||||
//InnerObject_EditInlineNewClass_Instanced = NewObject<UMyProperty_InnerSub_EditInlineNew>(this);
|
||||
|
||||
Modify();
|
||||
|
||||
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
|
||||
PropertyEditorModule.NotifyCustomizationModuleChanged();
|
||||
}
|
||||
```
|
||||
|
||||
## 蓝图效果:
|
||||
|
||||

|
||||
|
||||
可以观察到:
|
||||
|
||||
- 结构的属性默认支持展开内部属性
|
||||
- 带有ShowInnerProperties的UMyProperty_InnerSub* InnerObject_ShowInnerProperties;支持展开属性
|
||||
- 带有EditInline和Instanced的UMyProperty_InnerSub* 也都支持展开内部属性,也可以观察到他们的meta是一致的,都带有EditInline=true
|
||||
- 只有EditInlineNew的UCLASS的UMyProperty_InnerSub_EditInlineNew* InnerObject_EditInlineNewClass;其对象引用不支持展开属性,说明在类上设置EditInlineNew并没有作用。
|
||||
- 但是我们也观察到InnerObject_EditInlineNewClass_Instanced的设置里支持直接创建对象,因为其类上有EditInlineNew。而InnerObject_Instanced上并不支持直接创建对象,因为其类UMyProperty_InnerSub上并没有EditInlineNew,因此不会出现在可选框里。
|
||||
|
||||
## 扩展例子:
|
||||
|
||||
在源码中搜索观察到UChildActorComponent::ChildActorTemplate上也会带有ShowInnerProperties,则就是一个典型的应用,以便让我们直接在熟悉细节面板里直接编辑ChildActor的属性数据。
|
||||
|
||||
但假如我们去掉这个ShowInnerProperties,我们可以来前后对比一下效果:
|
||||
|
||||
```cpp
|
||||
class UChildActorComponent : public USceneComponent
|
||||
{
|
||||
UPROPERTY(VisibleDefaultsOnly, DuplicateTransient, Category=ChildActorComponent, meta=(ShowInnerProperties))
|
||||
TObjectPtr<AActor> ChildActorTemplate;
|
||||
}
|
||||
|
||||
void UMyProperty_Inner::RemoveActorMeta()
|
||||
{
|
||||
FProperty* prop = UChildActorComponent::StaticClass()->FindPropertyByName(TEXT("ChildActorTemplate"));
|
||||
prop->RemoveMetaData(TEXT("ShowInnerProperties"));
|
||||
}
|
||||
|
||||
void UMyProperty_Inner::AddActorMeta()
|
||||
{
|
||||
FProperty* prop = UChildActorComponent::StaticClass()->FindPropertyByName(TEXT("ChildActorTemplate"));
|
||||
prop->SetMetaData(TEXT("ShowInnerProperties"), TEXT(""));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 对比效果:
|
||||
|
||||

|
||||
|
||||
可以发现,去除ShowInnerProperties后,ChildActorTemplate属性退化成一个普通的对象引用,我们无法在上面直接编辑对象的内部属性。
|
||||
|
||||
## 原理:
|
||||
|
||||
源码里最典型的例子是ChildActorTemplate,这样就可以直接显示出内部的属性。
|
||||
|
||||
```cpp
|
||||
class UChildActorComponent : public USceneComponent
|
||||
{
|
||||
UPROPERTY(VisibleDefaultsOnly, DuplicateTransient, Category=ChildActorComponent, meta=(ShowInnerProperties))
|
||||
TObjectPtr<AActor> ChildActorTemplate;
|
||||
}
|
||||
```
|
||||
|
||||
作用的源码:
|
||||
|
||||
```cpp
|
||||
void FPropertyNode::InitNode(const FPropertyNodeInitParams& InitParams)
|
||||
{
|
||||
const bool bIsObjectOrInterface = CastField<FObjectPropertyBase>(MyProperty) || CastField<FInterfaceProperty>(MyProperty);
|
||||
// we are EditInlineNew if this property has the flag, or if inside a container that has the flag.
|
||||
bIsEditInlineNew = GotReadAddresses && bIsObjectOrInterface && !MyProperty->HasMetaData(Name_NoEditInline) &&
|
||||
(MyProperty->HasMetaData(Name_EditInline) || (bIsInsideContainer && OwnerProperty->HasMetaData(Name_EditInline)));
|
||||
bShowInnerObjectProperties = bIsObjectOrInterface && MyProperty->HasMetaData(Name_ShowInnerProperties);
|
||||
|
||||
if (bIsEditInlineNew)
|
||||
{
|
||||
SetNodeFlags(EPropertyNodeFlags::EditInlineNew, true);
|
||||
}
|
||||
else if (bShowInnerObjectProperties)
|
||||
{
|
||||
SetNodeFlags(EPropertyNodeFlags::ShowInnerObjectProperties, true);
|
||||
}
|
||||
}
|
||||
|
||||
void FItemPropertyNode::InitExpansionFlags(void)
|
||||
{
|
||||
FProperty* MyProperty = GetProperty();
|
||||
|
||||
if (TSharedPtr<FPropertyNode>& ValueNode = GetOrCreateOptionalValueNode())
|
||||
{
|
||||
// This is a set optional, so check its SetValue instead.
|
||||
MyProperty = ValueNode->GetProperty();
|
||||
}
|
||||
|
||||
bool bExpandableType = CastField<FStructProperty>(MyProperty)
|
||||
|| (CastField<FArrayProperty>(MyProperty) || CastField<FSetProperty>(MyProperty) || CastField<FMapProperty>(MyProperty));
|
||||
|
||||
if (bExpandableType
|
||||
|| HasNodeFlags(EPropertyNodeFlags::EditInlineNew)
|
||||
|| HasNodeFlags(EPropertyNodeFlags::ShowInnerObjectProperties)
|
||||
|| (MyProperty->ArrayDim > 1 && ArrayIndex == -1))
|
||||
{
|
||||
SetNodeFlags(EPropertyNodeFlags::CanBeExpanded, true);
|
||||
}
|
||||
}
|
||||
void FPropertyNode::RebuildChildren()
|
||||
{
|
||||
if (HasNodeFlags(EPropertyNodeFlags::CanBeExpanded) && (ChildNodes.Num() == 0))
|
||||
{
|
||||
InitChildNodes();
|
||||
if (ExpandedPropertyItemSet.Size() > 0)
|
||||
{
|
||||
FPropertyNodeUtils::SetExpandedItems(ThisAsSharedRef, ExpandedPropertyItemSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
特别注意,这里的bShowInnerObjectProperties的判断条件是bIsObjectOrInterface 且有meta,因此该特性只作用于对象引用上。然后如果判断有EPropertyNodeFlags::ShowInnerObjectProperties,则继续设置EPropertyNodeFlags::CanBeExpanded,最后导致展开对象的属性。
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowInnerProperties/Untitled 1.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowInnerProperties/Untitled 1.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowInnerProperties/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowInnerProperties/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,64 @@
|
||||
# ShowOnlyInnerProperties
|
||||
|
||||
- **功能描述:** 把结构属性的内部属性直接上提一个层级直接展示
|
||||
- **使用位置:** UPROPERTY
|
||||
- **元数据类型:** bool
|
||||
- **限制类型:** FStruct属性
|
||||
- **关联项:** [ShowInnerProperties](../ShowInnerProperties/ShowInnerProperties.md)
|
||||
- **常用程度:** ★★★
|
||||
|
||||
把结构属性的内部属性直接上提一个层级直接展示,而不是像默认一样归属于一个可展开的父级结构。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FMyPropertyInner InnerStruct;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ShowOnlyInnerProperties))
|
||||
FMyPropertyInner InnerStruct_ShowOnlyInnerProperties;
|
||||
```
|
||||
|
||||
## 效果对比:
|
||||
|
||||

|
||||
|
||||
可以发现InnerStruct_ShowOnlyInnerProperties的内部属性直接就显示在对象的当前层级上,而InnerStruct的属性有一个结构名称作为Category来展开。
|
||||
|
||||
## 原理:
|
||||
|
||||
在遇见FStructProperty的时候,会开始判断ShowOnlyInnerProperties来决定是否要创建一个可展开的Category,或者还是直接把内部属性展示出来。有了ShowOnlyInnerProperties,就会直接递归迭代到其内部属性。
|
||||
|
||||
```cpp
|
||||
void DetailLayoutHelpers::UpdateSinglePropertyMapRecursive(FPropertyNode& InNode, FName CurCategory, FComplexPropertyNode* CurObjectNode, FUpdatePropertyMapArgs& InUpdateArgs)
|
||||
{
|
||||
static FName ShowOnlyInners("ShowOnlyInnerProperties");
|
||||
// Whether or not to push out struct properties to their own categories or show them inside an expandable struct
|
||||
// This recursively applies for any nested structs that have the ShowOnlyInners metadata
|
||||
const bool bPushOutStructProps = bIsStruct && !bIsCustomizedStruct && Property->HasMetaData(ShowOnlyInners);
|
||||
|
||||
if (bRecurseIntoChildren || LocalUpdateFavoriteSystemOnly)
|
||||
{
|
||||
// Built in struct properties or children of arras
|
||||
UpdateSinglePropertyMapRecursive(ChildNode, CurCategory, CurObjectNode, ChildArgs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void FObjectPropertyNode::GetCategoryProperties(const TSet<UClass*>& ClassesToConsider, const FProperty* CurrentProperty, bool bShouldShowDisableEditOnInstance, bool bShouldShowHiddenProperties,
|
||||
const TSet<FName>& CategoriesFromBlueprints, TSet<FName>& CategoriesFromProperties, TArray<FName>& SortedCategories)
|
||||
{
|
||||
if (CurrentProperty->HasMetaData(Name_ShowOnlyInnerProperties))
|
||||
{
|
||||
const FStructProperty* StructProperty = CastField<const FStructProperty>(CurrentProperty);
|
||||
if (StructProperty)
|
||||
{
|
||||
for (TFieldIterator<FProperty> It(StructProperty->Struct); It; ++It)
|
||||
{
|
||||
GetCategoryProperties(ClassesToConsider, *It, bShouldShowDisableEditOnInstance, bShouldShowHiddenProperties, CategoriesFromBlueprints, CategoriesFromProperties, SortedCategories);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowOnlyInnerProperties/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/ShowOnlyInnerProperties/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
# ThumbnailSize
|
||||
|
||||
- **功能描述:** 改变缩略图的大小。
|
||||
- **使用位置:** UCLASS, UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** bool
|
||||
- **关联项:** [DisplayThumbnail](DisplayThumbnail/DisplayThumbnail.md)
|
||||
|
||||
改变缩略图的大小。但发现并不会起作用。
|
||||
|
||||
## 原理:
|
||||
|
||||
```cpp
|
||||
void SObjectPropertyEntryBox::Construct( const FArguments& InArgs )
|
||||
{
|
||||
// check if the property metadata wants us to display a thumbnail
|
||||
const FString& DisplayThumbnailString = PropertyHandle->GetProperty()->GetMetaData(TEXT("DisplayThumbnail"));
|
||||
if(DisplayThumbnailString.Len() > 0)
|
||||
{
|
||||
bDisplayThumbnail = DisplayThumbnailString == TEXT("true");
|
||||
}
|
||||
|
||||
// check if the property metadata has an override to the thumbnail size
|
||||
const FString& ThumbnailSizeString = PropertyHandle->GetProperty()->GetMetaData(TEXT("ThumbnailSize"));
|
||||
if ( ThumbnailSizeString.Len() > 0 )
|
||||
{
|
||||
FVector2D ParsedVector;
|
||||
if ( ParsedVector.InitFromString(ThumbnailSizeString) )
|
||||
{
|
||||
ThumbnailSize.X = (int32)ParsedVector.X;
|
||||
ThumbnailSize.Y = (int32)ParsedVector.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/Untracked/Untitled.png
(Stored with Git LFS)
Normal file
BIN
03-UnrealEngine/Gameplay/UObject/UnrealSpecifiers/Meta/Object/Untracked/Untitled.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@@ -0,0 +1,86 @@
|
||||
# Untracked
|
||||
|
||||
- **功能描述:** 使得TSoftObjectPtr和FSoftObjectPath的软对象引用类型的属性,不跟踪记录资产的 。
|
||||
- **使用位置:** UPROPERTY
|
||||
- **引擎模块:** Object Property
|
||||
- **元数据类型:** bool
|
||||
- **限制类型:** TSoftObjectPtr,FSoftObjectPath
|
||||
- **常用程度:** ★
|
||||
|
||||
使得TSoftObjectPtr和FSoftObjectPath的软对象引用类型的属性,不跟踪记录资产的 。
|
||||
|
||||
一般默认情况,属性上的软对象引用也是会产生资产的引用依赖,虽然在Load本身的时候,不会像硬引用一样也去加载其他软引用对象。但是因为引用关系依然存在,因此在cook的时候,或者资产重定向的时候都会去检查这些软引用对象,确保其也会被cook进去,或者正常的处理。
|
||||
|
||||
而当你想在属性上记录“引用”一些资产,以便之后加载使用,但是又不想产生真正的资产引用依赖,这个时候就可以用untracked。源码中应用的不多,这是比较稀少的情况下。
|
||||
|
||||
和transient标记的区别是,transient属性在序列化的时候也不会序列化,因为其ctrl+S保存后重启编辑器会丢失值。transient属性既不产生资产引用关系也序列化保存值,Untracked属性会序列化保存值但不产生资产引用关系。
|
||||
|
||||
## 测试代码:
|
||||
|
||||
```cpp
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class INSIDER_API UMyProperty_Soft :public UDataAsset
|
||||
{
|
||||
GENERATED_BODY()
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
TSoftObjectPtr<UStaticMesh> MyStaticMesh;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (Untracked))
|
||||
TSoftObjectPtr<UStaticMesh> MyStaticMeshUntracked;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
FSoftObjectPath MySoftMesh;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (Untracked))
|
||||
FSoftObjectPath MySoftMeshUntracked;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Transient)
|
||||
TSoftObjectPtr<UStaticMesh> MyStaticMeshTransient;
|
||||
};
|
||||
```
|
||||
|
||||
## 蓝图效果:
|
||||
|
||||
在蓝图中建立一个UMyProperty_Soft DataAsset资产,然后设置其属性值。然后查看其引用的资源,会发现Untracked的属性,其设置的资产并没有出现在引用关系中。当然Transient的属性也不在引用关系中。
|
||||
|
||||

|
||||
|
||||
## 原理:
|
||||
|
||||
Untracked元数据,会设置为ESoftObjectPathCollectType::NeverCollect的选项。继续搜索会发现带有NeverCollect的FSoftObjectPath,其上面的资产package 不会被算到资产引用里,从而不会带到upackage Import表里。源码中有多处地方带有这个NeverCollect 的类似判断。
|
||||
|
||||
```cpp
|
||||
bool FSoftObjectPathThreadContext::GetSerializationOptions(FName& OutPackageName, FName& OutPropertyName, ESoftObjectPathCollectType& OutCollectType, ESoftObjectPathSerializeType& OutSerializeType, FArchive* Archive) const
|
||||
{
|
||||
#if WITH_EDITOR
|
||||
bEditorOnly = Archive->IsEditorOnlyPropertyOnTheStack();
|
||||
|
||||
static FName UntrackedName = TEXT("Untracked");
|
||||
if (CurrentProperty && CurrentProperty->GetOwnerProperty()->HasMetaData(UntrackedName))
|
||||
{
|
||||
// Property has the Untracked metadata, so set to never collect references if it's higher than NeverCollect
|
||||
CurrentCollectType = FMath::Min(ESoftObjectPathCollectType::NeverCollect, CurrentCollectType);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
FArchive& FImportExportCollector::operator<<(FSoftObjectPath& Value)
|
||||
{
|
||||
FName CurrentPackage;
|
||||
FName PropertyName;
|
||||
ESoftObjectPathCollectType CollectType;
|
||||
ESoftObjectPathSerializeType SerializeType;
|
||||
FSoftObjectPathThreadContext& ThreadContext = FSoftObjectPathThreadContext::Get();
|
||||
ThreadContext.GetSerializationOptions(CurrentPackage, PropertyName, CollectType, SerializeType, this);
|
||||
|
||||
if (CollectType != ESoftObjectPathCollectType::NeverCollect && CollectType != ESoftObjectPathCollectType::NonPackage)
|
||||
{
|
||||
FName PackageName = Value.GetLongPackageFName();
|
||||
if (PackageName != RootPackageName && !PackageName.IsNone())
|
||||
{
|
||||
AddImport(Value, CollectType);
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
```
|
Reference in New Issue
Block a user