游戏框架源码-C#游戏框架uFrame解构及游戏框架设计

1. 概述

uFrame是为Unity3D开发者提供的一个框架插件,它本身模仿了MVVM的架构模式(实际上它不包含Model部分,多了一个Controller部分)。 因为是用在Unity3D中,所以它为开发者提供了一套基于Editor的可视化编辑工具,可以用来管理代码结构等。

需要强调的是游戏框架源码,它的一个重要概念,也是软件工程中的一个重要概念,就是关注点分离(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都属于Element,View是用Element构成的游戏世界中可见的对象。

这是 uFrame 中名为“Player”的元素的样子:

2.2. 可移植性

通过刚才的例子,我们可以看到ViewModel和Controller实际上是在幕后进行的。 他们只需要实现纯粹的逻辑代码,而不需要关心如何在游戏中直观地显示出来。 正是因为不需要关心具体的性能,ViewModel和Controller才具有可移植性。 在U3D项目中,View需要挂载在游戏对象上。 同时,它也是具体游戏世界与具体逻辑代码之间的桥梁。 ViewModel和Controller通过View和uFrame连接到U3D。

因此,我们无法通过Controller来访问View,因为一般情况下它们并不知道彼此的存在,Controller只会与ViewModel进行交互,从而保持整体结构的清晰。

同时,我们不应该直接通过ViewModel获取View,因为ViewModel应该只关心自己的数据,而不是哪个View绑定了自己。

2.3. MVVM 和控制器

既然说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层主要用于解释数据,不应该包含太多逻辑。

游戏框架源码-C#游戏框架uFrame解构及游戏框架设计

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

public interface IDevelopType
{
    void SetInfoByType();
}

这个socket将前面代码的TODO部分总结成一个方法SetInfoByType,只需要实现socket的不同类(如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的过程就是依赖注入。 这里应该清楚,依赖注入只是一个进程的名称。

游戏框架源码-C#游戏框架uFrame解构及游戏框架设计

通过阅读uFrame的源码,最直观的印象是,一个好的设计必须隔离变化,这样当变化的部分发生变化时,未变化的部分不会受到影响。 只有这样,才能适用于各种情况。 为了实现这一点,就需要使用面向对象中的多态性。 使用多态性后,类与类之间不再有直接的依赖关系,而是依赖于一个具体的socket。 这样,客户端类就是具体的服务类,不能直接在内部实例化。

但这样做的结果是,客户端类在运行时客观上需要特定的服务类来提供服务,因为socket无法实例化来提供服务,从而形成“客户端类不能依赖于特定的服务类”和“客户端类不能依赖于特定的服务类”。客户端类需要“具体服务类”这样的一对矛盾。 为了解决这个矛盾,开发者提出了一种模式:客户端类(如上例中的EquipDevelopPanelScript)定义一个注入点(临时变量typeInfo),用于服务类(实现IDevelopType的具体类) socket(如SplitTypeClass等)注入,然后根据具体情况,实例化服务类,注入到客户类中,这样就解决了这个矛盾。

uFrame的基本思想是利用依赖注入和面向接口编程的方式使代码前馈,这也值得我们研究。

比如下面uFrame的核心代码就大量使用了面向socket的思想来解耦:

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

4. 经理中的经理

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

之后我们可能会考虑将代码放在不同的单例中,但是可能会导致一个单例中的代码过多,而且和刚才最直接的做法没有本质区别,虽然单例很多,但是由于缺乏组织,代码仍然分散各处,不适合维护和扩展。 因此,我们需要一个可以组织代码来构建我们的项目的表单。

更好的想法是根据业务将代码定义成一些子系统,通过相应的管理器进行管理,比如UISysManager、GameStateSysManager等。 一个子系统可以封装很多内容,但只通过管理器暴露了一些套接字,使整个子系统成为一个黑盒子,外部调用者通过子系统暴露的套接字进行操作。 而这种Manager需要有更高级的Manager来管理,这样整个游戏结构在逻辑上就构建成了一个树形结构,如下图所示:

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

这样做的好处是代码逻辑层次清晰,逻辑模块化便于管理,通过管理器的socket实现对逻辑对象的访问,从而规范了游戏中对象的操作方法。 例如,如果我想获得一个 UI,我只需要像这样调用它:

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

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

游戏框架源码-C#游戏框架uFrame解构及游戏框架设计

uFrame中也包含类似的思想,它为我们提供了一个名为SubSystem的控件。 在uFrame的编辑器设计器中,子系统如下所示:

并且每个SubSystem也会对应设计器中的SystemLoader类的一个实例,用于在运行时初始化子系统。

5. 使用 UniRX 进行响应式编程

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

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

uFrame实现响应式编程的方式是引入UniRx库。 需要说明的是,Rx库是Google推出的响应式扩展框架,但由于Rx库在Unity3D中运行失败,并且iOS中存在IL2CPP兼容性问题,所以后来有人为Unity3D重新绘制了Rx库,即UniRx图书馆。

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

IObservable 套接字定义如下:

public interface IObservable
{
    IDisposable Subscribe(IObserver observer);
}

IObserver套接字定义如下:

public interface IObserver
{
    void OnCompleted();
    void OnError(Exception error);
    void OnNext(T value);
}

在uFrame中,很多地方都使用了这两个socket来实现观察者模式。 例如,ViewModel中的Subscribe参数是IObserver的集合:

public IDisposable Subscribe(IObserver observer)
{
    PropertyChangedEventHandler propertyChanged = (sender, args) =>
    {
        var property = sender as IObservableProperty;
        //if (property != null)
            observer.OnNext(property);
    };
    PropertyChanged += propertyChanged;
    return Disposable.Create(() => PropertyChanged -= propertyChanged);
}

自然的IObserver集合是基于观察者模式设计的。 观察者模式的关键在于被观察的对象具有一些行为或属性,观察者可以注册个别感兴趣的属性或行为。 当被观察的状态发生变化时,观察者会收到通知(通常会发起一场storm),然后调用相应的storm方法。 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().Subscribe(this.RequestMainMenuScreenCommandHandler);

其中,this.OnEvent方法会返回一个IObservable对象,因此我们接下来可以调用Subscribe(handler)来订阅风暴T,每当风暴T发布(Publish)时,就会调用相应的处理程序。

六、研究总结

uFrameMVVM架构无疑非常简单且易于扩展。 它所采用的一些架构设计思想值得学习和借鉴。 比如借助依赖注入,整个框架面向接口编程,因此具有很强的可扩展性。 引入了响应式编程的思想,实现了各部分之间基于发布订阅模式的通信方式,进一步消除了各模块之间的耦合性,使得代码易于维护和测试。 最后,它的整体逻辑结构也有一些Manager of Managers的思想,使得各个模块能够得到有效的管理和组织,从而使得基于这个结构的游戏逻辑层次清晰。

但由于插件提供的设计器依赖于Unity3D的Editor进行可视化操作,因此可能会在Editor中造成一些潜在的风险,比如游戏内部系统太多会导致Editor的可视化区域难以管理,或者我们正在开发的编辑器操作不当导致了一些未知的问题。 甚至因为代码是第三方提供的,uFrame的版本替换可能会导致很多问题(1.5到1.6已经改变很多)等等。

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

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 游戏源码 游戏框架源码-C#游戏框架uFrame解构及游戏框架设计 https://www.wkzy.net/game/180552.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务