实现需要实现V\VM的分离:
相信有网页前端经验的朋友相对于MVVM已经相当的熟悉了,如果不熟悉的话也可以自行百度一下;但是前端的MVVM概念是较难照搬到游戏开发中的,毕竟两者面对的问题区别较大。但是我觉得V(view)和VM(viewModel)这两个概念是可以套用到游戏开发中来的:V继承MonoBehaviour,控制显示及MonoBehaviour相关事务,它根据VM的数据自动做出相应变化——也就是说,VM为纯逻辑代码,V为MonoBehaviour代码。
但是这似乎在通常环境下较难实现,究竟V要如何能自动更新状态呢?这里就需要RX登场了~
首先,在Unity中使用RX需要导入UniRx库,可以到AssetStore中搜到,或者去GitHub:https://github.com/neuecc/UniRx下载后手动导入;
UniRx库除了基础的.NET RX标准API以外,还带有一些针对Unity的API,在整理完.NET RX学习笔记后会继续介绍这部分的API……(真的有整理完的那一天么?= =b)
UniRx库导入到项目以后,我们只要在文件上加上“using UniRx;”即可,像这样:
现在就可以复习一下基础知识了,比如:
好的,能跑,没问题~那么我们现在就要开始利用RX响应式的特征分离V和VM了~(至于为什么要游戏中,至少是Unity中要分离、分离有什么好处就不细展开了,这里只讨论实践部分)
首先我们来设计VM(ViewModel)。为什么要先设计ViewModel呢?因为ViewModel是作为可观测物(Observable)让观察者,也就是View来订阅的。在这里我对VM的构想是这样的:
首先用于存放属性,属性的类型、数量不能写死;
其次为了资源管理需要统一处理订阅物生命周期;
因此在写VM之前需要先写一个属性类。这个属性类我是这样写的:
ViewModel作为属性的所有者,属性的订阅和生命周期都在其所有者内统一处理
这里之所以选用泛型的Subject是因为属性也有可能存在需要参照其他属性进行变化的情况,而Subject即是可观测物又是观察者的特性能满足这一要求,因此在这里,我们将一个Subject<T>作为属性的可观测物,并且在value属性的set中设置了如果新的value与lastValue不同就OnNext,从而实现了“值发生变化自动通知”的功能。
额……等等,你那Subject不是private的么?
没事,Subject不是用于直接订阅的,就算撇开其他不说,直接暴露出来,XX属性.Subject.Subscribe……写起来不是也很麻烦么?这样:
就可以直接从属性订阅,写起来像这样:XX属性.Subscribe……,短了一截,又避免直接暴露。
然后是其他的一些部分:
看到这里可能有朋友会注意到我并没有将 Subscribe 方法归类到 ISubjectProperty implementation 下,看一下 ISubjectProperty 怎么写的:
我确实没有将 Subscribe 方法写到接口里,这是因为SubjectProperty为了具体实现起来方便写成了一个泛型类,而VM需要一个无论任何类型都可以一起存放的字典——有的朋友说了,那你可以用object类型啊,我的考虑是,使用object类型无非两种方式:
1、在VM中用,但这样使用属性时都要AS一下,写起来怪麻烦的;
2、在SubjectProperty中将泛型替换成object——值类型属性会装拆箱消耗;
因此我采用了一个非泛型的接口的方式。这种方式虽然在使用 Subscribe 方法时也需要AS一下,但至少其他几种情况下不需要AS了,写起来不那么麻烦,同时我也写了一个扩展犯法方法以供链式操作:
使用这个方法后的写法像是这样:XX属性.AsSubjectProperty<T>().Subscribe……
当然本人只是一个3D美术,对程序认识有限,有更好方法还望程序大佬赐教。
OK,那么属性类到此结束,下面正式开始写VM部分。VM部分也很简单,首先来构建属性容器:
显而易见,我使用了一个索引器的方式来存放属性,其其实内部是一个字典:
将字典的各种相关方法在外部实现一下调用,这种浆糊代码没什么可看的,就不展开了~
构造函数(ViewModel不继承Mono):
资源管理部分:
AddSubscription方法会在属性类的Subscribe方法中被调用,如果产生的订阅物所属的ViewModel(的Dictionary<int, List<IDisposable>> _subscriptions)中没有,就添加进来统一管理;
同时一个ViewModel有可能接受到多个View的订阅,因此Dictionary<int, List<IDisposable>> _subscriptions的“int”为View的ID(由Unity的API: GetInstanceID()产生),1指定为ViewModel所拥有的属性的订阅物(因为GetInstanceID()产生的ID均为负数。1改为任何正数都可以,有把握肯定不会重复的负数也可以)。
资源管理部分完毕,再就是一些字段,这个就因具体项目而异了,比如我的:
这3个其实都不是很有泛用性,因为假如你不需要例如游戏中途改变操作角色这样的功能其实Controller这样的类大可不必独立出来的,直接混写在VM里也没什么大不了的,与此同时ID也没什么大用处了;同时如果你的V和VM是一对一的,而不是一对多的话references计数也没什么存在意义了,差不多就是这样。
OK!VM部分也结束了,下面来说说V。
View是继承Mono的,这点不多讲了;V需要什么呢?首先是上面提到过的ID,即使你是单对单的关系也需要ID(因为还有属性),我的实现如下:
然后是所属的ViewModel及其相关属性:
set中所用到的SetViewModel方法:
为一系列Mono方法写了相应的可供子类覆写方法,其中OnAwake方法中获取了具体的ID:
资源管理部分:
好啦!代码说明到此为止,可以开始我们的实验啦!撒花!
实验1:角色数值变化:
模拟场景:角色受到攻击、死亡
题外话:RX也可以处理按钮事件哦!
实验1开始辣~~~~鼓掌!!!!!
首先我们先给场景中添加一个按钮~
我们用RX的方式来添加一下它的点击功能(其实用RX还要管理生命周期怪麻烦的,这里只是为了展示一下,平时还是不推荐用RX来处理按钮功能)
点击后:
现在我们来写一个VM来模拟一个角色:
再来写V(本次实验中暂时用LOG来代替具体表现):
在继承了View的子类中,只需要覆写SetUpObservation方法并在方法中进行订阅事宜即可。
回到最开始的temptest3这个脚本,新建两个变量:
然后在start中初始化它们,再改写一下按钮功能(View附着在了一个空物体上):
运行游戏,点击按钮,会看到如下输出:
每点击一次HP就少1,小等于0后宣布死亡,而这一切是V自动Log出来的,VM和类似C的脚本(temptest3)并没有进行任何操作。我们可以再进行进一步的实验:
实验2:根据VM改变现实:
废话不多说,直接上代码:这是VM:
在写V之前,先向场景中添加两个UGUI组建,一个是Image,一个是Dropdown;然后为Image新建一个材质,然后将新建的材质赋予Image:
然后这是V:
同时还需要一个工具类,根据Dropdown的选项来返回颜色:
修改temptest3中的代码,在start中订阅Dropdown并使Image的颜色随Dropdown的选项变化:
运行游戏,就可以看到Image的颜色随Dropdown的选项而变化了:
好辣~写到这里也花了不少时间了~这一次的实验就到这里。大家可以发现这次的实验代码都写的比较马虎,很多地方可以进一步优化,该管理的生命周期也全都没有管理,有兴趣的朋友可以在自己实践的时候进行优化。
这里的RX应用都还非常非常初级,复杂的例如有限状态机的配合使用都还没有展开去说,但是原理总归是这样了,复杂的留待以后再继续(到时可能会结合我之前自己的一个GITHUB上的开源U3D依赖注入框架使用会更加的方便,然而解释说明那个框架又会是另外一个巨大的工程了,所以……可能会鸽啊~呵呵……………………)
最后,毕竟本人只是一个3D美工,关于V/VM分离部分的实现如有什么本人未发现的问题或更好的思路还望程序员大佬们给与指导,谢谢!
暂无关于此日志的评论。