框架學習筆記:Unity3D的MVC框架——StrangeIoC


作為從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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM