C#游戏框架uFrame

C#游戏框架uFrame兼谈游戏架构设计

c#语言规范

阅读目录

回到目录

1.概览

uFrame是提供给Unity3D开发者使用的一个框架插件,它本身模仿了MVVM这种架构模式(事实上并不包含Model部分,且多出了Controller部分)。因为用于Unity3D,所以它向开发者提供了一套基于Editor的可视化编辑工具,可以用来管理代码结构等。
需要指出的是它的一个重要的理念,同时也是软件工程中的一个重要理念就是关注分离(Separation of concern,SoC)。uFrame借助控制反转(IoC)/依赖注入(DI)实现了这种分离,从而进一步实现了MVVM这种模式。且在1.5版本之后,引入了UniRx库,引进了响应式编程的思想。
本文主要描述uFrame的这种设计思路以及uFrame本身的一些重要概念,且文中的uFrame版本为1.6。

回到目录

2.基本概念

2.1.清晰且简单

uFrame本身实现了一套MVVM的架构模式。我们之前更熟悉MVC架构模式,虽然MVC分层方式清楚,但是如果使用不当很可能让大量代码都集中在Controller之中,从而造成Controller的臃肿,甚至很多时候Controller和View会产生很多耦合。

而MVVM和MVC最大的一个区别是引入了ViewModel的概念。从名字上看,ViewModel是一种针对视图的模型。由于引入了ViewModel,从而解放了Controller。具体到Unity3D项目,使用uFrame我们可以将U3D中和视觉相关的内容和真正的核心逻辑剥离。

在uFrame中,使用Element这个概念将业务分拆成三部分:

  • ViewModel:保存游戏中对象的数据结构,例如血量、经验、金钱等等。
  • Controller:处理游戏业务逻辑。例如加血、减血之类的。
  • View:游戏世界中可以见的对象,和ViewModel绑定,以在游戏中进行展现。

其中ViewModel和Controller是属于Element的,View是配合Element而产生的游戏世界中的可见对象。
下面是一个的名为“Player”的Element在uFrame中的样子:

2.2.可移植性

通过刚刚的例子,我们可以看到ViewModelController事实上是处在幕后的,它们只需要实现纯逻辑代码即可,完全不需要关心在游戏中视觉上如何展示。正是因为不必关心具体的表现如何,所以ViewModel和Controller是具备移植性的。而在U3D项目中,View需要挂载在游戏对象上,同时它也是连接具体的游戏世界和抽象的逻辑代码之间的桥梁,通过View,uFrame将ViewModelController与U3D连接。
因此,我们不能通过Controller来访问View,因为正常情况下它们是不知道彼此的存在的,Controller将只和ViewModel进行交互,这样才能保持整体结构的清晰。
同时,我们也不应该通过ViewModel直接获取View,这是因为ViewModel应该只关心它自己的数据,而不关心到底是哪个View绑定了自己。

2.3.MVVM和Controller

既然说uFrame模仿了MVVM的架构,但是和传统的MVVM相比,uFrame却多出了一个Controller。

因此需要在这里指出,uFrame中的Controller用来配合ViewModel封装逻辑。 这是因为在uFrame中逻辑并不在ViewModel中,相反,当我们执行一条命令时,是对应的Controller来执行相应的逻辑。游戏逻辑有时有可能会十分复杂,但是由于将游戏逻辑移到了Controller中,因此ViewModel是十分轻量级的。

回到目录

3.依赖注入

3.1.面向接口编程

在介绍依赖注入之前,我们先来看一段项目中的代码。

class EquipDevelopPanelScript : IPanelScript
{
    ...

    public void SetType(DevelopType Type)
    {
        ...
        if(Type == DevelopType.Split)
        {
            TODO
        }
        else if(Type == xxx)
        {
            TODO
        }
        else if(Type == xxxx)
        {
            TODO
        }
        ...
    }
    ...
}

可以看到:

首先,在这段代码中我们设计的EquipDevelopPanelScript类(处在UI层的类!)的SetType方法很长(170+行),并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同,无非是根据不同的类型来设置显示内容。

再者,我认为这个设计比较大的一个问题是违反了OCP原则(开放关闭原则,指设计应该对扩展开放,对修改关闭。)。在这个设计中,如果以后我们增加一个新的UI类型,我们就要打开EquipDevelopPanelScript,修改SetType方法。而我们的代码应该是对修改关闭的,当有新UI加入的时候,应该使用扩展完成,避免修改已有代码。

一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同的UI类型看成一个策略,那么引入策略模式(Strategy Pattern,即将逻辑分别封装起来,让他们之间可以相互替换,此模式使得逻辑的变化独立于使用者。)是明智的选择。

最后,说一个小的问题,UI层主要是用来对数据进行展现,不应该包含过多的逻辑。

因此我们采用这样的思路:面向接口而不是具体的类(或逻辑)编程,使得我们可以轻松的替换具体的实现。所以,我们可以定义一个接口Interface:

public interface IDevelopType
{
    void SetInfoByType();
}

该接口将之前代码中TODO的部分归纳为了一个方法SetInfoByType,而只需要实现该接口的不同类(例如SplitTypeClass)重写SetInfoByType方法,便实现了在UI层中去除具体逻辑的功能。之后,我们只需要根据不同的要求,提供实现了IDevelopType接口的不同的类即可。
所以之前的100多行代码可以变成了这样的2行代码:

IDevelopType typeInfo = XXXX.GetInfoByType(Type);
teypInfo.SetInfoByType();

使用这种思路将之前的代码重构之后,我们能获得什么好处呢?

1.代码结构变得很清晰了,虽然类的数量增加了(因为if...else块中的逻辑被封装成了类),但是每个类中方法的代码都非常短,没有了以前SetType方法那种很长的方法,也没有了冗长的if…else。

2.类的职责更明确了,UI层的类的主要作用是来将数据展示出来,具体的逻辑交给别的类来处理。

3.引入Strategy策略模式后,不但消除了重复性代码,更重要的是使得设计符合了开闭原则。如果以后要加一个新UI类型,只要新建一个类,实现IDevelopType接口,当需要使用这个UI类型时,我们只要实例化一个新UI类型类,并赋给局部变量typeInfo即可,已有的EquipDevelopPanelScript代码不用改动。这样就实现了对扩展开放,对修改关闭。

3.2.依赖注入的本质

好了,说了这么多依赖注入在哪里呢?其实它早就存在了。

我们再仔细看看刚刚的设计,经过这样设计之后,有个基本的问题被解决了:现在EquipDevelopPanelScript类的SetType方法不再依赖具体的UI类型,而仅仅依赖一个IDevelopType接口,接口是不能实例化的,但最终还是会被赋予一个实现了IDevelopType接口的具体UI类型类。

这里,实例化一个具体的UI类型类,并赋给变量typeInfo的过程,就是依赖注入,这里要清楚,依赖注入其实只是一个过程的称谓。

通过阅读uFrame的源代码,最直观的印象是:一个良好的设计必须做到将变化隔离,使得变化部分发生变化时,不变部分不受影响。只有这样才有可能适用于各种情况。为了做到这一点,就要利用面向对象中的多态性,使用多态性后,类和类之间便不再直接存在依赖,取而代之的是依赖于一个抽象的接口,这样,客户类就不能在内部直接实例化具体的服务类。

但是这样做的结果是客户类在运作中又客观需要具体的服务类提供服务,因为接口是不能实例化去提供服务的,于是就产生了“客户类不能依赖具体服务类”和“客户类需要具体服务类”这样一对矛盾。为了解决这个矛盾,开发人员提出了一种模式:客户类(如上例中的EquipDevelopPanelScript)定义一个注入点(临时变量typeInfo),用于服务类(实现IDevelopType接口的具体类,如SplitTypeClass等等)的注入,之后根据具体情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。

uFrame的基本思想便是使用依赖注入、面向接口编程使代码解耦,这些也是值得我们学习的地方。
例如下面这段uFrame的核心代码,大量的使用面向接口的思路,解除耦合:

 //参数只要实现IDisposable接口即可,不是具体的类型
public IDisposable AddBinding(IDisposable binding)
{
    if (!Bindings.ContainsKey(-1))
    {
        Bindings[-1] = new List<IDisposable>();
    }
    Bindings[-1].Add(binding);
    return binding;
}

回到目录

4.Manager of Managers

如果在Unity3D项目开发中没有考虑过架构的问题,那么最常见也最直接的一种做法就是在游戏场景中创建一个空的GameObject,然后挂上所有与GameObject无关的逻辑控制的脚本,并且使用GameObject.Find()访问对象数据。这样做最直接,但这个选择却十分糟糕,因为逻辑代码散落在各处,基本没有可维护性。

之后,我们可能会考虑将代码放在不同的单例中,但是有可能会导致一个单例的代码过多的问题,且和刚刚那个最直接的做法没有本质的区别,虽然存在很多单例,但是由于缺少组织,代码还是散落在各处,不适宜维护拓展。因此,我们需要一种可以组织代码的方式来架构我们的项目。

一个更好的思路是将代码按照业务划分成一些子系统,并通过相应的管理器来管理,例如UISysManager、GameStateSysManager等等。一个子系统内可以封装很多内容,但是只通过管理器对外暴露一些接口,使得整个子系统成为一个黑箱,外部调用者通过子系统暴露在外的接口进行操作。而这些Manager又需要被更高层级的Manager进行管理,使得整个游戏架构按照逻辑构造成了树状的结构,如下图:

                      Fox(游戏最高层管理器或者称为总入口)
                       /                                  /                                   /                              LogicMgr(逻辑管理)        HttpMgr(网络管理)
              /    |   \                /                  /     |    \              /                   /      |     \            /                UISysManager XXXXMgr XXXXMgr YYYMgr      YYYYMgr

这样做的优点便是代码的逻辑层次清晰,将逻辑模块化易于管理,且将对逻辑对象的访问都通过管理器的接口实现,从而规范了对游戏内对象的操作方式。例如我想要获取一个UI,只需要这样调用:

UIClass ui = Fox.LogicMgr.UISysManager.GetUI(id);

作为UI子系统外的调用者无需关心GetUI内部发生了什么,他需要做的仅仅是使用UI系统管理器提供的接口来获取目标UI。

uFrame中也包含类似的思想,它为我们提供了一个称为SubSystem的控件,在uFrane的Editor设计器中SubSystem是这样子的:

且每个SubSystem在设计器中都会对应一个System Loader类的实例,用来在运行时对子系统进行初始化等工作。

回到目录

5.利用UniRX实现响应式编程

uFrame框架1.6版本中处理View的绑定时大量的使用了响应式编程的思想。

所谓的响应式编程指的是:使用异步数据流进行编程,而所谓的异步数据流简单的说就是按时间排序的事件序列。而我们需要做的就是监听或者订阅(Subscribe)事件流,当事件触发(Publish)时响应即可。换句话说,这是一种观察者模式或者说订阅发布模式的实现。

uFrame实现响应式编程的方式是引入了UniRx库。需要说明的是Rx库是微软推出的一个响应式拓展的框架,但是由于Rx库无法在Unity3D中运行且存在iOS中IL2CPP兼容性的问题,因此后来有人为Unity3D重写了Rx库,也就是UniRx库。

为了实现观察者模式,UniRx提供了两个关键接口:IObservable和IObserver。

IObservable接口定义如下:

public interface IObservable<out T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

IObserver接口定义如下:

public interface IObserver<in T>
{
    void OnCompleted();

    void OnError(Exception error);

    void OnNext(T value);
}

在uFrame中,很多地方会使用这两个接口以实现观察者模式,例如在ViewModel中的订阅方法Subscribe的参数就是一个IObserver的集合:

public IDisposable Subscribe(IObserver<IObservableProperty> observer)
{
    PropertyChangedEventHandler propertyChanged = (sender, args) =>
    {
        var property = sender as IObservableProperty;
        //if (property != null)
            observer.OnNext(property);
    };

    PropertyChanged += propertyChanged;
    return Disposable.Create(() => PropertyChanged -= propertyChanged);
}

自然IObserver集合是基于观察者模式设计的。观察者模式的关键在于被观察的对象有一些行为或者属性,观察者可以注册某些感兴趣的属性或者行为。当被观察者发生状态改变时,会通知观察者(通常是发起一个事件),之后会有相应该事件的方法被调用,uFrame借助UniRx实现了这种模式。

下面我们就通过一个小例子来看看这种观察者模式在uFrame中的实现:

View中将指定的LevelSelectButton和RequestMainMenuScreenCommand进行绑定:

  this.BindButtonToHandler(LevelSelectButton, () =>
  {
      Publish(new RequestMainMenuScreenCommand()
      {
          ScreenType = typeof (LevelSelectScreenViewModel)
      });
  });

绑定的代码可以重写成以下形式可能更容易理解,即Publish发布一个事件:

var evt= new RequestMainMenuScreenCommand();
evt.ScreenType = typeof(LevelSelectScreenViewModel);
Publish(evt);

Controller中订阅/监听RequestMainMenuScreenCommand,并注册回调函数:

this.OnEvent<RequestMainMenuScreenCommand>().Subscribe(this.RequestMainMenuScreenCommandHandler);

其中this.OnEvent方法会返回一个IObservable的对象,所以我们可以接着调用Subscribe(handler)来订阅事件T,每当T事件被发布(Publish),对应的handler就会被调用。

回到目录

6.研究总结

uFrameMVVM架构无疑是十分简洁和易拓展的。它所使用的一些架构设计的思想十分值得我们学习和借鉴。例如利用依赖注入,使整个架构面向接口编程,因而具备了很强的拓展性。引入响应式编程的思想,实现了各个部分之间基于发布订阅模式的通信方式,更加消除了各个模块之间的耦合,使得代码易于维护和测试。最后,其整体逻辑架构也有一些Manager of Managers的思想,各个模块之间能够有效的管理和组织,使得基于该架构的游戏逻辑层次清晰。

但是,由于该插件提供的设计器要依赖Unity3D的Editor进行可视化操作,因此有可能会导致Editor方面的一些潜在风险,例如游戏内部系统过多会导致Editor的可视化区域难以管理,或者是我们在开发中对Editor的不当操作导致一些未知的问题。甚至由于是第三方提供的代码,因此uFrame的版本更迭可能会带来很多问题(1.5到1.6发生了很大的变化)等等。

因此,建议重点学习和掌握工具所提供的思想和设计思路。

时间: 2024-10-10 12:45:47

C#游戏框架uFrame的相关文章

【总结】游戏框架与架构设计(Unity为例)

使用框架开发游戏 优点:耦合性低,重用性高,部署快,可维护性高,方便管理.提高开发效率,降低开发难度 缺点:增加了系统结构和实现的复杂性,需要额外花费精力维护,不适合小型程序,易影响运行效率   常见框架 MVC  表现层(View):游戏画面.UI 逻辑层(Controller):数据接口,操作控制,AI 数据层(Model):数据保存,图片.声音等资源 我的SFramework中,View层是单独的,Model我放在基类中,Controller则在派生类,实现了MVC的分离(如果要重构的话我

libgdx游戏框架介绍

libgdx作为上层为java,底层c和c++的游戏引擎.简直是优秀得一塌糊涂.  这个游戏框架最初只有1个人在维护,现在已经加入不少人了,越来越给力. libgdx的架构 很清晰, 我们先从包的结构分析: assets 代表资源包,用于资源加载等管理. audio 音频包,游戏需要播放声音时用. files 文件处理包,内部主要对象是FileHandle ,如果你用过Libgdx肯定对次很熟悉,libgdx加载纹理图片等都是通过此对象. graphics 绘画相关,就是我们要把游戏中的控件或者

Android游戏框架Libgdx使用入门

转载自:http://blog.csdn.net/cping1982/article/details/6176191 Libgdx作者博客:http://www.badlogicgames.com/ Libgdx项目地址:http://code.google.com/p/libgdx/ Libgdx是一款支持2D与3D游戏开发的游戏类库,兼容大多数微机平台(标准JavaSE实现,能执行在Mac.Linux.Windows等系统)与Android平台(Android1.5以上就可以使用.Andro

使用Html5+C#+微信 开发移动端游戏详细教程 :(三)使用html5引擎搭建游戏框架

教程里的案例我们是通过H5游戏引擎开发,目前H5的游戏引擎比较好用的是白鹭,不过对于新手来说白鹭的开发环境和工具使用过于复杂,这里推荐一个国内大神编写的游戏引擎:lufylegend. 直接在页面引入Js文件,就可以开发了,运行效率非常高效,语法是仿AS3语法,懂C#的人上手会很快. Lufylegend引擎具体的API和使用方法可以参考官网和论坛: http://www.lufylegend.com/api/zh_CN/out/classes/FPS.html 之前微信上有一款"怪兽必须死&q

JS写小游戏(一):游戏框架

前言 前一阵发现一个不错的网站,都是一些用html5+css+js写的小游戏,于是打算学习一番,写下这个系列博客主要是为了加深理解,当然也有一些个人感悟,如果英文好可以直接Click Here. 概述 一般,小游戏都要关注两个问题:刷新和交互.因为游戏一般是动态的,所以需要不断刷新.JavaScript是单线程,如果用C语言写过贪吃蛇之类的小游戏,应该知道,单线程一般是挂起一个时间来达到动态效果.比如C语言的Sleep(),JS的setInterval()等.但是js还有一种更高性能的方法req

【开源java游戏框架libgdx专题】-01-libgdx介绍

libgdx是一款开源的java游戏框架,而且还实现了Desktop/Android/BlackBerry/iOS/HTML5这些些平台的跨平台开发.官方网址:https://libgdx.badlogicgames.com/ 框架项目截图(Eclipse需要安装Gradle): libgdx项目采用工具自动生成项目(可以自己配置扩展): 原文由博主 乐智 编辑撰写,版权归博主所有. 原文地址 http://www.dtblog.cn/1086.html 转载请注明出处!

cocos2dx游戏开发——微信打飞机学习笔记(二)——游戏框架

一.游戏的基本框架: WelcomeScene    ——>    GameScene   ——>   GameOverScene ||                                       ||                                    || ∨                                      ∨                                   ∨ WelcomeLayer            

基于状态机的游戏框架

一 定义 有限状态机就是一个具有有限数量状态, 并且能够根据相应的操作从一个状态变换到另一个状态, 而在同一时刻只能处在一种状态下的智能体. 英文:Finite State Machine 简称:FSM 二 最简单的状态机 最简单的状态机:if-else 实际上if-else就是一个最有两种状态的状态机,分别是true和false 三 伪状态机 当两种情况不能满足我们的需求时,我们可以用if-else if -...-else, 不过,为了方便,我们可以使用switch-case代替 首先,定义

【开源java游戏框架libgdx专题】-05-模块描述与上下文

模块描述(Modules overview) Input:为所有的平台提供一个统一的输入模型和处理程序. 获取触摸示例: 1 if (Gdx.input.isTouched()) { 2 System.out.println("Input occurred at x="+Gdx.input.getX() +", y="+Gdx.input.getY()); 3 } Graphics:能够使用硬件提供的OpenGL ES在屏幕上绘制图片 获取OpenGL API 2.