游戏引擎Godot系统架构分析

Posted by 熊耀华 on 8 December 2023

软件工程和建筑工程一样,都是管控复杂性,在混沌中寻早秩序。

前言

开发游戏是我从小以来的梦想。 在我看来游戏是技术和艺术的融合,是最高的艺术形式。 一个成功的游戏需要融合几方面的专业知识:

  • 心理学、行为学:决定了游戏的核心玩法是否有趣。
  • 文学:决定了游戏的故事情节是否有吸引力。
  • 视觉艺术:决定了视觉效果是否震撼。
  • 听觉艺术:决定了背景音乐和音效是否能烘托气氛。
  • 计算机仿真:决定了游戏世界的物理规则是否真实可信。
  • 计算机图形学:决定了视觉效果的上限。
  • 并行计算:决定了游戏世界的规模和游戏的流畅程度。

因为种种原因没能走上专业游戏开发者这条道路,但是依然保留了这个业余爱好。幸运的 是游戏开发中的各种技术在其他专业领域,甚至是我的工作领域,都能起到重要的作用。

以前主要开发自娱自乐的小品级游戏,都是从零开始编程实现。这种方式虽然有助于深入理 解基本原理,但在业余爱好者有限的时间精力投入下无法实现太多功能。 因此最近两年在考虑转向现成游戏引擎。

当前主流的商业引擎是Unity和Unreal,提供能先进和完善的功能,适用于开发大制作的3A 级别游戏。但我作为业余爱好者,不大能接受他们臃肿的体积、严苛的授权许可、以及随时卡你脖子的不确定性。

因此,我主要考虑开源引擎。在简单比较后,我在大量开源引擎中选择关注两个:Godot 和Bevy。其中Godot可能是当前最好的开源引擎,架构合理、功能比较完备、技术带头人靠 谱、社区活跃。在Unity闹剧后隐隐有成为独立开发者最爱引擎的趋势。

Bevy引擎更加年轻,劣势是功能不及Godot完善,优势是没有历史包袱更加新锐。 Bevy的技 术带头人曾经是Godot的主力贡献者之一,后来因为引擎架构上技术理念不同而分道扬镖创 立了Bevy。 我的理解两者的理念并无优劣之分,只是侧重点不同。 分歧的焦点是引擎是否 应该以ECS作为基础; Godot更关注引擎的易用性和上手难度,而Bevy更关注引擎的性 能和可定制性。简单来说Godot新手友好,Bevy老手友好。

考虑自身需求,现阶段Godot更适合我,因此准备先写这个系列,记录分析Godot架构的 心得。

Godot总体架构

广义的Godot引擎包括两个部分:编辑器和组件库。编辑器是一个带窗口的应用程序,用来 编辑游戏场景和代码。组件库提供了各种组成游戏的功能组件。开发者通过编辑器来组合功 能组件,编写逻辑代码,实现游戏。

有意思的是,Godot编辑器本身也是基于组件库开发,可以认为是一个另类的Godot“游戏”。 这种设计的好处首先是减少了对其他软件包的依赖,减少代码数量。更重要的是,这样可 以强迫Godot引擎的开发者自己首先使用引擎,方便发现问题,提高质量。按照软件工程“专 业”的说法,这叫做“Eat your own dog food”。

接下来的分析主要针对组件库。从最抽象的层面看,无论什么游戏本质上都是大量实体 (Item)的相互作用。由于存在大量实体,实体之间广泛存在作用关系。如果将实体具象化 为点,作用关系具象化为连线,那么游戏看起来就像是一团乱麻。

游戏程序员最重要的任务可以说是从这一团乱麻中梳理出头绪,建立秩序。而提供概念框 架,方便梳理,正是游戏引擎的重要功能。这套框架要在简单易用和灵活表现力两个矛盾 的要求之间寻找最佳平衡点。用Godot创始人自己的话说,取得平衡的方式是“Make simple things easy, hard things possible”。即设计中优先照顾基本的、也是最大众的需求;对 于高级的、特殊的需求不直接支持,但留好后门,让有需要的人自己去实现。

场景树

基于上述理念,Godot的核心概念框架称为场景树(SceneTree)。场景树由场景(Scene)和节点(Node)组成,其中节点表示游戏世界中的某个实体,多个节点构成的树状结构称为场景。例如人形游戏角色由头、身体、四肢构成,那么头、身体、四肢都是节点,组成的场景表示角色。

场景树支持嵌套(Recursion)和实例化(Instantiation)。嵌套是指场景中可以包含子场景,例如角色场景包含四肢,其中左手不是一个节点,而是另外一个场景,包含上臂、下臂、手掌、五指。实例化是指可以将一个场景复制成不同的独立实体。例如游戏中包含三个角色,可以将角色场景实例化三次分别表示。每一个实例可以有不同的位置、动作等状态。

我认为场景树这套框架最大的优点在于,鼓励开发者对游戏世界自顶向下,逐步细化;同时发现共性,利用实例化减少重复。例如开发一个模拟城市游戏,我可以先粗略的把世界分解为道路和建筑,如果需要可以将建筑细化为楼层、房间;同时我会意识到很多房间完全相同,可以用一个场景的不同实例表示。

可以说场景树是Godot最核心的设计决策,其他功能设计都围绕场景树展开。

节点间通信

场景树将游戏世界按所属关系组织成了树状结构的节点。游戏进程中节点之间相互存在通信的需求。为此Godot提供了三套不同的通信机制:

  • 调用节点方法(Method call)
  • 信号机制(Signal & Slot)
  • 组广播(Group call)

三种机制有各自的特点和适用情况。首先最简单的是调用节点方法,属于面向对象编程的基本操作。调用方法的优势是概念简单、资源开销小。但是方法调用的前提是找到被调用者;对于在场景树中位置固定的节点,或者调用者的子节点,这不是问题,因此适用。例如角色节点要调用他自己脑袋节点上的闪光方法。

游戏逻辑中常常会出现消息接收目标不明确的情况。例如,当角色受伤时需要通知屏幕上方的血条缩短。这种情况下血条节点不在角色节点的直接控制下,无法直接调用,此时正确的方法是使用信号机制。信号机制将消息的发送和接收断开,角色节点受伤时只负责发送受伤信号,血条节点只负责接收到受伤信号时更新血条。至于应该如何连接信号,则需要理解整个场景树结构的开发者指定。

这种设计的优势是降低了消息发送端和接收端的耦合,方便调整场景树结构。如果血条节点需要移动到其他位置,开发者只需更新连接角色和血条的那一行代码,而角色和血条的内部逻辑不需要改变。

关于方法调用和信号机制的使用时机,Godot开发者总结了一句精辟的话“Signal up, call down”(对上(级节点)发信号,对下(级节点)调用)。

组广播的作用是对大量的同类节点发消息。例如游戏里面主角放了一个大招,所有敌人都要掉血。这个功能理论上我们可以用消息机制实现,但是那样要求将所有敌人都和主角连接,太过繁琐。如果用组广播实现,那只需要创建所有敌人时都加入一个组,比如叫“Enemy”,然后主角放大招的时候对“Enemy”组广播伤害消息。这样代码简洁得多。

资源

资源是可在节点之间共享数据的通称。最常见的资源包括模型、纹理、材质等等。资源可以在节点之间共享,还可以在其他资源之间共享。例如一个模型由多个人物节点共享,而一个材质又可以由多个模型共享。

应用案例

为了更好理解前述的架构,我们来看几个实际应用案例。假设要用Godot开发横版过关游戏。其中资源担负如下作用:

  • 游戏素材,如贴图、估计、音乐、音效;
  • 游戏数据,例如各种角色的属性定义文件,包括攻击力、防御力、移动速度、最大生命值等。

节点和场景的应用有以下几类:

  • 小规模场景表示主角、敌人等角色,或者移动平台、暗门等可互动物体。这些场景通常会同时有多个实例,例如同样的敌人有很多个。
  • 关卡场景包含主角、敌人、平台、暗门的大量实例,还包含一些独特的布景。游戏中有多个关卡场景,但给定时刻只有正在进行的关卡场景被实例化。
  • 图形用户界面场景,表示游戏屏幕上的菜单、窗口。
  • 游戏状态场景,如玩家的等级、积分,需要独立于关卡存在。

总结

本文分析Godot引擎的高层架构。从最终用户层面看,Godot引擎是一个场景树,由节点、场景、资源共同构成。如果开发简单游戏,理解到这一层已经足够了,但开发复杂游戏需要更深入的理解。场景树概念之下实际上是一个个服务器(Server),节点、资源本质上都是服务器的前端代理。关于服务器的内容将在下一篇里面介绍。