Get Start StrangeIOC for Unity3D


   好久沒有發blog了,因為只發原創內容,而去年發布的那幾篇后來發現隨便百度到處都是轉載的或者各種網站自動扒的,我覺得既然大家都不尊重這種東西就沒必要發上來了!不過由於工作原因最近在看Unity的一個IOC框架:StrangeIOC,官方的文檔都不是很好理解,找到了一篇比較好的GetStart文章,順手翻譯一下,一來方便自己加深理解,二來還是想共享出來,沒事,隨意轉吧,拜托注明下出處!原文在這里(不太清楚有沒有被牆)

譯文:

  Strange是一個Unity3D中用於控制反轉的第三方框架,控制反轉(IOC-Inversion of Control)思想是類間解耦的一個重要方法,對於我來說,任何解耦技術都值得去學習。什么是IOC?這里有詳細解答。IOC框架已經在企業級開發和其他非游戲軟件的開發中成為了主流,並且可以說已經非常成熟。我覺得它可以幫助游戲開發變得更加容易測試,更好的進行協作開發。我非常想嘗試它看看到底可以在游戲開發過程中起到多大的幫助程度。

  Strange使用起來真的像他的名字一樣,非常"奇怪"。我發現它對於初學者來說,使用起來真的非常"鬧心",比如你想試着去寫一個"Hello World"都非常不容易。 這里是StrangeIOC框架的說明頁面,但是這上面並沒有一個真正意義上的"新手引導"來幫助我們了解Strange的工作機制,這就是你現在看到現在這篇文章的意義-用StrangeIOC框架寫一個HelloWorld。
 
一些提醒:
  • 在閱讀本篇文章之前,最好先去上面提到的官方說明頁面了解一下Strange框架的架構(看看它的每個部分的功能以及怎么整合到一塊工作的)。
  • 這篇文檔使用的是signal(消息)而非event(事件)(因為相比event我更喜歡signal)
  • 我不會把文檔中的Unity項目提供出來,因為我希望大家自己動手去做,這樣肯定會學到更多:)
  • 這個Hello World示例只是簡單的提供注入綁定(injection binding)、命令綁定(command binding)、調解綁定(mediation binding)的示例。
Signal
  建立一個空Unity項目,下載並且解壓Strange框架到Assets文件夾中,我們只需要框架的腳本,把"examples"和".doc"文件夾去除,在Unity的的結構應該是這樣的:
    
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類.別急,我們馬上會看到怎么去使用它。

  Strange采用"Contexts"的概念來識別不同的問題域或者子模塊。在實際的游戲項目中,你可以有多個"Contexts",比如游戲邏輯、資源、持久層、統計分析、社交模塊等等。我們在這個實例中只用了一個"Context"。
  一個預構建的context在Strange中稱為MVCSContext,MVCSContext默認使用event機制,我們來創建另外一種context父類,改造成使用signal機制,我們其他的context要繼承這個SignalContext。
  在Scripts下創建名為SignalContext.cs的腳本:
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");
        }
 
    }
}

 

 

  現在我們為這個HelloWorld示例創建一個自定義的context類HelloWorldContext.cs:
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()方法中調用發出。

 

  最后一步就是創建一個MonoBehaviour來在Unity中管理context,在Scripts文件夾下創建HelloWorldBootstrap.cs:
using System;
 
using UnityEngine;
 
using strange.extensions.context.impl;
 
namespace Game {
    public class HelloWorldBootstrap : ContextView {
 
        void Awake() {
            this.context = new HelloWorldContext(this);
        }
 
    }
}

 

用於在Unity中管理Strange context的接口類通常命名為“xxxBootstrap”,當然這只是一個建議,如果你樂意你可以隨意起名字。這里唯一需要注意的是繼承Strange框架的ContextView類的類需要是一個MonoBehaviour,我們在Awake()里分配了一個我們自定義好的context實例給繼承的變量"context"。
  創建一個空場景命名為"HelloStrange",創建一個EmptyObject命名為Bootstrap,把我們之前創建的HelloWorldBootstrap add上來。可以跑一下這個場景,之前程序正確的話,你應該看到控制台的"Hello World"輸出了。
 
Injection in Mediator
  到目前為止寫這么一大堆東西只是輸出一句“HelloWorld”,是不是被Strange搞得頭都大了?其實做到現在這一步已經大致為你梳理出來Strange的一些機制了。首先我們有了一個能跑的context,從這一步開始,我們就可以添加view和相應的mediator,還可以使用injection binder把一個實例映射到一些可注入controllers/commands和mediators的接口中,而這些接口並不需要關心這個實例是怎么來的。接下來就是見證奇跡的時刻了!
  一般我們做游戲編程的時候,會有一堆單例管理器(singleton managers)比如EnemyManager、AsteroidManager、CombatManager等等,假如需要同一個實例給任意一個管理器去調用有很多解決方案,比如我們可以使用GameObject.Find() 或者為這個類添加一個靜態單例(GetInstance() static method),OK,讓我們看看有了Strange以后,這樣的情形可以怎么去解決:創建一個名為"ISomeManager"的接口,模擬一個上面說的那種manager,在Scripts文件夾創建ISomeManager.cs腳本
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方法。

  接下來就是最后的工作,我們需要把待綁的類映射到Strange Context中。打開我們之前寫的HelloWorldContext腳本,在mapBindings()方法中添加代碼:
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();
}

 

在HelloWorld scene中,添加一個名為"View"的GameObject,add HelloWorldView 腳本,運行場景,你應該能看到當我們按下"Manage"按鈕時,控制台輸出"Manager implemented as a normal class"。
ISomeManager in action
你會發現Strange自動把HelloWorldMediator腳本掛載到了"View"GameObject上面。注意我們之前並沒有手動把HelloWorldMediator腳本掛載到"View"GameObject上。
Where did the mediator came from?
 
MonoBehaviour Manager
  大部分時候,我們需要類似於上面的manager但是實現類是一個MonoBehaviour,這樣我們才能使用例如協程、序列化的Unity特性。
   接下來創建實現MonoBehaviour接口的manager實例,看看怎么在Strange中進行bind。
  創建一個實現MonoBehaviour接口的manager,在Script文件夾下,命名為ManagerAsMonobehaviour.cs
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腳本
  編輯HelloWorldContext腳本的mapBindings()方法:
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)。
Manager is now a MonoBehaviour
 
Injection in Command
  到目前為止我們為HelloWorldMediator注入了一個ISomeManager的一個實例,並且可以直接使用它。這樣做其實並不是很理想,一個Mediator應該是在view層和controller層之間的一個薄層。我們需要盡量使Mediator層不去關心應該在Manager類去做的那部分復雜的邏輯處理代碼。雖然這么做也可以,我們還是用signal把這部分映射到command層吧。
  編輯HelloWorldSignals.cs腳本,添加一個DoManagementSignal:
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方法執行。
修改HelloWorldMediator類:
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)出去。
最后,在HelloWorldContext.mapBindings()方法中添加這個signal-command映射。
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);
}

 


運行場景,效果和之前一樣,但是我們在代碼層面把這塊代碼重構了。
 
最后
  你會注意到這篇文章動不動就提到"作者建議"這樣的話,這是因為作者的建議確實是一個比較重要的選擇。比如說你可以在你的view層中直接注入各種實例,可能你壓根不想去創建什么mediator層!我想說的是你可以根據你的實際需要來決定使用Strange的方法,但是我選擇了根據作者的建議來使用它因為對於我來說這樣還不錯。
  如果你把這篇文章看到了這里,你可能會很疑惑:"我為什么要在我的項目里搞這么多復雜又多余的層?"其實在我自己的項目中應用這個框架也是處於探索研究階段,現在的感受Strange有一個好處是強行讓我把每個模塊划分了。比如這樣一個情形:用Strange Context處理SimpleSQL數據持久化,對於現有的游戲邏輯代碼,沒有使用Strange的部分,他們間的通信通過signal來進行。
  我們還在另外一個RPG項目用了Strange,我希望用了它以后可以像它的口號一樣,確實有助於代碼間的協作。到目前來看我沒法跟你宣稱它在我的項目中有多好,因為我們也只是在起步階段,但是至少到目前為止對於我們來說它工作的還不錯。我們的模式是使用多個Context,一個程序員負責一個Context,然后通過signal來與其他人的Context通信。


免責聲明!

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



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