使用UnityWeld实现Unity中的前端UI绑定

不知不觉中写的博客的内容竟然有些“聚类”化了……之前有说过ASP.NET中的数据绑定,这次来说说Unity中的UI数据绑定。

Unity中的UI设计实际上也是一个棘手的问题,因为uGUI默认的实现模式就像是WinForm,需要大量的后台胶水代码去处理数据。当UI变得复杂时,相关处理代码也会越来越繁杂冗余,令人生厌。有WinForm就有WPF,如何使用先进的MVVM模式和绑定来实现Unity中的UI呢?AssetStore中已有不少相关素材,但大多是收费的,在此我只推荐两个免费的产品:UnityWeldMarkLight(XUML)。后者留待以后再述,因为它比较heavy,在较小的项目或是老项目中导入它不是很划算(需要对老项目进行大量改造和重写),只建议从头开始新项目时选用。本文介绍的UnityWeld更适合老项目改造后使用。

为什么要使用绑定

MVVM模式的好处网上说的肯定比我清楚,而且上一篇讲ASP.NET绑定的时候已经说过了。这里以我的项目举一个例子:

在“猫娘壁纸”中,有一个开关(Toggle)是控制是否可以拖拽猫娘的,当启用时,你可以用鼠标把猫娘拖拽到屏幕的其他位置,而禁用时则无法被拖拽。在uGUI中,当用户点击开关时是如何处理事件的呢?

这是一个OnValueChanged(bool)类型的事件,要绑定到一个签名相符的方法上,其中bool的值是当前点击完后的状态(比如开关本来是关/false的,用户点了一下,就启用了它,此时传入的bool为true)。

在我们绑定的SwitchDragMode方法中,我们要从内部启用猫娘的拖拽,然后进行保存设置等操作……

    public void SwitchDragMode(bool canDrag)
    {
        _emote?.SetDragActive(canDrag);
        if (_dragToggle != null)
        {
            PlayerPrefs.SetInt("Drag", _dragToggle.isOn ? 1 : 0);
            PlayerPrefs.Save();
        }
        _emote?.SavePlayerPosition();
    }

这里已经出现了一个麻烦事:如果我们要记住现在是否能拖拽,必须准备一个属性(变量),在方法中将bool保存下来,否则就要在场景中去找到这个开关(可以在编辑器中将Toggle拖拽到Script中的变量槽,也可以用代码GameObject.Find("DragToggle").GetComponent<Toggle>())并保存对于它的引用,然后用Toggle.isOn来取得现在开关的状态。这一过程就已经出现了很多令人不爽的胶水代码了。如果我们程序中还有另一个地方会更改能否拖拽——比如一个“初始化设置”按钮,那么就注定要去寻找并保存Toggle的引用了,因为此时我们必须要去手动设置它的状态。

接下来出现了新需求:

此开关位于屏幕右上方,有些用户将任务栏放在屏幕右侧或者上侧,则会挡住这个开关。于是我放置了两份相同的开关,分别位于左下和右上,视用户的选择显示其中一个。也就是说,同一个功能现在有两个入口。(这实际上是非常常见的,比如你要保存一个文件,既可以按Ctrl+S,也可以点工具栏中的存档图标,还可以从菜单栏中选择“文件-保存”。WPF解决这一问题就是采用Command——命令和命令绑定。)此时该如何保证两个开关的状态一致(即当一个开关状态变为true时,另一个也应该同时变为true)?

以上问题,都可以通过双向绑定来解决。类似于WPF或是之前文中提到过的DotVVM,我们可以将这(两)个开关绑定到一个bool类型的属性:当属性更改时自动通知UI改变状态;当UI被点击时自动更改属性。

    [Binding]
    public bool CanDrag
    {
        get { return _canDrag; }
        set
        {
            if (value == _canDrag) return;
            _canDrag = value;
            PlayerPrefs.SetInt("Drag", _canDrag ? 1 : 0);
            PlayerPrefs.Save();
            _emote?.SavePlayerPosition();
            OnPropertyChanged();
        }
    }

UnityWeld的使用

首先,从GitHub上下载UnityWeld的代码。

有两种使用方式:一种是UnityWeld.dll放到你项目的Plugins文件夹中,直接作为类库使用;另一种是将源码的UnityWeld_Editor部分复制到你项目中的Editor文件夹,将UnityWeld部分复制到你项目中(如Scripts文件夹)。如果有较强的代码洁癖或是IDE加载比较卡,可以用dll,否则建议用源码,因为方便日后按照自己的需求进行修改。

在项目中,创建一个ViewModel类,它实现INotifyPropertyChanged接口:

[Binding]
public class MainViewModel : MonoBehaviour, INotifyPropertyChanged

注意在类上使用[Binding]标签。

此时如果你有使用R#,请使用R#的Implement INotifyPropertyChanged功能,R#会自动帮你实现OnPropertyChanged方法,而且后续写绑定用属性的时候可以用To property with change notification,自动生成更改通知模式的属性,非常好用。如果没有R#,使用VS自带的“实现接口”(Alt+Enter)也是可以的。

然后创建一个bool型的更改通知模式的属性CanDrag,代码参见上文。(如果使用R#,上面需要自己写的只有三行。)

随后到Unity编辑器中,将ViewModel脚本挂到Canvas(所有uGUI控件的Parent)上。

找到我们需要使用MVVM模式的开关Toggle,添加TwoWayPropertyBinding组件:

上图中还出现了我们以前使用的OnValueChanged,现在我们已经不需要使用它了,因此将以前的设置清除。

View event中选择view(视图,此处即Toggle)中会触发属性更改的事件,这里是onValueChanged;

View property选择属性的更改会影响到view的哪个属性,这里选择isOn(即CanDrag设为true时,Toggle.isOn也将更改为true,即开关进入开状态);

View-model property即为对应的属性,设置为ViewModel.CanDrag(切记为ViewModel和CanDrag添加[Binding]标签,否则这里会找不到)。如果你需要相反的bool设置——当CanDrag为true时让Toggle.isOn为false,可以将View adapter设置为BoolInversionAdapter(你也可以仿照着实现更高级的adapter,比如将bool转为string以对应string型的属性)。

至此就大功告成了。对于上面提到的“两个开关控制一个属性”问题,我们只需对TwoWayPropertyBinding组件进行复制(Copy Component),然后粘贴到另一个开关之上就可以了。此时两个开关都对应着同一个属性,自然会同时发生变化。而初始化设置的操作也只需设置CanDrag的值,设置后两个开关的状态都会自动更新到对应的状态。上面提到的什么“SetDragActive”之类的UI方法也可以删掉了,也无需再寻找、保存Toggle的引用了。

下图是未使用MVVM模式时,在游戏初始化时寻找和保存各类UI引用的代码,在UI元素较多时是很繁琐的。当使用MVVM模式后,这些代码大部分都被简化掉了。

此外,以往在保存设置时不得不使用Toggle的引用来获取当前状态,现在也可以换成可读性更好的属性了:

    public void OnApplicationQuit()
    {
        PlayerPrefs.SetInt("Drag", _dragToggle.isOn ? 1 : 0);
        PlayerPrefs.SetInt("Quality", _qualityToggle.isOn ? 1 : 0);
        PlayerPrefs.Save();
    }

    public void OnApplicationQuit()
    {
        PlayerPrefs.SetInt("Drag", CanDrag ? 1 : 0);
        PlayerPrefs.SetInt("Quality", HighQuality ? 1 : 0);
        PlayerPrefs.Save();
    }

 

其他

除了上面列举的双向绑定,你还可以使用单向绑定、事件绑定(针对按钮等)、Dropdown绑定(针对下拉框)、Active绑定(通过bool属性切换GameObject是否Active)等。其中我特别推荐针对slider使用双向绑定,我一开始用uGUI的slider的时候就发现用起来很麻烦,用上MVVM一定会简洁很多,现在看来确实如此。

 

添加评论

Loading