文章

UE5 StructUtils

FInstancedStruct

$FInstancedStruct$ works similarly as instanced UObject* property but is USTRUCTs.

$FInstancedStruct$ 的工作模式类似 实例化的UObject* 属性.

  • 实现结构体的多态
1
2
3
4
5
6
UPROPERTY(EditAnywhere, Category = Foo, meta = (BaseStruct = "/Script/ModuleName.TestStructBase"))
FInstancedStruct Test;

//限定为“子类”
UPROPERTY(EditAnywhere, Category = Foo, meta = (BaseStruct = "/Script/ModuleName.TestStructBase", ExcludeBasestruct))
TArray<FInstancedStruct> TestArray;

蓝图返回“通配”结构体的实现

函数的主要步骤如下:

  1. P_GET_XXX 系列宏 用于在DECLARE_FUNCTION生成的函数中获取传递的参数,按入参顺序调用
  2. 使用Stack.MostRecentPropertyStack.MostRecentPropertyAddress获取输入参数的属性和地址。
  3. 使用Stack.StepCompiledIn<FStructProperty>(nullptr)读取输入参数,这里的输入参数是一个通配符类型的值。
  4. 调用P_FINISH宏表示参数读取完成。
  5. 检查ValuePropValuePtr是否有效。如果无效,抛出一个异常并中止执行。
  6. 如果输入参数有效,将结果参数RESULT_PARAM初始化为输入结构体的实例。这里使用FInstancedStruct类型,并调用InitializeAs方法。
1
2
3
4
5
6
7
8
9
10
//StructUtilsFunctionLibrary.h
public:
    /**
     * Retrieves data from an InstancedStruct if it matches the output type.
     */
    UFUNCTION(BlueprintCallable, CustomThunk, Category = "Utilities|Instanced Struct", meta = (CustomStructureParam = "Value", ExpandEnumAsExecs = "ExecResult", BlueprintInternalUseOnly="true"))
    static void GetInstancedStructValue(EStructUtilsResult& ExecResult, UPARAM(Ref) const FInstancedStruct& InstancedStruct, int32& Value);

private:
    DECLARE_FUNCTION(execGetInstancedStructValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//StructUtilsFunctionLibrary.cpp

#include UE_INLINE_GENERATED_CPP_BY_NAME(StructUtilsFunctionLibrary)

#define LOCTEXT_NAMESPACE "UStructUtilsFunctionLibrary"

void UStructUtilsFunctionLibrary::GetInstancedStructValue(EStructUtilsResult& ExecResult, const FInstancedStruct& InstancedStruct, int32& Value)
{
	// We should never hit this! stubs to avoid NoExport on the class.
	checkNoEntry();
}

DEFINE_FUNCTION(UStructUtilsFunctionLibrary::execGetInstancedStructValue)
{
	P_GET_ENUM_REF(EStructUtilsResult, ExecResult);
	P_GET_STRUCT_REF(FInstancedStruct, InstancedStruct);

	// Read wildcard Value input.
	Stack.MostRecentPropertyAddress = nullptr;
	Stack.MostRecentPropertyContainer = nullptr;
	Stack.StepCompiledIn<FStructProperty>(nullptr);
	
	const FStructProperty* ValueProp = CastField<FStructProperty>(Stack.MostRecentProperty);
	void* ValuePtr = Stack.MostRecentPropertyAddress;

	P_FINISH;

	ExecResult = EStructUtilsResult::NotValid;

	if (!ValueProp || !ValuePtr)
	{
		FBlueprintExceptionInfo ExceptionInfo(
			EBlueprintExceptionType::AbortExecution,
			LOCTEXT("InstancedStruct_GetInvalidValueWarning", "Failed to resolve the Value for Get Instanced Struct Value")
		);

		FBlueprintCoreDelegates::ThrowScriptException(P_THIS, Stack, ExceptionInfo);
	}
	else
	{
		P_NATIVE_BEGIN;
		if (InstancedStruct.IsValid() && InstancedStruct.GetScriptStruct()->IsChildOf(ValueProp->Struct))
		{
			ValueProp->Struct->CopyScriptStruct(ValuePtr, InstancedStruct.GetMemory());
			ExecResult = EStructUtilsResult::Valid;
		}
		else
		{
			ExecResult = EStructUtilsResult::NotValid;
		}
		P_NATIVE_END;
	}
}

#undef LOCTEXT_NAMESPACE

InstancedStructContainer

可以存放不同 Struct 的 Array,支持 UPROPERTY 和序列化.

  • 没有 编辑器UI,如果需要编辑器UI的话,可以用 TArray<FInstancedStruct>
  • Array中的 Struct实例 和 index 在内存上是连续的

    • $ Index$ : FInstancedStructContainer.FItem.Offset
    • 内存块:FInstancedStructContainer.Memory
  • 由于内存对齐,内存的实际占用会大于 Struct 和 Index 总大小;
  • 每个 Item 需要额外的 16 bytes 内存占用
  • 如果业务情景的 Item是大致一致的 Size,那么使用 TArray<TVariant<>> 可能性能最佳;
  • 添加新item时的代价比传统的 TArray<>
    • 需要更新内存布局
    • 初始化是通过 UScriptStruct 完成
    • 如果可能的话,应在chunks中添加和删除 item
  • 和传统的 TArray 实现一样,在 new item 时不会分配额外的内存空间
    • 可以使用 Reserve() 来分配已经明确大小的内存

PropertyBag

在结构体中保存对象,引擎中的 $StateTree$ 的参数是用这个结构来存储的

Instanced property bag allows to create and store a bag of properties.

When used as editable property, the UI allows properties to be added and removed, and values to be set.

The value is stored as a struct, the type of the value is never serialized, instead the composition of the properties is saved with the instance, and the type is recreated on load. The types with same composition of properties share same type (based on hashing).

  • 在运行时根据传入的子类型的hash来动态创建不同的UPropertyBag,这样就保证了不同结构的PropertyBag是不一样的类型,而相同的结构类型是一致的
  • 如果有动态创建虚幻带反射的类型需求时,比如版本更新后,想用脚本热更新创建新的类型,就可以考虑参考这样的实现

UPROPERTY() meta tags:

- $ FixedLayout $ : Property types cannot be altered, but values can be. This is useful if e.g. if the bag layout is set by code.

NOTE: 向实例添加或删除属性的操作成本相当高,因为它会创建新的 UPropertyBag,重新分配内存,并将所有值复制过去。

Example usage, this allows the bag to be configured in the UI:

1
2
UPROPERTY(EditDefaultsOnly, Category = Common)
FInstancedPropertyBag Bag;

Changing the layout from code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const FName TemperatureName(TEXT("Temperature"));
static const FName IsHotName(TEXT("bIsHot"));

FInstancedPropertyBag Bag;

// Add properties to the bag, and set their values.
// Adding or removing properties is not cheap, so better do it in batches.
Bag.AddProperties({
	{ TemperatureName, EPropertyBagPropertyType::Float },
	{ CountName, EPropertyBagPropertyType::Int32 }
});

// Amend the bag with a new property.
Bag.AddProperty(IsHotName, EPropertyBagPropertyType::Bool);
Bag.SetValueBool(IsHotName, true);

// Get value and use the result
if (auto Temperature = Bag.GetValueFloat(TemperatureName); Temperature.IsValid())
{
	float Val = Temperature.GetValue();
}

SharedStruct

$ FSharedStruct $ 的工作方式类似于 TSharedPtr<FInstancedStruct>,但去除了双指针间接引用(double pointer indirection)(一个指针用于 FInstancedStruct,另一个指针用于它封装的结构体内存)

  • 由于其实现方式,目前无法从 struct referencestruct view 转换为 SharedStruct

  • 这种结构体类型也可以转换为 FStructView,并且作为参数传递时,与 FInstancedStruct 一样,这是首选的方式。

如果调用代码希望保留指向结构体的共享指针,可以将 FSharedStruct 作为参数传递,但建议将其作为

1
const FSharedStruct&

传递,以限制不必要的重新计数。

StructArrayView

A generic, transient view of a homogeneously-typed array of instances of a specific UScriptStruct

StructTypeBitSet

TStructTypeBitSet 用于存储给定 UStruct 子类型的 “存在” 信息。关于可用子结构体的信息是懒加载的 - 内部的 FStructTracker 在第一次遇到给定类型时为其分配一个新索引。

本质就是一个BitArray。在Mass中也有大量使用:ECS需要快速获取Archtype中Component的多个类型信息,直接遍历会非常不效率,这个类就相当于是将引擎中所有的类都进行唯一编码,每个类型占1位,当Archtype使用了哪个类型,对应类型的Bit就置1,这样就能快速获取类型了。而这些类型第一次传入时就会被注册,因此一共占多少位,并且每个类型对应编码是多少,是按先来后到的顺序确定的,每次运行都有可能不一样,但同一次运行时的编码是唯一确定的。

要创建特定类型的实例化,您还需要提供一个将实例化静态 FStructTracker 实例的类型。

不过,提供的宏会隐藏这些细节。

要为任意结构体类型 FFooBar 声明一个位集类型,请在您的头文件或 cpp 文件中添加以下内容:

1
DECLARE_STRUCTTYPEBITSET(FMyFooBarBitSet, FFooBar);

其中 FMyFooBarBitSet 是您在代码中可以使用的类型别名。要将您的类型暴露给其他模块,请使用 DECLARE_STRUCTTYPEBITSET_EXPORTED,如下所示:

1
 DECLARE_STRUCTTYPEBITSET_EXPORTED(MYMODULE_API, FMyFooBarBitSet, FFooBar);

You also need to instantiate the static FStructTracker added by the DECLARE* macro.

You can easily do it by placing the following in your cpp file (continuing the FFooBar example):

1
DEFINE_TYPEBITSET(FMyFooBarBitSet);

StructView

  • $FConstStructView$ is “typed” struct pointer, it contains const pointer to struct plus UScriptStruct pointer.
  • $FConstStructView$ does not own the memory and will not free it when out of scope.
  • It should be only used to pass struct pointer in a limited scope, or when the user controls the lifetime of the struct being stored.

E.g. instead of passing ref or pointer to a FInstancedStruct, you should use FConstStructView or FStructView to pass around a view to the contents.

  • $FConstStructView$ is passed by value.
  • $FConstStructView$ is similar to FStructOnScope, but FConstStructView is a view only (FStructOnScope can either own the memory or be a view)
本文由作者按照 CC BY 4.0 进行授权