GAMES104课程笔记15-Gameplay Complexity and Building Blocks
这个系列是GAMES104-现代游戏引擎:从入门到实践(GAMES 104: Modern Game Engine-Theory and Practice)的同步课程笔记。本课程会介绍现代游戏引擎所涉及的系统架构、技术点以及引擎系统相关的知识。本节课主要介绍游戏引擎中的玩法系统。
Overview
玩法系统是游戏引擎最重要的部分,它是区分游戏引擎和渲染器以及物理引擎的核心。实际上玩法系统往往会贯穿整个游戏引擎,与引擎的其它系统进行交互,这样才能满足游戏设计师的需求。
另一方面现代游戏的玩法是极其丰富的,即使是同一类型的游戏也具有多种多样的表现形式。这些丰富的游戏内容都需要通过玩法系统来进行实现。
而在游戏行业中玩法系统还面临着快速迭代的问题,同一个游戏的核心玩法在开发运营初期和后期可能会有着巨大的差别。因此玩法系统在设计时也需要考虑需求变更和快速迭代。
Event Mechanism
玩法系统的核心是事件机制(event mechanism),在游戏世界中不同类型的GO会通过事件/消息的方式进行通信从而驱动游戏世界的运行。
Publish-Subscribe Pattern
在游戏引擎中一般会使用publish-subscribe这样的模式来实现具体的通信过程。在publish-subscribe模式中每个GO有着自己的publish功能来向其它GO发送事件,同时自身的subscribe功能来实现接收事件以及相应的反馈。当然在publish-subscribe模式中还需要一个event dispatcher来执行高效的事件派送。因此在publish-subscribe模式中的三要素为事件定义(event definition)、注册回调(callback registration)以及事件派送(event dispatching)。
Event Definition
事件定义是实现事件机制的第一步,最简单的定义方式是为事件定义一个类型以及相应的参数。
因此我们可以使用继承的方式来实现不同类型事件的定义。但这种方式的缺陷在于玩法系统往往是由设计师来实现的,而对于游戏引擎来说一般无法由程序员事先确定。
在现代游戏引擎中会使用反射和代码渲染的方式来允许设计师自定义事件类型和相关的数据。
Callback Registration
当GO接收到事件后就会激活调用相应的回调函数来改变自身的状态,而为了正确地使用回调函数则首先需要对不同的回调函数进行注册。
回调函数的一大特点在于它的注册和执行往往是分开的。这一特点可能会导致回调函数调用时相关的一些GO可能已经结束了生命周期,因此回调函数的安全性是整个事件系统的一大难题。
为了处理这样的问题我们可以使用强引用(strong reference)这样的机制来锁定相关GO的生命周期。强引用会保证所有和回调函数相关的资源在回调函数调用前不会被回收,从而确保系统的安全。
当然强引用的方式在一些场景下可能是不合适的,很多时候我们希望某些资源可以正确的卸载掉。因此我们还可以使用弱引用(weak reference)的机制在调用回调函数时判断资源是否已经被卸载掉。当然弱引用机制的滥用可能会影响整个系统的性能。
Event Dispatching
事件会通过分发系统来实现消息的传递。由于游戏中每一时刻往往存在着成千上万个GO和相应的回调函数,我们需要一个非常高效的分发系统才能保证游戏的实时性。
最简单的分发机制是把消息瞬时发出去。这种方式的缺陷在于它会阻塞前一个函数的执行,从而形成一个巨大的调用栈使得系统难以调试;此外很多回调函数在执行时会申请一些额外的资源,这就容易导致游戏的帧率很难稳定。
现代游戏引擎中更常用的分发方式是使用事件队列(event queue)来收集当前帧上所有的事件,然后在下一帧再进行分发和处理。
由于event queue中有不同类型的事件,因此我们还需要结合序列化和反序列化的操作来进行存储。
event queue一般会使用ring buffer这样的数据结构来实现,这样可以重用一块统一的内存空间来提升效率。
现代游戏引擎中往往会同时有多个不同的event queue来处理不同类型的事件,每个queue用来保存一些相对独立的事件。这种方式可以便于我们进行调试,也可以提升系统的性能。
当然event queue也有一些自身的问题。首先event queue无法保证event执行的顺序,同时对于一些实时性的事件event queue可能会导致执行的延误。
Game Logic
在事件机制的基础上就可以开始设计游戏的逻辑了。在早期的游戏开发中会直接使用C++来编写游戏的逻辑来获得更高的运行效率,而随着游戏系统变得越来越复杂这种直接基于高级语言的开发方式就不能满足开发者的需求了。
除此之外游戏引擎面对的用户往往是设计师而不是程序员,对于设计师来说直接使用编程语言来设计游戏可能是过于复杂的。
因此在现代游戏引擎中往往会使用脚本语言来实现游戏的开发工作,它的优势在于它可以直接在虚拟机上运行。
因此我们可以把游戏中的很多逻辑使用脚本语言来实现。
脚本语言的另一大特点是可以进行热更新。
当然脚本语言也有许多缺点,比如说它的效率一般会比较低,因此选择合适的脚本语言是十分重要的。
目前游戏行业中常用的脚本语言包括Lua、Python、C#等。
Visual Scripting
可视化脚本(visual scripting)是现代游戏引擎几乎必备的功能。和传统的脚本语言相比可视化脚本无需开发者直接进行编程,因此它对于设计师和艺术家而言更容易进行掌握。像虚幻和unity这样的商业级游戏引擎都实现了自身的可视化脚本功能。
当然可视化脚本本身也是一种编程语言,它需要实现相应的变量、语句、表达式、控制流程、函数甚至面向对象编程等功能。
以虚幻引擎的blueprint功能为例,每个变量需要封装数据类型以及作用域两种属性。而在可视化脚本中,不同类型的数据会使用不同的颜色来方便用户进行区分。
在变量的基础上可以构造表达式,在可视化脚本中会使用节点来进行表示。
而控制语句则会使用三角形的符号来表示。
类似于高级语言中函数的概念,我们可以在可视化脚本中把节点按照一定的顺序连接起来形成一张图。
我们还可以把数据和函数打包在一起,这样就形成了类的概念。
除此之外在可视化脚本中往往还需要提供一些方便用户操作的功能,如查找节点、调试等功能。
可视化脚本也有一些自身的问题。首先可视化脚本很难进行协作,在大型开发团队中很难把每个开发者定义的脚本直接组装到一起,往往需要通过大量人工的重新排序才能得到正确的结果;同时当脚本中的节点过多时整个脚本在视觉上可能会非常繁琐,让人难以阅读。
Character, Control and Camera
角色(character)、控制(control)以及镜头(camera)是构成玩家体验最重要的元素。
Character
角色是指游戏中如何设置玩家控制角色以及NPC的运动和各种状态。
在现代游戏中角色的运动往往涉及大量不同的动画和状态改变,我们需要非常多的细节才能设计出符合人认知的角色运动。同时在游戏世界中角色还需要和环境中的各种元素进行互动,往往需要结合游戏引擎其它系统进行设计。
因此角色系统一般需要一个非常复杂的状态机模型来描述角色状态的切换和转移。
Control
控制系统的核心问题是要兼容玩家不同的输入设备。
来自控制器的信号通过控制系统会转换成游戏可以识别的输入,然后通过事件机制来控制角色。
同时我们还可以利用控制器的反馈功能来进一步提升玩家的游戏体验。
Camera
镜头会直接描述玩家视角中看到的场景和事物。
从实现角度来说,我们只要设置好相机的位置和视角就可以直接利用渲染系统来实现镜头的功能。
在游戏场景中最常见的情况是相机要绑定在玩家控制的角色身上,随着角色的运动一起运动。同时我们还需要保证相机在运动的过程中不要出现穿墙等各种问题。
镜头系统的一大难点在于如何根据角色的状态来调整相机的相关参数,使游戏画面更接近于人眼的真实反映。
同时镜头系统也需要考虑各种相机特效的实现,包括镜头抖动、动态模糊等。
在复杂场景中往往还会有多个相机同时存在,因此我们也需要管理这些相机和相关参数。
整个镜头系统对于提升和改善玩家的游戏体验起着至关重要的作用。
因此游戏引擎中的镜头系统需要允许用户使用脚本或是可视化脚本来编辑相机的各种属性和参数。