有没有一种方法可以在运行时轻松地将数据绑定到 UI

问题描述 投票:0回答:2

上下文

所以在研究了一些关于 UI 工具包之后,我决定将它用于我的项目,因为我非常习惯 CSS 并且我鄙视 Unity3D 提供的“默认/遗留”UI 系统。

不幸的是,它处于非常早期的开发阶段,他们似乎正朝着“编辑器 GUI”的方向发展。不管我选择它是因为它设计了 UI,比我用通常的“Unity3D UI”做事的方法更快。

目标

我的目标相对简单,我想创建一个基于某些变量更新 UI 的行为,同时隐藏/抽象此行为(换句话说,我想将 UI 的值绑定到数据类中的某个整数)。

在未来,由于我慢慢地将所有内容迁移到多人游戏环境,这将变得更加复杂

主要问题

经过数小时的搜索/阅读文档,并强行解决问题,我设法找到了一个简单的解决方案,由于我对一些非常相似的行为进行了硬编码,因此我将简化该解决方案:

GameObject trackCanvas = GameObject.Find("Canvas");
UIDocument docum= trackCanvas.GetComponent<UIDocument>();
Label foodUI = docum.rootVisualElement.Q<Label>(name: "FoodVar");
        
if(playerResources!= null){
    SerializedObject pR = new SerializedObject(playerResources);
                
    SerializedProperty foodProp = pR.FindProperty("food");
                
    foodUI.BindProperty(foodProp);
                
}else{
    foodUI.Unbind();
}

非常简单的解决方案,但我节省了一些思考时间。在我尝试构建它之前,这一切都像一个魅力一样......我看到与导入 UnityEditor 有关的多个错误,我开始删除它(因为我几乎导入所有内容并且仅在 CleanUp 上我开始看到有必要或没有必要)

不幸的是,在这个脚本上我不能,在重新阅读了细则的文档后(这不是很好......)我读到 SerializedObject 不能用于“生产/运行时/构建”版本,因为它依赖于 UnityEditor最终产品上不会存在。

这真的让我很恼火,因为这是一个非常雄辩的解决方案。

后缀

手册似乎建议可以使用 UnityEditor 命名空间。不幸的是,从他们非常模糊的教程中我无法弄清楚它是如何工作的(我提到了坦克示例,它似乎只使用 unityeditor 因为他们希望能够在编辑模式下绑定东西,而绑定本身似乎是通过 uxml 完成的)

我已经尝试了一些东西,但一切似乎都脱离了上下文,就像有一些 serializedField 会神奇地与 uxml 绑定只是因为绑定路径与变量名相同

然后我想,如果 unity 不希望我在运行时模式下使用编辑器的东西,我会强迫它,所以复制粘贴它的一些类然后以某种方式破解它应该不难。不幸的是,Unity 不仅有严格的专有许可,不允许您以任何方式修改它的软件,而且一些注释、函数等……受到保护(尤其是他们使用的 C 东西)

然后我想到了手工做,我得出了两个选择:

  1. 只需将 food.value = resources.food 放入某种更新中,并希望当我将其迁移到多人游戏环境时它不会产生任何类型的问题

  2. 或者做一些更复杂的事情,比如我称之为会更新用户界面的某种委托,理论上效率更高,因为我只更新需要的东西。

自从我做 RTS 以来,我认为价值观会不断变化,所以我对两者都很分歧。让我想坚持已经完成的解决方案

当我讨厌文档的结构、绕过源代码有多么困难,以及文档对于与 CSS 非常相似的行为的冗长感觉时,这会变得更加紧张

长话短说:

在不依赖 Unity Editor 的 UI 工具包中是否有 BindProperty() 的替代方法?

c# unity3d ui-toolkit
2个回答
4
投票

您可以创建一个包装类来保存您的值,它可以在包装值更改时调用一个事件。

public interface IProperty<T> : IProperty
{
    new event Action<T> ValueChanged;
    new T Value { get; }
}

public interface IProperty
{
    event Action<object> ValueChanged;
    object Value { get; }
}

[Serializable]
public class Property<T> : IProperty<T>
{
    public event Action<T> ValueChanged;
    
    event Action<object> IProperty.ValueChanged
    {
        add => valueChanged += value;
        remove => valueChanged -= value;
    }
    
    [SerializeField]
    private T value;
    
    public T Value
    {
        get => value;
        
        set
        {
            if(EqualityComparer<T>.Default.Equals(this.value, value))
            {
                return;
            }
            
            this.value = value;
            
            ValueChanged?.Invoke(value);
            valueChanged?.Invoke(value);
        }
    }
    
    object IProperty.Value => value;

    private Action<object> valueChanged;
    
    public Property(T value) => this.value = value;
    
    public static explicit operator Property<T>(T value) => new Property<T>(value);
    public static implicit operator T(Property<T> binding) => binding.value;
}

在此之后,您可以创建类似于 Unity 自己的 BindProperty 的自定义扩展方法,它使用此包装器而不是 SerializedProperty。

public static class RuntimeBindingExtensions
{
    private static readonly Dictionary<VisualElement, List<(IProperty property, Action<object> binding)>> propertyBindings = new Dictionary<VisualElement, List<(IProperty property, Action<object> binding)>>();

    public static void BindProperty(this TextElement element, IProperty property)
    {
        if(!propertyBindings.TryGetValue(element, out var bindingsList))
        {
            bindingsList = new List<(IProperty, Action<object>)>();
            propertyBindings.Add(element, bindingsList);
        }

        Action<object> onPropertyValueChanged = OnPropertyValueChanged;
        bindingsList.Add((property, onPropertyValueChanged));

        property.ValueChanged += onPropertyValueChanged;
        
        OnPropertyValueChanged(property.Value);
        
        void OnPropertyValueChanged(object newValue)
        {
            element.text = newValue?.ToString() ?? "";
        }
    }

    public static void UnbindProperty(this TextElement element, IProperty property)
    {
        if(!propertyBindings.TryGetValue(element, out var bindingsList))
        {
            return;
        }

        for(int i = bindingsList.Count - 1; i >= 0; i--)
        {
            var propertyBinding = bindingsList[i];
            if(propertyBinding.property == property)
            {
                propertyBinding.property.ValueChanged -= propertyBinding.binding;
                bindingsList.RemoveAt(i);
            }
        }
    }

    public static void UnbindAllProperties(this TextElement element)
    {
        if(!propertyBindings.TryGetValue(element, out var bindingsList))
        {
            return;
        }

        foreach(var propertyBinding in bindingsList)
        {
            propertyBinding.property.ValueChanged -= propertyBinding.binding;
        }

        bindingsList.Clear();
    }
}

用法:

public class PlayerResources
{
    public Property<int> food;
}

if(playerResources != null)
{
    foodUI.BindProperty(playerResources.food);
}

更新: 还添加了用于取消绑定属性的扩展方法,并使 BindProperty 立即更新元素上的文本。


0
投票

我的方式,也许不是更好,该类直接实现绑定解决方案并使用 c# 反射,绑定是绑定路径的两种方式(使用标签、输入字段和编辑器/统一上的切换进行测试),并将尝试与所有匹配视觉元素的孩子:

基类(灵感来自上一篇文章):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UIElements;

public interface INotifyProperty
{
    event UnityAction<object, string> OnValueChanged;
    void UpdateValue(string fieldName, object value);
    void Bind(VisualElement visualElement);
}

public class NotifyProperty : INotifyProperty
{
    Dictionary<string, VisualElement> bindingPaths = new Dictionary<string, VisualElement>();

    public event UnityAction<object, string> OnValueChanged;
    public List<FieldInfo> fields = new List<FieldInfo>();
    public List<PropertyInfo> propertyInfos = new List<PropertyInfo>();
    public virtual void Bind(VisualElement element)
    {
        fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).ToList();
        propertyInfos = GetType().GetProperties().ToList();
        bindingPaths = GetBindingPath(element);

        // permit to affect value to bind element
        propertyInfos.ForEach(p =>
        {
            p.SetValue(this, p.GetValue(this));
        });

    }

    public virtual void NotifyValueChanged<T>(T newValue, [CallerMemberName] string property = "")
    {
        OnValueChanged?.Invoke(newValue, property);
        ValueChanged(newValue, property);
    }

    private void ValueChanged<T>(T arg0, string arg1)
    {
        if (bindingPaths.TryGetValue(arg1.ToLower(), out var element))
        {
            if (element is INotifyValueChanged<T> notifElement)
            {
                notifElement.value = arg0;
            }
            else if (element is INotifyValueChanged<string> notifString)
            {
                notifString.value = arg0?.ToString() ?? string.Empty;
            }
        }
    }


    public Dictionary<string, VisualElement> GetBindingPath(VisualElement element, Dictionary<string, VisualElement> dico = null)
    {
        if (dico == null)
        {
            dico = new Dictionary<string, VisualElement>();
        }

        if (element is BindableElement bindElement)
        {
            if (!string.IsNullOrEmpty(bindElement.bindingPath))
            {
                dico.Add(bindElement.bindingPath.ToLower(), bindElement);


                if (fields.Exists(f => f.Name == bindElement.bindingPath.ToLower()))
                {


                    // we register callback only for matching path, add other callback if you want support other control
                    bindElement.RegisterCallback<ChangeEvent<string>>((val) =>
                    {
                        UpdateValue(bindElement.bindingPath, val.newValue);
                    });

                    bindElement.RegisterCallback<ChangeEvent<bool>>((val) =>
                    {
                        UpdateValue(bindElement.bindingPath, val.newValue);
                    });
                }

            }

            if (element.childCount > 0)
            {
                foreach (var subElement in element.Children())
                {
                    GetBindingPath(subElement, dico);
                }
            }
        }
        else
        {
            if (element.childCount > 0)
            {
                foreach (var subElement in element.Children())
                {
                    GetBindingPath(subElement, dico);
                }
            }
        }
        return dico;
    }

    public virtual void UpdateValue(string fieldName, object value)
    {
        fields.FirstOrDefault(f => f.Name == fieldName)?.SetValue(this, value);
    }
}

实施:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

public class LoginProperties : NotifyProperty
{
    private string email;
    public string Email
    {
        get { return email; }
        set
        {
            email = value;
            NotifyValueChanged(value);
        }
    }
    private string status;
    public string Status
    {
        get { return status; }
        set
        {
            status = value;
            NotifyValueChanged(value);
        }
    }

    private bool save;
    public bool Save
    {
        get { return save; }
        set
        {
            save = value;
            NotifyValueChanged(value);
        }
    }
}

uxml

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <Style src="project://database/Assets/UI/Style.uss?fileID=7433441132597879392&amp;guid=710cabf677cea49439f4f5c169631b72&amp;type=3#Style" />
    <ui:VisualElement style="padding-left: 10px; padding-right: 10px; padding-top: 20px; padding-bottom: 20px; flex-grow: 1; justify-content: flex-start;">
        <ui:VisualElement style="align-items: center; justify-content: flex-end; margin-top: 100px; margin-bottom: 10px;">
            <ui:Label text="Hero" display-tooltip-when-elided="true" style="font-size: 100px;" />
            <ui:VisualElement style="width: 205px; height: 100px; background-image: url(&apos;project://database/Assets/publish/background.png?fileID=2800000&amp;guid=d21a7e330f5e8964ca379e4865e84764&amp;type=3#background&apos;);" />
        </ui:VisualElement>
        <ui:VisualElement />
        <ui:VisualElement style="flex-grow: 1; align-items: stretch; justify-content: flex-start;">
            <ui:Label text="status" display-tooltip-when-elided="true" binding-path="status" name="lblStatus" style="font-size: 24px; margin-bottom: 30px;" />
            <ui:TextField picking-mode="Ignore" label="Email :" binding-path="email" name="txtLogin" style="flex-direction: column;" />
            <ui:Toggle usage-hints="None" value="true" text=" Save email" binding-path="save" name="tgSave" class="toggle" style="flex-direction: row; align-items: center; justify-content: flex-start; flex-grow: 0; flex-shrink: 0; padding-bottom: 30px; padding-top: 20px;" />
            <ui:Button text="Connect" display-tooltip-when-elided="true" name="btnConnect" style="background-color: rgba(0, 0, 50, 0.71);" />
        </ui:VisualElement>
    </ui:VisualElement>
</ui:UXML>

用法:

using Assets.Service;
using HeroModels;
using link.magic.unity.sdk;
using Nethereum.RPC.Eth;
using Nethereum.Signer;
using Nethereum.Web3.Accounts;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;

public class LoginManager : MonoBehaviour
{
    VisualElement root;
    Button btnConnect;
    Label lblStatus;

    private string keyPass = "logkey";
    private string keyLogin = "log";

    PlayerService playerService;

    LoginProperties loginProperties = new LoginProperties();

    // Start is called before the first frame update
    void Start()
    {
        root = GetComponent<UIDocument>().rootVisualElement;
        btnConnect = root.Q<Button>();
        lblStatus = root.Q<Label>("lblStatus");

        loginProperties.Bind(root);
        loginProperties.Save = true;

        btnConnect.clicked += BtnConnect_clicked;

        if (PlayerPrefs.HasKey(keyLogin))
        {
            loginProperties.Email = PlayerPrefs.GetString(keyLogin);
        }

    }

    private void BtnConnect_clicked()
    {
        Login();
    }

    private void PlayerService_PlayerLoaded(object sender, HeroModels.PlayerDto e)
    {
        GameContext.Instance.Player = e;
        SceneManager.LoadScene("Map");
    }

    // Update is called once per frame
    void Update()
    {

    }

    public async void Login()
    {
        try
        {
            loginProperties.Status = "Login ...";
            lblStatus.style.color = Color.white;
            if (loginProperties.Save)
            {
                PlayerPrefs.SetString(keyLogin, loginProperties.Email);
                //PlayerPrefs.SetString(keyPass, password?.text);
            }
            else
            {
                PlayerPrefs.SetString(keyLogin, string.Empty);
                //PlayerPrefs.SetString(keyPass, string.Empty);
            }
            PlayerPrefs.Save();

            // top secret code hidden


        }
        catch (System.Exception ex)
        {
            loginProperties.Status = ex.Message;
            lblStatus.style.color = Color.red;
        }

    }

    private async Task<string> Sign(string account, string message)
    {
        var personalSign = new EthSign(Magic.Instance.Provider);
        var res = await personalSign.SendRequestAsync(account, message);
        return res;
    }

    private void PlayerService_PlayerNotExist(object sender, System.EventArgs e)
    {
        SceneManager.LoadScene("CreatePlayerScene");
    }
}

© www.soinside.com 2019 - 2024. All rights reserved.