前言 写这篇博客的目的不再是为了一个一个单独去学习了解四人帮的设计模式(以前看四人帮的设计模式时就是这样,并没有结合游戏开发深入思考这些设计模式的真正用处和好处)。
本篇博客的侧重点是针对实战游戏开发过程中用到的设计模式进行深入学习了解(能够运用在实际项目中的东西才是真正能发挥价值的 )。
下面几个问题,是本篇博客需要解答的疑问:
为什么需要****设计模式?
***设计模式会给游戏开发带来什么好处?
什么情况下适合在实际项目中使用***模式?
接下来本文将结合《Game-Programming-Patterns》书籍以及项目实战开发过程中遇到的问题就游戏编程模式而言进行深入学习和分析理解。
《Game-Programming-Patterns》的作者是Bob Nystrom,一个在EA工作了8年的游戏程序开发者。 下面给出官方网站链接:Game-Programming-Patterns
Note: 后面加上””号的内容表示是从书里截取的内容。
游戏架构 为什么这里要提游戏架构了? 作为程序员在写代码的过程中,会发现整个游戏有很多模块,这些模块各自负责不同的功能,所有模块整合到一起才组成了完整的游戏框架。
一个游戏并不是把所有的代码都编写在一个main函数里就能完成的,这样的代码既不具有可读性也不具备扩展性以及维护性。
而游戏架构就是为了让游戏开发变得高度可扩展,可读性高,降低维护成本等。在一个好的游戏架构上编写实现一个功能可能只需要修改很少几处或者添加很少几处代码即可完成。而不好的游戏架构可能会导致你编写了上千行代码去实现一个小功能(这里无论是维护成本还是理解成本都是不能接受的)。
“好的设计意味着当我作出改动,整个程序就好像正等着这种改动。我可以加使用几个函数调用完成任务,而代码库本身无需改动。”
解耦对于上面提到的扩展性和维护成本起到了关键作用,后面会详细学习了解。
下面再引用一句作者对于软件架构的目标的话: “最小化在编写代码前需要了解的信息。”
落实到真正的游戏开发过程中,我们往往要考虑开发周期,开发成本,游戏设计复杂度等,不是说任何游戏开发都往复杂的好的游戏架构上去设计就是正确的,也要结合实际情况分析。(但对于大型游戏开发,好的架构一般来说是必不可少的)
下面是作者给出的几个建议;
“抽象和解耦让扩展代码更快更容易,但除非确信需要灵活性,否则不要在这上面浪费时 间。”
“在整个开发周期中考虑并为性能设计,但是尽可能推迟那些底层的,基于假设的优化, 那会锁死代码。”
“快速地探索游戏的设计空间,但不要跑的太快,在身后留下烂摊子。毕竟,你总得回来 打扫。”
“如果打算抛弃这段代码,就不要尝试将其写完美。摇滚明星将旅店房间弄得一团糟,因 为他们知道明天他们就走人了。”
“如果你想要做出让人享受的东西,那就享受做它的过程。”
游戏设计模式 接下来我们将结合游戏开发,实战分析学习设计模式带来的好处和实际运用的地方。
命令模式 首先看看作者是如何定义命令模式的: “命令是具现化的方法调用。”
再看看四人帮是如何定义命令模式的: “Encapsulate a request as an object, thereby letting users parameterize clients with different requests, queue or log requests, and support undoable operations”(将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化; 对请求排队或记录请求日志,以及支持可撤销的操作。)
看完定义后更加糊涂了,让我们还是结合实例来学习理解。 假设我们通过不同的按键点击去控制玩家的行为,不考虑任何扩展性的前提下我们可能写出下面的代码:
1 2 3 4 5 6 7 8 9 10 11 void InputHandler::handleInput(){ if (Input.GetKeyDown(KEY_A)) { Attack(); } else if (Input.GetKeyDown(KEY_SPACE)) { Jump(); } }
上面的硬编码确实实现了我们的功能需求,但当我们想改变按键所代表的行为时(比如玩家配置修改按钮功能),我们会发现上面的代码没法满足需求,因为我们硬编码写死了指定按钮的行为。
我们需要支持按钮动态绑定行为的功能,这时命令模式就起作用了,命令模式把游戏行为封装成对象,通过动态绑定不同的命令可以实现同一按钮实现不同的行为。 首先我们需要定义一个命令基类:
1 2 3 4 public absctract class Command { public abstract void execute () ; }
然后实现对应行为的命令类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class AttackCommand : Command { public override void execute () { Attack(); } } public class JumpCommand : Command { public override void execute () { Jump(); } }
此时我们响应输入不再是直接执行特定行为,而是执行绑定在按钮上的命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class InputHandler { private Command Key_A; private Command Key_Space; public void handleInput () { if (Input.GetKeyDown(KEY_A)) { Key_A.execute(); } else if (Input.GetKeyDown(KEY_SPACE)) { Key_Space.execute(); } } }
玩家配置按钮行为,在代码里的表现就只是对于Key_A和Key_Space进行不同的行为命令绑定即可,因为我们把请求对象化 了(四人帮里提到的定义),所以可以通过动态绑定对象来绑定不同的命令实现动态绑定行为。
上面的代码还有个问题就是特定命令里的行为并不知道需要表现指定行为的对象,上面的写法是属于全局访问的一种形式,这样做耦合度太高且不灵活。 还记得四人帮定义里提到请求对象化后可以对客户参数化 ,这里的客户就是我们要服务要表现的行为对象。
因为我们把请求对象化 了,那么我们对于请求的执行添加一些额外的信息也就是对于命令进行传参的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public abstract class Command { public abstract void execute (GameActor actor ) ; }; public class JumpCommand : Command { public override void execute (GameActor actor ) { actor.Jump(); } } Command* command = inputHandler.handleInput(); if (command){ command->execute(actor); }
这样一来我们的命令请求就知道应该是对谁做行为操作了。
通过命令模式,我们在按钮和角色操作之间增加了一层重定向,使我们可以通过按钮操作任何角色的行为(只需向命令传入不同的角色即可)。
在真正的实战游戏开发中,角色的AI会使用命令模式,这样一来可以对于所有的角色重用命令行为,而且通过模拟命令队列,我们可以实现AI行为队列的功能(一个行为接一个行为)。这个后续会结合Behavior Designer插件学习来进一步深入了解:
待续……
Note:
我们不止可以通过定义类(Command)实现请求对象化,我们也可以通过闭包的形式把请求转化成可返回的函数对象来实现请求对象化。这里需要了解第一公民函数 等概念。
命令模式可以让我们轻易做到撤销和重做的功能。
框架模式 关于框架模式最初了解过MVC,只知道是为了解耦数据,显示以及控制逻辑的一种模式。 后来陆陆续续了解到还有MVP,MVVM等模式。 为什么会有这么多MV**的框架模式了? 核心思想是通过解决M和V的耦合问题来实现界面分离。
接下来我们重点要学习了解MVC和MVVM模式以及实战运用,MVP会简单带过。
参考博客 :谈谈对MVC、MVP和MVVM的理解 谈 MVC、MVP 和 MVVM 架构模式 Unity 应用架构设计—— MVVM 模式的设计和实施 (Part 1) mvc的实现
MVC MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。
MVC只是一种框架模式,针对不同的平台环境的实现方式会有些区别,这里本人的理解如下:
模型(Model): 数据结构以及数据处理等。
视图(View):专注于显示,比如前端UI显示
控制器(Controller):连接(解耦)模型和视图,如处理视图的请求,更新模型数据,通知视图变化等
这里我们结合实例动手实现一个简单的MVC模式来加深理解: 需求: 用户通过点击按钮触发一个随机数显示在文本上。
分析事例中的MVC: 视图(View): 负责显示最终结果。
模型(Model): 负责存储随机数数据。
控制器(Controller): 负责响应View的点击事件,处理随机数逻辑,修改Model里的随机数数据,通知View更新显示。
这里Model和View之间的更新通知我们会用到监听者模式,Model是发布者,视图是订阅者。 接下来看下实际的代码设计:
监听者模式部分: ObserverBase.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 using System.Collections;using System.Collections.Generic;using UnityEngine;public interface ObserverBase { void update (SubjectBase subject ) ; }
SubjectBase.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 using System.Collections;using System.Collections.Generic;using UnityEngine;public class SubjectBase { private List<ObserverBase> mObserverList; public SubjectBase () { mObserverList = new List<ObserverBase>(); } public virtual void addObserver (ObserverBase observer ) { mObserverList.Add(observer); } public virtual void deleteObserver (ObserverBase observer ) { mObserverList.Remove(observer); } public virtual void notifyAllObserver () { foreach (var obesrver in mObserverList) { obesrver.update(this ); } } }
MVC部分: UIView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class UIView : MonoBehaviour , ObserverBase { public Text mTxtRandomNumber; public Button mBtnRandomNumber; void Start () { mBtnRandomNumber.onClick.AddListener(UIController.Instance.OnBtnRandomNumberClick); } public void update (SubjectBase subject ) { refreshView(subject); } private void refreshView (SubjectBase subject ) { var uimodel = subject as UIModel; mTxtRandomNumber.text = uimodel.mRandomNumber.ToString(); } }
UIModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 using System.Collections;using System.Collections.Generic;using UnityEngine;public class UIModel : SubjectBase { public static UIModel Instance { get { if (mInstance == null ) { mInstance = new UIModel(); } return mInstance; } } private static UIModel mInstance; public int mRandomNumber; public void randomNumber () { mRandomNumber = Random.Range(0 , 100 ); notifyAllObserver(); } }
UIController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 using System.Collections;using System.Collections.Generic;using UnityEngine;public class UIController { public static UIController Instance { get { if (mInstance == null ) { mInstance = new UIController(); } return mInstance; } } private static UIController mInstance; private UIController () { } public void OnBtnRandomNumberClick () { UIModel.Instance.randomNumber(); } }
初始化相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 using System.Collections;using System.Collections.Generic;using UnityEngine;public class GameProgrammingPattern : MonoBehaviour { private UIModel mModel; private UIView mView; void Awake () { mModel = UIModel.Instance; mView = this .transform.GetComponentInChildren<UIView>(); } void Start () { mModel.addObserver(mView); } }
可以看到我们把UIView和UIModel设计成了监听者模式用于UIModel变化时通知UIView刷新显示。 通过划分V和M,把视图和数据严格分离开,视图只关心显示,模型只关心数据,其他所有的操作变化都是通过控制器去处理。
分析:
上面的MVC里V和M并没有完全解耦,视图依然依赖于模型的数据来显示(当然我们可以通过回调注册替换监听者模式的形式去实现V和M的完全解耦)。(参考: Unity3D用MVC框架思想实现的小例子 )
V和M的解耦,让我们可以重复利用M和V,只要控制器处理好V和M逻辑即可。(比如View不变,改变Model数据来源,我们重新编写一个新的Contoller去处理View和新的Model之间的逻辑关系就能实现对View的重用。反之Model重用同理。)
正因为V和M的解耦,所有逻辑相关处理都在C里,使得C看起来过于臃肿。
问题:
UI操作和数据原本就是大量交互在一起的,使用MVC使V和M分离,使得代码显的过于复杂,Controller这一层过于臃肿,对于后期维护并不友好。
游戏开发中,大部分时候UI变化很大,View层的重用不太现实。
下面给出知乎上对于游戏里使用MVC模式的讨论(本人比较认同flashyiyi的分析):如何在Unity中实现MVC模式?
结论: 游戏开发需要根据项目需求制定合理的框架设计,并非一种MVC就能包治百病,过度的设计有时候会带来可读性和维护性上的大大降低,Pure MVC并不适用于大部分游戏开发。
MVP MVP用展示器代替了控制器,而展示器是可以直接更新视图,所以MVP中展示器可以处理视图的请求并递送到模型又可以根据模型的变化更新视图,实现了视图和模型的完全分离。
MVVM MVVM是MVP更进一步的发展,把软件系统分为三个基本部分:模型(Model)、视图(View)和视图模型(ViewModel),关系如下图所示:
模型(Model): 数据结构,基础数据等(不关心业务逻辑)。
视图(View):专注于显示,比如前端UI显示
视图模型(ViewModel):连接模型和视图,暴露视图所关心的数据和属性,负责修改Model数据,视图模型和视图是双向绑定的 。
从上面的关系图以及介绍,可以看出MVVM里V和M通过VM完全隔离开来,V的更新通过V和VM的Data Bidning(数据绑定)以及Command模式等形式触发更新显示响应等。M的修改是通过VM来操作,V只关心和VM绑定的数据用于显示,真正的数据是在M里(比如M里存的是一个玩家的名字,VM暴露给V的数据可能是一个地址+名字格式的数据形式)。
MVVM优缺点(来源:MVC、MVP、MVVM架构分析与比较 ): 优点: 1、低耦合。View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。 2、可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。 3、独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,生成xml代码。 4、ViewModel解决MVP中View(Activity)和Presenter相互持有对方应用的问题,界面由数据进行驱动,响应界面操作无需由View(Activity)传递,数据的变化也无需Presenter调用View(Activity)实现,使得数据传递的过程更加简洁,高效。
缺点: 1、ViewModel中存在对Model的依赖。 2、数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。 3、IDE不够完善(修改ViewModel的名称对应的xml文件中不会自动修改等)。
关键词:
Data Binding(数据绑定)
Event Based Programming
Command
Code Behind
后面我们会实战实现一套MVVM里深入理解里面的相关概念,主要参考博客:Unity 应用架构设计—— MVVM 模式的设计和实施 (Part 1)
Unity MVVM实现 要是在Unity里实现MVVM架构,那么我们必须先实现一套Unity里的Data Binding。
Data Binding in Unity 如何在Unity里实现Data Bidning? Data Binding的核心是在修改数据时能触发回调通知,能将数据和回调绑定起来。 明白了这一点,回顾C#里设置数据一般是通过方法或者属性或者直接访问public成员变量,要想实现回调触发,我们可以通过C#里的Property设置(set)的形式去确保回调相应。
基础实现 实现一个可触发回调的Property属性抽象: BindableProperty.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 using System.Collections;using System.Collections.Generic;using UnityEngine;public class BindableProperty <T > { public delegate void ValueChangedDelegate (T oldvalue, T newvalue ) ; public ValueChangedDelegate OnValueChanged; public T Value { get { return mValue; } set { T oldvalue = mValue; mValue = value ; ValueChanged(oldvalue, mValue); } } private T mValue; private void ValueChanged (T oldvalue, T newvalue ) { if (OnValueChanged != null ) { OnValueChanged(oldvalue, newvalue); } } }
这样一来我们定义VM的Property时就可以按如下方式定义,然后让绑定V里的View到VM里对应的Property上即可:
1 2 public readonly BindableProperty<string > Name = new BindableProperty<string >();public readonly BindableProperty<int > AgeDetail = new BindableProperty<int >();
按照VM负责和V的数据绑定以及负责M的修改,我们可以定义ViewModel如下: MVVMViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class MVVMViewModel { public readonly BindableProperty<string > Name = new BindableProperty<string >(); public readonly BindableProperty<int > Age = new BindableProperty<int >(); private MVVMModel Model; public void initializeModel (MVVMModel m ) { Model = m; Name.Value = Model.Name; Age.Value = Model.Age; } public void saveModel () { Model.Name = Name.Value; Model.Age = Age.Value; } }
按照V的定义只关心视图,我们可以定义View如下: MVVMView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class MVVMView : MonoBehaviour { public InputField mInputFieldName; public Text mTxtName; public Text mTxtAge; public Button mBtnSave; private MVVMViewModel mViewModel; public void bindViewModel (MVVMViewModel vm ) { mViewModel = vm; } public void initialize () { #region Bind VM to V Part mViewModel.Name.OnValueChanged = onNameChanged; mViewModel.Age.OnValueChanged = onAgeChanged; #endregion #region Bind V To VM mInputFieldName.onValueChanged.AddListener(onInputNameChanged); mBtnSave.onClick.AddListener(onSaveClick); #endregion } #region Bind VM to V Part private void onNameChanged (string oldname, string newname ) { mInputFieldName.text = newname; mTxtName.text = newname; } private void onAgeChanged (int oldvalue, int newvalue ) { mTxtAge.text = newvalue.ToString(); } #endregion #region Bind V To VM private void onInputNameChanged (string newname ) { mViewModel.Name.Value = newname; } private void onSaveClick () { mViewModel.saveModel(); } #endregion }
按照M的定义只关心数据结构定义,我们可以定义Model如下: MVVMModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class MVVMModel { public string Name { get ; set ; } public int Age { get ; set ; } public MVVMModel () { Name = "TonyTang" ; Age = 28 ; } }
接下来测试MVVM里的Data Binding: GameProgrammingPattern.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 using System.Collections;using System.Collections.Generic;using UnityEngine;public class GameProgrammingPattern : MonoBehaviour { #region MVC Part private UIModel mModel; private UIView mView; #endregion #region MVVM Part private MVVMModel mMVVMModel; private MVVMView mMVVMView; private MVVMViewModel mMVVMViewModel; private WaitForSeconds mAgeChangeFrequency; #endregion void Awake () { initMonoScripts(); #region MVC Part mModel = UIModel.Instance; mView = this .transform.GetComponentInChildren<UIView>(); #endregion #region MVVM Part mMVVMViewModel = new MVVMViewModel(); mMVVMModel = new MVVMModel(); mMVVMView = this .transform.GetComponentInChildren<MVVMView>(); mAgeChangeFrequency = new WaitForSeconds(5.0f ); #endregion } void Start () { #region MVC Part mModel.addObserver(mView); #endregion #region MVVM Part mMVVMViewModel.initializeModel(mMVVMModel); mMVVMView.bindViewModel(mMVVMViewModel); mMVVMView.initialize(); CoroutineManager.Singleton.startCoroutine(changeViewModelAgeCoroutine()); #endregion } private void initMonoScripts () { this .gameObject.AddComponent<CoroutineManager>(); } IEnumerator changeViewModelAgeCoroutine () { while (true ) { yield return mAgeChangeFrequency; mMVVMViewModel.Age.Value = Random.Range(0 , 100 ); } } }
我们的测试UI如下: InputField里动态输入文字,会看到右边的文本动态跟新了,同时下方的Age文本因为携程动态修改数据刷新显示: 上面的表现可以看出,我们通过绑定V和VM里的数据实现了动态修改VM数据后,V自动跟着刷新显示,V变化VM自动更新,做到了数据驱动刷新显示,完全隔离了V和M。
解耦V和VM 上面的实现有一个比较严重的问题,就是V和VM严重的耦合了,V里通过保存一个指定VM类型的实例对象来访问VM,如果我们想将V绑定到另一个VM的话,就发现代码需要改动才能支持,同时上面的代码是没有处理动态绑定VM时对原始VM动态绑定部分的解除。这里为了改进这一点,我们需要了解几个概念:
依赖倒置原则(DIP)高层模块不应依赖于低层模块,两者应该依赖于抽象。 抽象不不应该依赖于实现,实现应该依赖于抽象。
控制反转(IoC)控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取。
依赖注入(DI)依赖注入(DI),它提供一种机制,将需要依赖(低层模块)对象的引用传递给被依赖(高层模块)对象。
引用的文章里对几个核心概念做了如下总结,这里直接搬过来:依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念)。 控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。 依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。 IoC容器:依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)。
上面的核心概念是高层模块不应该直接依赖底层模块,而是依赖于抽象,然后通过依赖注入的形式实现反转依赖,解耦高层模块和底层模块。
针对解耦V和VM,让我们修改上面的实现: 为了解耦V和VM,根据依赖倒置的原则,我们需要让V依赖于VM的接口而非具体的类,同时通过依赖注入的形式传入实现控制反转。 IMVVMView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 using System.Collections;using System.Collections.Generic;using UnityEngine;public interface IMVVMView { IMVVMViewModel mMVVMViewModelContext { get ; set ; } }
IMVVMViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using System.Collections;using System.Collections.Generic;using UnityEngine;public interface IMVVMViewModel {}
为了实现ViewModel可动态绑定修改,我们还需定义一个View的父类,用于实现动态响应ViewModel的绑定修改: MVVMBaseView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class MVVMBaseView : MonoBehaviour , IMVVMView { protected readonly BindableProperty<IMVVMViewModel> ViewModelProperty = new BindableProperty<IMVVMViewModel>(); public IMVVMViewModel mMVVMViewModelContext { get { return ViewModelProperty.Value; } set { if (!mIsInitialized) { OnInitialize(); mIsInitialized = true ; } ViewModelProperty.Value = value ; } } protected bool mIsInitialized; public MVVMBaseView () { } protected void OnInitialize () { ViewModelProperty.OnValueChanged += OnBindingContextChanged; } protected virtual void OnBindingContextChanged (IMVVMViewModel oldvm, IMVVMViewModel newvm ) { } }
修改原来的MVVMView实现: MVVMView.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class MVVMView : MVVMBaseView { public InputField mInputFieldName; public Text mTxtName; public Text mTxtAge; public Button mBtnSave; public MVVMViewModel ViewModel { get { return (MVVMViewModel)mMVVMViewModelContext; } } protected override void OnBindingContextChanged (IMVVMViewModel oldvm, IMVVMViewModel newvm ) { base .OnBindingContextChanged(oldvm, newvm); if (oldvm != null ) { #region Bind VM to V Part MVVMViewModel ovm = oldvm as MVVMViewModel; ovm.Name.OnValueChanged -= onNameChanged; ovm.Age.OnValueChanged -= onAgeChanged; #endregion #region Bind V To VM mInputFieldName.onValueChanged.RemoveListener(onInputNameChanged); mBtnSave.onClick.RemoveListener(onSaveClick); #endregion } if (ViewModel != null ) { #region Bind VM to V Part ViewModel.Name.OnValueChanged += onNameChanged; ViewModel.Age.OnValueChanged += onAgeChanged; #endregion #region Bind V To VM mInputFieldName.onValueChanged.AddListener(onInputNameChanged); mBtnSave.onClick.AddListener(onSaveClick); #endregion } } #region Bind VM to V Part private void onNameChanged (string oldname, string newname ) { mInputFieldName.text = newname; mTxtName.text = newname; } private void onAgeChanged (int oldvalue, int newvalue ) { mTxtAge.text = newvalue.ToString(); } #endregion #region Bind V To VM private void onInputNameChanged (string newname ) { ViewModel.Name.Value = newname; } private void onSaveClick () { ViewModel.saveModel(); } #endregion }
MVVMViewModel只需要修改来实现IMVVMViewModel即可: MVVMViewModel.cs
1 2 3 4 public class MVVMViewModel : IMVVMViewModel { ****** }
测试代码也要跟着修改: GameProgrammingPattern.cs
1 2 3 4 5 6 7 8 9 10 11 void Start (){ #region MVVM Part mMVVMViewModel.initializeModel(mMVVMModel); mMVVMView.mMVVMViewModelContext = mMVVMViewModel; CoroutineManager.Singleton.startCoroutine(changeViewModelAgeCoroutine()); #endregion }
结果依然是正常双向绑定V和VM,实现V和M隔离,但现在我们的V不在和VM强耦合了,可以动态绑定到同类型不同的VM实例对象:
VM如何通知V V和VM在MVVM里是通过数据绑定实现的双向绑定,但假设V里点击一个按钮后修改VM里的数据后并不需要更新View刷新显示,而是需要VM通知View数据处理完毕并将最新数据返回做后续处理了(比如弹窗口提示之类)?
这时候我们需要的是一种通知数据处理完毕后回调通知的形式,我们很容易想到的一种实现方案是传递回调的形式: 伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class View : BaseView { ****** private void onBtnClick () { mViewModel.ChangeName(newname, ChangeNameCallback); } private void ChangeNameCallback (string name ) { } }
但这样在View里编写Code Behind代码会导致View层被污染,导致View层包含逻辑代码。同时也打断了MVVM里ViewModel的Unit Test特性。
我们可以通过绑定按钮点击到ViewModel的一个Command(包含操作的数据对象以及检查是否可执行以及最终逻辑回调等)里,把逻辑部分搬到ViewModel层实现Zero Code Behind并不破坏ViewModel的Unit Test。也可以考虑直接调用VM方法在方法里写逻辑。如果是涉及到View层表现,可以考虑绑定属性,在VM写逻辑去修改对应属性触发View层表现。
这里并没有太弄懂Command存在的意义(和直接在VM层写逻辑的区别是什么?),所以这里没有去实现这一套东西。
简化V和VM绑定代码 从前面的事例可以看到,V和VM的绑定都是成对出现,属于有规律可循的代码(后续可以自动化代码生成来解决),但这里我们希望解决的不是自动化代码的问题,而是提供一套更方便的代码绑定方案,解决大量复杂代码的编写。
实现思考:
通过统一的一个类来实现简化V和VM的绑定。
因为要访问VM的属性对象,需要通过反射访问(没想到更好的方案)
因为涉及到反射实现,这里觉得并不好(一是效率方面,二是直观上查找不到属性引用(维护方面)),不如单纯的实现自动化代码生成的形式。所以这里就不去实现这一套方案了,详细参考下文作者实现:Unity应用架构设计(1)—— MVVM 模式的设计和实施(Part 2)
VM和VM之间通信 现实开发过程中,我们的View和View有些时候是需要通信的,虽然View实现了和ViewModel的双向绑定,但并没有解决View和View之间的通信问题。
实现方便来说我们会去写如下代码实现View与View之间的访问:
1 2 3 4 5 6 7 public class ViewA { public void notifyViewB () { ViewB.UpdateView(); } }
但这样的代码是的View与View之间强耦合了,并不推荐这样实现,我们使用MVVM模式的目的是为了解耦V和M,同时实现V和VM之间数据驱动刷新。这里我们需要解决View和View之间ViewModel与ViewModel之间通信的问题。
首先V和VM是一对一的,V和VM是双向绑定的,所以要实现View和View之间的沟通交流,其实只要实现VM和VM之间的沟通交流即可。VM和VM是一对多的,为了解耦,我们可以通过一个中介者来解耦并统一管理ViewModel之间的沟通交流,然后使用订阅者模式实现消息订阅的形式模拟实现ViewModel之间一对多的沟通交流。
首先核心的消息统一订阅分发类: MessageDispatcher.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 using System.Collections;using System.Collections.Generic;using UnityEngine;public class MessageDispatcher : SingletonTemplate <MessageDispatcher >{ public delegate void MessageDelegate (object sender, params object [] paras ) ; private readonly Dictionary<string , MessageDelegate> mMessageMap = new Dictionary<string , MessageDelegate>(); public void subscribe (string message, MessageDelegate handler ) { if (!string .IsNullOrEmpty(message)) { if (!mMessageMap.ContainsKey(message)) { mMessageMap.Add(message, handler); } else { mMessageMap[message] += handler; } } else { Debug.LogError("不能订阅空的字符串消息!" ); } } public void unSubscribe (string message, MessageDelegate handler ) { if (!string .IsNullOrEmpty(message)) { if (mMessageMap.ContainsKey(message)) { mMessageMap[message] -= handler; } } else { Debug.LogError("不能取消空的字符串消息!" ); } } public void publish (string message, object sender, params object [] paras ) { if (!string .IsNullOrEmpty(message)) { if (mMessageMap.ContainsKey(message)) { mMessageMap[message](sender, paras); } } else { Debug.LogError("不能分发空的字符串消息!" ); } } }
修改ViewModel1的代码,添加消息监听: MVVMViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using System;using System.Collections;using System.Collections.Generic;using UnityEngine;public class MVVMViewModel : IMVVMViewModel { ****** public void registerMessageListener () { MessageDispatcher.Singleton.subscribe("ChangeName" , onChangeNameMessageHandler); } public void unregisterMessageListener () { MessageDispatcher.Singleton.unSubscribe("ChangeName" , onChangeNameMessageHandler); } public void saveModel () { Model.Name = Name.Value; Model.Age = Age.Value; } private void onChangeNameMessageHandler (object sender, object [] paras ) { Name.Value = paras[0 ] as string ; } }
添加View 2和ViewModel2相关代码: MVVMView2.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class MVVMView2 : MVVMBaseView { public Button mBtnChangeViewModel1Name; public MVVMViewModel2 ViewModel { get { return (MVVMViewModel2)mMVVMViewModelContext; } } protected override void OnBindingContextChanged (IMVVMViewModel oldvm, IMVVMViewModel newvm ) { base .OnBindingContextChanged(oldvm, newvm); if (oldvm != null ) { #region Bind VM to V Part #endregion #region Bind V To VM mBtnChangeViewModel1Name.onClick.RemoveListener(onNotifyViewModel1Click); #endregion } if (ViewModel != null ) { #region Bind VM to V Part #endregion #region Bind V To VM mBtnChangeViewModel1Name.onClick.AddListener(onNotifyViewModel1Click); #endregion } } #region Bind VM to V Part #endregion #region Bind V To VM private void onNotifyViewModel1Click () { ViewModel.notifyViewModel1NameChange(); } #endregion }
MVVMViewModel2.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 using System.Collections;using System.Collections.Generic;using UnityEngine;public class MVVMViewModel2 : IMVVMViewModel { public void notifyViewModel1NameChange () { MessageDispatcher.Singleton.publish("ChangeName" , this , "Hello World!" ); } }
初始化View2和ViewModel2相关的绑定: GameProgrammingPattern.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 using System.Collections;using System.Collections.Generic;using UnityEngine;public class GameProgrammingPattern : MonoBehaviour { #region MVVM Part ****** private MVVMViewModel2 mMVVMViewModel2; private MVVMView2 mMVVMView2; #endregion void Awake () { initMonoScripts(); #region MVVM Part ****** mMVVMViewModel2 = new MVVMViewModel2(); mMVVMView2 = this .transform.GetComponentInChildren<MVVMView2>(); #endregion } void Start () { #region MVVM Part mMVVMViewModel.initializeModel(mMVVMModel); mMVVMViewModel.registerMessageListener(); mMVVMView.mMVVMViewModelContext = mMVVMViewModel; CoroutineManager.Singleton.startCoroutine(changeViewModelAgeCoroutine()); mMVVMView2.mMVVMViewModelContext = mMVVMViewModel2; #endregion } ****** }
运行点击ViewModel2的按钮可以看到我们成功通过消息订阅和分发达到ViewModel和ViewModel之间交流的目的,成功实现了解耦ViewModel和ViewModel之间的通信:
优化
自动化生成View和ViewModel属性绑定部分代码 待续……
MVVM使用思考 可以看到MVVM相比传统MVC模式,将V和M完全解耦,V和M不需要关心对方,通过ViewModel去实现V和M的桥接,通过双向绑定实现View的数据驱动刷新显示。通过消息分发注册等类似机制实现ViewModel与ViewModel之间的沟通交流,实现低耦合的MVVM框架。 View可以绑定不同的ViewModel,快速实现不同数据刷新显示。 同时因为V和M完全分离,开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
MVVM应用范围思考:
独立开发,方便测试,MVVM比较适合多人合作项目,设计人员和开发人员可以同时开工,
代码重用性,开发人员之间的代码有很高的重用性(重用V或者重用VM都有可能),开发人员之间的代码不会互相污染。
适用于数据驱动的不复杂的游戏UI部分(数据驱动刷新显示)。
待续……
ECS 待续……
引用 网站 谈谈对MVC、MVP和MVVM的理解 谈 MVC、MVP 和 MVVM 架构模式 Unity 应用架构设计—— MVVM 模式的设计和实施 (Part 1) mvc的实现 如何在Unity中实现MVC模式? 跟踪数据结构的变更 MVC、MVP、MVVM架构分析与比较
书籍 《Game-Programming-Patterns》 – Robert.Nystrom 《Design Pattern》 – GoF(四人帮)