由于工作原因最近在看unity的一个IOC框架:StrangeIOC,官方的文档都不是很好理解,找到了一篇比较好的GetStart文章,顺手翻译一下,一来方便自己加深理解,二来还是想共享出来,
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
Scenes
Scripts在Scripts文件夹下新建名为HelloWorldSignals.cs的c#脚本,这个类将包含所有用到的signal,让我们coding起来:
1
2
3
4
5
6
7
8
9
|
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的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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的类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
[/color]
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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脚本
1
2
3
4
5
6
7
8
9
10
11
|
[/color]
namespace
Game {
public
interface
ISomeManager {
/**
* Perform some management
*/
void
DoManagement();
}
}
|
这就是我们示例当中的manager接口,注意:Strange的作者建议我们总是使用一个接口然后通过injectionBinder将它映射到一个真正的实现类,当然,你也可以使用多对多的映射。接下来我们创建一个具体实现类,在Scripts文件夹下创建ManagerAsNormalClass.cs脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
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()方法中添加代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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:
1
2
3
4
5
6
7
8
9
10
11
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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映射。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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