--- title: Ue4 c++ UProperty反射 PostEditChangeProperty date: 2022-12-09 13:40:28 excerpt: tags: UObject rating: ⭐⭐⭐ --- ## 反射系统 https://ikrima.dev/ue4guide/engine-programming/uobject-reflection/uobject-reflection/ ## Property类型判断 - UStructProperty:结构体 - UMapProperty:TMap - UArrayProperty:TArray 属性判断是否是数组或是Map ```c++ 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) { } } } } ``` ## 属性变化回调函数 ```c# 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 大概率是这个 ```c++ template<> struct TStructOpsTypeTraits<FBox2D> : public TStructOpsTypeTraitsBase2<FBox2D> { enum { WithIdenticalViaEquality = true, WithNoInitConstructor = true, WithZeroConstructor = true, }; }; IMPLEMENT_STRUCT(Box2D); ``` ## BlueprintCompilerCppBackendValueHelper.cpp ```c++ 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,我们先来看上篇里生成的代码: ```c++ 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++结构的信息不能通过模板探测出来的,就需要我们手动标记提供了,所以具体的代码是: ```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,方便的方法是: ```c++ 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会生成UEnum,struct会生成UScriptStruct,所以把参数换成UEnum::StaticClass()就可以获得所有的UEnum*对象了,UScriptStruct::StaticClass()就是所有的UScriptStruct*了,最后就可以根据这些类型对象来反射获取类型信息了。 而如果要精确的根据一个名字来查找某个类型对象,就可以用UE4里另一个方法: ```c++ 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>`,可以通过它筛选遍历字段。 ```c++ 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; ``` #### 查看继承 ```c++ //得到类型对象后,也可以遍历查看它的继承关系。 遍历继承链条: 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); } } ``` #### 获取设置属性值 ```c++ 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); ``` #### 反射调用函数 ```c++ //方法原型 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, ¶ms); return params.ReturnValue; } //使用 int r=InvokeFunction(obj,"Func",123.f); ``` ProcessEvent也是UE4里事先定义好的非常方便的函数,内部会自动的处理蓝图VM的问题。当然,更底层的方法也可以是: ```c++ //调用1 obj->ProcessEvent(func, ¶ms); //调用2 FFrame frame(nullptr, func, ¶ms, nullptr, func->Children); obj->CallFunction(frame, ¶ms + func->ReturnValueOffset, func); //调用3 FFrame frame(nullptr, func, ¶ms, nullptr, func->Children); func->Invoke(obj, frame, ¶ms + func->ReturnValueOffset); ``` #### 运行时修改类型 让我们继续扩宽一下思路,之前已经详细讲解过了各大类型对象的构造过程,最后常常都是到UE4CodeGen_Private里的调用。既然我们已经知道了它运行的逻辑,那我们也可以仿照着来啊!我们也可以在常规的类型系统注册流程执行完之后,在游戏运行的半途过程中,动态的去修改类型甚至注册类型,因为说到底UE4编辑器也就是一个特殊点的游戏而已啊!这种方式有点类似C#的emit的方式,用代码去生成代码然后再编译。这些方式理论上都是可以通的,我来提供一些思路用法,有兴趣的朋友可以自己去实现下,代码贴出来就太长了。 1. 修改UField的MetaData信息,其实可以改变字段在编辑器中的显示信息。MetaData里有哪些字段,可以在ObjectMacros.h中自己查看。 2. 动态修改UField的相应的各种Flags数据,比如PropertyFlags,StructFlags,ClassFlags等,可以达成在编辑器里动态改变其显示行为的效果。 3. 动态添加删除UEnum对象里面的Names字段,就可以动态给enum添加删除枚举项了。 4. 动态地给结构或类添加反射属性字段,就可以在蓝图内创建具有不定字段的结构了。当然前提是在结构里预留好属性存放的内存,这样UProperty的Offset才有值可指向。这么做现在想来好像也不知道能用来干嘛。 5. 同属性一样,其实参照对了流程,也可以动态的给蓝图里暴露函数。有时候这可以达成某种加密保护的奇效。 6. 可以动态的注册新结构,动态的构造出来相应的UScriptStruct其实就可以了。 7. 动态注册新类其实也是可以的,只不过UClass的构造稍微要麻烦点,不过也没麻烦到哪去,有需求了就自然能照着源码里的流程自己实现一个流程出来。 8. 再甚至,其实某种程度上的用代码动态创建蓝图节点,填充蓝图VM指令其实也是可行的。只不过想了想好像一般用不着上这种大手术。