代码改变世界

框架学习笔记:Unity3D的MVC框架——StrangeIoC

2015-12-07 18:40  阿诚de窝  阅读(10001)  评论(1编辑  收藏  举报

作为从AS3页游走过来的人,看见StrangeIoC会额外亲切,因为StrangeIoC的设计和RobotLegs几乎一致,作为一款依赖注入/控制反转(IoC)的MVC框架,StrangeIoC除了使我们的程序结构更加解耦合理外,还为我们提供了大量方便的功能(这里主要是和PureMVC进行对比)。

RobotLegs和Event

这一节是题外话,对AS3无感的童鞋请跳过。

StrangeIoC首页的文档有如下的记录:

以及:

我猜作者以前一定是一个ASer,除了设计上和RobotLegs一致外,StrangeIoC还设计了两种事件机制:AS3的内置原生事件机制(Dispatcher)和一个AS3的扩展事件类库(AS3-Signals)。

有兴趣的童鞋可以点击下面的连接了解这些技术:

RobotLegs

ROBOTLEGS轻量级AS3框架

AS3-Signals

Signals框架介绍(一)基本用法

Signals框架介绍(二)高级事件

Signals框架介绍(三)原生事件

示例

下面我们来基于StrangeIoC框架搭建一个简单的示例,更多的示例可以参考框架自带的例子。

下载和导入StrangeIoC

开源地址:https://github.com/strangeioc/strangeioc

我们可以在Release页面下载到发布的版本,比如我这里下了v0.6.1的版本,我们把下载下来的文件进行解压;

导入框架:新建一个Unity3D的项目,新建一个名为StrangeIoC的文件夹,将解压文件夹下的StrangeIoC\scripts文件夹拷贝到我们新建的文件夹中即可(貌似有一个报错,我们先忽略它);

创建根容器

这里的容器其实就是一个GameObject对象,理论上游戏中所有要使用到StrangeIoC提供注入功能的GameObject都必须添加到这个根容器中,我们创建一个GameRoot的脚本,将该脚本绑定到根容器上,内容:

 1 using strange.extensions.context.impl;
 2 
 3 /// <summary>
 4 /// 游戏根容器类.
 5 /// </summary>
 6 public class GameRoot : ContextView
 7 {
 8     void Awake()
 9     {
10         //创建游戏上下文对象并启动
11         context = new GameContext(this, true);
12         context.Start();
13     }
14 }

该类负责启动MVC框架,这里我们用到了一个名为GameContext的类,我们先创建这个类,代码如下:

 1 using strange.extensions.context.api;
 2 using strange.extensions.context.impl;
 3 using UnityEngine;
 4 
 5 /// <summary>
 6 /// 游戏上下文.
 7 /// </summary>
 8 public class GameContext : MVCSContext
 9 {
10     public GameContext () : base()
11     {
12     }
13 
14     public GameContext(MonoBehaviour view, bool autoStartup) : base(view, autoStartup)
15     {
16     }
17 
18     protected override void mapBindings()
19     {
20         mapModel();
21         mapView();
22         mapController();
23 
24         //调用 StartUp 命令启动程序
25         commandBinder.Bind(ContextEvent.START).To<StartUpCommand>().Once();
26     }
27 
28     /// <summary>
29     /// 映射模型.
30     /// </summary>
31     private void mapModel()
32     {
33         injectionBinder.Bind<IMainUIService>().To<MainUIService>().ToSingleton();
34         injectionBinder.Bind<IMainUIModel>().To<MainUIModel>().ToSingleton();
35 
36         injectionBinder.Bind<ISkillService>().To<SkillService>().ToSingleton();
37         injectionBinder.Bind<ISkillModel>().To<SkillModel>().ToSingleton();
38     }
39 
40     /// <summary>
41     /// 映射视图.
42     /// </summary>
43     private void mapView()
44     {
45         mediationBinder.Bind<MainUIView>().To<MainUIMediator>();
46 
47         mediationBinder.Bind<SkillUIView>().To<SkillUIMediator>();
48     }
49 
50     /// <summary>
51     /// 映射控制器.
52     /// </summary>
53     private void mapController()
54     {
55         commandBinder.Bind(NotificationCenter.OPEN_SKILL_UI).To<OpenSkillUICommand>();
56         commandBinder.Bind(NotificationCenter.CLOSE_SKILL_UI).To<CloseSkillUICommand>();
57         commandBinder.Bind(NotificationCenter.SEND_MSG_TO_SKILL_UI).To<SendMsgToSkillUICommand>();
58 
59         commandBinder.Bind(NotificationCenter.SKILL_REQUEST).To<SkillRequestCommand>();
60     }
61 }

GameContext(游戏上下文)的作用是关联整个MVC的所有模块,可以说整个MVC架构各个模块之间是相互解耦的,但一定会有一个地方进行各个模块之间的关联,否则每个模块之间是无法通讯的,那么GameContext就是干这件事的,可以说GameContext是整个MVC系统中唯一耦合了所有模块的地方。

下面我们详解一下GameContext中注册的代码意义:

injectionBinder.Bind<ISkillModel>().To<SkillModel>().ToSingleton();

把类型SkillModel作为单例绑定到ISkillModel上,结果是每次通过[Inject]标签注入ISkillModel类型的对象时,获取的都是同一个SkillModel的实例。

mediationBinder.Bind<MainUIView>().To<MainUIMediator>();

把MainUIMediator绑定到MainUIView之上,结果是每次添加带有MainUIView脚本的GameObject到舞台时就会自动创建MainUIMediator对象绑定到该GameObject。

commandBinder.Bind(NotificationCenter.OPEN_SKILL_UI).To<OpenSkillUICommand>();

把NotificationCenter.OPEN_SKILL_UI绑定到OpenSkillUICommand之上,结果是当抛出NotificationCenter.OPEN_SKILL_UI时,就会创建一个OpenSkillUICommand的实例并运行。

commandBinder.Bind(ContextEvent.START).To<StartUpCommand>().Once();

添加了Once之后,表示立即执行且执行后马上解除绑定。

启动程序

StartUpCommand执行启动程序的指令,我们看看他的代码:

 1 using strange.extensions.command.impl;
 2 using strange.extensions.context.api;
 3 using UnityEngine;
 4 
 5 /// <summary>
 6 /// 程序启动命令.
 7 /// </summary>
 8 public class StartUpCommand : EventCommand
 9 {
10     [Inject(ContextKeys.CONTEXT_VIEW)]
11     public GameObject contextView { get; set; }
12 
13     public override void Execute()
14     {
15         //获取 UI 画布
16         Transform canvas = contextView.transform.FindChild("Canvas");
17         //加载并添加 MainUI
18         GameObject go = Resources.Load("Prefabs/MainUI", typeof(GameObject)) as GameObject;
19         GameObject mainUI = GameObject.Instantiate(go) as GameObject;
20         //添加视图脚本, 或者直接绑定到预制件中都可以
21         mainUI.AddComponent<MainUIView>();
22         mainUI.transform.SetParent(canvas, false);
23     }
24 }

这里我们将绑定了ContextView的GameObject进行注入,然后添加我们的MainUI到场景,对应的中介类也会自动添加。

模块相关

接下来的开发就可以按模块划分了,每个模块和其它模块都可以做到不耦合。每个模块会分出3个层次,我们接下来依次来看看具体的实现,以Skill模块为例:

视图和中介类

视图负责显示相关的逻辑,但是视图无法访问到数据也无法直接调用请求接口,视图要和模型与其它模块交互需要借助中介类来完成。

视图类

 1 using strange.extensions.dispatcher.eventdispatcher.api;
 2 using strange.extensions.mediation.impl;
 3 using UnityEngine.UI;
 4 
 5 public class SkillUIView : View
 6 {
 7     public const string REQUEST_BTN_CLICK = "requestBtnClick";
 8 
 9     [Inject]
10     public IEventDispatcher dispatcher { get; set; }
11 
12     private Text outputText;
13     private Button requestBtn;
14 
15     public void Init()
16     {
17         outputText = this.gameObject.transform.FindChild("OutputText").GetComponent<Text>();
18 
19         requestBtn = this.gameObject.transform.FindChild("RequestBtn").GetComponent<Button>();
20         requestBtn.onClick.AddListener(RequestBtnClickHandler);
21     }
22 
23     private void RequestBtnClickHandler()
24     {
25         dispatcher.Dispatch(REQUEST_BTN_CLICK);
26     }
27 
28     public void AddText(string content)
29     {
30         outputText.text += content + "\n";
31     }
32 }

视图类注入的IEventDispatcher是用来和中介类进行通信的,按照规则,视图类也只可以和中介类进行通信。

中介类

 1 using strange.extensions.dispatcher.eventdispatcher.api;
 2 using strange.extensions.mediation.impl;
 3 
 4 public class SkillUIMediator : EventMediator
 5 {
 6     [Inject]
 7     public SkillUIView view { get; set; }
 8 
 9     public override void OnRegister()
10     {
11         dispatcher.AddListener(NotificationCenter.SKILL_UI_ADD_MSG, skillUIAddMsgHandler);
12 
13         view.dispatcher.AddListener(SkillUIView.REQUEST_BTN_CLICK, requestBtnClickHandler);
14 
15         view.Init();
16     }
17 
18     public override void OnRemove()
19     {
20         dispatcher.RemoveListener(NotificationCenter.SKILL_UI_ADD_MSG, skillUIAddMsgHandler);
21 
22         view.dispatcher.RemoveListener(SkillUIView.REQUEST_BTN_CLICK, requestBtnClickHandler);
23     }
24 
25     private void skillUIAddMsgHandler(IEvent evt)
26     {
27         view.AddText((string)evt.data);
28     }
29 
30     private void requestBtnClickHandler(IEvent evt)
31     {
32         dispatcher.Dispatch(NotificationCenter.SKILL_REQUEST);
33     }
34 }

中介类接收到视图类的消息后可以转发给其它模块或进行远程请求和数据修改,其它模块发送的消息也由中介类进行接收后通知到视图类。

Command控制器

命令模式可以用来执行一段具体的代码,通常请求数据和修改数据也放在Command中,如下:

 1 using strange.extensions.command.impl;
 2 using strange.extensions.dispatcher.eventdispatcher.api;
 3 
 4 public class SkillRequestCommand : EventCommand
 5 {
 6     [Inject]
 7     public ISkillModel model { get; set; }
 8 
 9     [Inject]
10     public ISkillService service { get; set; }
11 
12     public override void Execute()
13     {
14         //这里有异步操作, 为了使 Command 对象不被释放, 我们需要调用下面的方法持有当前 Command 对象的引用
15         Retain();
16 
17         service.dispatcher.AddListener(SkillService.RECEIVE_DATA, OnReceiveDataHandler);
18         service.Request("http://www.game.com/mygame.php?id=1000");
19     }
20 
21     private void OnReceiveDataHandler(IEvent evt)
22     {
23         service.dispatcher.RemoveListener(SkillService.RECEIVE_DATA, OnReceiveDataHandler);
24 
25         model.data = ((SkillMsgVO)evt.data).msg;
26         dispatcher.Dispatch(NotificationCenter.SKILL_UI_ADD_MSG, model.data);
27 
28         //异步操作完成, 可以释放对象了
29         Release();
30     }
31 }

需要注意的是,如果有异步操作,需要调用Retain和Release来保证Command不被垃圾回收销毁。

Model和Service

Model用来储存数据,Service用来进行远程请求。

Model

1 public class SkillModel : ISkillModel
2 {
3     public string data { get; set; }
4 }

我们另外还创建了一个接口用来定义这个模型,这样的好处是方便后期替换具体的实现类。

Service

 1 using System.Collections;
 2 using strange.extensions.context.api;
 3 using strange.extensions.dispatcher.eventdispatcher.api;
 4 using UnityEngine;
 5 
 6 public class SkillService : ISkillService
 7 {
 8     public const string RECEIVE_DATA = "receiveData";
 9 
10     [Inject(ContextKeys.CONTEXT_VIEW)]
11     public GameObject contextView { get; set; }
12 
13     [Inject]
14     public IEventDispatcher dispatcher { get; set; }
15 
16     public void Request(string url)
17     {
18         contextView.GetComponent<GameRoot>().StartCoroutine(WaitASecond());
19     }
20 
21     private IEnumerator WaitASecond()
22     {
23         yield return new WaitForSeconds(1.0f);
24 
25         dispatcher.Dispatch(RECEIVE_DATA, new SkillMsgVO{msg="我是服务端返回的数据!"});
26     }
27 }

由于只是示例,所以并没有真正的请求远程,而是使用Unity3D的协程做了一个一秒延迟的模拟。

VO

即ValueObject,这并不是框架要求的内容,一般我们会把各个模块之间进行传递的数据定义为一个数据对象,而VO可以作为这类数据的命名后缀。

1 public class SkillMsgVO
2 {
3     public string msg;
4 }

和PureMVC的不同

  1. PureMVC是.Net平台下的开源MVC框架,可以用在Unity或其他基于C#语言的项目中。
  2. StrangeIoC是专门为Unity3D开发的MVC框架,其注入和中介类的设计都是针对Unity3D的特点开发,移植到其他非Unity3D的C#项目需要修改底层实现。
  3. PureMVC是MVC架构,其Model层是按代理模式设计的,我们会将数据保存和远程请求用一个代理类公开其部分的接口给到模块使用,比如SkillProxy只会公开技能相关的接口给到技能模块调用。
  4. StrangeIoC是MVCS架构,其Model层分为Model和Service两个,分别处理数据保存和远程请求,是分开的,一般使用Command来处理特定的数据修改和远程请求。

源码下载

下一篇博客我们来解析一下神奇的注入和自动生成中介类是如何实现的。

http://pan.baidu.com/s/1dDc0io5