作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
爱德华多·迪亚斯·达科斯塔
Verified Expert in Engineering

Eduardo是一名拥有12年以上客户端和前端应用开发经验的开发者. 他总是乐于学习新事物.

PREVIOUSLY AT

无人机比赛联盟
Share

第一次编程的人通常会开始学习与经典的交易 Hello World program. 从那时起,更大的任务必然接踵而至. 每一次新的挑战都会给我们带来重要的教训:

项目越大,意大利面就越大.

很快,很容易看出,在大小团队中,一个人不能随心所欲. 代码必须维护,并且可能会持续很长时间. 你工作过的公司不可能每次他们想要修复或改进代码库的时候就查你的欧博体育app下载,然后问你(你也不希望他们这么做)。.

This is why 软件设计模式 exist; they impose simple rules to dictate the overall structure of a software project. 它们帮助一个或多个程序员分离大型项目的核心部分,并以标准化的方式组织它们, 当遇到不熟悉的代码库部分时,消除混乱.

These rules, 当所有人都跟着, 允许更好地维护和导航遗留代码, 新代码的添加速度也会更快. 较少的时间用于规划开发方法. 由于问题不是以一种方式出现的,所以不存在灵丹妙药的设计模式. 必须仔细考虑每种模式的优缺点, 找到最适合眼前挑战的人.

在本教程中,我将把我的经验与流行的联系起来 Unity游戏开发平台 以及用于游戏开发的模型-视图-控制器(MVC)模式. 在我七年的成长中, 在游戏开发过程中,我遇到了很多麻烦, 使用这种设计模式,我已经获得了很好的代码结构和开发速度.

我将首先解释一点Unity的基础架构,实体-组件模式. 然后,我将继续解释MVC是如何在其上适配的,并使用一个小的模拟项目作为示例.

Motivation

在软件的文献中,我们会发现大量的设计模式. 尽管他们有自己的一套规则, 开发人员通常会做一些规则调整,以便更好地使模式适应他们的特定问题.

这种“编程的自由”证明我们还没有找到一个, 软件设计的确定方法. Thus, 这篇文章并不是你问题的最终解决方案, but rather, 展示两种众所周知的模式的好处和可能性:实体-组件和模型-视图-控制器.

实体-组件模式

实体-组件(EC)是一种设计模式,我们首先定义组成应用程序的元素层次结构(实体)。, and later, 我们定义每个组件将包含的特性和数据(组件). 用更“程序员”的术语来说,实体可以是一个包含0个或多个组件的数组对象. 让我们像这样描述一个实体:

Some-entity [component0, component1,] ...]

下面是EC树的一个简单示例.

- app[应用程序]
   - game [Game]
      -播放器[键盘输入,渲染]
      - enemies
         蜘蛛[SpiderAI, Renderer]
         -食人魔[食人魔,渲染器]
      - ui [UI]
         - hud [hud, MouseInput, Renderer]
         暂停菜单[PauseMenu, MouseInput, Renderer]
         - victory-modal [VictoryModal, MouseInput, Renderer]
         -失败-模态[失败模态,鼠标输入,渲染]

EC是缓解多重继承问题的一种很好的模式, 在这种情况下,复杂的类结构可能会引入 diamond problem where a class D, 继承两个类, B and C, 具有相同基类A, 会因为B和C对A特征的修改不同而产生冲突吗.

图片:钻石问题

这类问题在游戏开发中很常见,因为继承经常被广泛使用.

通过将功能和数据处理程序分解为更小的组件, 它们可以在不同的实体中附加和重用,而不依赖于多个继承, by the way, 甚至不是c#或Javascript的特性, Unity使用的主要语言).

实体-组件的不足之处

作为OOP之上的一层,EC有助于整理和更好地组织代码体系结构. However, 在大型项目中, 我们仍然“过于自由”,我们可能会发现自己处于“功能海洋”中。, 很难找到正确的实体和组件, 或者弄清楚它们应该如何相互作用. 有无数种方法可以为给定的任务组装实体和组件.

图片:ec特征海洋

避免混乱的一种方法是在Entity-Component之上强加一些额外的指导方针. For example, 我喜欢把软件分成三个不同的类别:

  • 一些处理原始数据,允许它被创建,读取,更新,删除或搜索.e., the CRUD concept).
  • 另一些实现了与其他元素交互的接口, 检测与其范围相关的事件,并在事件发生时触发通知.
  • Finally, 一些元素负责接收这些通知, 制定业务逻辑决策, 并决定如何处理数据.

幸运的是,我们已经有了一个以这种方式运行的模式.

模型-视图-控制器(MVC)模式

The 模型-视图-控制器模式 (MVC)将软件分成三个主要组件:模型(数据CRUD), 视图(接口/检测)和控制器(决策/动作). MVC足够灵活,甚至可以在ECS或OOP之上实现.

游戏和UI开发通常都需要等待用户的输入, 或其他触发条件, 在适当的地方发送这些事件的通知, 决定如何应对, 并相应地更新数据. 这些操作清楚地显示了这些应用程序与MVC的兼容性.

该方法引入了另一个抽象层,它将有助于软件规划, 并且还允许新程序员在更大的代码库中导航. 通过将思维过程分解成数据, interface, and decisions, 开发人员可以减少为了添加或修复功能而必须搜索的源文件的数量.

Unity and EC

让我们先仔细看看Unity带给我们什么.

Unity是一个基于ec的开发平台,其中所有实体都是实例 GameObject 以及使它们“可见”的特征,” “moveable,” “interactable,” and so on, 由类扩展提供 Component.

Unity编辑器 Hierarchy Panel and Inspector Panel 提供一种强大的方式来组装应用程序, 附加组件, 配置它们的初始状态,用比通常少得多的源代码引导你的游戏.

截图:层次面板
层级面板,右边有四个游戏对象

截图:检查器面板
带有GameObject组件的检查面板

Still, 正如我们讨论过的, 我们可能会遇到“功能太多”的问题,并发现自己处于一个巨大的层次结构中, 到处都是特色, 让开发者的生活变得更加艰难.

以MVC的方式思考, we can, instead, 首先根据功能划分事物, 像下面的例子一样构建我们的应用程序:

截图:unity MVC结构示例

将MVC应用于游戏开发环境

Now, 我想介绍对通用MVC模式的两个小修改, 这有助于适应我遇到的使用MVC构建Unity项目的独特情况:

  1. MVC类引用很容易分散在代码中. - Within Unity, 开发人员通常必须拖放实例以使其可访问, 或者通过繁琐的find语句来访问它们,比如 GetComponent( ... ). -如果Unity崩溃或某些bug使所有拖动的引用消失,将会出现丢失引用地狱. -这使得有必要有一个根引用对象,通过它的所有实例 Application 可以到达并恢复吗.
  2. 一些元素封装了应该高度可重用的一般功能, 它自然不属于模型的三个主要类别之一, View, or Controller. 我喜欢简单地称之为 Components. 它们也是实体组件意义上的“组件”,但只是在MVC框架中充当助手. —例如:a Rotator Component, 它只以给定的角速度旋转物体而不通知, store, 或者做任何决定.

为了帮助缓解这两个问题,我提出了一个修改后的模式,我称之为 AMVCC或应用程序-模型-视图-控制器-组件.

图片:amvcc图

  • Application -单一入口点到您的应用程序和容器的所有关键实例和应用程序相关的数据.
  • MVC -你现在应该知道了. :)
  • Component -可以重用的小的、包含良好的脚本.

这两个修改已经满足了我使用过的所有项目的需求.

Example: 10 Bounces

举个简单的例子,让我们看看一款名为 10 Bounces,其中我将利用AMVCC模式的核心元素.

游戏设置很简单:A Ball with a SphereCollider and a Rigidbody (在“Play”之后开始下降),a Cube 作为地面和5个脚本组成AMVCC.

Hierarchy

在编写脚本之前,我通常从层次结构开始,创建类和资产的大纲. 始终遵循这种新的AMVCC风格.

截图:构建层次结构

如我们所见, view GameObject包含了所有的视觉元素和其他元素 View scripts. The model and controller 对于小型项目,gameobject通常只包含它们各自的脚本. 对于更大的项目,它们将包含带有更具体脚本的GameObjects.

当有人浏览你的项目想要访问:

  • Data: Go to application > model > ...
  • Logic/Workflow: Go to application > controller > ...
  • 呈现/接口/检测: Go to application > view > ...

如果所有的团队都遵循这些简单的规则,那么遗留项目就不会成为问题.

请注意,没有 Component 容器,因为, 正如我们讨论过的, 它们更加灵活,可以在开发者闲暇时附加到不同的元素上.

Scripting

注意:下面显示的脚本是实际实现的抽象版本. 详细的实现不会给读者带来太多好处. 然而,如果你想探索更多, here’s the link 到我个人的Unity MVC框架,Unity MVC. 您将找到实现大多数应用程序所需的AMVCC结构框架的核心类.

让我们看一下的脚本结构 10 Bounces.

Before starting, 对于那些不熟悉Unity工作流程的人, 让我们简单说明一下脚本和GameObjects是如何协同工作的. 在Unity中,“组件”,在实体组件的意义上,由 MonoBehaviour class. 在运行时存在, 开发者应该将其源文件拖放到GameObject中(这是实体-组件模式中的“实体”),或者使用命令 AddComponent(). 在此之后,脚本将被实例化并准备在执行期间使用.

To begin, 我们定义Application类(AMVCC中的“A”), 哪个将是包含所有实例化游戏元素引用的主要类. 我们还将创建一个名为 Element,这使我们能够访问应用程序的实例及其子MVC实例.

记住这一点,让我们定义 Application 类(AMVCC中的“A”),它将具有唯一的实例. 里面有三个变量, model, view, and controller,将在运行时为我们提供所有MVC实例的访问点. 这些变量应该是 MonoBehaviours with public 对所需脚本的引用.

然后,我们还将创建一个名为 Element,这使我们能够访问应用程序的实例. 这种访问将允许每个MVC类相互访问.

注意,这两个类都有扩展 MonoBehaviour. 它们是附加到游戏对象“实体”上的“组件”。.

/ / BounceApplication.cs

//应用程序中所有元素的基类.
公共类BounceElement: MonoBehaviour
{
   //允许访问应用程序和所有实例.
   公共BounceApplication应用程序{获取{返回GameObject.FindObjectOfType(); }}
}

// 10 bounses入口点.
类BounceApplication: MonoBehaviour
{
   //对MVC根实例的引用.
   公共bouncmodel模型;
   公共BounceView视图;
   公共bounceconcontroller控制器;

   // Init这里的东西
   void Start() { }
}

From BounceElement 我们可以创建MVC核心类. The BounceModel, BounceView, and BounceController 脚本通常充当更专门化实例的容器, 但由于这是一个简单的例子,只有视图将有一个嵌套结构. 模型和控制器可以分别在一个脚本中完成:

// BounceModel.cs

//包含与应用程序相关的所有数据.
BounceModel: BounceElement
{
   // Data
   Public int反弹;  
   public int winCondition;
}
// BounceView .cs

//包含与应用相关的所有视图.
BounceView: BounceElement
{
   //指向球
   public BallView球;
}
// BallView.cs

//描述Ball视图及其特性.
公共类BallView: BounceElement
{
   //只有这个是必要的. 物理学在做剩下的工作.
   //碰撞时调用的回调.
   无效OnCollisionEnter(){应用程序.controller.OnBallGroundHit (); }
}
/ / BounceController.cs

//控制应用程序的工作流.
公共类bounceccontroller: BounceElement
{
   //处理击球事件
   OnBallGroundHit ()
   {
      app.model.bounces++;
      Debug.日志(“反弹”+应用.model.bounce);
      if(app.model.bounces >= app.model.winCondition)
      {
         app.view.ball.enabled = false;
         app.view.ball.GetComponent().isKinematic=true; // stops the ball
         OnGameComplete ();
      } 
   }

   //处理获胜条件
   公共void OnGameComplete(){调试.Log(“Victory!!”); }
}

创建了所有脚本后,我们可以继续附加和配置它们.

层次结构布局应该是这样的:

-应用程序[BounceApplication]
    - model [BounceModel]
    - controller [BounceController]
    - view [BounceView]
        - ...
        - ball [BallView]
        - ...

Using the BounceModel 作为一个例子,我们可以在Unity的编辑器中看到它的样子:

截图:BounceModel IN检查器
BounceModel with the bounces and winCondition fields.

设置好所有脚本并运行游戏后,我们应该在 Console Panel.

屏幕截图:控制台输出

Notifications

如上面的例子所示,当球落地时,它的视图执行 app.controller.OnBallGroundHit () 这是一种方法. 无论如何,对应用程序中的所有通知这样做都不是“错误的”. However, in my experience, 我使用在AMVCC Application类中实现的简单通知系统获得了更好的结果.

的布局来实现它 BounceApplication to be:

/ / BounceApplication.cs

类BounceApplication 
{
   //迭代所有控制器并委托通知数据
   //这个方法很容易找到,因为每个类都是" BounceElement "并且有一个" app " 
   // instance.
   public void Notify(string p_event_path, Object p_target, params Object [] p_data)
   {
      BounceController[] controller_list = GetAllControllers();
      foreach(BounceController c in controller_list)
      {
         c.OnNotification (p_event_path p_target p_data);
      }
   }

   //获取所有场景控制器.
   public BounceController[] GetAllControllers() {/* ... */ }
}

Next, 我们需要一个新的脚本,所有的开发人员将添加通知事件的名称, 哪些可以在执行期间分派.

/ / BounceNotifications.cs

//该类将提供对事件字符串的静态访问.
类BounceNotification
{
   公共字符串BallHitGround = "球.hit.ground”;
   GameComplete =“游戏.complete”;
   /* ...  */
   GameStart = "游戏.start”;
   静态公共字符串SceneLoad =“场景.load”;
   /* ... */
}

这一点很容易看出, this way, 代码的易读性得到了提高,因为开发人员不需要在整个源代码中搜索 controller.OnSomethingComplexName 方法,以便了解在执行期间可能发生的操作类型. 只需检查一个文件,就可以了解应用程序的整体行为.

现在,我们只需要调整 BallView and BounceController 来处理这个新系统.

// BallView.cs

//描述Ball视图及其特性.
公共类BallView: BounceElement
{
   //只有这个是必要的. 物理学在做剩下的工作.
   //碰撞时调用的回调.
   无效OnCollisionEnter(){应用程序.通知(BounceNotification.BallHitGround,this); }
}
/ / BounceController.cs

//控制应用程序的工作流.
公共类bounceccontroller: BounceElement
{
   //处理击球事件
   OnNotification(string p_event_path,Object p_target,params Object [] p_data)
   {
      开关(p_event_path)
      {
         案例BounceNotification.BallHitGround:
            app.model.bounces++;
            Debug.日志(“反弹”+应用.model.bounce);
            if(app.model.bounces >= app.model.winCondition)
            {
               app.view.ball.enabled = false;
               app.view.ball.GetComponent().isKinematic=true; // stops the ball
               //通知自身和其他可能对事件感兴趣的控制器
               app.通知(BounceNotification.GameComplete,);            
            }
         break;
         
         案例BounceNotification.GameComplete:
            Debug.Log(“Victory!!”);
         break;
      } 
   }
}

较大的项目会有很多通知. So, 避免得到一个大的开关箱结构, 建议创建不同的控制器,并让它们处理不同的通知范围.

AMVCC在现实世界

这个例子展示了AMVCC模式的一个简单用例. 根据MVC的三个要素调整你的思维方式, 并学习将实体可视化为有序的层次结构, 这些技能是需要打磨的吗.

在较大的项目中, 开发人员将面临更复杂的场景,并怀疑某些东西应该是视图还是控制器, 或者一个给定的类是否应该更彻底地分成更小的类.

经验法则(作者:Eduardo)

任何地方都没有“MVC排序的通用指南”. 但是,我通常遵循一些简单的规则来帮助我确定是否将某些东西定义为模型, View, 或控制器,以及何时将给定类拆分为更小的部分.

通常,当我考虑软件架构或编写脚本时,这是有机地发生的.

Class Sorting

Models

  • 保存应用程序的核心数据和状态,例如player health or gun ammo.
  • 序列化、反序列化和/或在类型之间转换.
  • 加载/保存数据(本地或web).
  • 通知控制人员操作的进度.
  • 为游戏存储游戏状态 有限状态机.
  • 从不访问视图.

Views

  • 能否从模型中获取数据,以便向用户呈现最新的游戏状态. 例如,View方法 player.Run() 可内服 model.speed 展现玩家的能力.
  • 永远不应该改变模型.
  • 严格实现其类的功能. For example:
    • A PlayerView 是否应该执行输入检测或修改游戏状态.
    • 视图应该充当一个具有接口和重要事件通知的黑盒.
    • 不存储核心数据(如速度,健康,生命,…).

Controllers

  • 不存储核心数据.
  • 有时是否可以过滤不需要的视图的通知.
  • 更新和使用模型的数据.
  • 管理Unity的场景工作流.

Class Hierarchy

在这种情况下,我没有遵循很多步骤. Usually, 我认为,当变量开始显示太多的“前缀”时,需要拆分某些类,,或者同一元素的太多变体开始出现(比如 Player 类或 Gun types in an FPS).

例如,单个 Model 包含玩家数据将有很多 playerDataA playerDataB,... or a Controller 处理玩家通知 OnPlayerDidA OnPlayerDidB,.... 我们想要减少脚本的大小和摆脱 player and OnPlayer prefixes.

我用a来演示一下 Model 类,因为只使用数据更容易理解.

在编程过程中,我通常从单个开始 Model 类保存游戏的所有数据.

// Model.cs

class Model
{
   公共浮动playerHealth;
   查看playerLives

   playerGunPrefabA;
   public int playerGunAmmoA;

   playerGunPrefabB;
   playerGunAmmoB;

   // Ops Gun[C D E ...] will appear...
   /* ... */

   公众持股;
   public int gamlevel;
}

很容易看出,游戏越复杂,变量就越多. 有了足够的复杂性,我们最终会得到一个巨大的类,包含 model.playerABCDFoo variables. 嵌套元素将简化代码完成,并为数据变体之间的切换提供空间.

// Model.cs

class Model
{
   public PlayerModel player;  // Container of the Player data.
   public GameModel game;      // Container of the Game data.
}
// GameModel.cs

class GameModel
{
   public float speed;         // Game running speed (influencing the difficulty)
   public int level;           // Current game level/stage loaded
}
// PlayerModel.cs

类PlayerModel
{
   public float health;        // Player health from 0.0 to 1.0.
   public int lives;           // Player “retry” count after he dies.
   public GunModel[] guns;     // Now a Player can have an array of guns to switch ingame.
}
// GunModel.cs

class GunModel
{
   public GunType type;        // Enumeration of Gun types.
   public GameObject prefab;   // Template of the 3D Asset of the weapon.
   public int ammo;            // Current number of bullets
   public int clips;           // Number of reloads possible
}

使用这种类配置, 开发人员可以直观地在源代码中导航,一次一个概念. 让我们假设一款第一人称射击游戏, 在那里武器和它们的配置可以变得非常多. The fact that GunModel 包含在一个类中,允许创建一个列表 Prefabs (预先配置的游戏对象可以在游戏中快速复制和重复使用).

相反,如果枪支信息全部存储在一个 GunModel 类,在变量中如 gun0Ammo, gun1Ammo, gun0Clips等等,然后用户,在遇到需要存储的时候 Gun 数据,则需要存储整个 Model 包括不想要的 Player data. 在这种情况下,很明显,一个新的 GunModel 上课会更好.

图片:类层次结构
改进类层次结构.

任何事情都有两面性. 有时可能会不必要地过度划分并增加代码的复杂性. 只有经验才能磨练您的技能,以便为您的项目找到最佳的MVC排序.

新的游戏开发特殊能力解锁:Unity游戏与MVC模式.

Conclusion

有大量的软件模式. 在这篇文章中,我试图展示在过去的项目中对我帮助最大的一个. Developers 应该经常吸收新知识,但也要经常质疑它. 我希望本教程能帮助你学到一些新东西, 与此同时, 作为你发展自己风格的垫脚石.

另外,我真的鼓励您研究其他模式,并找到最适合您的模式. 一个好的起点是 这篇维基百科文章,产品种类繁多,各具特色.

如果您喜欢AMVCC模式并想要测试它,不要忘记尝试我的库, Unity MVC,其中包含启动AMVCC应用程序所需的所有核心类.

聘请Toptal这方面的专家.
Hire Now
爱德华多·迪亚斯·达科斯塔

爱德华多·迪亚斯·达科斯塔

Verified Expert in Engineering

阿雷格里港-里约格兰德州,巴西

2015年5月20日成为会员

About the author

Eduardo是一名拥有12年以上客户端和前端应用开发经验的开发者. 他总是乐于学习新事物.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

PREVIOUSLY AT

无人机比赛联盟

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

世界级的文章,每周发一次.

输入您的电子邮件,即表示您同意我们的 privacy policy.

Toptal开发者

Join the Toptal® community.