BlueRoseNote/03-UnrealEngine/Gameplay/UObject/Ue4 c++ UProperty反射 PostEditChangeProperty.md
2023-06-29 11:55:02 +08:00

20 KiB
Raw Blame History

title, date, excerpt, tags, rating
title date excerpt tags rating
Ue4 c++ UProperty反射 PostEditChangeProperty 2022-12-09 13:40:28 UObject

反射系统

https://ikrima.dev/ue4guide/engine-programming/uobject-reflection/uobject-reflection/

Property类型判断

  • UStructProperty结构体
  • UMapPropertyTMap
  • UArrayPropertyTArray 属性判断是否是数组或是Map
FProperty* PropertyThatChanged = PropertyChangedEvent.Property;
if ( PropertyThatChanged != nullptr )
{
    if (PropertyThatChanged->IsA(FArrayProperty::StaticClass()))
    {
        FArrayProperty* ArrayProp = CastField<FArrayProperty>(PropertyThatChanged);
        FProperty* InnerProp = ArrayProp->Inner;
        if (InnerProp->IsA(FStructProperty::StaticClass()))
        {
            const FToonRampData* ToonRampData = ArrayProp->ContainerPtrToValuePtr<FToonRampData>(InnerProp, 0);
            if(ToonRampData)
            {
            }
        }
    }else if(PropertyThatChanged->IsA(FMapProperty::StaticClass()))
    {
        FArrayProperty* ArrayProp = CastField<FArrayProperty>(PropertyThatChanged);
        FProperty* InnerProp = ArrayProp->Inner;
        if (InnerProp->IsA(FStructProperty::StaticClass()))
        {
            const FToonRampData* ToonRampData = ArrayProp->ContainerPtrToValuePtr<FToonRampData>(InnerProp, 0);
            if(ToonRampData)
            {
            }
        }
    }
    }

属性变化回调函数

virtual void EditorApplyTranslation(const FVector& DeltaTranslation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
virtual void EditorApplyRotation(const FRotator& DeltaRotation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
virtual void EditorApplyScale(const FVector& DeltaScale, const FVector* PivotLocation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
virtual void PostEditMove(bool bFinished) override;
virtual void PostEditComponentMove(bool bFinished) override;

没有Struct()却可以在蓝图找到类型问题笔记

Traits

大概率是这个

template<>
struct TStructOpsTypeTraits<FBox2D> : public TStructOpsTypeTraitsBase2<FBox2D>
{
    enum
    {
        WithIdenticalViaEquality = true,
        WithNoInitConstructor = true,
        WithZeroConstructor = true,
    };
};
IMPLEMENT_STRUCT(Box2D);

BlueprintCompilerCppBackendValueHelper.cpp

bool FEmitDefaultValueHelper::SpecialStructureConstructor(const UStruct* Struct, const uint8* ValuePtr, /*out*/ FString* OutResult)
{
    ...
    if (TBaseStructure<FBox2D>::Get() == Struct)
    {
        if (OutResult)
        {
            const FBox2D* Box2D = reinterpret_cast<const FBox2D*>(ValuePtr);
            *OutResult = FString::Printf(TEXT("CreateFBox2D(FVector2D(%s, %s), FVector2D(%s, %s), %s)")
                , *FEmitHelper::FloatToString(Box2D->Min.X)
                , *FEmitHelper::FloatToString(Box2D->Min.Y)
                , *FEmitHelper::FloatToString(Box2D->Max.X)
                , *FEmitHelper::FloatToString(Box2D->Max.Y)
                , Box2D->bIsValid ? TEXT("true") : TEXT("false"));
        }
        return true;
    }
    ...
}
struct FStructAccessHelper_StaticData
{
    TMap<const UScriptStruct*, FString> BaseStructureAccessorsMap;
    TMap<const UScriptStruct*, bool> SupportsDirectNativeAccessMap;
    TArray<FSoftClassPath> NoExportTypesWithDirectNativeFieldAccess;
    static FStructAccessHelper_StaticData& Get()
    {
        static FStructAccessHelper_StaticData StaticInstance;
        return StaticInstance;
    }
private:
    FStructAccessHelper_StaticData()
    {
        // These are declared in Class.h; it's more efficient to access these native struct types at runtime using the specialized template functions, so we list them here.
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FRotator>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FTransform>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FLinearColor>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FColor>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FVector>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FVector2D>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FRandomStream>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FGuid>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FTransform>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FBox2D>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFallbackStruct>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatRangeBound>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatRange>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32RangeBound>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32Range>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFloatInterval>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FInt32Interval>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFrameNumber>::Get());
        MAP_BASE_STRUCTURE_ACCESS(TBaseStructure<FFrameTime>::Get());
        {
            // Cache the known set of noexport types that are known to be compatible with emitting native code to access fields directly.
            TArray<FString> Paths;
            GConfig->GetArray(TEXT("BlueprintNativizationSettings"), TEXT("NoExportTypesWithDirectNativeFieldAccess"), Paths, GEditorIni);
            for (FString& Path : Paths)
            {
                NoExportTypesWithDirectNativeFieldAccess.Add(FSoftClassPath(Path));
            }
        }
    }
}

大钊的文章中有相似的代码

https://zhuanlan.zhihu.com/p/26019216

Struct的收集 对于Struct我们先来看上篇里生成的代码

static FCompiledInDeferStruct Z_CompiledInDeferStruct_UScriptStruct_FMyStruct(FMyStruct::StaticStruct, TEXT("/Script/Hello"), TEXT("MyStruct"), false, nullptr, nullptr);  //延迟注册
static struct FScriptStruct_Hello_StaticRegisterNativesFMyStruct
{
    FScriptStruct_Hello_StaticRegisterNativesFMyStruct()
    {
        UScriptStruct::DeferCppStructOps(FName(TEXT("MyStruct")),new UScriptStruct::TCppStructOps<FMyStruct>);
    }
} ScriptStruct_Hello_StaticRegisterNativesFMyStruct;  

https://zhuanlan.zhihu.com/p/59553490

ICppStructOps的作用 很多朋友在看源码的时候可能会对UScriptStruct里定义的ICppStructOps类以及模板子类TCppStructOps<CPPSTRUCT>感到疑惑。其实它们是C++的一种常见的架构模式,用一个虚函数基类定义一些公共操作,再用一个具体模板子类来实现,从而既可以保存类型,又可以有公共操作接口。 针对于UE4这里来说ICppStructOps就定义了这个结构的一些公共操作。而探测这个C++结构的一些特性就交给了TCppStructOps<CPPSTRUCT>类里的TStructOpsTypeTraits<CPPSTRUCT>。一些C++结构的信息不能通过模板探测出来的,就需要我们手动标记提供了,所以具体的代码是:

template <class CPPSTRUCT>
struct TStructOpsTypeTraitsBase2
{
    enum
    {
        WithZeroConstructor = false, // 0构造内存清零后就可以了说明这个结构的默认值就是0
        WithNoInitConstructor = false, // 有个ForceInit的参数的构造用来专门构造出0值结构来
        WithNoDestructor = false, // 是否没有结构有自定义的析构函数, 如果没有析构的话DestroyStruct里面就可以省略调用析构函数了。默认是有的。结构如果是pod类型则肯定没有析构。
        WithCopy = !TIsPODType<CPPSTRUCT>::Value, // 是否结构有自定义的=赋值函数。如果没有的话在CopyScriptStruct的时候就只需要拷贝内存就可以了
        WithIdenticalViaEquality = false, // 用==来比较结构
        WithIdentical = false, // 有一个自定义的Identical函数来专门用来比较和WithIdenticalViaEquality互斥
        WithExportTextItem = false, // 有一个ExportTextItem函数来把结构值导出为字符串
        WithImportTextItem = false, // 有一个ImportTextItem函数把字符串导进结构值
        WithAddStructReferencedObjects = false, // 有一个AddStructReferencedObjects函数用来添加结构额外的引用对象
        WithSerializer = false, // 有一个Serialize函数用来序列化
        WithStructuredSerializer = false, // 有一个结构结构Serialize函数用来序列化
        WithPostSerialize = false, // 有一个PostSerialize回调用来在序列化后调用
        WithNetSerializer = false, // 有一个NetSerialize函数用来在网络复制中序列化
        WithNetDeltaSerializer = false, // 有一个NetDeltaSerialize函数用来在之前NetSerialize的基础上只序列化出差异来一般用在TArray属性上进行优化
        WithSerializeFromMismatchedTag = false, // 有一个SerializeFromMismatchedTag函数用来处理属性tag未匹配到的属性值一般是在结构进行升级后但值还是原来的值这个时候用来把旧值升级到新结构时使用
        WithStructuredSerializeFromMismatchedTag = false, // SerializeFromMismatchedTag的结构版本
        WithPostScriptConstruct = false,// 有一个PostScriptConstruct函数用在蓝图构造脚本后调用
        WithNetSharedSerialization = false, // 指明结构的NetSerialize函数不需要用到UPackageMap
    };
};
template<class CPPSTRUCT>
struct TStructOpsTypeTraits : public TStructOpsTypeTraitsBase2<CPPSTRUCT>
{
};

举个小例子假如你看到编辑器里某个属性想在C++里去修改它的值结果发现它不是public的甚至有可能连头文件都是private的这个时候如果对类型系统结构理解不深的人可能就放弃了但懂的人就知道可以通过这个对象遍历UProperty来查找到这个属性从而修改它。 还有一个例子是如果你做了一个插件调用了引擎编辑器本身的Details面板属性但又想隐藏其中的一些字段这个时候如果不修改引擎往往是难以办到的但是如果知道了属性面板里的属性其实也都是一个个UProperty来的这样你就可以通过对象路径获得这个属性然后开启关闭它的某些Flags来达成效果。这也算是一种常规的Hack方式。

《InsideUE4》UObject十三类型系统-反射实战

https://zhuanlan.zhihu.com/p/61042237

获取类型对象

如果想获取到程序里定义的所有的class方便的方法是

TArray<UObject*> result;
GetObjectsOfClass(UClass::StaticClass(), result);   //获取所有的class和interface
GetObjectsOfClass(UEnum::StaticClass(), result);   //获取所有的enum
GetObjectsOfClass(UScriptStruct::StaticClass(), result);   //获取所有的struct

GetObjectsOfClass是UE4已经写好的一个很方便的方法可以获取到属于某个UClass下面的所有对象。因此如果用UClass::StaticClass()本身就可以获得程序里定义的所有class。值得注意的是UE4里的接口是有一个配套的UInterface对象来存储元数据信息它的类型也是用UClass表示的所以也会获得interface。根据前文enum会生成UEnumstruct会生成UScriptStruct所以把参数换成UEnum::StaticClass()就可以获得所有的UEnum对象了UScriptStruct::StaticClass()就是所有的UScriptStruct了,最后就可以根据这些类型对象来反射获取类型信息了。 而如果要精确的根据一个名字来查找某个类型对象就可以用UE4里另一个方法

template< class T >
inline T* FindObject( UObject* Outer, const TCHAR* Name, bool ExactClass=false )
{
    return (T*)StaticFindObject( T::StaticClass(), Outer, Name, ExactClass );
}
UClass* classObj=FindObject<UClass>(ANY_PACKAGE,"MyClass");   //获得表示MyClass的UClass*

遍历字段

在获取到了一个类型对象后就可以用各种方式去遍历查找内部的字段了。为此UE4提供了一个方便的迭代器TFieldIterator<T>,可以通过它筛选遍历字段。

const UStruct* structClass; //任何复合类型都可以
//遍历属性
for (TFieldIterator<UProperty> i(structClass); i; ++i)
{
    UProperty* prop=*i;
}
//遍历函数
for (TFieldIterator<UFunction> i(structClass); i; ++i)
{
    UFunction* func=*i;
    //遍历函数的参数
    for (TFieldIterator<UProperty> i(func); i; ++i)
    {
        UProperty* param=*i;
        if( param->PropertyFlags & CPF_ReturnParm ) //这是返回值
        {
        }
    }
}
//遍历接口
const UClass* classObj; //只有UClass才有接口
for (const FImplementedInterface& ii : classObj->Interfaces)
{
    UClass* interfaceClass = ii.Class;
}
//遍历枚举
const UEnum* enumClass;
for (int i = 0; i < enumClass->NumEnums(); ++i)
{
    FName name = enumClass->GetNameByIndex(i);
    int value = enumClass->GetValueByIndex(i);
}
//遍历元数据
#if WITH_METADATA
const UObject* obj;//可以是任何对象但一般是UField才有值
UMetaData* metaData = obj->GetOutermost()->GetMetaData();
TMap<FName, FString>* keyValues = metaData->GetMapForObject(obj);
if (keyValues != nullptr&&keyValues->Num() > 0)
{
    for (const auto& i : *keyValues)
    {
        FName key=i.Key;
        FString value=i.Value;
    }
}
#endif
//查找属性
UProperty* UStruct::FindPropertyByName(FName InName) const
{
    for (UProperty* Property = PropertyLink; Property != NULL; Property = Property->PropertyLinkNext)
    {
        if (Property->GetFName() == InName)
        {
            return Property;
        }
    }
    return NULL;
}
//查找函数
UFunction* UClass::FindFunctionByName(FName InName, EIncludeSuperFlag::Type IncludeSuper) const;

查看继承

//得到类型对象后,也可以遍历查看它的继承关系。 遍历继承链条:
const UStruct* structClass; //结构和类
TArray<FString> classNames;
classNames.Add(structClass->GetName());
UStruct* superClass = structClass->GetSuperStruct();
while (superClass)
{
    classNames.Add(superClass->GetName());
    superClass = superClass->GetSuperStruct();
}
FString str= FString::Join(classNames, TEXT("->")); //会输出MyClass->UObject
//那反过来,如果想获得一个类下面的所有子类,可以这样:
const UClass* classObj; //结构和类
TArray<UClass*> result;
GetDerivedClasses(classObj, result, false);
//函数原型是
void GetDerivedClasses(UClass* ClassToLookFor, TArray<UClass *>& Results, bool bRecursive);
//那么怎么获取实现了某个接口的所有子类呢?
TArray<UObject*> result;
GetObjectsOfClass(UClass::StaticClass(), result);
TArray<UClass*> classes;
for (UObject* obj : result)
{
    UClass* classObj = Cast<UClass>(obj);
    if (classObj->ImplementsInterface(interfaceClass))//判断实现了某个接口
    {
        classes.Add(classObj);
    }
}

获取设置属性值

template<typename ValueType>
ValueType* UProperty::ContainerPtrToValuePtr(void* ContainerPtr, int32 ArrayIndex = 0) const
{
    return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
}
template<typename ValueType>
ValueType* UProperty::ContainerPtrToValuePtr(UObject* ContainerPtr, int32 ArrayIndex = 0) const
{
    return (ValueType*)ContainerVoidPtrToValuePtrInternal(ContainerPtr, ArrayIndex);
}
void* UProperty::ContainerVoidPtrToValuePtrInternal(void* ContainerPtr, int32 ArrayIndex) const
{
    //check...
    return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}
void* UProperty::ContainerUObjectPtrToValuePtrInternal(UObject* ContainerPtr, int32 ArrayIndex) const
{
    //check...
    return (uint8*)ContainerPtr + Offset_Internal + ElementSize * ArrayIndex;
}
//获取对象或结构里的属性值地址,需要自己转换成具体类型
void* propertyValuePtr = property->ContainerPtrToValuePtr<void*>(object);
//包含对象引用的属性可以获得对象
UObject* subObject = objectProperty->GetObjectPropertyValue_InContainer(object);
//也因为获取到的是存放属性值的指针地址,所以其实也就可以*propertyValuePtr=xxx;方便的设置值了。当然如果是从字符串导入设置进去UE4也提供了两个方法来导出导入
//导出值
virtual void ExportTextItem( FString& ValueStr, const void* PropertyValue, const void* DefaultValue, UObject* Parent, int32 PortFlags, UObject* ExportRootScope = NULL ) const;
//使用
FString outPropertyValueString;
property->ExportTextItem(outPropertyValueString, property->ContainerPtrToValuePtr<void*>(object), nullptr, (UObject*)object, PPF_None);
//导入值
const TCHAR* UProperty::ImportText( const TCHAR* Buffer, void* Data, int32 PortFlags, UObject* OwnerObject, FOutputDevice* ErrorText = (FOutputDevice*)GWarn ) const;
//使用
FString valueStr;
prop->ImportText(*valueStr, prop->ContainerPtrToValuePtr<void*>(obj), PPF_None, obj);

反射调用函数

//方法原型
int32 UMyClass::Func(float param1);
UFUNCTION(BlueprintCallable)
int32 InvokeFunction(UObject* obj, FName functionName,float param1)
{
    struct MyClass_Func_Parms   //定义一个结构用来包装参数和返回值就像在gen.cpp里那样
    {
        float param1;
        int32 ReturnValue;
    };
    UFunction* func = obj->FindFunctionChecked(functionName);
    MyClass_Func_Parms params;
    params.param1=param1;
    obj->ProcessEvent(func, &params);
    return params.ReturnValue;
}
//使用
int r=InvokeFunction(obj,"Func",123.f);

ProcessEvent也是UE4里事先定义好的非常方便的函数内部会自动的处理蓝图VM的问题。当然更底层的方法也可以是

//调用1
obj->ProcessEvent(func, &params);
//调用2
FFrame frame(nullptr, func, &params, nullptr, func->Children);
obj->CallFunction(frame, &params + func->ReturnValueOffset, func);
//调用3
FFrame frame(nullptr, func, &params, nullptr, func->Children);
func->Invoke(obj, frame, &params + func->ReturnValueOffset);

运行时修改类型

让我们继续扩宽一下思路之前已经详细讲解过了各大类型对象的构造过程最后常常都是到UE4CodeGen_Private里的调用。既然我们已经知道了它运行的逻辑那我们也可以仿照着来啊我们也可以在常规的类型系统注册流程执行完之后在游戏运行的半途过程中动态的去修改类型甚至注册类型因为说到底UE4编辑器也就是一个特殊点的游戏而已啊这种方式有点类似C#的emit的方式用代码去生成代码然后再编译。这些方式理论上都是可以通的我来提供一些思路用法有兴趣的朋友可以自己去实现下代码贴出来就太长了。

  1. 修改UField的MetaData信息其实可以改变字段在编辑器中的显示信息。MetaData里有哪些字段可以在ObjectMacros.h中自己查看。
  2. 动态修改UField的相应的各种Flags数据比如PropertyFlagsStructFlagsClassFlags等可以达成在编辑器里动态改变其显示行为的效果。
  3. 动态添加删除UEnum对象里面的Names字段就可以动态给enum添加删除枚举项了。
  4. 动态地给结构或类添加反射属性字段就可以在蓝图内创建具有不定字段的结构了。当然前提是在结构里预留好属性存放的内存这样UProperty的Offset才有值可指向。这么做现在想来好像也不知道能用来干嘛。
  5. 同属性一样,其实参照对了流程,也可以动态的给蓝图里暴露函数。有时候这可以达成某种加密保护的奇效。
  6. 可以动态的注册新结构动态的构造出来相应的UScriptStruct其实就可以了。
  7. 动态注册新类其实也是可以的只不过UClass的构造稍微要麻烦点不过也没麻烦到哪去有需求了就自然能照着源码里的流程自己实现一个流程出来。
  8. 再甚至其实某种程度上的用代码动态创建蓝图节点填充蓝图VM指令其实也是可行的。只不过想了想好像一般用不着上这种大手术。