所以在研究了一些关于 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 东西)
然后我想到了手工做,我得出了两个选择:
只需将 food.value = resources.food 放入某种更新中,并希望当我将其迁移到多人游戏环境时它不会产生任何类型的问题
或者做一些更复杂的事情,比如我称之为会更新用户界面的某种委托,理论上效率更高,因为我只更新需要的东西。
自从我做 RTS 以来,我认为价值观会不断变化,所以我对两者都很分歧。让我想坚持已经完成的解决方案
当我讨厌文档的结构、绕过源代码有多么困难,以及文档对于与 CSS 非常相似的行为的冗长感觉时,这会变得更加紧张
在不依赖 Unity Editor 的 UI 工具包中是否有 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);
}
更新: 还添加了用于取消绑定属性的扩展方法,并使 BindProperty 立即更新元素上的文本。
我的方式,也许不是更好,该类直接实现绑定解决方案并使用 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&guid=710cabf677cea49439f4f5c169631b72&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('project://database/Assets/publish/background.png?fileID=2800000&guid=d21a7e330f5e8964ca379e4865e84764&type=3#background');" />
</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");
}
}