找回密码
 立即注册
查看: 314|回复: 4

【UE4/5】利用反射系统写代码:Runtime自动生成UI

[复制链接]
发表于 2022-1-15 12:32 | 显示全部楼层 |阅读模式
使用UE4的开发同学一定对"*.generated.h"这个名字不陌生,Unreal定义了一系列的宏,来帮助开发者将自定义的字段和函数添加至反射系统,其他模块就可以利用这些反射信息来做很多事情,比如蓝图系统利用反射系统可以对标记为UPROPERTY的变量生成UI
UPROPERTY(Category = "Road", VisibleDefaultsOnly, BlueprintReadOnly)
                FVector position;
但是这个生成UI的过程仅仅限于编辑器阶段,我们想在Runtime实现同等的效果,举例说明:
- 设计师在xxx.blueprint中定义了一些变量,编辑器节点会同步自动生成UI,并可以进行参数的调整
- 在Runtime阶段,希望能够同样生成对应的UI,并进行参数的调整,特别是在一些配置项的调整上


实际上我们需要解决三个问题:
- 获取到所有需要生成UI的变量
- 根据变量类型生成对应的Item
- 将UI的变更,映射到对应的变量值上
【本文假设读者已经对反射系统有所了解,并只记录其中的关键代码】
第一个问题

我们把所有的需要反射生成UI的变量放在一个category中,或者命名时都加个前缀等等,那么遍历UObject的FProperty,即可获取到所有想要的变量的名字和类型【放在一个枚举值中】
USTRUCT(BlueprintType)
struct FGPParameterInfo
{
        GENERATED_BODY()
                UPROPERTY(EditAnywhere, BlueprintReadWrite)
                EVariableTypes type;
        UPROPERTY(EditAnywhere, BlueprintReadWrite)
                FString struct_member_name;

        FGPParameterInfo() {}
        FGPParameterInfo(EVariableTypes _type, FString _name) :type(_type), struct_member_name(_name) {}
};
TArray<FGPParameterInfo> UXXXXXXX::GetAllPropertiesInfoUnderCategory(UObject* inObject, const FString& category)
{
        UClass* inClass = inObject->GetClass();
        TArray<FGPParameterInfo> result;
        for (TFieldIterator<FProperty> It(inClass, EFieldIteratorFlags::ExcludeSuper); It; ++It)
        {
                if (FProperty* StructMember= *It)
                {
#if WITH_EDITORONLY_DATA

                        auto map = StructMember->GetMetaDataMap();
                        if (map && map->Contains("Category") && (*map)["Category"] == category)
#endif
                        {
                                FGPParameterInfo info;
                                info.struct_member_name = StructMember->GetName();
                                FString type = StructMember->GetCPPType();
                                if (type == FString("float"))
                                {
                                        info.type = EVariableTypes::Float;
                                }
                                else if (type == FString("int32"))
                                        info.type = EVariableTypes::Int;
                                else if (type == FString("FString"))
                                        info.type = EVariableTypes::String;
                                else if (type == FString("FLinearColor"))
                                        info.type = EVariableTypes::LinearColor;
                                else if (type == FString("bool"))
                                        info.type = EVariableTypes::Bool;
                                else if (type == FString("UTexture2D*"))
                                        info.type = EVariableTypes::Texture2D;
                                else if (type == FString("TArray"))
                                        info.type = EVariableTypes::TArray;
                                else
                                {
                                //这里没有写完,只是当作示例
                                        FStructProperty* StructProperty = Cast<FStructProperty>(StructMember);
                                        if (StructProperty)
                                        {
                                                info.type = EVariableTypes::CustomStruct;
                                        }
                                }
                                result.Add(info);
                        }
                }
        }
        return result;
}
需要注意的是TArray<> 、TMap<>、TSet<>、Struct在Property这套系统中分别对应了Array、Map、Set、Struct等类型,并没有真正拿到其中变量的类型,这个在第三个问题取值和赋值的时候,我们一块贴一下代码
第二个问题

当我们拿到某个变量的名字和类型时,就可以根据类型匹配设计师预先制作好的UI Item,比如Float型就create 一个float对应的widget,以此类推,至于category以及其他小图标,设计师自己定义了一堆type和map来标记,这个不再赘述。
第三个问题

如何取值和赋值,先看基本类型的取值和赋值,以String为例
基本类型

void UXXXXX::SetGPPropertyString(UObject* object, const FString& paramName, FString bValue)
{
        if (UClass* cl = object->GetClass())
        {
                if (FProperty* Property = cl->FindPropertyByName(*paramName))
                {
                        if (FStrProperty* StrProperty = Cast<FStrProperty>(Property))
                        {
                                StrProperty ->SetPropertyValue(Property->ContainerPtrToValuePtr<void*>(object), bValue);
                        }
                }
        }
}
看起来比较简单,但是有个工程化的问题需要考虑,因为这个值要公开给蓝图,好像每个值都要定义取值和赋值函数,要写很多重复的代码,当然这里如果所有的UMG使用slate来制作,所有流程控制在c++也不至于这么复杂,我们这边由于工作流上的一些问题,是使用了C++ + UMG这套流程,正好后面要做RuntimeBP, 不可避免的要解决UMG中取值和赋值的过程,我这里的解决方案是利用union封装了一个结构体,根据type来取值就好,同理可以封装Array
// This stuct is used to have a dynamic variable depending on the Enum above this
USTRUCT(BlueprintType)
struct FNodeVarArgs
{
        GENERATED_BODY()
                UPROPERTY(EditAnywhere, BlueprintReadWrite)
                EVariableTypes m_VariableType;
        NODE_VAR_ARG_UNION VariableData;
        // Strings are messed up with TUnions so this variable is here to store string data, this also allows to use this string as a meta for other values
        FString StringData = "";

        int GetSubtypeIndex() const
        {
                return VariableData.CurrentSubtypeIndex;
        }
        ....
}
复杂类型

Map、Array、Set的获取变量类型
通常其Property内部都会记录一个变量对应的Property,比如FMapProperty中的KeyProp和ValueProp,那么再根据基本类型来处理就好
class COREUOBJECT_API FMapProperty : public FMapProperty_Super
{
        DECLARE_FIELD(FMapProperty, FMapProperty_Super, CASTCLASS_FMapProperty)

        // Properties representing the key type and value type of the contained pairs
        FProperty*       KeyProp;
        FProperty*       ValueProp;
        FScriptMapLayout MapLayout;
        EMapPropertyFlags MapFlags;
        ...
        }Map、Array、Set的取值赋值
通常借助 FScriptXXXHelper 来操作,以Array为例
void Uxxxx::SetPropertyValuesOfArray(UObject* object, const FString& paramName, const FNodeVarArgsArray& value)
{
        UClass* inClass = object->GetClass();
        if (FProperty* Property = inClass->FindPropertyByName(*paramName))
        {
                if (UArrayProperty* ArrayProperty = Cast<UArrayProperty>(Property))
                {
                        FProperty* innerProp = ArrayProperty->Inner;
                        void* StructValue = ArrayProperty->ContainerPtrToValuePtr<void>(object);

                        if (innerProp->IsA(UStrProperty::StaticClass()))
                        {
                                TArray<FString>  ArrayOfStrings;

                                for (auto& arr : value.Array)
                                {
                                        ArrayOfStrings.Add(arr.GetStringArg());
                                }
                                FScriptArrayHelper ArrayHelper(ArrayProperty, StructValue);
                                ArrayHelper.Resize(ArrayOfStrings.Num());
                                UStrProperty* TEMP = Cast<UStrProperty>(innerProp);
                                for (int i = 0; i < ArrayOfStrings.Num(); i++)
                                {
                                        TEMP->SetPropertyValue(ArrayHelper.GetRawPtr(i), ArrayOfStrings);
                                }
                        }else if (innerProp->IsA(UFloatProperty::StaticClass()))
                        {
                        ...
                                }
        }
}
Struct获取变量类型

FStructProperty内部持有一个class UScriptStruct* Struct;遍历这个Struct中对应的Property即可,其实所有的复杂类型的取值、赋值、获取变量类型都应该是递归下去了,我们这里以Struct举例
//structValue: 该property对应的真正的值
//StructProperty: 对应的Property
// StructPtr:取值时,将取出的值放在这里;赋值时,就从此ptr中取值
//bIsSet: true表明取值,false表明赋值
// bNeedOutInfo: 是否需要输出变量类型
void IterateThroughStructProperty(void* structValue, UStructProperty* StructProperty, void* StructPtr,bool bIsSet/*true is set*/,bool bNeedOutInfo,TArray<FGPParameterInfo>& Info);
TArray<FGPParameterInfo> UDataVRuntimeBpLibrary::GetAllPropertiesOfStruct(UObject* structParent, const FString& structName)
{
        TArray<FGPParameterInfo> result;
        UClass* inClass = structParent->GetClass();
        if (FProperty* Property = inClass->FindPropertyByName(*structName))
        {
                FStructProperty* StructProperty = Cast<FStructProperty>(Property);
                void* StructValue = Property->ContainerPtrToValuePtr<void>(structParent);
                IterateThroughStructProperty(StructValue, StructProperty, nullptr, false, true, result);
        }
        return result;
}
其中的IterateThroughStructProperty函数实现如下:
void ParseProperty(void* object, FProperty* Property, void* ValuePtr,bool bIsSet , bool bNeedOutInfo, TArray<FGPParameterInfo>& Info);
//structValue: 该property对应的真正的值
//StructProperty: 对应的Property
// StructPtr:取值时,将取出的值放在这里;赋值时,就从此ptr中取值
//bIsSet: true表明取值,false表明赋值
// bNeedOutInfo: 是否需要输出变量类型
void IterateThroughStructProperty(void* structValue, UStructProperty* StructProperty, void* StructPtr,bool bIsSet/*true is set*/,bool bNeedOutInfo,TArray<FGPParameterInfo>& Info)
{
        // Walk the structs' properties
        UScriptStruct* Struct = StructProperty->Struct;
        for (TFieldIterator<FProperty> It(Struct); It; ++It)
        {
                FProperty* Property = *It;

                // This is the variable name if you need it
                FString VariableName = Property->GetName();

                // Never assume ArrayDim is always 1
                for (int32 ArrayIndex = 0; ArrayIndex < Property->ArrayDim; ArrayIndex++)
                {
                        // This grabs the pointer to where the property value is stored
                        void* ValuePtr = Property->ContainerPtrToValuePtr<void>(StructPtr, ArrayIndex);
                        // Parse this property
                        ParseProperty(structValue,Property, ValuePtr, bIsSet,bNeedOutInfo,Info);
                }
        }
}
ParseProperty 可以说是上述基本类型的囊括版,在这里详细判断InProperty 有可能的所有类型,这里把enum单独列出来了,其写法稍微有所不同,如果这个InProperty的类型是Map、Array、Set、Struct将进行递归,这里我仅列出Array和Struct
void ParseProperty(void* PropertyStructValue, FProperty* InProperty, void* InOutValuePtr, bool bIsSet, bool bNeedOutInfo, TArray<FGPParameterInfo>& Info)
{
        FString struct_member_name = InProperty->GetName();
        // Here's how to read integer and float properties
        if (UNumericProperty* NumericProperty = Cast<UNumericProperty>(InProperty))
        {
                if (NumericProperty->IsFloatingPoint())
                {
                        if (bNeedOutInfo)
                                Info.Add({ EVariableTypes::Float, struct_member_name });
                        else
                        {
                                if (bIsSet)
                                {
                                        FloatValue = NumericProperty->GetFloatingPointPropertyValue(InOutValuePtr);
                                        void* valuePtrOfStruct = NumericProperty->ContainerPtrToValuePtr<void>(PropertyStructValue);
                                        NumericProperty->SetFloatingPointPropertyValue(valuePtrOfStruct, FloatValue);
                                }
                                else
                                {
                                        void* valuePtrOfStruct = NumericProperty->ContainerPtrToValuePtr<void>(PropertyStructValue);
                                        FloatValue = NumericProperty->GetFloatingPointPropertyValue(valuePtrOfStruct);
                                        NumericProperty->SetFloatingPointPropertyValue(InOutValuePtr, FloatValue);
                                }
                        }
                }
                else if (NumericProperty->IsInteger())
                {
                        ...
                }
        }
        // How to read booleans
        if (UBoolProperty* BoolProperty = Cast<UBoolProperty>(InProperty))
        {
                ....
        }

        // Reading names
        if (UNameProperty* NameProperty = Cast<UNameProperty>(InProperty))
        {
                ,,,...
        }
        //Reading enums
        if (UEnumProperty* EnumProperty = Cast<UEnumProperty>(InProperty))
        {
                if (bNeedOutInfo)
                        Info.Add({ EVariableTypes::Enum, struct_member_name }); else
                {
                        if (bIsSet)
                        {
                                int64 EnumValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(InOutValuePtr);;
                                void* valuePtrOfStruct = EnumProperty->ContainerPtrToValuePtr<void>(PropertyStructValue);
                                EnumProperty->GetUnderlyingProperty()->SetIntPropertyValue(valuePtrOfStruct, EnumValue);
                        }
                        else
                        {
                                void* valuePtrOfStruct = EnumProperty->ContainerPtrToValuePtr<void>(PropertyStructValue);
                                int64 EnumValue = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(valuePtrOfStruct);
                                EnumProperty->GetUnderlyingProperty()->SetIntPropertyValue(InOutValuePtr, EnumValue);
                        }
                }
        }
        // Reading strings
        if (UStrProperty* StringProperty = Cast<UStrProperty>(InProperty))
        {
                ...
        }

        // Reading texts
        if (UTextProperty* TextProperty = Cast<UTextProperty>(InProperty))
        {
                ...
        }

        // Reading an array
        if (UArrayProperty* ArrayProperty = Cast<UArrayProperty>(InProperty))
        {
                // We need the helper to get to the items of the array            
                FScriptArrayHelper Helper(ArrayProperty, InOutValuePtr);
                for (int32 i = 0, n = Helper.Num(); i < n; ++i)
                {
                        ParseProperty(PropertyStructValue,ArrayProperty->Inner, Helper.GetRawPtr(i),bIsSet, bNeedOutInfo, Info);
                }
        }

        // Reading a nested struct
        if (UStructProperty* StructProperty = Cast<UStructProperty>(InProperty))
        {
                void* StructValue1 = StructProperty->ContainerPtrToValuePtr<void>(PropertyStructValue);
                IterateThroughStructProperty(StructValue1,StructProperty, InOutValuePtr, bIsSet,bNeedOutInfo,Info);
        }
}
到这里基本上就结束了,这么看下来,这块获取类型、取值和赋值对于简单类型和复杂类型并没有做特别好的封装,应该是可以写一个大一统的函数来完成所有类型信息的获取和取值赋值,这里我们仅简单记录思路和demo 代码,待后续整理。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-1-15 12:41 | 显示全部楼层
提个问题,在第一部分获取到需要生成UI的变量时利用的是Category,通过检索元数据得到。但是要在Runtime生成UI,而元数据是Editor only的,这是冲突的吗?还是说获取到这些变量是在Runtime之前,获取到之后将它们保存下来,而动态的改变变量的值并实时反映给屏幕才是在Runtime时做的?
发表于 2022-1-15 12:43 | 显示全部楼层
这个得话,有几种解决方案:1. 根据变量的前缀来获取到需要生成ui的变量。2. 另外添加一个map,把需要生成ui的变量名放到map中,检索这个map即可拿到变量名
发表于 2022-1-15 12:48 | 显示全部楼层
emmmm所以原来楼主当时实现时并没有采用你文章中讲述的这个方案是吗
emmm
发表于 2022-1-15 12:49 | 显示全部楼层
采用的是map 的方案,文章中的代码是在编辑器中实现的
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 21:37 , Processed in 0.092433 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表