c# - 有没有一种方法可以在运行时轻松地将数据绑定(bind)到UI

标签 c# unity-game-engine ui-toolkit

上下文

因此,在对 UI 工具包进行了一些研究之后,我决定将其用于我的项目,因为我非常习惯 CSS,并且鄙视 Unity3D 提供的“默认/旧版”UI 系统。

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

目标

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

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

主要问题

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

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 不会存在于最终产品中。

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

后缀

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

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

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

然后我考虑手动完成,我得到了两个选择:

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

  2. 或者做一些更复杂的事情,比如我称之为更新 UI 的某种委托(delegate),理论上效率更高,因为我只更新需要的内容。

自从我开始做即时战略游戏以来,我认为这些值(value)观会不断变化,所以我对两者都抱有很大分歧。让我想要坚持已经完成的解决方案

当我讨厌文档的结构、浏览源代码有多么困难,以及文档的长度与 CSS 非常相似的行为时,这让我感到压力更大

TL;博士:

UI 工具包中是否有不依赖 Unity 编辑器的 BindProperty() 替代方案?

最佳答案

您可以创建一个包装类来保存您的值,只要包装的值发生更改,它就可以调用一个事件。

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);
}

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

关于c# - 有没有一种方法可以在运行时轻松地将数据绑定(bind)到UI,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73595753/

相关文章:

c# - TextAsset 返回 null

android - Unity Android 无法发布 APK,签名为调试

c# - unity UxmlAttributeDescription 不设置值(重置值)

c# - XP上的C#应用​​程序崩溃

c# - 方法可以拒绝 Vector2 和 Vector3 的错误类型吗

c# - MonoTouch 绑定(bind)类型<implementingprotocol>

c# - 使用 TypeForwardedToAttribute 的正确方法是什么?

unity-game-engine - 无法部署到 HoloLens(错误 80004005)