UE4/Unity3d 根据元数据自动生成与更新UI
大家可能发现一些大佬讲UE4,首先都会讲类型系统,知道UE4会根据宏标记生成一些特定的内容,UE4几乎所有高级功能都离不开这些内容,一般来说,我们不会直接去使用它。
今天这个Demo内容希望能加深大家的理解,为什么有这个东东了,主要是由于我们产品需要很多根据环境调整的参数,我们需要提供很多UI,一个结构包含五六个参数,对应的参数与UI绑定事件,东东很简单,但是太多了,一多就容易出事,参数结构又在调整,然后就乱了。
参考UE4本身编辑器根据对应结构自动生成UI,UI的更改能更新到对象上,以及JSON的序列化,UI与对象属性绑定更新应该是完全没有问题的。主要设计想了下,首先是UI的设计用蓝图来做,其余所有逻辑相关全用C++方便,UI自动更新对象,对象变动后,也可在代码中去更新UI,使用要简单,类型自动转换。
整个逻辑最主要的一点,简单说就是我们要找到如何根据字符串去更新结构体里的字段,如同C#里的反射FieldInfo::SetValue ,FieldInfo::GetValue 等方法,我们知道C#根据元数据做到的,而UE4的类或是结构打上宏标记,UHT会根据类上的宏生成元数据在对应 .generated.h 文件里,这里不多讲,如果大家有兴趣,可以移步到 大钊《InsideUE4》UObject(三)类型系统设定和结构 这章的前后几章,在这只是指出这里的元数据可以找到每个字段的名称与在对应对象位置上的偏移,在这,我们只需要用这二点就够了。
先看一下结果:
上面是一个假定结构,下面是根据我们需求生成的UI模式,UI的设计在蓝图可以设计成自己的样式,在这就用默认的。
因为UE4里不支持我们去自定义元数据,所以我们需要自己去定义我们要生成的UI描述数据,这个数据和元数据共同对应对象里的字段名,如下是BaseAttribute以及所有UI对应的扩展描述。
#pragma once #include "CoreMinimal.h" #include <functional> #include "UObject/EnumProperty.h" struct BaseAttribute { protected: int index = -1; public: FString MemberName; FString DisplayName; public: virtual int GetIndex() { return index; }; virtual ~BaseAttribute() {}; }; struct ToggleAttribute : public BaseAttribute { public: ToggleAttribute() { index = 0; } public: bool DefaultValue = false; }; struct InputeAttribute : public BaseAttribute { public: InputeAttribute() { index = 1; } public: FString DefaultValue = ""; }; struct SliderAttribute : public BaseAttribute { public: SliderAttribute() { index = 2; } public: float DefaultValue = 0.0f; float range = 1.0f; float offset = 0.0f; bool bAutoRange = false; bool bInt = false; }; struct DropdownAttribute : public BaseAttribute { public: DropdownAttribute() { index = 3; } public: int DefaultValue = 0; FString Parent; bool bAutoAddDefault = false; std::function<TArray<FString>(int)> onFillParentFunc; TArray<FString> options; public: void InitOptions(bool bAuto, TArray<FString> optionArray) { bAutoAddDefault = bAuto; options = optionArray; }; void InitOptions(bool bAuto, FString parent, std::function<TArray<FString>(int)> onFunc) { bAutoAddDefault = bAuto; Parent = parent; onFillParentFunc = onFunc; } };
简单说下,BaseAttribute定义一个MemberName,这个数据与元数据,对象字段联系在一起,这个的定义要和对象字段一样,这样才能去找到对应元数据并互相更新,DisplayName表示描述信息,如上用来显示提示的text,而SliderAttribute用来生成一个Slider与编辑窗口联动的UI,一般有个范围,是否是整数。而DropdownAttribute用来生成下拉列表框,并支持联动。
对应上面的UI描述,然后就是对应的UI模板,只是因为不同类型,但是处理过程有很多相同之处,在这我们用C++Template,UE4的UObject不支持Template,我们使用模板实例化。
template<typename T> class MRCOREUE4_API ComponentTemplate { public: typedef T Type; //DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnValueChange, ComponentTemplate<T>*, compent, T, t); //TBaseDelegate<void, ComponentTemplate<T>*, T> onValueChange; //FOnValueChange onValueChange; public: ComponentTemplate() {} virtual ~ComponentTemplate() {} public: //UI显示名称 FString DisplayName; //UI更新后回调 std::function<void(ComponentTemplate<T>*, T)> onValueChange; //UI对应描述,初始化UI时使用 BaseAttribute* attribute; //UI对应的元数据 UProperty* uproperty = nullptr; public: //UI更新后统一调用事件onValueChange virtual void OnValueChange(T value) {}; //数据更新后去调用UI刷新 virtual void Update(T value) {}; }; UCLASS() class MRCOREUE4_API UBaseAutoWidget : public UUserWidget { GENERATED_BODY() public: UBaseAutoWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {}; public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = AutoWidget) UTextBlock * text; int index = -1; FString memberName; public: //用于蓝图子类赋值UI给C++中 UFUNCTION(BlueprintNativeEvent) void InitWidget(); virtual void InitWidget_Implementation() {}; //初始化UI事件与默认值 virtual void InitEvent() { }; };
ComponentTemplate负责处理和具体类型有关的数据,UBaseAutoWidget处理C++与蓝图的交互,还有UI的初始化。我们看下USliderInputWidget具体实现。
UCLASS() class MRCOREUE4_API USliderInputWidget : public UBaseAutoWidget, public ComponentTemplate<float> { GENERATED_BODY() public: USliderInputWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) {}; public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = AutoWidget) USlider * slider; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = AutoWidget) UEditableTextBox * textBlock; private: //大小 float size = 1.0f; //起始值 float offset = 0.0f; //textbox是否正在编辑 bool bUpdate = false; //是否自动选择范围 bool bAutoRange = false; //是否整形 bool bInt = false; private: UFUNCTION(BlueprintCallable) void onTextChange(const FText& rtext); UFUNCTION(BlueprintCallable) void onSliderChange(float value); float getSliderValue(); void setSliderValue(float value); protected: virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; public: UFUNCTION(BlueprintCallable) virtual void OnValueChange(float value) override; virtual void Update(float value) override; public: virtual void InitEvent() override; }; void USliderInputWidget::onTextChange(const FText& text) { if (bUpdate || !text.IsNumeric()) return; float slider = getSliderValue(); float value = FCString::Atof(*(text.ToString())); if (slider != value) setSliderValue(value); } void USliderInputWidget::onSliderChange(float value) { if (onValueChange) onValueChange(this, getSliderValue()); } float USliderInputWidget::getSliderValue() { float fv = slider->GetValue(); float value = fv * size + offset; if (bInt) value = (int)value; return value; } void USliderInputWidget::setSliderValue(float value) { if (bAutoRange) { offset = value - size * 0.5f; slider->SetValue(0.5f); } else { slider->SetValue(FMath::Clamp((value - offset) / size, 0.0f, 1.f)); } } void USliderInputWidget::NativeTick(const FGeometry & MyGeometry, float InDeltaTime) { auto play = GetOwningLocalPlayer(); if (!play) return; auto cont = play->GetPlayerController(GEngine->GetWorld()); if (!cont) return; bool bFocus = textBlock->HasUserFocusedDescendants(cont); if (bFocus) return; bUpdate = true; float svalue = getSliderValue(); if (bInt) svalue = (int)svalue; FText text = FText::AsNumber(svalue); textBlock->SetText(text); bUpdate = false; } void USliderInputWidget::OnValueChange(float value) { if (onValueChange) onValueChange(this, value); } void USliderInputWidget::Update(float value) { setSliderValue(value); } void USliderInputWidget::InitEvent() { textBlock->OnTextChanged.AddDynamic(this, &USliderInputWidget::onTextChange); slider->OnValueChanged.AddDynamic(this, &USliderInputWidget::onSliderChange); SliderAttribute* aslider = (SliderAttribute*)(attribute); size = aslider->range; offset = aslider->offset; bInt = aslider->bInt; bAutoRange = aslider->bAutoRange; //text->SetText(FText::FromString(aslider->DisplayName)); textBlock->SetText(FText::AsNumber(aslider->DefaultValue)); slider->SetValue(aslider->DefaultValue); if (bInt) slider->SetStepSize(1.0f / size); }
其余几种类型的Widget实现更加简单,这里就不具体列出来了。
对应如上C++的模板,我们需要具体的蓝图设计出漂亮的界面,主要是如下三步:
首先设计我们的UI显示样式,然后在类设置里父类选择我们设计的类,最后重载前面C++里的UBaseAutoWidget里的InitWidget,把对应的UI指针给我们设计里类里。
最后,就是如何绑定上面的UI与对象,使之UI与对象能互相更新。
#pragma once #include "CoreMinimal.h" #include "CoreMinimal.h" #include "BaseAttribute.h" #include "UMG.h" #include "BaseAutoWidget.h" using namespace std; using namespace std::placeholders; template<class U> class ObjAttribute { private: //绑定的对象 U * obj = nullptr; //绑定对象的元结构 UStruct* structDefinition = nullptr; //生成UI的box UVerticalBox* panel = nullptr; //绑定对象元数据描述 TArray<BaseAttribute*> attributeList; //根据元数据描述生成的UI TArray<UBaseAutoWidget*> widgetList; //是否绑定 bool bBind = false; //当绑定对象改变后引发的回调 std::function<void(ObjAttribute<U>*, FString name)> onObjChangeHandle = nullptr; public: //绑定一个对象到UVerticalBox上,根据arrtList与对象的元数据以及对应UI模版生成UI在box上,对应UI的变动会自动更新到对象内存数据中 void Bind(U* pobj, UVerticalBox* box, TArray<BaseAttribute*> arrtList, TArray<UClass*> templateWidgetList, UWorld* world) { if (bBind) return; obj = pobj; structDefinition = U::StaticStruct(); attributeList = arrtList; panel = box; for (auto attribute : attributeList) { int index = attribute->GetIndex(); //TSubclassOf<UToggleAutoWidget> togglewidget = LoadClass<UToggleAutoWidget>(nullptr, TEXT("WidgetBlueprint'/Game/UI/togglePanel.togglePanel_C'")); auto templateWidget = templateWidgetList[index]; if (!templateWidget) continue; if (index == 0) InitWidget<UToggleAutoWidget>(attribute, templateWidget, world); if (index == 1) InitWidget<UInputWidget>(attribute, templateWidget, world); else if (index == 2) InitWidget<USliderInputWidget>(attribute, templateWidget, world); else if (index == 3) InitWidget<UDropdownWidget>(attribute, templateWidget, world); } for (auto widget : widgetList) { panel->AddChild(widget); } UpdateDropdownParent(); bBind = true; }; //1 probj = null如直接更新内存数据,然后反馈给UI //2 probj != null绑定的同类型别的对象,用这对象更新UI,并且后续UI更新反馈给此新对象 void Update(U* pobj = nullptr) { if (!bBind) return; if (pobj) obj = pobj; for (auto widget : widgetList) { int index = widget->index; if (index == 0) GetValue<UToggleAutoWidget>(widget); if (index == 1) GetValue<UInputWidget>(widget); else if (index == 2) GetValue<USliderInputWidget>(widget); else if (index == 3) GetValue<UDropdownWidget>(widget); } } //当对应结构的UI改动更新对象后,返回的回调,第一个参数表示当前对象,第二个参数表示对应字段名 void SetOnObjChangeAction(std::function<void(ObjAttribute<U>*, FString name)> onChange) { onObjChangeHandle = onChange; } //返回当前Bind的对象 U* GetObj() { return obj; } //根据字段名得到对应UToggleAutoWidget/UInputWidget/USliderInputWidget/UDropdownWidget template<typename A> A* GetWidget(FString name) { for (auto widget : widgetList) { A* awidget = dynamic_cast<A*>(widget); if (awidget && awidget->memberName == name) { return awidget; } } return nullptr; } private: template<typename A> void InitWidget(BaseAttribute* attribute, UClass* widgetTemplate, UWorld* world) { auto widget = CreateWidget<A>(world, widgetTemplate); widget->onValueChange = std::bind(&ObjAttribute<U>::SetValue<A::Type>, this, _1, _2); widget->attribute = attribute; widget->uproperty = FindProperty(attribute->MemberName); widget->index = attribute->GetIndex(); //调用对应蓝图的UI赋值 widget->InitWidget(); //关联UI的事件到如上的onValueChange中 widget->InitEvent(); widget->memberName = attribute->MemberName; widget->text->SetText(FText::FromString(attribute->DisplayName)); widgetList.Add(widget); } //在对应的Widget上直接保存此UProperty对象,此后更新数据/UI更快 UProperty* FindProperty(FString name) { for (TFieldIterator<UProperty> It(structDefinition); It; ++It) { UProperty* Property = *It; if (Property->GetName() == name) { return Property; } } return nullptr; } //当对应的UI改动后,UI影响对应obj的值,泛型t表示对应UI返回的数据 //ComponentTemplate对应的泛型t是固定的,但是数据结构里的字段类型可多种,转化逻辑在如下写好就行 template<typename T> void SetValue(ComponentTemplate<T>* widget, T t) { if (widget->uproperty != nullptr) { ValueToUProperty(widget->uproperty, t); if (onObjChangeHandle) { onObjChangeHandle(this, widget->uproperty->GetName()); } } }; void ValueToUProperty(UProperty* Property, bool t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UBoolProperty *BoolProperty = Cast<UBoolProperty>(Property)) { BoolProperty->SetPropertyValue(Value, t); } }; void ValueToUProperty(UProperty* Property, float t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UNumericProperty *NumericProperty = Cast<UNumericProperty>(Property)) { if (NumericProperty->IsFloatingPoint()) { NumericProperty->SetFloatingPointPropertyValue(Value, (float)t); } else if (NumericProperty->IsInteger()) { NumericProperty->SetIntPropertyValue(Value, (int64)t); } } }; void ValueToUProperty(UProperty* Property, FString t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UStrProperty *StringProperty = Cast<UStrProperty>(Property)) { StringProperty->SetPropertyValue(Value, t); } } void ValueToUProperty(UProperty* Property, int t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UNumericProperty *NumericProperty = Cast<UNumericProperty>(Property)) { if (NumericProperty->IsFloatingPoint()) { NumericProperty->SetFloatingPointPropertyValue(Value, (int64)t); } else if (NumericProperty->IsInteger()) { NumericProperty->SetIntPropertyValue(Value, (int64)t); } } else if (UEnumProperty* EnumProperty = Cast<UEnumProperty>(Property)) { EnumProperty->GetUnderlyingProperty()->SetIntPropertyValue(Value, (int64)t); } } //从对应的obj里去取值更新UI,会转到ComponentTemplate::Update //同SetValue,ComponentTemplate类型固定,数据结构类型可多种,多种需要写相应的转化逻辑 template<typename A>//template<typename T, typename A> void GetValue(UBaseAutoWidget* baseWidget)//ComponentTemplate<T>* widget, T* t) { A* widget = (A*)baseWidget; if (widget->uproperty != nullptr) { A::Type t; if (UPropertyToValue(widget->uproperty, t)) widget->Update(t); } //A* widget = (A*)baseWidget; //for (TFieldIterator<UProperty> It(structDefinition); It; ++It) //{ // UProperty* Property = *It; // FString PropertyName = Property->GetName(); // if (PropertyName == widget->attribute->MemberName) // { // A::Type t; // if (UPropertyToValue(Property, t)) // widget->Update(t); // } //} }; bool UPropertyToValue(UProperty* Property, bool& t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UBoolProperty *BoolProperty = Cast<UBoolProperty>(Property)) { bool value = BoolProperty->GetPropertyValue(Value); t = value; return true; } return false; }; bool UPropertyToValue(UProperty* Property, float& t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UNumericProperty *NumericProperty = Cast<UNumericProperty>(Property)) { if (NumericProperty->IsFloatingPoint()) { float value = NumericProperty->GetFloatingPointPropertyValue(Value); t = value; return true; } else if (NumericProperty->IsInteger()) { int value = NumericProperty->GetSignedIntPropertyValue(Value); t = (float)value; return true; } } return false; }; bool UPropertyToValue(UProperty* Property, FString& t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UStrProperty *StringProperty = Cast<UStrProperty>(Property)) { FString value = StringProperty->GetPropertyValue(Value); t = value; return true; } return false; } bool UPropertyToValue(UProperty* Property, int& t) { void* Value = Property->ContainerPtrToValuePtr<uint8>(obj); if (UNumericProperty *NumericProperty = Cast<UNumericProperty>(Property)) { if (NumericProperty->IsFloatingPoint()) { float value = NumericProperty->GetFloatingPointPropertyValue(Value); t = (int)value; return true; } else if (NumericProperty->IsInteger()) { int value = NumericProperty->GetSignedIntPropertyValue(Value); t = value; return true; } } else if (UEnumProperty* EnumProperty = Cast<UEnumProperty>(Property)) { UEnum* EnumDef = EnumProperty->GetEnum(); t = EnumProperty->GetUnderlyingProperty()->GetSignedIntPropertyValue(Value); return true; } return false; }; void UpdateDropdownParent() { for (auto widget : widgetList) { panel->AddChild(widget); if (widget->index == 3) { UDropdownWidget* dropWidget = (UDropdownWidget*)widget; DropdownAttribute* dropAttribut = (DropdownAttribute*)(dropWidget->attribute); if (dropAttribut->Parent.IsEmpty()) continue; for (auto widget : widgetList) { if (widget->index == 3 && widget->memberName == dropAttribut->Parent) { dropWidget->parent = (UDropdownWidget*)widget; dropWidget->InitParentEvent(); } } } } } };
主要是Bind这个方法,根据描述生成不同UI,先调用InitWidget,把蓝图里的UI绑定到我们要操作的对象上,然后InitEvent根据设定去绑定UI事件到SetValue上.而Update表示从UI里的数据去更新UI,处理在GetValue上,和SetValue反的。在这说下,UI返回给我们的数据类型是固定的,而结构体可能有很多不同类型,在不同的方法里自己写转化函数就行。其中GetValue/SetValue的实现就是靠的元数据记录了当前的指针里的偏移,并把这个偏移里数据去用UI更新或是拿出来更新UI.
如下是具体如何使用。
#pragma once #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "BaseAutoWidget.h" #include "ObjAttribute.h" #include "PanelManager.generated.h" USTRUCT(BlueprintType) struct MRCOREUE4_API FColorKeySetting { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) bool bUseBlue = true; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) int blend1 = 2; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) float blend2 = 0.5f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) float blend3 = 1.0f; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) FString name = "123"; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = AutoWidget) int cameraIndex = 1; }; /** * */ UCLASS() class MRCOREUE4_API UPanelManager : public UUserWidget { GENERATED_BODY() public: void OnKeyValue(ObjAttribute<FColorKeySetting>* objAttribut, FString name); private: FColorKeySetting keySetting = {}; TArray<BaseAttribute*> keyArrList; ObjAttribute<FColorKeySetting> objKey; TArray<UClass*> templateList; public: UFUNCTION(BlueprintCallable) void InitTemplate(UClass * toggleTemplate, UClass * inputeTemplate, UClass * sliderTemplate, UClass * dropdownTemplate); UFUNCTION(BlueprintCallable) void BindKey(UVerticalBox * keyBox); }; void UPanelManager::InitTemplate(UClass * toggleTemplate, UClass * inputeTemplate, UClass * sliderTemplate, UClass * dropdownTemplate) { templateList.Add(toggleTemplate); templateList.Add(inputeTemplate); templateList.Add(sliderTemplate); templateList.Add(dropdownTemplate); } void UPanelManager::BindKey(UVerticalBox * keyBox) { ToggleAttribute* tb = new ToggleAttribute(); tb->MemberName = "bUseBlue"; tb->DisplayName = "User Blue"; tb->DefaultValue = false; keyArrList.Add(tb); InputeAttribute* ib = new InputeAttribute(); ib->MemberName = "name"; ib->DisplayName = "Name"; keyArrList.Add(ib); SliderAttribute* sb1 = new SliderAttribute(); sb1->MemberName = "blend1"; sb1->DisplayName = "blend1 one"; sb1->DefaultValue = 1; sb1->range = 5; sb1->bInt = true; keyArrList.Add(sb1); SliderAttribute* sb2 = new SliderAttribute(); sb2->MemberName = "blend2"; sb2->DisplayName = "blend2 one"; sb2->DefaultValue = 0; sb2->range = 5; sb2->bAutoRange = true; keyArrList.Add(sb2); SliderAttribute* sb3 = new SliderAttribute(); sb3->MemberName = "blend3"; sb3->DisplayName = "blend3 one"; sb3->DefaultValue = 0; sb3->range = 5; sb3->offset = 1.0f; keyArrList.Add(sb3); TArray<FString> options; options.Add("Real camera1."); options.Add("Real camera2."); options.Add("Real camera3."); options.Add("Real camera4."); DropdownAttribute* db = new DropdownAttribute(); db->MemberName = "cameraIndex"; db->DisplayName = "Camera Index"; db->InitOptions(true, options); keyArrList.Add(db); auto word = this->GetWorld(); objKey.Bind(&keySetting, keyBox, keyArrList, templateList, word); objKey.Update(); objKey.SetOnObjChangeAction(std::bind(&UPanelManager::OnKeyValue, this, _1, _2)); } void UPanelManager::OnKeyValue(ObjAttribute<FColorKeySetting>* objAttribut, FString name) { GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, name); }
把我们生成蓝图UMG直接给InitTemplate里对应UClass,可以根据不同的情况用不同的蓝图UMG模板,然后传入一个UVerticalBox给我们,根据对象生成的UI就自动在这个Box里显示。
Unity直接使用C#,C#本身有完整的元数据体系,我们新增我们想要的特性类,直接指定到对应字段上就行,余下差不多和上面UE4里的用法一样。
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class BaseAutoAttribute : Attribute { protected int index = -1; protected string displayName = string.Empty; protected int order = 100; protected MemberInfo memberInfo = null; public string DisplayName { get { return displayName; } set { displayName = value; } } public int Order { get { return order; } set { order = value; } } public int Index { get { return index; } set { index = value; } } public MemberInfo Member { get { return memberInfo; } set { memberInfo = value; } } public void SetValue<T, U>(ref T obj, U member) { //结构装箱,不然SetValue自动装箱的值拿不到 object o = obj; if (memberInfo is FieldInfo) { var field = memberInfo as FieldInfo; if (typeof(U) != field.FieldType) { object ov = ToolHelp.ChangeType(member, field.FieldType); field.SetValue(o, ov); } else { field.SetValue(o, member); } } obj = (T)o; } public T GetValue<T>(ref object obj) { T t = default(T); if (memberInfo is FieldInfo) { var field = memberInfo as FieldInfo; var tv = field.GetValue(obj); if (typeof(T) == field.FieldType) t = (T)tv; else t = (T)ToolHelp.ChangeType(tv, typeof(T)); } return t; } } public class SliderInputAttribute : BaseAutoAttribute { private float range = 1.0f; private float min = 0; private float max = 1.0f; private bool bAutoRange = false; private float defaultValue = 0.0f; private bool bInt = false; public SliderInputAttribute() { index = 0; } public float Range { get { return range; } set { range = value; } } public float Min { get { return min; } set { min = value; } } public float Max { get { return max; } set { max = value; } } public bool BAutoRange { get { return bAutoRange; } set { bAutoRange = value; } } public float DefaultValue { get { return defaultValue; } set { defaultValue = value; } } public bool BInt { get { return bInt; } set { bInt = value; } } } public class InputAttribute : BaseAutoAttribute { private string defaultValue = string.Empty; public InputAttribute() { index = 1; } public string DefaultValue { get { return defaultValue; } set { defaultValue = value; } } } public class ToggleAttribute : BaseAutoAttribute { private bool defaultValue = false; public ToggleAttribute() { index = 2; } public bool DefaultValue { get { return defaultValue; } set { defaultValue = value; } } } public class DropdownAttribute : BaseAutoAttribute { private int defaultValue = 0; public string Parent = string.Empty; public DropdownAttribute() { index = 3; } public int DefaultValue { get { return defaultValue; } set { defaultValue = value; } } }
然后同上UI模板类的设计。
public class BaseComponent : MonoBehaviour { public Text text; private string displayName = string.Empty; private BaseAutoAttribute baseAttribute = null; public string DisplayName { get { return displayName; } set { displayName = name; text.text = displayName; } } public BaseAutoAttribute BaseAttribute { get { return baseAttribute; } set { baseAttribute = value; } } public virtual void UpdateUI(object obj) { } public virtual void Init() { } } public class BaseTemplateComponent<T> : BaseComponent { public Action<T, BaseTemplateComponent<T>> onValueChangeAction; public Action<T> onValueChangeAfter; public void SetValueChangeAction(Action<T, BaseTemplateComponent<T>> onAction) { onValueChangeAction = onAction; } public void OnValueChange(T value) { if (onValueChangeAction != null) { onValueChangeAction(value, this); if (onValueChangeAfter != null) onValueChangeAfter(value); } } public override void UpdateUI(object obj) { T value = BaseAttribute.GetValue<T>(ref obj); SetValue(value); } public virtual void SetValue(T value) { } } public class SliderInputComponent : BaseTemplateComponent<float> { public Slider slider; public InputField field; private float range = 1.0f; private bool bUpdate = false; private bool bAutoRange = false; // Use this for initialization void Start() { slider.onValueChanged.AddListener(OnValueChange); field.onValueChanged.AddListener(OnFieldChange); } private void Update() { if (!field.isFocused) { bUpdate = true; field.text = slider.value.ToString(); bUpdate = false; } } public void OnFieldChange(string value) { if (bUpdate) return; float tv = 0.0f; if (float.TryParse(value, out tv)) { if (tv != slider.value) { if (bAutoRange) { slider.minValue = tv - range / 2.0f; slider.maxValue = tv + range / 2.0f; } slider.value = tv; } } } private void setValue(float defaultValue, float range, bool bInt = false) { this.range = range; this.bAutoRange = true; slider.maxValue = defaultValue + range / 2.0f; slider.minValue = defaultValue - range / 2.0f; slider.value = defaultValue; //field.text = defaultValue.ToString(); slider.wholeNumbers = bInt; } private void setValue(float defaultValue, float minValue, float maxValue, bool bInt = false) { slider.maxValue = maxValue; slider.minValue = minValue; slider.value = defaultValue; slider.wholeNumbers = bInt; } public override void SetValue(float value) { if (bAutoRange) { slider.maxValue = value + range / 2.0f; slider.minValue = value - range / 2.0f; } slider.value = value; } public override void Init() { SliderInputAttribute attribute = BaseAttribute as SliderInputAttribute; if (attribute.BAutoRange) { setValue(attribute.DefaultValue, attribute.Range, attribute.BInt); } else { setValue(attribute.DefaultValue, attribute.Min, attribute.Max, attribute.BInt); } } }
然后同上的的ObjectAttribute设计。
public class ObjectAttribute<T> { private T obj; private RectTransform panel; private List<BaseAutoAttribute> attributes = null; private List<BaseComponent> components = null; public event Action<ObjectAttribute<T>, string> OnChangeEvent; public ObjectAttribute() { } public T Obj { get { return obj; } } /// <summary> /// 在一个panel上生成UI,UI会生动更新到到Obj上的各个属性,只有UI改变才会改变属性 /// </summary> /// <param name="p"></param> public void Bind(T t, RectTransform p, List<BaseComponent> templateList) { obj = t; attributes = ToolHelp.GetAttributes(typeof(T)); panel = p; components = new List<BaseComponent>(); int count = attributes.Count; for (int i = 0; i < count; i++) { var attr = attributes[i]; var component = templateList[attr.Index]; var initComp = GameObject.Instantiate(templateList[attr.Index], panel); initComp.BaseAttribute = attr; if (attr.Index == 0) InitComponent<SliderInputComponent, float>(initComp as SliderInputComponent); else if (attr.Index == 1) InitComponent<InputComponent, string>(initComp as InputComponent); else if (attr.Index == 2) InitComponent<ToggleComponent, bool>(initComp as ToggleComponent); else if (attr.Index == 3) InitComponent<DropdownComponent, int>(initComp as DropdownComponent); components.Add(initComp); } UpdateDropdownParent(); } /// <summary> /// 从数据更新UI,注意,这个函数只在初始化或是有必要时调用,不要每桢调用 /// </summary> public void Update() { if (components == null) return; foreach (var comp in components) { comp.UpdateUI(obj); } } public void Update(T t) { obj = t; Update(); } public U GetComponent<U>(string memberName) where U : BaseComponent { U component = default(U); foreach (var comp in components) { if (comp is U) { if (comp.BaseAttribute.Member.Name == memberName) { component = comp as U; break; } } } return component; } private void InitComponent<U, A>(U component) where U : BaseTemplateComponent<A> { int index = component.BaseAttribute.Index; component.text.text = component.BaseAttribute.DisplayName; component.Init(); component.SetValueChangeAction(OnValueChange); } private void OnValueChange<U>(U value, BaseTemplateComponent<U> component) { component.BaseAttribute.SetValue(ref obj, value); if (OnChangeEvent != null) OnChangeEvent(this, component.BaseAttribute.Member.Name); //OnChangeEvent?.Invoke(this, component.BaseAttribute.Member.Name); } private void UpdateDropdownParent() { foreach (var comp in components) { if (comp.BaseAttribute.Index == 3) { DropdownComponent dc = comp as DropdownComponent; DropdownAttribute da = comp.BaseAttribute as DropdownAttribute; if (!string.IsNullOrEmpty(da.Parent)) { var parent = GetComponent<DropdownComponent>(da.Parent); if (parent != null) { dc.parent = parent; } } } } } }
使用。
/// <summary> /// 图像处理 /// </summary> [Serializable] public struct TextureOperate { [Toggle(DisplayName = "FilpX", Order = 0)] public bool bFlipX;// = false; [Toggle(DisplayName = "FilpY", Order = 1)] public bool bFlipY;// = false; public int mapR;// = 0; public int mapG;// = 1; public int mapB;// = 2; public int mapA;// = 3; [SliderInput(DisplayName = "Left", Order = 2)] public float left; [SliderInput(DisplayName = "Top", Order = 3)] public float top; [SliderInput(DisplayName = "Right", Order = 4)] public float right; [SliderInput(DisplayName = "Bottom", Order = 5)] public float bottom; }; ObjectAttribute<TextureOperate> objKt = new ObjectAttribute<TextureOperate>(); var templateList = UIManager.Instance.uiRoot.Components; objKt.Bind(cameraSetting.keyingTo, ktPanel, templateList); objKt.Update();
好吧,UE4与Unity3D在设计几乎一样,没的办法,公司二个引擎都在用,并且我也没找到各自引擎有更好的设计方法。
不过写的时候还是感觉有些细节上的不一样。具体如下个人感觉。
1 C++的模版比C#的泛型还是宽松的多,C++的模板完全和动态语言似的,只要你有,你就能用,配合实例化,具体化真的好玩。而C#的泛型约束的功能确实限制太多。
2 C#在这改成使用指针,破坏性太大,如果是结构体,数据更新后,需要自己去拿,网上指出C#有些隐藏的关键字可以引用,使用了下感觉很怪就又回退了。
3 C#里自带的Convert.ChangeType可以免去很多代码,但是特定转不了还是要自己单独去写。
4 C#里反射操作听说性能不好,而上面UE4里C++元数据的实现可以看到,不会有任何性能问题,直接指向指针位移。
5 C#本身有元数据设计,最后在使用时C#的实现看起来要整洁的多。