大家好。我是一位经验丰富的 Unity 开发人员。我决定参加 Hackernoon 和 Tatum Games 的比赛。在第一篇文章中,我将讨论 Unity 游戏开发项目的架构,并回顾我遇到的最常见的方法。而且,当然,我会告诉你为什么我是一个受虐狂,为什么我会来到我心爱的 HMVС (HMVP)。 PS 此处描述的所有内容均为主观意见。每个人都需要从整体上考虑开发和项目的细节,但总的来说,最好的架构是不存在的,并且以一种方便高效的方式为团队组合不同的方法:D MonoBehaviour 和面向组件的编程 (COP) 让我们从初学者最常使用的最基本的方法开始。我不想说这种方法不好,只是大多数开发人员习惯于从 OOP(面向对象编程)的角度进行思考。正确使用 COP(面向组件编程)需要不同的思维方式。同时,Unity 对基于 MonoBehaviour 的 COP 的实现看起来并不理想。 因此,基本方法意味着整个游戏将构建在具有 MonoBehaviour 组件的 GameObject 上,这允许您将不同的子系统分解成小块并从中构建游戏。 { private Awake() { } } 然而,细节决定成败。这种方法会导致扩展困难,尤其是在大型项目上,不必要的链接,Unity 引擎盖下的反射问题,以及与 Unity API 的紧密联系,这可能会在以后导致问题,特别是如果你想在客户端复制代码和服务器。 单例 单例管理是第一个可能想到的也是最可怕的事情。从本质上讲,Singleton 是包含在场景或整个项目中的单个实例中的对象,需要它来绑定和管理我们游戏中的分离系统。 一个简单的单例例子,我在初级开发者中经常看到: public sealed class MySingleton { private static MySingleton _instance = null; private MySingleton() { } public static MySingleton Instance { get { if (_instance == null) { _instance = new MySingleton(); } return _instance; } } public void OperationX() { } } 在小型项目上,这不会导致不必要的问题。项目越大,越难控制。更不用说内存泄漏,Singleton 会导致构建测试、组织多线程和太长脚本的问题(经常出现在新手开发人员身上)。 这里有一些关于单例组织通常是什么样子的(并且远非最正确的): 那么,你怎么知道 Singleton 是不是邪恶的呢? 当它绑定你游戏中的所有逻辑并控制一切时 一堆链接直接丢进去的时候 当它的尺寸变大时 当您已经开始调试或内存管理时,尤其是当 Singleton 是一个 GameObject 时 什么时候使用单例更好? 较小的项目 当内存管理不是问题,并且您管理事件而不是转发直接链接时 对于小型系统,例如,管理音频或作为分析系统的端点采集 现在让我们讨论 DI 容器。 DI 容器和他妈的 Zenject 哦,哦,很多人都在推动 Zenject 并使用 DI 容器实现依赖项。事实上,很多人把这个庞大的框架当作一个普通的Singleton来使用。 像往常一样,我在项目中看到它: 本质上,需要一个 DI 容器来放置引用并解析最终对象中的依赖关系。来自同一 Zenject 的最简单示例: public class TestInstaller : MonoInstaller { public override void InstallBindings() { Container.Bind<string>().FromInstance("Hello World!"); Container.Bind<Greeter>().AsSingle().NonLazy(); } } public class Greeter { public Greeter(string message) { Debug.Log(message); } } 这种方法很好,但直到事情开始变得复杂为止: DI-Container 本质上与 Singleton 相同但有所改进,它创建到容器本身的绑定 通常,会创建“千米长”的安装程序类来进行依赖绑定 新手很难以更多的责任分离为代价来理解,尽管后来是一种可扩展的方法 由于容器和无处不在的绑定而难以调试 将普通的 DI 容器变成服务定位器非常容易 当然,DI 容器是在合适的人手中组织代码的好方法,但您需要接受高水平的培训,以防止事情陷入狂欢。 纯粹形式的 MVC 为什么是纯粹的形式?因为它很容易理解。我们有一个控制器、模型和视图来组织一个项目。但只要有 MVC,它的子类型就和 MVP、MVVM 等一样多。 但现在我们将专注于所有示例的基本且可访问的示例: 好处是显而易见的——我们将控件(用户输入)与数据和视图(用户在屏幕上看到的内容)分开。通信通常是事件驱动的,并在应用程序容器中初始化。这消除了对凝聚力的需求;我们只与事件沟通。 但是,这里有一些缺点: 随着项目规模的扩大,我们的安装类应用(同一个容器)增长 MVC 的水平排列创建了大量不同的类,这些类之间松散地联系在一起 现在让我们讨论另一种方法。 容器中的 MVC 另一种可能的情况是将我们的 MVC 三元组链接到一个 DI 容器中。这样,我们可以更好地控制应用程序之间的连接,但是很容易将所有东西都变成一个服务定位器。 该方法是不同的,因为我们不是将控制器与事件链接起来,而是通过容器解析我们的控制器,然后处理事件。然而,与通常的 DI 容器一样,这里仍然会出现问题,但是出现的复杂性增加了,并且创建了更多的类。但是,我们将表示、模型和控制器分开。 HMVC/HMVP 这是我想多谈一点的地方,因为作为一个受虐狂,我已经非常喜欢这种方法了。有了它,我们创建了 MVC 的树状划分,尽管代码库大大增加,但它提供了几个优势。 那么,让我们看看我最常用的交互方案: 它是如何工作的? 最初,我们使用 GameInstaller 创建一个空场景,它将分别为每个场景加载容器。 GameInstaller 类本身存储通常负责大型系统(例如,音频处理)的全局(顶级)三元组,并存储整个游戏生命周期的一般事件。 然后 GameInstaller 加载您需要的场景容器,它初始化自身内部的顶级三元组(例如,通用玩家控制器),然后初始化自身内部的子控制器(例如,大炮控制器)。如此继续下降。所有分支的通信都是通过事件和动态字段进行的。 这听起来很复杂,但实际上要简单得多:这种方法使我们能够轻松地分离所有三元组,同时在它们的孩子之间保持足够的上下文联系。初始化每个演示者首先从父级获取事件的上下文。 我看到这种方法有几个优点: 项目场景几乎可以立即加载,我们的对象(包括视图)可以在加载树时按需初始化。如果我们在发送事件之前不需要加载带有设置的视图或游戏商店,那么我们不会存储事件以外的任何内容 个体三合会的紧密结构和隔离 由于事件导致的弱内聚 以事件为代价的动态 在三元组分支上而不是通过容器进行调试相当容易 有一些缺点: 如果您需要通过 20 个三元组的树将一个事件串联起来,这将是一项相当漫长的工作,但该方法涉及良好的初始设计 该项目的大型代码库,但结构良好 如果您需要将分支连接在一起,这对您来说是一个巨大的挑战,需要通过十几个类来处理事件 一般来说,HMVC/HMVP 用于子系统高度隔离、高内存要求和游戏资源的组织良好的项目。但与其他方法相比,习惯它可能需要更长的时间。 结论 组织项目的每种方法都有其用武之地。这完全取决于设计目标。如果您需要紧凑的架构和快速的内存处理,并且需要快速和动态的资源,请选择 HMVC。如果您需要毫不费力地快速制作项目原型,请将所有内容都写在 Singletons 中。