好久沒有發blog了,因為只發原創內容,而去年發布的那幾篇后來發現隨便百度到處都是轉載的或者各種網站自動扒的,我覺得既然大家都不尊重這種東西就沒必要發上來了!不過由於工作原因最近在看Unity的一個IOC框架:StrangeIOC,官方的文檔都不是很好理解,找到了一篇比較好的GetStart文章,順手翻譯一下,一來方便自己加深理解,二來還是想共享出來,沒事,隨意轉吧,拜托注明下出處!原文在這里(不太清楚有沒有被牆)
譯文:
Strange是一個Unity3D中用於控制反轉的第三方框架,控制反轉(IOC-Inversion of Control)思想是類間解耦的一個重要方法,對於我來說,任何解耦技術都值得去學習。什么是IOC?這里有詳細解答。IOC框架已經在企業級開發和其他非游戲軟件的開發中成為了主流,並且可以說已經非常成熟。我覺得它可以幫助游戲開發變得更加容易測試,更好的進行協作開發。我非常想嘗試它看看到底可以在游戲開發過程中起到多大的幫助程度。
- 在閱讀本篇文章之前,最好先去上面提到的官方說明頁面了解一下Strange框架的架構(看看它的每個部分的功能以及怎么整合到一塊工作的)。
- 這篇文檔使用的是signal(消息)而非event(事件)(因為相比event我更喜歡signal)
- 我不會把文檔中的Unity項目提供出來,因為我希望大家自己動手去做,這樣肯定會學到更多:)
- 這個Hello World示例只是簡單的提供注入綁定(injection binding)、命令綁定(command binding)、調解綁定(mediation binding)的示例。
Assets StrangeIoC scripts
在Assets文件夾下創建"Game"文件夾,即用來創建Hello World示例的文件夾。文件夾的的結構應該是這樣的:
Assets Game Scenes Scripts在Scripts文件夾下新建名為HelloWorldSignals.cs的c#腳本,這個類將包含所有用到的signal,讓我們coding起來:
using System; using strange.extensions.signal.impl; namespace Game { public class StartSignal : Signal {} }
在Strange中,這個signal的概念非常像觀察者模式(observer pattern)中的事件(events)。在這里,它以命名類的方式實現了繼承Strange的Signal類.別急,我們馬上會看到怎么去使用它。
using System; using UnityEngine; using strange.extensions.context.impl; using strange.extensions.command.api; using strange.extensions.command.impl; using strange.extensions.signal.impl; namespace Game { public class SignalContext : MVCSContext { /** * Constructor */ public SignalContext (MonoBehaviour contextView) : base(contextView) { } protected override void addCoreComponents() { base.addCoreComponents(); // bind signal command binder injectionBinder.Unbind<ICommandBinder>(); injectionBinder.Bind<ICommandBinder>().To<SignalCommandBinder>().ToSingleton(); } public override void Launch() { base.Launch(); Signal startSignal = injectionBinder.GetInstance<StartSignal>(); startSignal.Dispatch(); } } }
在"Scripts"文件夾下創建一個新文件夾"Controller",到這里有了一點MVC模式的特征。Strange作者建議我們應該以指令類(Command Class)的形式實現各個Controller接口,這個文件夾將包含所有的Command類,現在我們創建一個在StartSignal指令調用時執行的指令。在Controller文件夾下創建名為HelloWorldStartCommand.cs的類:
using System; using UnityEngine; using strange.extensions.context.api; using strange.extensions.command.impl; namespace Game { public class HelloWorldStartCommand : Command { public override void Execute() { // perform all game start setup here Debug.Log("Hello World"); } } }
using System; using UnityEngine; using strange.extensions.context.impl; namespace Game { public class HelloWorldContext : SignalContext { /** * Constructor */ public HelloWorldContext(MonoBehaviour contextView) : base(contextView) { } protected override void mapBindings() { base.mapBindings(); // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) on Launch() commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once(); } } }
在這里,我們把StartSignal類綁定(bind)給了HelloWorldStartCommand類。這樣在StartSignal的實例被調用時,HelloWorldStartCommand會進行實例化(instantiated)和執行(executed),注意在我們的示例中StartSignal信號會在SignalContext.Launch()方法中調用發出。
using System; using UnityEngine; using strange.extensions.context.impl; namespace Game { public class HelloWorldBootstrap : ContextView { void Awake() { this.context = new HelloWorldContext(this); } } }

namespace Game { public interface ISomeManager { /** * Perform some management */ void DoManagement(); } }
這就是我們示例當中的manager接口,注意:Strange的作者建議我們總是使用一個接口然后通過injectionBinder將它映射到一個真正的實現類,當然,你也可以使用多對多的映射。接下來我們創建一個具體實現類,在Scripts文件夾下創建ManagerAsNormalClass.cs腳本:
using System; using UnityEngine; namespace Game { public class ManagerAsNormalClass : ISomeManager { public ManagerAsNormalClass() { } #region ISomeManager implementation public void DoManagement() { Debug.Log("Manager implemented as a normal class"); } #endregion } }
如果你仔細在看你可能會發現這是一個沒有MonoBehaviour的manager,別急,一會再介紹怎么bind有MonoBehaviour的
現在我們來創建一個簡單的交互場景,效果是當一個Button按下時,ISomeManager的DoManagement函數執行,這里我們有一個要求:用MVC思想---對controll層(ISomeManager)和view層(控制Button觸發事件的腳本)完全解耦,view層只需要通知controll層:"hey!button被點擊了",至於接下來發生什么交由controll層進行邏輯處理。
現在缺一個view層,把它創建出來吧---在Game文件夾下創建"View"文件夾,創建HelloWorldView.cs腳本:
using System; using UnityEngine; using strange.extensions.mediation.impl; using strange.extensions.signal.impl; namespace Game { public class HelloWorldView : View { public Signal buttonClicked = new Signal(); private Rect buttonRect = new Rect(0, 0, 200, 50); public void OnGUI() { if(GUI.Button(buttonRect, "Manage")) { buttonClicked.Dispatch(); } } } }
這里繼承的Strange框架中的View類已經包含了MonoBehaviour。所有使用Strange context的View層類都必須繼承這個Strange的View類,我們剛剛創建的View類只有一個交互功能:在點擊名為"Manage"的Button后,調用一個 generic signal(通用信號) 。
Strange作者建議對每個View創建對應的Mediator。Mediator是一個薄層,他的作用是讓與之對應的View和整個程序進行交互。mediation binder的作用是把View映射到它對應的mediator上。所以接下來為View層創建對應的mediator---在"view"文件夾下創建HelloWorldMediator.cs腳本:
using System; using UnityEngine; using strange.extensions.mediation.impl; namespace Game { public class HelloWorldMediator : Mediator { [Inject] public HelloWorldView view {get; set;} [Inject] public ISomeManager manager {get; set;} public override void OnRegister() { view.buttonClicked.AddListener(delegate() { manager.DoManagement(); }); } } }
在這段代碼里我們可以看到神奇的"Inject"標注(Inject attribute)。這個"Inject"標注只能和變量搭配使用,當一個變量上面有"Inject"標注時,意味着Strange會把這個變量的一個實例自動注入到它對應映射的context中。據此從我們上面的代碼來分析,在這里我們獲取到了"view"和"manager"的實例,並且不用去關心這些個實例是怎么來的。
OnRegister()是一個可以被重寫的方法,它用來標記實例注入完成已經可以使用了,它的意義主要是進行初始化,或者說做准備。在上面的類中,OnRegister方法中為HellowWorldView.buttonClicked signal添加了一個監聽器,這個監聽器的邏輯是按下就執行manager.DoManagement方法。
protected override void mapBindings() { base.mapBindings(); // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch() commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once(); // bind our view to its mediator mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>(); // bind our interface to a concrete implementation injectionBinder.Bind<ISomeManager>().To<ManagerAsNormalClass>().ToSingleton(); }


using System; using UnityEngine; namespace Game { public class ManagerAsMonoBehaviour : MonoBehaviour, ISomeManager { #region ISomeManager implementation public void DoManagement() { Debug.Log("Manager implemented as MonoBehaviour"); } #endregion } }
在HelloStrangeScene中,創建一個新的GameObject名為"Manager",add 上面創建好的 ManagerAsMonobehaviour腳本
protected override void mapBindings() { base.mapBindings(); // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch() commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once(); // bind our view to its mediator mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>(); // REMOVED!!! //injectionBinder.Bind<ISomeManager>().To<ManagerAsNormalClass>().ToSingleton(); // bind the manager implemented as a MonoBehaviour ManagerAsMonoBehaviour manager = GameObject.Find("Manager").GetComponent<ManagerAsMonoBehaviour>(); injectionBinder.Bind<ISomeManager>().ToValue(manager); }
與把ISomeManager映射為一個類型相反,我們把這個ManagerAsMonobehaviour映射為一個實例值(instance value)。

using System; using strange.extensions.signal.impl; namespace Game { public class StartSignal : Signal {} public class DoManagementSignal : Signal {} // A new signal! }
我們創建command映射到signal:在Controller文件夾下創建一個腳本DoManagementCommand.cs
using System; using UnityEngine; using strange.extensions.context.api; using strange.extensions.command.impl; namespace Game { public class DoManagementCommand : Command { [Inject] public ISomeManager manager {get; set;} public override void Execute() { manager.DoManagement(); } } }
在這個類,我們把ISomeManager注入到command類,並且在Execute方法中讓它的DoManagement方法執行。
using System; using UnityEngine; using strange.extensions.mediation.impl; namespace Game { public class HelloWorldMediator : Mediator { [Inject] public HelloWorldView view {get; set;} [Inject] public DoManagementSignal doManagement {get; set;} public override void OnRegister() { view.buttonClicked.AddListener(doManagement.Dispatch); } } }
現在我們的mediator類中已經沒有任何對ISomeManager接口的調用了。取而代之的是要在mediator類獲取到DoManagementSignal的實例,當button點擊時,這個類會發出DoManagementSignal。mediator層不需要知道任何manager的事情,它只管發送信號(signal)出去。
protected override void mapBindings() { base.mapBindings(); // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch() commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once(); commandBinder.Bind<DoManagementSignal>().To<DoManagementCommand>().Pooled(); // THIS IS THE NEW MAPPING!!! // bind our view to its mediator mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>(); // bind the manager implemented as a MonoBehaviour ManagerAsMonoBehaviour manager = GameObject.Find("Manager").GetComponent<ManagerAsMonoBehaviour>(); injectionBinder.Bind<ISomeManager>().ToValue(manager); }
運行場景,效果和之前一樣,但是我們在代碼層面把這塊代碼重構了。