好久没有发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的的结构应该是这样的:

  1. Assets
  2. StrangeIoC
  3. scripts

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

  1. Assets
  2. Game
  3. Scenes
  4. Scripts

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

  1. using System;
  2.  
  3. using strange.extensions.signal.impl;
  4.  
  5. namespace Game {
  6.  
  7. public class StartSignal : Signal {}
  8.  
  9. }

在Strange中,这个signal的概念非常像观察者模式(observer pattern)中的事件(events)。在这里,它以命名类的方式实现了继承Strange的Signal类.别急,我们马上会看到怎么去使用它。

  Strange采用"Contexts"的概念来识别不同的问题域或者子模块。在实际的游戏项目中,你可以有多个"Contexts",比如游戏逻辑、资源、持久层、统计分析、社交模块等等。我们在这个实例中只用了一个"Context"。
  一个预构建的context在Strange中称为MVCSContext,MVCSContext默认使用event机制,我们来创建另外一种context父类,改造成使用signal机制,我们其他的context要继承这个SignalContext。
  在Scripts下创建名为SignalContext.cs的脚本:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.context.impl;
  6. using strange.extensions.command.api;
  7. using strange.extensions.command.impl;
  8. using strange.extensions.signal.impl;
  9.  
  10. namespace Game {
  11. public class SignalContext : MVCSContext {
  12.  
  13. /**
  14. * Constructor
  15. */
  16. public SignalContext (MonoBehaviour contextView) : base(contextView) {
  17. }
  18.  
  19. protected override void addCoreComponents() {
  20. base.addCoreComponents();
  21.  
  22. // bind signal command binder
  23. injectionBinder.Unbind<ICommandBinder>();
  24. injectionBinder.Bind<ICommandBinder>().To<SignalCommandBinder>().ToSingleton();
  25. }
  26.  
  27. public override void Launch() {
  28. base.Launch();
  29. Signal startSignal = injectionBinder.GetInstance<StartSignal>();
  30. startSignal.Dispatch();
  31. }
  32.  
  33. }
  34. }

在"Scripts"文件夹下创建一个新文件夹"Controller",到这里有了一点MVC模式的特征。Strange作者建议我们应该以指令类(Command Class)的形式实现各个Controller接口,这个文件夹将包含所有的Command类,现在我们创建一个在StartSignal指令调用时执行的指令。在Controller文件夹下创建名为HelloWorldStartCommand.cs的类:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.context.api;
  6. using strange.extensions.command.impl;
  7.  
  8. namespace Game {
  9. public class HelloWorldStartCommand : Command {
  10.  
  11. public override void Execute() {
  12. // perform all game start setup here
  13. Debug.Log("Hello World");
  14. }
  15.  
  16. }
  17. }
  现在我们为这个HelloWorld示例创建一个自定义的context类HelloWorldContext.cs:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.context.impl;
  6.  
  7. namespace Game {
  8. public class HelloWorldContext : SignalContext {
  9.  
  10. /**
  11. * Constructor
  12. */
  13. public HelloWorldContext(MonoBehaviour contextView) : base(contextView) {
  14. }
  15.  
  16. protected override void mapBindings() {
  17. base.mapBindings();
  18.  
  19. // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) on Launch()
  20. commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once();
  21. }
  22.  
  23. }
  24. }

在这里,我们把StartSignal类绑定(bind)给了HelloWorldStartCommand类。这样在StartSignal的实例被调用时,HelloWorldStartCommand会进行实例化(instantiated)和执行(executed),注意在我们的示例中StartSignal信号会在SignalContext.Launch()方法中调用发出。

  最后一步就是创建一个MonoBehaviour来在Unity中管理context,在Scripts文件夹下创建HelloWorldBootstrap.cs:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.context.impl;
  6.  
  7. namespace Game {
  8. public class HelloWorldBootstrap : ContextView {
  9.  
  10. void Awake() {
  11. this.context = new HelloWorldContext(this);
  12. }
  13.  
  14. }
  15. }
用于在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. namespace Game {
  2. public interface ISomeManager {
  3.  
  4. /**
  5. * Perform some management
  6. */
  7. void DoManagement();
  8.  
  9. }
  10. }

这就是我们示例当中的manager接口,注意:Strange的作者建议我们总是使用一个接口然后通过injectionBinder将它映射到一个真正的实现类,当然,你也可以使用多对多的映射。接下来我们创建一个具体实现类,在Scripts文件夹下创建ManagerAsNormalClass.cs脚本:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. namespace Game {
  6. public class ManagerAsNormalClass : ISomeManager {
  7.  
  8. public ManagerAsNormalClass() {
  9. }
  10.  
  11. #region ISomeManager implementation
  12. public void DoManagement() {
  13. Debug.Log("Manager implemented as a normal class");
  14. }
  15. #endregion
  16.  
  17. }
  18. }

如果你仔细在看你可能会发现这是一个没有MonoBehaviour的manager,别急,一会再介绍怎么bind有MonoBehaviour的

现在我们来创建一个简单的交互场景,效果是当一个Button按下时,ISomeManager的DoManagement函数执行,这里我们有一个要求:用MVC思想---对controll层(ISomeManager)和view层(控制Button触发事件的脚本)完全解耦,view层只需要通知controll层:"hey!button被点击了",至于接下来发生什么交由controll层进行逻辑处理。

现在缺一个view层,把它创建出来吧---在Game文件夹下创建"View"文件夹,创建HelloWorldView.cs脚本:

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.mediation.impl;
  6. using strange.extensions.signal.impl;
  7.  
  8. namespace Game {
  9. public class HelloWorldView : View {
  10.  
  11. public Signal buttonClicked = new Signal();
  12.  
  13. private Rect buttonRect = new Rect(, , , );
  14.  
  15. public void OnGUI() {
  16. if(GUI.Button(buttonRect, "Manage")) {
  17. buttonClicked.Dispatch();
  18. }
  19. }
  20.  
  21. }
  22. }

这里继承的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. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.mediation.impl;
  6.  
  7. namespace Game {
  8. public class HelloWorldMediator : Mediator {
  9.  
  10. [Inject]
  11. public HelloWorldView view {get; set;}
  12.  
  13. [Inject]
  14. public ISomeManager manager {get; set;}
  15.  
  16. public override void OnRegister() {
  17. view.buttonClicked.AddListener(delegate() {
  18. manager.DoManagement();
  19. });
  20. }
  21.  
  22. }
  23. }

在这段代码里我们可以看到神奇的"Inject"标注(Inject attribute)。这个"Inject"标注只能和变量搭配使用,当一个变量上面有"Inject"标注时,意味着Strange会把这个变量的一个实例自动注入到它对应映射的context中。据此从我们上面的代码来分析,在这里我们获取到了"view"和"manager"的实例,并且不用去关心这些个实例是怎么来的。

OnRegister()是一个可以被重写的方法,它用来标记实例注入完成已经可以使用了,它的意义主要是进行初始化,或者说做准备。在上面的类中,OnRegister方法中为HellowWorldView.buttonClicked signal添加了一个监听器,这个监听器的逻辑是按下就执行manager.DoManagement方法。

  接下来就是最后的工作,我们需要把待绑的类映射到Strange Context中。打开我们之前写的HelloWorldContext脚本,在mapBindings()方法中添加代码:
  1. protected override void mapBindings() {
  2. base.mapBindings();
  3.  
  4. // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch()
  5. commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once();
  6.  
  7. // bind our view to its mediator
  8. mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>();
  9.  
  10. // bind our interface to a concrete implementation
  11. injectionBinder.Bind<ISomeManager>().To<ManagerAsNormalClass>().ToSingleton();
  12. }
在HelloWorld scene中,添加一个名为"View"的GameObject,add HelloWorldView 脚本,运行场景,你应该能看到当我们按下"Manage"按钮时,控制台输出"Manager implemented as a normal class"。
你会发现Strange自动把HelloWorldMediator脚本挂载到了"View"GameObject上面。注意我们之前并没有手动把HelloWorldMediator脚本挂载到"View"GameObject上。
 
MonoBehaviour Manager
  大部分时候,我们需要类似于上面的manager但是实现类是一个MonoBehaviour,这样我们才能使用例如协程、序列化的Unity特性。
  接下来创建实现MonoBehaviour接口的manager实例,看看怎么在Strange中进行bind。
  创建一个实现MonoBehaviour接口的manager,在Script文件夹下,命名为ManagerAsMonobehaviour.cs
  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. namespace Game {
  6. public class ManagerAsMonoBehaviour : MonoBehaviour, ISomeManager {
  7.  
  8. #region ISomeManager implementation
  9. public void DoManagement() {
  10. Debug.Log("Manager implemented as MonoBehaviour");
  11. }
  12. #endregion
  13.  
  14. }
  15. }

在HelloStrangeScene中,创建一个新的GameObject名为"Manager",add 上面创建好的 ManagerAsMonobehaviour脚本

  编辑HelloWorldContext脚本的mapBindings()方法:
  1. protected override void mapBindings() {
  2. base.mapBindings();
  3.  
  4. // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch()
  5. commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once();
  6.  
  7. // bind our view to its mediator
  8. mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>();
  9.  
  10. // REMOVED!!!
  11. //injectionBinder.Bind<ISomeManager>().To<ManagerAsNormalClass>().ToSingleton();
  12.  
  13. // bind the manager implemented as a MonoBehaviour
  14. ManagerAsMonoBehaviour manager = GameObject.Find("Manager").GetComponent<ManagerAsMonoBehaviour>();
  15. injectionBinder.Bind<ISomeManager>().ToValue(manager);
  16. }

与把ISomeManager映射为一个类型相反,我们把这个ManagerAsMonobehaviour映射为一个实例值(instance value)。

 
Injection in Command
  到目前为止我们为HelloWorldMediator注入了一个ISomeManager的一个实例,并且可以直接使用它。这样做其实并不是很理想,一个Mediator应该是在view层和controller层之间的一个薄层。我们需要尽量使Mediator层不去关心应该在Manager类去做的那部分复杂的逻辑处理代码。虽然这么做也可以,我们还是用signal把这部分映射到command层吧。
  编辑HelloWorldSignals.cs脚本,添加一个DoManagementSignal:
  1. using System;
  2.  
  3. using strange.extensions.signal.impl;
  4.  
  5. namespace Game {
  6.  
  7. public class StartSignal : Signal {}
  8.  
  9. public class DoManagementSignal : Signal {} // A new signal!
  10.  
  11. }

我们创建command映射到signal:在Controller文件夹下创建一个脚本DoManagementCommand.cs

  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.context.api;
  6. using strange.extensions.command.impl;
  7.  
  8. namespace Game {
  9. public class DoManagementCommand : Command {
  10.  
  11. [Inject]
  12. public ISomeManager manager {get; set;}
  13.  
  14. public override void Execute() {
  15. manager.DoManagement();
  16. }
  17.  
  18. }
  19. }

在这个类,我们把ISomeManager注入到command类,并且在Execute方法中让它的DoManagement方法执行。

修改HelloWorldMediator类:
  1. using System;
  2.  
  3. using UnityEngine;
  4.  
  5. using strange.extensions.mediation.impl;
  6.  
  7. namespace Game {
  8. public class HelloWorldMediator : Mediator {
  9.  
  10. [Inject]
  11. public HelloWorldView view {get; set;}
  12.  
  13. [Inject]
  14. public DoManagementSignal doManagement {get; set;}
  15.  
  16. public override void OnRegister() {
  17. view.buttonClicked.AddListener(doManagement.Dispatch);
  18. }
  19.  
  20. }
  21. }

现在我们的mediator类中已经没有任何对ISomeManager接口的调用了。取而代之的是要在mediator类获取到DoManagementSignal的实例,当button点击时,这个类会发出DoManagementSignal。mediator层不需要知道任何manager的事情,它只管发送信号(signal)出去。

最后,在HelloWorldContext.mapBindings()方法中添加这个signal-command映射。
  1. protected override void mapBindings() {
  2. base.mapBindings();
  3.  
  4. // we bind a command to StartSignal since it is invoked by SignalContext (the parent class) during on Launch()
  5. commandBinder.Bind<StartSignal>().To<HelloWorldStartCommand>().Once();
  6. commandBinder.Bind<DoManagementSignal>().To<DoManagementCommand>().Pooled(); // THIS IS THE NEW MAPPING!!!
  7.  
  8. // bind our view to its mediator
  9. mediationBinder.Bind<HelloWorldView>().To<HelloWorldMediator>();
  10.  
  11. // bind the manager implemented as a MonoBehaviour
  12. ManagerAsMonoBehaviour manager = GameObject.Find("Manager").GetComponent<ManagerAsMonoBehaviour>();
  13. injectionBinder.Bind<ISomeManager>().ToValue(manager);
  14. }

运行场景,效果和之前一样,但是我们在代码层面把这块代码重构了。

 
最后
  你会注意到这篇文章动不动就提到"作者建议"这样的话,这是因为作者的建议确实是一个比较重要的选择。比如说你可以在你的view层中直接注入各种实例,可能你压根不想去创建什么mediator层!我想说的是你可以根据你的实际需要来决定使用Strange的方法,但是我选择了根据作者的建议来使用它因为对于我来说这样还不错。
  如果你把这篇文章看到了这里,你可能会很疑惑:"我为什么要在我的项目里搞这么多复杂又多余的层?"其实在我自己的项目中应用这个框架也是处于探索研究阶段,现在的感受Strange有一个好处是强行让我把每个模块划分了。比如这样一个情形:用Strange Context处理SimpleSQL数据持久化,对于现有的游戏逻辑代码,没有使用Strange的部分,他们间的通信通过signal来进行。
  我们还在另外一个RPG项目用了Strange,我希望用了它以后可以像它的口号一样,确实有助于代码间的协作。到目前来看我没法跟你宣称它在我的项目中有多好,因为我们也只是在起步阶段,但是至少到目前为止对于我们来说它工作的还不错。我们的模式是使用多个Context,一个程序员负责一个Context,然后通过signal来与其他人的Context通信。

Get Start StrangeIOC for Unity3D的更多相关文章

  1. 框架学习笔记:Unity3D的MVC框架——StrangeIoC

    作为从AS3页游走过来的人,看见StrangeIoC会额外亲切,因为StrangeIoC的设计和RobotLegs几乎一致,作为一款依赖注入/控制反转(IoC)的MVC框架,StrangeIoC除了使 ...

  2. 关于StrangeIOC框架

    在Unity上进行开发,请先看对其开发模式应用的讨论: http://www.reddit.com/r/Unity3D/comments/1nb06h/unity_design_patterns_an ...

  3. Unity3D手游开发实践

    <腾讯桌球:客户端总结> 本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身.虽然基于Unity3D,很多东西同样适用于Cocos.本文从以下10大点进行阐述: 架构设计 原生插件/ ...

  4. StrangeIoc框架学习

    StrangeIoc是一款基于MVCS的一种框架,是对MVC思想的扩展,是专门针对Unity3D开发的一款框架,非常好用. 一.MVCS分别代表什么 MVCS框架是一种模块的分离,一种写代码的规则,目 ...

  5. (转)Unity3D手游开发实践

    作者:吴秦出处:http://www.cnblogs.com/skynet/本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接). (转)& ...

  6. Unity3d学习 预设体(prefab)的一些理解

    之前一直在想如果要在Unity3d上创建很多个具有相同结构的对象,是如何做的,后来查了相关资料发现预设体可以解决这个问题! 预设体的概念: 组件的集合体 , 预制物体可以实例化成游戏对象. 创建预设体 ...

  7. Unity3d入门 - 关于unity工具的熟悉

    上周由于工作内容较多,花在unity上学习的时间不多,但总归还是学习了一些东西,内容如下: .1 根据相关的教程在mac上安装了unity. .2 学习了unity的主要的工具分布和对应工具的相关的功 ...

  8. TDD在Unity3D游戏项目开发中的实践

    0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...

  9. warensoft unity3d 更新说明

    warensoft unity3d 组件的Alpha版本已经发布了将近一年,很多网友发送了改进的Email,感谢大家的支持. Warensoft Unity3D组件将继续更新,将改进的功能如下: 1. ...

随机推荐

  1. 【Nutch2.2.1源代码分析之4】Nutch加载配置文件的方法

    小结: (1)在nutch中,一般通过ToolRunner来运行hadoop job,此方法可以方便的通过ToolRunner.run(Configuration conf,Tool tool,Str ...

  2. Altium Designer 生成 Mach3 G代码的程序

    Altium Designer做PCB设计,还是很方便的,最近头脑发热,在网上买了一套CNC机床,用来做钻孔用,但是翻来翻去,基本上所有的软件都是铣削功能,而且很多软件很复杂.翻了好几天,发现没有什么 ...

  3. dedecms likearticle 调用附加表的字段调用方式

    [field:id runphp='yes'] $aid = @me; $row = $GLOBALS['dsql']->GetOne("Select 字段名 From `dede_a ...

  4. Jasper_passValue_return value from the subreport to main report

    create a variable In subreport  say returnValue variable class type --> whatever u want calculati ...

  5. iotop

    iotop命令是专门显示硬盘IO的命令,界面风格类似top命令.这个命令只有在kernelv2.6.20及以后的版本中才有.   1.直接yum安装,rh6的光盘里有包. yum install io ...

  6. Swing透明和变换

    以前或许大家对一个UI组件是否透明没有那么关心,但是自从Vista的毛玻璃出现后,UI透明就成了大家非常关注的一个话题,于是Java阵营开始了铺天盖地的讨论如何实现透明的效果,但是很不幸的是无论组件如 ...

  7. 杭电2059(dp)

    龟兔赛跑 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submis ...

  8. ecshop 管理员不需要旧密码

  9. WPF ICommand 用法

    基础类,继承与ICommand接口 using System; using System.Collections.Generic; using System.Linq; using System.Te ...

  10. VIm变成sublime (转)

    sublime在ubuntu下始终支持不是很好, 特别是对中文输入的支持,还有一些插件在ubuntu下也不能用. 在ubuntu下还是用vim吧.  我们一起把vim变成sublime. 只需要三步 ...