Unity StrangeIoc框架 (二)
MVCSContex :the big picture
1.应用程序的入口是一个类成为ContextView,这是一个Monobehavior实例化MVCSContext
2.用MVCSContext来执行各种绑定。
3.派发器是一个通信总线,允许你再程序发送消息,在mvcscontext中他们发送的是TimEvents, 或者你可以按照上面的步骤重写Context 来使用Signals
4.命令类由TimeEvents或信号触发,然后他会执行一些app逻辑。
5.模型存储状态
6.services与app意外的部分通信(比如接入的faebook)
7.界面脚本附加到物体上 : 玩家与游戏的交互
8.Mediators(中介)也是monobehavior 但是他也可以将view部分和其他部分隔离开来
这张图战士了这些部分是如何一起工作的
大致介绍完了 下面来如何建立工程
一个ContextView开始
ContextView 是一个Monobehaviour 用来实例你的Context(上下文) MyFirstProjectRoot 是ContextView的子类, 这里是应用程序的开始
using System; using UnityEngine; using strange.extensions.context.impl; using strange.extensions.context.api;
namespace strange.examples.myfirstproject { public class MyFirstProjectRoot : ContextView { void Awake() { //Instantiate the context, passing it this instance. context = new MyFirstContext(this,ContextStartupFlags.MANUAL_MAPPING);
context.Start(); } } }
这里要使用 strange.extensions.context.impl 和 using strange.extensions.context.api 命名空间
ContextView定义了一个属性称为上下文当然是指我们上下文。我们只需要定义它是什么我们写一个叫MyFirstContext的脚本。this 指的是MyFirstProjectRoot,他告诉Context 哪个GameObject被认为是ContextView。ContextStartupFlags.MANUAL_MAPPING表明一旦我们开始一切将会继续。 调用context.Start()让它付诸行动 。 如果不调用Start则不会继续进行
- ContextStartupFlags.AUTOMATIC : 上下文将自动映射绑定和启动(默认的)
- ContextStartupFlags.MANUAL_MAPPING : 上线文会启动,然后在核心绑定后,在实例化或任何自定义绑定之前 将停止映射,必须调用Start()才可继续进行
- ContextStartupFlags.MANUAL_LAUNCH : 上线文会启动,然后在核心绑定后 , 在调用ContextEvent.START 或者类似的信号前停止。必须使用Launch()继续
The Context binds(上下文绑定)
Context(上下文)是所有绑定发生的地方,如果没有绑定,Strange应用只是一堆断开连接的部分。Context是为混乱带来秩序的胶水。从我们扩展MVCSContext,我们得到了一大堆的核心绑定,MVCSContext是为了给我们所有我们需要干净的结构 一个控制反转风格的应用:一个注射(injector)、命令总线、模型和服务支持,和中介界面。
using System; using UnityEngine; using strange.extensions.context.api; using strange.extensions.context.impl; using strange.extensions.dispatcher.eventdispatcher.api; using strange.extensions.dispatcher.eventdispatcher.impl; namespace strange.examples.myfirstproject { public class MyFirstContext : MVCSContext { public MyFirstContext (MonoBehaviour view) : base(view) { } public MyFirstContext (MonoBehaviour view, ContextStartupFlags flags) : base(view, flags) { } protected override void mapBindings() { injectionBinder.Bind<IExampleModel>() .To<ExampleModel>() .ToSingleton(); injectionBinder.Bind<IExampleService>() .To<ExampleService>() .ToSingleton(); mediationBinder.Bind<ExampleView>() .To<ExampleMediator>(); commandBinder.Bind(ExampleEvent.REQUEST_WEB_SERVICE) .To<CallWebServiceCommand>(); commandBinder.Bind(ContextEvent.START) .To<StartCommand>().Once (); } } }
像你看到的那样,我们扩展了MVCSContext,这意味着我们继承其所有映射(探索它类的深度 ,你会发现它的有趣)。我们已经有一个injectionBinder和commandBinder和dispatcher调度员。注意,调度程序可以在整个应用程序,和CommandBinder耦合,所以任何事件派遣可以触发回调也触发命令commands和序列sequences。
这里的映射是完全符合你的期待如果你读到的各种组件注入,我们映射一个模型和一个服务都是单例。我们将只有一个视图(ExampleView)在这个例子中,我们将它绑定到一个中介(ExampleMediator)。最后,我们映射两个命令。这两个比较重要的是StartCommand绑定到一个特殊的事件:ContextEvent.START.这是事件触发启动你的应用。你需要绑定一些命令或者队列到它身上想init()为进入你的应用程序。我们绑定了.Once(),一个特殊的方法,在一次结束时被解开Unbinds。
注意这里有一个postBindings()方法。这是一个十分有用的地方放一些你需要在绑定之后运行的代码。但是他运行在Launch()之后,MVCSContext用这个方法去处理任何Views界面哪一个在寄存器中更早(在mapBindings之后被调用)。另一个明显的和有用的情况 在postBindings()中调用DontDestroyOnLoad(ContextView)。在你加载一个新的场景时用来保留ContextView(and the Context)。
A Command fires(一个命令被触发)
ContextEvent.START 被处罚,因为它被绑上了StartCommand, 一个新的StartCommand实例将被实例化出来并且执行。
using System; using UnityEngine; using strange.extensions.context.api; using strange.extensions.command.impl; using strange.extensions.dispatcher.eventdispatcher.impl; namespace strange.examples.myfirstproject { public class StartCommand : EventCommand { [Inject(ContextKeys.CONTEXT_VIEW)] public GameObject contextView{get;set;} public override void Execute() { GameObject go = new GameObject(); go.name = "ExampleView"; go.AddComponent<ExampleView>(); go.transform.parent = contextView.transform; } } }
StartCommand 扩展 EventCommand 意味着这是固定的命令CommandBinder可以处理, 他继承的所有东西都来自command 和 EventCommand。特别是,继承EventCommand意味着你得到一个IEvent注入,并且你可以访问dispatcher。
如果你只是扩展命令,您不会有自动访问这些对象,但是你依旧可以手动注入他们
[Inject(ContextKeys.CONTEXT_DISPATCHER)] IEventDispatcher dispatcher{get;set;} [Inject] IEvent evt{get;set;}
注意所使用的两种不同类型的注入。IEventDispatcher和GameObject 都是用名字创建多个实例。这是因为我们想引用这些对象的非常具体的版本。我们不希望是任意一个GameObject。我们需要一个标记像ContextView。我们也不接受任何旧IEventDispatcher。唯一一个将在上下文间通信,他标志为ContextKeys.CONTEXT_DISPATCHER。另一方面,Ievent是一个简单的映射用于这个特殊的命令(技术上他映射到一个value),所以没有必要的名字。
依赖我们将使用在当前场景是ContextView,他们添加子视图到它。
Execute()方法通过CommandBinder自动触发。大多数情况下 , 执行的顺序是这样的
- 实例化Command命令绑定到Ievent.type
- 注入依赖关系,包括Ievent本身
- 调用Excute()
- 删除Command命令
命令不需要立即清理干净,但是我们将会得一点。如果你查看了Execute()里面的代码,你将会发现他是纯粹的Unity。创建一个GameObject,附上MonoBehaviour,然后设置它的父亲为ContextView。我们使用的是具体的MonoBehaviour(代码),然而,恰好是一个Strange IView,自从我们在context中映射这个界面。
mediationBinder.Bind<ExampleView>().To<ExampleMediator>();
这个界面是自动调度的,这意味着一个新的ExampleMediator刚刚创建!
A View is mediated(一个界面被调度)
如果你花费了一些时间为Unity编写代码,你创建一个界面,你需要调用Monobehavior,但是重点在于那个界面没有在屏幕上显示的东西。我不打算花时间执行ExampleView代码。你可以看下示例文件,如果怕你已经知道C#和Unity你不需要他。我只想引起两位的注意力。首先:
public class ExampleView : View
通过扩展View,你将会得到连接每个View到Context的代码。使用Strange 你不再需要扩展View或者重写里面的方法。但是如果你不扩展View,你依旧需要实现IView 接口。这需要确保你MonoBehaviour上下文可以操作。
第二项指出
[Inject] public IEventDispatcher dispatcher{get; set;}
注意 我们注入IEventDispatcher。但是跟StartCommand不是同一个调度。仔细看看代码第一个写在EventCommand(我上面显示)是这样的
[Inject(ContextKeys.CONTEXT_DISPATCHER)] public IEventDispatcher dispatcher{get; set;}
通过命名注入,指定的命令使用常见的上下文调度员。这个界面不应该注入dispatcher。中介的目的是隔离应用程序的视图 反之亦然Strange允许注入View。但这功能最好的时候严格限制,注入本地调度员与中介沟通很好。所以注入配置/布局文件(这是有用的,如果你发布到多个平台)。但如果你听我的劝告,不要注入入一个模型或服务或其他池外扩展的视图以及中介。
告诉你正确的方法:对于大多数开发人员来说,最难的是掌握整个框架的概念。一个视图应该只显示和输入。当某些输入发生,视图应该通知媒体。中介Mediator(允许注入上下文调度员)抽象的观点,关注与应用程序的其余部分。这个保护应用程序的视图代码,这通常和保护你的界面是混乱的,相反的情况是如此。
注意,基本视图类使用标准MonoBehaviour处理程序 Awake()
, Start()
, and OnDestroy()。如果你重写这些处理程序,确保你调用了base.Awake()等。这样Strange才能正常运行。
观察调度者
using System; using UnityEngine; using strange.extensions.dispatcher.eventdispatcher.api; using strange.extensions.mediation.impl; namespace strange.examples.myfirstproject { public class ExampleMediator : EventMediator { [Inject] public ExampleView view{ get; set;} public override void OnRegister() { view.dispatcher.AddListener (ExampleView.CLICK_EVENT, onViewClicked); dispatcher.AddListener (ExampleEvent.SCORE_CHANGE, onScoreChange); view.init (); } public override void OnRemove() { view.dispatcher.RemoveListener (ExampleView.CLICK_EVENT, onViewClicked); dispatcher.RemoveListener (ExampleEvent.SCORE_CHANGE, onScoreChange); Debug.Log("Mediator OnRemove"); } private void onViewClicked() { Debug.Log("View click detected"); dispatcher.Dispatch(ExampleEvent.REQUEST_WEB_SERVICE, "http://www.thirdmotion.com/"); } private void onScoreChange(IEvent evt) { string score = (string)evt.data; view.updateScore(score); } } }
在最上方 我们注入了ExampleView。这是调度者Mediator如何知道调度那个界面。介质可以知道很多关于他们的界面。中介通常被认为是“废品(信口开河的)代码”,因为它是非常特殊的细节视图和应用程序。当然这个中介可以知道视图有一个调度者和这个调度这个程序的事件成为ExampleView.CLICK_EVENT。通过监听这个事件,中介建立了一个处理程序(onViewClicked())告诉其余的应用这个点击意味着什么。视图不应该发送REQUEST_WEB_SERVICE事件。界面只是界面。它应该像这样派发事件HELP_BUTTON_CLICKED, COLLISION, SWIPE_RIGHT。这应该是Mediator中介者的工作,映射这些事件到应用程序的其余有意义的部分。如REQUEST_HELP MISSILE_ENEMY_COLLISION PLAYER_RELOAD.他后面的事件映射到命令,这些命令会调用帮助系统,计算分数增加(增加得分模型)或确定是否允许玩家重新加载。
OnRegister()和OnRemove()方法像Mediator调度者的构造与析构函数。OnRegister()在注入后发生。所以我经常用它来设置监听和调用Init()方法实例界面、OnRemove()发生在Monobehavior的OnDestroy()被调用之后。它触发的时候你可以用来清理监听。确定你移除了你的监听,否则会产生不正确的垃圾回收。
最后注意 通过扩展EventMediator我们有公共的dispatcher。 调度者在总线监听SCORE_CHANGE事件。
Another Command fires(其他命令触发)
让我们看回Context 这行有我们忽略的问题:
commandBinder.Bind(ExampleEvent.REQUEST_WEB_SERVICE).To<CallWebServiceCommand>();
这里的意思是任何时候公共的总线收到这个事件,它会启动CallWebServiceCommand。但是他吸引你的注意力是因为通过不同的方法使用命令。
using System; using System.Collections; using UnityEngine; using strange.extensions.context.api; using strange.extensions.command.impl; using strange.extensions.dispatcher.eventdispatcher.api; namespace strange.examples.myfirstproject { public class CallWebServiceCommand : EventCommand { [Inject] public IExampleModel model{get;set;} [Inject] public IExampleService service{get;set;} public override void Execute() { Retain (); service.dispatcher.AddListener (ExampleEvent.FULFILL_SERVICE_REQUEST, onComplete); string url = evt.data as string service.Request(url); } private void onComplete(IEvent result) { service.dispatcher.RemoveListener (ExampleEvent.FULFILL_SERVICE_REQUEST, onComplete); model.data = result.data as string; dispatcher.Dispatch(ExampleEvent.SCORE_CHANGE, evt.data); Release (); } } }
我们监听service,调用一个方法。我们使用事件中有效data数据来触发mediator调度者service.Request(the url)。当service结束,他派发。触发onComplate()。我们取消监听映射,设置一个值的模型,派发SCORE_CHANGE那个哪个调度者收到就做相应的处理。
但如果你一直密切关注你会记得,我之前提到过,命令后立即清理干净在Execute()完成时。所以为什么不是这个命令不会被垃圾收集。答案是最顶端调用Retain()方法保留。Retain()标志着这个命令为免除清理。这将会保持知道调用了Release()之后。显然,这意味着调用了Release()是非常重要的,否则会造成运行中的内存泄露风险。
不要允许模型和服务监听事件。 利用他们的调度者来监听事件
Mapping Across Contexts(穿过Contexts的映射)
一般来说你要遵守上下文边界。毕竟,它的边界是有原因的 :它允许应用程序的部分功能的隔离,使程序变得更加模块化。但有时有一些对象,也许是一个模型、一个服务、或者一个信号需要需要跨多个上下文访问。
injectionBinder.Bind<IStarship>().To<HeartOfGold>().ToSingleton().CrossContext();
添加CrossContext()信号绑定需要实例化穿过context边界。它将提供给所有孩子contexts。注意,也可以覆盖一个CrossContext绑定。如果你隐射局部的key,本地的绑定将会覆盖CrossContext的那一个。
到这里所有的文档内容已经结束了。自己着手做几个小栗子能快速的了解和适应这样的架构。架构的学习 确实可以改变人的编码习惯 。 就这几天对文档的阅读,感觉收获良多 。 希望有学习的小伙伴能够一起交流交流。