01 - Gameplay玩法系统基础

本文将简要介绍一些游戏引擎中Gameplay玩法系统的基础内容。例如:

  • 面临的挑战;
  • 事件机制;
  • 游戏逻辑与脚本系统,可视化脚本;
  • 游戏开发中的3C是什么(Character,Camera,Controller)

面临的挑战

游戏玩法需要与引擎的各种模块进行交互,例如下图:

并且游戏玩法是多样可拓展的,就算是在同一个游戏里也有不同的玩法,例如下图:

此外,游戏玩法也是快速迭代的。例如堡垒之夜最初是MMO游戏,然后很快地在两个月左右转换为吃鸡模式。

事件机制

游戏对象之间需要“沟通”,早期人们通常是写死的,这样降低了代码的可维护性和可重用性,有悖游戏引擎的设计理念:

于是便有了事件机制(Event/Message Mechanism)。游戏对象之间通过Message进行交流,对象A向对象B发出Message,对象B则根据回调进行相应处理:

发布-订阅模式

这是引擎事件系统的设计模式,对于各种游戏对象(发布者)发出的消息,需要用事件分发器来接受,最后由它将信息发送到对应的订阅者中。订阅者不必知道发布者是谁。

因此,事件系统需要完成以下三件事:

  1. 事件/消息的定义;
  2. 回调函数的注册;
  3. 事件/消息的分发;

事件的定义

事件通常由事件种类和事件参数构成。一些引擎内部或固定的事件(例如按键、鼠标移动)可以作为枚举硬编码到代码中。

而引擎使用者自定义的事件处理起来有些麻烦。以前人们通过面向对象的方法,将自定义的事件写为继承于事件基类的代码,但这样做会编译很多次引擎,效率可能会低。

现在使用自定义事件则不是一个难题。例如虚幻引擎会将用户在编辑器中自定义的事件转换为格式固定的代码,然后通过dll注入等方式让引擎在运行时得到自定义事件的代码信息,最后通过反射显示在用户的编辑器中。

其他引擎则会在事件系统上搭建一个脚本层,通过其他脚本来自定义事件。

回调函数的注册

回调函数是提前写好的一小段逻辑,当订阅者接收到事件时,对应的回调函数将会被触发执行。

在从回调函数注册到回调函数被触发的时间段内,很有可能会出问题,例如回调函数注册后,被接收的对象被销毁就会触发空指针错误。

早期人们使用强引用来规避这个问题,如果该对象内有注册好的回调函数,就不要销毁他。因此在回调函数注销后,及时销毁对象,否则会造成内存泄漏的隐患。

现在人们使用弱引用,在触发回调函数前,先检验一下它的所属对象是否被销毁,没被销毁才触发。

事件的分发

接下来需要对事件进行分发,有多种事件分发方式:

立即模式

接收到事件后立即发出,可能会导致上层函数出现顿卡现象。

立即模式还会带来函数调用栈爆炸的问题。例如下图一个炸弹爆炸,触发周围的炸弹爆炸,导致函数调用栈很深:

立即模式无法保证各个系统并行化运行:

事件队列

使用事件队列可以有效解决上面的问题。在当前帧收集所有事件到队列里,然后在下一帧处理。

为了在内存中存取不同种类的事件,还需要引擎的序列化和反序列化系统帮忙。将当前帧的各种事件序列化到一块内存中,然后需要用的时候通过反序列化把事件取出来。

事件队列的具体实现是一个环状队列,在一块环形内存中进行读写。这样的好处是如果游戏中的某个对象错误地一直在发送事件,事件队列满后会忽略该对象发送的事件,从而防止因内存溢出导致的引擎和游戏崩溃。

此外,事件还要进行分类,以便引擎进行高效并行处理,也方便debug。

事件队列也存在一些问题:

  1. 并行处理可能会让事件的执行顺序不一样,从而在逻辑上出问题。健壮的引擎同时会保留立即处理事件的能力,并让延迟处理的事件在形如PreTick()PostTick()中处理。

  2. 有些关联性强的事件可能会出现拖沓。事件队列分帧处理事件,如果当前帧事件和下一帧事件关联性较大,则会出现拖沓现象。

游戏逻辑与脚本

编译型语言

早期游戏逻辑是直接用C++写死的,这样方便但不好重用。每次改完逻辑,都需要重新编译整个项目,很麻烦。并且编译型语言也很难进行热更新,即在玩家游玩的过程中实时推出更新,不影响玩家游玩体验的同时修好了bug。

脚本语言

概念

脚本语言易上手,容易热更新(解释性语言),且不怕崩溃。它通常跑在虚拟机上面,崩溃了不会影响到引擎本体。

脚本语言首先会被编译成二进制字节码,然后在虚拟机上运行。虽然运行速度会慢一些,但足够健壮。

对象管理

对于游戏里的对象,是交给引擎本体管理还是交给脚本系统管理?

如果要交给引擎管理,需要写的严谨一点,且脚本在使用对象前需要判断它是否有效。并且通过脚本产生的对象也要被引擎管理,比较麻烦。

如果要交给脚本系统管理,则需要依赖脚本的垃圾回收GC系统。使用GC可以将脚本变量用完即扔,但GC会影响游戏的运行效率。

脚本系统架构

现在脚本系统的架构有两种说法:

  1. 第一种是让引擎本体管理游戏世界,大多数游戏逻辑由引擎本体执行,脚本拓展了引擎本体的代码逻辑。

  2. 第二种则更倾向于让脚本系统管理游戏世界,引擎则更像一个SDK,脚本负责调用引擎的各个功能。

热更新

脚本实现热更新的思路就是替换函数指针,并重置相关变量的值。

语言的选择

  • Lua:健壮且成熟,轻量效率高。但没有类库,甚至面向对象的类都要自己写。
  • Python:支持反射,面向对象,有很多库。但消耗的内存更大。
  • C#:可以通过Mono工具将其变为脚本语言。学习成本低,支持面向对象,社区和各种库的支持也强大。

可视化脚本语言

现代游戏引擎都要提供可视化脚本功能,这是因为用代码写脚本还是有一定的学习成本,且可能会出现一些报错,例如隐藏的编译错误。

组成

和脚本一样,可视化的脚本也要有变量,表达式,控制流,函数等概念:

  1. 变量,有类型和作用域。详见下图:

    要对变量进行可视化,需要明确它的类型(唯一的颜色),并且通过管线的连接来让变量在不同节点间“输入输出”。

  2. 语句和表达式,语句需要表示一些将要执行的动作(例如赋值,执行函数),表达式则需要进行求值运算。详见下图:

    要对语句和表达式可视化,需要将它们变为一个个节点:

  3. 控制流,控制代码的执行顺序,如顺序、条件和循环。

    要将它们可视化,可以通过定义节点并用管线连接。

  4. 函数:将输入参数进行处理,返回输出参数。有输入参数,函数体,返回值三个部分。

    可视化后的结构如下,内部有三部分,外部则只是一个简单的节点:

  5. 类。有时候还需要一个整体来存储一些变量,函数,就需要用到类。

    可视化则通过定义继承于代码类的蓝图类。

这些结构通过管线的连接成为一个个游戏逻辑,并且在创作过程中对用户友好。

调试

也要支持可视化脚本的调试,执行到哪个节点,就对该节点高亮,鼠标移上去还能看到变量的当前值。

存在的问题

几个人同时修改一个脚本,会出现冲突,而解决冲突会变得很麻烦。因为可视化编程是一个图 ,不好合并冲突。

此外,当逻辑过于复杂时,可视化编程的结构就是一个蜘蛛网。因此逻辑最好占满一个屏幕就行了。

运行原理

和脚本一样,可视化脚本需要通过图编译器将它编译为字节码,然后再进入虚拟机中运行。

当然,也能将可视化脚本先翻译为某个脚本,然后再编译成字节码放入虚拟机中运行。这样还省去自己编写图编译器。

游戏3C

游戏中的3C就是角色Character,控制Control和相机Cameera。这三个元素是玩家游戏体验的核心,要做好它们很重要,也很不容易。

角色

角色和Npc如何移动,战斗,和游戏世界交互。最基础的元素就是移动了,例如游戏《巫师三》中的移动,细节满满:

当然,除了移动以外,角色还要和游戏世界互动。例如悬挂在物体上、在冰面上行走、在水中潜水等等。这需要和引擎各模块(物理,动画,声音等)进行和谐的交互。

这种互动一般通过状态机来帮忙控制:

控制

需要处理各种各样的输入设备,将其转换为游戏引擎内部的唯一控制方式。

  • 控制应该对玩家友好。例如瞄准操作,玩家按下鼠标右键进入瞄准状态,画面会聚焦于准星,并启动辅助瞄准方便玩家操作。
  • 控制应该有一些反馈。对于一些有体感的输入设备(如手柄),游戏中的反馈也会实时反馈到输入设备上。例如人物死亡后手柄会震动。
  • 控制应该考虑到不同的环境。输入也是有上下文环境的,同一套输入在不同环境下的操作也不一样。例如人物走路和开车时,方向键的含义不同。
  • 控制也应有一些组合与顺序。例如格斗游戏的连招就需要一些按键顺序。

相机

相机则为玩家提供展示世界的平台,如何高效使用相机也很重要。

相机由POV和FOV定义:

并且相机也要和玩家绑定:

  • 为了玩家更好的游戏体验,相机和玩家绑定的距离不是固定的,也就是“弹簧臂(Spring Arm)这一概念。

  • 还有剧情过场相关的运镜操作,需要在一段时间轴内对相机的相关参数进行调整。

  • 此外一些后处理效果也必不可少,例如体现打击感的屏幕抖动。为了方便艺术家调整优化效果,相机的一些效果也能通过蓝图类去控制。

  • 在游戏世界中存在很多相机,但玩家一次只能用一个相机。这时候就需要相机管理器来管理玩家当前使用的相机(例如FPS模式和TPS模式间的切换)。

参考资料

  • GAMES104 (boomingtech.com)

待阅读的资料

事件机制

  1. Are Callbacks Always Asynchronous
  2. Unreal Engine Documentation Actor Communication
  3. Publish-subscribe pattern
  4. Unreal Engine Event Dispatcher

脚本系统

  1. A Classification of Scripting Systems for Entertainment and Serious Computer Games, 2011
  2. Programming Language and Compiler Benchmarks
  3. Lua official site
  4. LuaJIT official site
  5. Unity C# Scripting Manual
  6. Implementing a Scripting Engine
  7. Scripting Virtual Machine and Bytecode

可视化脚本

  1. Unreal Engine Blueprints Visual Scripting
  2. Unity Visual Scripting
  3. Programming Language Design
  4. Blueprint Merge Tool

Gameplay和3C

  1. Level Up! The Guide to Great Video Game Design 2nd Edition, Scott Rogers, 2014
  2. Game Engine Architecture Third Edition, Jason Gregory, 2018
  3. The 3Cs of Game Development