你們可能發現一些大佬講UE4,首先都會講類型系統,知道UE4會根據宏標記生成一些特定的內容,UE4幾乎全部高級功能都離不開這些內容,通常來講,咱們不會直接去使用它。數據結構
今天這個Demo內容但願能加深你們的理解,爲何有這個東東了,主要是因爲咱們產品須要不少根據環境調整的參數,咱們須要提供不少UI,一個結構包含五六個參數,對應的參數與UI綁定事件,東東很簡單,可是太多了,一多就容易出事,參數結構又在調整,而後就亂了。編輯器
參考UE4自己編輯器根據對應結構自動生成UI,UI的更改能更新到對象上,以及JSON的序列化,UI與對象屬性綁定更新應該是徹底沒有問題的。主要設計想了下,首先是UI的設計用藍圖來作,其他全部邏輯相關全用C++方便,UI自動更新對象,對象變更後,也可在代碼中去更新UI,使用要簡單,類型自動轉換。ide
整個邏輯最主要的一點,簡單說就是咱們要找到如何根據字符串去更新結構體裏的字段,如同C#裏的反射FieldInfo::SetValue ,FieldInfo::GetValue 等方法,咱們知道C#根據元數據作到的,而UE4的類或是結構打上宏標記,UHT會根據類上的宏生成元數據在對應 .generated.h 文件裏,這裏很少講,若是你們有興趣,能夠移步到 大釗《InsideUE4》UObject(三)類型系統設定和結構 這章的先後幾章,在這只是指出這裏的元數據能夠找到每一個字段的名稱與在對應對象位置上的偏移,在這,咱們只須要用這二點就夠了。函數
先看一下結果:性能
上面是一個假定結構,下面是根據咱們需求生成的UI模式,UI的設計在藍圖能夠設計成本身的樣式,在這就用默認的。ui
由於UE4裏不支持咱們去自定義元數據,因此咱們須要本身去定義咱們要生成的UI描述數據,這個數據和元數據共同對應對象裏的字段名,以下是BaseAttribute以及全部UI對應的擴展描述。 this
#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用來生成下拉列表框,並支持聯動。spa
對應上面的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具體實現。 3d
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#的實現看起來要整潔的多。