Unity3d架构之-Unity MVC框架 StrangeIoC

因为工做缘由最近在看unity的一个IOC框架:StrangeIOC,官方的文档都不是很好理解,找到了一篇比较好的GetStart文章,顺手翻译一下,一来方便本身加深理解,二来仍是想共享出来,html

Strange是一个unity3d中用于控制反转的第三方框架,控制反转(IOC-Inversion of Control)思想是类间解耦的一个重要方法,对于我来讲,任何解耦技术都值得去学习。什么是IOC?这里有详细解答。IOC框架已经在企业级开发和其余非游戏软件的开发中成为了主流,而且能够说已经很是成熟。我以为它能够帮助游戏开发变得更加容易测试,更好的进行协做开发。我很是想尝试它看看到底能够在游戏开发过程当中起到多大的帮助程度。
Strange使用起来真的像他的名字同样,很是”奇怪”。我发现它对于初学者来讲,使用起来真的很是”闹心”,好比你想试着去写一个”Hello World”都很是不容易。这里是StrangeIOC框架的说明页面,可是这上面并无一个真正意义上的”新手引导”来帮助咱们了解Strange的工做机制,这就是你如今看到如今这篇文章的意义-用StrangeIOC框架写一个HelloWorld。java

一些提醒:git

  • 在阅读本篇文章以前,最好先去上面提到的官方说明页面了解一下Strange框架的架构(看看它的每一个部分的功能以及怎么整合到一块工做的)。
  • 这篇文档使用的是signal(消息)而非event(事件)(由于相比event我更喜欢signal)
  • 我不会把文档中的Unity项目提供出来,由于我但愿你们本身动手去作,这样确定会学到更多:) 这个Hello World示例只是简单的提供注入绑定(injection binding)、命令绑定(command binding)、调解绑定(mediation binding)的示例。

示例 — HelloWorld


Signal
创建一个空Unity项目,下载而且解压Strange框架到Assets文件夹中,咱们只须要框架的脚本,把”examples”和”.doc”文件夹去除,在Unity的的结构应该是这样的:
Assets
StrangeIoC
scripts程序员

在Assets文件夹下建立”Game”文件夹,即用来建立Hello World示例的文件夹。文件夹的的结构应该是这样的:github

Assets    
Game       
Scenes

Scripts在Scripts文件夹下新建名为HelloWorldSignals.cs的c#脚本,这个类将包含全部用到的signal,让咱们coding起来:web

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的脚本:c#

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的类:api

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 = newHelloWorldContext(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 = newSignal();

private Rect buttonRect = newRect(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通讯。

须要StrangeIoC框架源码的童鞋能够前往unity3d assetstore官方商店自行下载

转载:http://www.unity.5helpyou.com/2645.html


StrangeIOC 下载地址

https://github.com/strangeioc/strangeioc

EventDipatcher方式
http://blog.csdn.net/superlinmeng/article/details/69388224