C#Unity框架

在Unity中搭建一套自己的MVVM框架

Spread the love

随着现在的UI系统越来越复杂,以前不用框架纯手撸的做法局限性也变得越来越大。

我现在来讲一讲我做UI中遇到的困难并且是如何改进并且最后做出一套MVVM框架的吧。

事件系统与数值绑定

早在我第一次实习的时候我就已经接触到了事件系统与数值绑定,关于事件系统可以在我前面的文章中看到。但是那时候的事件系统并没有进行过多的封装。

事件系统的链接在这里:事件分发器原理

那么数值绑定就是基于事件系统的一个小技巧了。

例如我在很多地方都要监听金币改变的事件,比如商场、大厅度需要显示玩家当前的金币数,那么我可以在金币属性的setter中触发金币改变的事件,然后在需要的地方进行监听就可以了。UI不需要知道到底是哪里过来的消息,完成很好的解耦。

数值绑定也是MVVM的基础,没有数值绑定也很难做好MVVM。

自动生成View

后来我开始做重度手游了,在重度手游中的UI量是非常大的。

在一套复杂的UI当中,可能变量定义与赋值可能都可以占掉上百行的代码,于是最初的时候我自己写了一个自动创建View代码的工具,帮助开发者不需要手撸变量,也不需要担心UI变动带来的UI路径改变,再也不用担心美术给过来的素材和原来完全不一样。只要代码逻辑改变不大的话要更新一个新的UI还是非常快的。

我们要做的只是将UI上所有需要使用的GameObject的前面标记上一个记号。

很简单对不对。

当我需要使用的时候我只需要定义一个变量并且然后在Awake中调用Init函数就可以将所有的UI变量进行复制,开发者想怎么取用就怎么取用。

虽然会用到反射,但是对于UI的使用场景来说问题并不是很大。实测来看感受不出有什么效率问题。

除此之外,我顺便把按钮的回调之类的也一起加了钩子,按钮的回调函数以一定的命名规范来进行搜索。

虽然引用是0,但是依旧会被按钮引用。

基于流的事件的必要性

在一次UI开发当中,我将事件的概念大量引入了逻辑当中。但是其中包含一个很大的问题,也就是大量事件触发时带来的效率问题。

例如我们在ScrollRect滚动的时候会触发一个OnValueChange事件,这个事件大量地触发了界面更新,导致整个界面很卡。

后来看到了ReactiveEx的思路,也就是基于流的事件,对整个事件流进行处理,不论是筛选或者是其他的。

于是我将事件系统进行了进一步的封装,提高了效率,并且支持了对事件流的操作,我可以控制筛选,或者在一帧当中只处理一次消息。

这就为我后面MVVM大量使用事件提供了基础条件。

在上图中,Text组件绑定了Model中的Money属性,但是每一帧只接受一次消息,避免了大量的修改消息导致界面效率下降。

具体的基于流的使用方法在前面的文章也有提到过:基于池的事件扩展

由于我自己实现的事件扩展并不像UniRx是基于线程轮询的,所以效率较低,所以重写了协程以获取更高的效率。

在之前的文章中也有提到相关内容:自己实现一个零GC的高效率协程

Model分离以及由此想到的依赖注入

通常情况下一个UI的数据是会与外部分享的,所以当我做完一个UI的时候有时候别人会向我要这个数据的接口,如果我将Model放入了UI中,

UI当时已经被销毁了,所以就不得不将变量写成静态的,导致代码非常难看。你会看到,数据会从一个UI类中取出来。

(因为当时还没有太多框架的概念,所以我当时嫌麻烦就直接这么做了,确实是很不好的做法)

最好的方式是将数据层抽出到另外一个类当中。

在最早的时候曾经使用过一个MonoBehavior作为View,而Model作为一个对象存在于View中。

这样的做法依旧是导致了UI销毁后无法获取数据的窘境。

所以光光是将数据层抽出到类中还是不够的,这个数据必须长期存在于内存当中。

这就导致了一个问题:我如何去找这个数据?

我们通常的做法是整个游戏有个总的Manager,例如GameManager,之后GameManager下面有很多不同的Manager,例如:PlayerManager、BattleManager等等。

我每次需要取的时候就必须用长长的一段代码去取一个值:

GameManager.playerManager.playerbag.money。

太累了不是吗?并且还导致了大量代码与Manager之间的依赖,我们一旦遇到大的结构变动就变得寸步难行。

恰好当时有在看Java当中Spring的内容,我发现依赖注入恰好就是合适的解决方案。

依赖注入的原理其实和事件分发器的原理很像,IoC容器也是一个中介者,帮助类找到各种各样的对象。

例如我需要一个View对象,它会帮助我找到,我不用操心任何中间逻辑。大大降低了系统的耦合度,并且也方便了开发以及分工。

假设别人需要我的一个属性,我只需要在容器中进行注册,同事就可以很轻松地获得我的属性而不需要了解背后的任何细节。

上图为对对象进行注册。

上图为注入。不过不要忘了提供上下文Context。

值得一提的是,我们可以控制所获取的对象是否为单例,如果是单例的话就直接返回已经生成过的值,否则重新生成一个对象。

说到底,就是自己实现了一个全局工厂。

该部分的具体实现,之后考虑在写一篇文章,因为也用到了反射,所以在想是否可以进行提前反射来减少消耗,不过这也是实现功能的后话了。

过早的优化是罪恶的。

进行过封装事件进行数据绑定

在封装了基于流的事件之后我对事件以及属性进行了可监听以及触发事件的扩展。

首先是属性,每当我设置值的时候都会进行事件的触发,并且这已经是基于流的事件,所以我可以控制其触发事件的频率、筛选事件等等。

要实现这一点,首先得对属性进行封装。

get为获取事件。set为设置事件。

数据绑定的原理其实就是这么简单。就是获取与设置的时候进行处理。
相对的,界面的事件绑定也是这样:

其中GetSetListenable就是将属性作为可监听对象,然后对属性进行监听,是对原本事件系统的简单封装。

不要忘了在界面销毁的时候卸载,这里的绑定使用的是自动卸载,当GameObject Destroy的时候自动卸载事件。

我们进行数据绑定的时候就只需要调用每一个组件的扩展方法即可,倘若默认的处理方法不够,则可以使用自定义的处理方式。

上图就是将两个组件绑定到了属性money上。

当输入框input的值改变的时候将会改变money的值,如果money改变,也会改变txtResult与inputInputField的内容。

MVVM的框架已经初具雏形了。

再论代码的自动生成

绑定部分还是在构想阶段,因为考虑到绑定过程对不同的UI对象与属性对象有不同的操作,如果使用反射来做这一步的话可行性如何还不是很清楚。

不管怎么样,构思一下还是可以的。而且Model的自动生成还是比较简单的。

之前有说到自动生成View的代码,现在我们来看看如何生成Model代码。

这一步只是在原本View的基础上进行了追加。

现在我们需要在绑定的UI上标记上绑定的属性名称。

以@分开GameObject名称与属性,属性又由属性名与属性类型构成。

自动生成的代码如下。

至此,我已经讲完了整个MVVM的组成(当然不包含未实现的)

不过后期肯定会有更新,因为要是不是自动绑定的话,生成Model根本就是没啥太大意义的。

自动绑定思考

在WPF中,完成了完全的界面与代码分离,利用了XAML进行了数据绑定。

当然我也希望我的框架能够自动生成Xml来进行数据绑定,实现起来其实也不难的,主要问题还是存在于绑定的语法定义。

如何才能更好地对不同组件与变量的绑定保持一个较好的灵活性呢。

特别是,如果我不想使用默认操作的时候我该如何处理这个事件。

简单的实现方法有直接反射类中的对应方法,但是不像XAML那样有IDE支持,自己实现的Xml绑定肯定还是得开发人员自己去找函数,并且反射的效率还有待考证。

还有一种方法就是通过Irony来将字符串解析成Expression Tree,然后Compile之后动态执行,这种方案是不错的,但是只支持C#4.0并且在IOS上能不能用还要另说(那你说它干嘛)

 

除了Xml绑定的另一种思路是与生成View的思路一样,生成一个Binder代码,在编辑器时反射。

这也是个不错的思路,但是仔细想想,之前的所有用CodeDom生成的代码都是比较简单的,如果要实现一个自动绑定的功能的话其实工作量还是不小的。

针对每一个UI组件都要有一个专门的生成代码。

而且这种方式,感觉一点也不浪漫!

Demo概览

那么说了这么多,我们还是来看看Demo。

首先我们创建一个TestUI并且集成与NormalUI(这是之前我写的UI框架,目前已经集成了IOC的功能)挂到一个GameObject上并且开始搭界面。

做界面的同时我们还要对UI组件进行标记。

在Inspector中我们的编辑器扩展能够帮助我们自动创建Prefab。

然后依次点击按钮自动生成View、Model、绑定代码(暂未完成)。

我们现在有三个类,分别是TestUI(VM)、TestUIView(V)、TestUIModel(M)。

为了在TestUI中获得V和M我们需要建立TestUIView和TestUIModel的变量,并且提供相应的Context。

这个Context需要我们自己编写,将获取变量的方式写入,并且标记为Bean

容器会自动根据类型寻找方法。

在我的框架中必须调用Awake,因为容器是在Awake中注入对象的。

由于目前我还没有编写自动绑定的功能,所以现在我们需要手动来进行绑定。

我们可以将v中的任意UI组件与m中任意属性进行绑定。

如下:

现在我们将整个程序进行运行。

可以发现当我们改变输入框Text也会随着一起改变。

消息流向为:input数据->发送消息给数据层->money改变->money发送消息给input与text->UI做出响应。

其中PoolInOneFrame只是为了展示扩展事件的功能,在此处并非必须。

我么可以通过流来控制整个事件的进程。

至此,我们的MVVM也初步完成了,除了自动绑定还需要继续开发。

在完成之后一定会和大家分享思路。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.