游戏引擎的Gameplay玩法系统基础
游戏引擎的Gameplay玩法系统基础

Gameplay的三大挑战

游戏玩法是由多个系统协作展示的
上图的格斗游戏就涉及到动画、特效、UI、输入系统、战斗表现系统等.
Gameplay程序特点就是一个杂学家,什么都要懂一点

游戏玩法存在多样性和可扩展性
上图的巫师三还有昆特牌的小游戏玩法

游戏玩法会快速地迭代,需求变化很快
上图的堡垒之夜二个月从MMO变成了吃鸡游戏
事件机制 Event Mechanism
事件机制目的是让Game objects之间的交互解耦
发行订阅模式 Publish-subscribe

发布者通过不同的事件类来创建消息
订阅者只接收它们订阅(感兴趣)的消息,一般不关注消息从哪产生
事件会通过Event Dispather(就像物流)注册的callback发送给订阅者
发行订阅模式的三个核心
事件定义

事件包含事件类型和相关数据
事件是根据策划(或游戏玩法)需求定义的

事件最容易想到的是基于基类扩展
需要注意的是很多事件是在游戏具体玩法中确定的,不可能全由代码扩展.

事件具有可扩展性,允许设计师在核心代码外扩展事件类型,
这些事件可能是通过纯数据的方式定义到工具链或者Gameplay系统中去,
比如Unreal引擎的蓝图事件扩展,会翻译成C++代码,在Runtime的时候把这些事件当成dll注入进去。
扩展性的Event定义是引擎很重要的一个功能。
回调注册

回调函数是某段代码的引用,它会在特定事件激活后触发(invoke).
回调函数需要提前注册


引擎中最复杂最容易出问题的是对象的生命周期和它回调函数的安全性
如图中的soldier可能会被提前销毁,会出现crash
C++或高级语言中强引用和弱引用都是在解决对象的生命周期问题。

强引用--在回调函数的有效期,相关的数据对象不被删除,这个会导致内存很难释放
强引用的主要用途是包含关系,就是Owner引用对象的生命周期和Owner相同,表达的是内存锁定关系。

弱引用--回调函数调用的时候需要额外判断相关的数据对象是否被销毁
当引用的对象会出现变化的时候推荐弱引用
消息(事件)分发

简单来看就是将消息分发给注册消息监听的GO, 并调用对应的callback函数

立即执行的消息分发机制
父函数需要等待回调函数执行完毕

存在的问题:当消息连续触发时,会产生调用堆栈很深甚至溢出的问题

存在的问题:触发的行为并不会很快完成,会导致游戏帧率下降

存在的问题:每帧消息回调的串式调用很难并发
使用消息队列的分发机制

把一帧的消息缓存到event quene中,在下一帧进行分发

需要存储大量的不同消息,把这些消息先序列化存储起来,用到时再集体反序列化(反射)。

一般使用Ring Buffer来管理Event Quene, 大小设置为每帧最多存储的消息数量
- 每次处理完一个消息,把head索引向前移动一步
- 每次增加新的消息后,把Tail索引向前移动一步
- 当head和Tail再次重合时候说明消息溢出

根据消息类型来创建不同的Right Buffer, 可以提高效率,也便于追踪

存在的问题:很难保证消息的执行顺序,当需要固定的顺序很难处理

因为消息要延迟一帧处理,需要需要同一帧处理的情况就有延迟
比如处理打击感时要及时效果, 就需要特殊处理
Info
消息系统需要支持PreTick、ImmediateTick,PostTick来方便处理各种情况
游戏逻辑与脚本系统

如果只使用编译语言,则
- 每次对游戏逻辑进行修改都要重新编译
- 当遇到错误代码时,很容易导致程序崩溃
- 当已发布游戏遇到 bug 时,难以进行热更新(及在线更新)。
- 很多玩法由设计师负责,并不是程序员,编译语言如C++对他们来说有门槛。
脚本语言
脚本语言有以下几个好处:
- 解释性语言,方便学习和编写
- 方便热更新
- 稳定,脚本语言会在VM沙盒中运行,它的崩溃不会导致游戏崩溃

对于内存里的GO来说,引擎和脚本同时指向它们,那么由谁管理 GO 的声明周期就是一个问题。

交由引擎控制 --- 适合单机
- 需要提供对象生命周期管理器
- 当脚本使用本地对象的时候不安全(可能被销毁)
- 当涉及游戏玩法时,在脚本中直接创建显然更简单。

交由脚本控制 --- 适合MMO类的大型游戏(会创建大量的GO)
- 对象的生命周期由脚本的 GC系统 自动管理,GC很慢
- 对象释放的时间不可控(GC 控制)
- 脚本中如果引用关系过于复杂容易发生内存泄漏
脚本语言的架构
第一种:特定的组件扩展成一个脚本

第二种:将引擎当成 SDK库 提供各种服务,用脚本调用各种service。(现在用的比较少)

其他
先进的脚本系统特征:热更新:

简单来看就是更新脚本逻辑,重新指向新的实现
需要注意在热更新时,全局变量的重置和处理
在游戏引擎架构时更考虑工程上的鲁棒性,现代引擎是一个编程语言的复合体
脚本语言的问题

几个受欢迎的脚本语言:

Lua比较轻量,需要增加扩展,很难加速
Python的库很强大,面向对象,虚拟机占用很高.

把C#变成一个脚本语言
可视化脚本

蓝图系统,现代引擎的标配
易于使用,对于非编程人员非常友好

可视化脚本会提供编程语言的核心功能,下面是可视化脚本的设计特点:

变量:采用管脚设计,不同变量对应不同颜色的管脚和线,很好分辨

表达式:使用结点来代表语句和表达式

控制流:通过执行引脚+执行线构建执行序列

函数:通过构建节点图来创建特殊的功能

类:基于上面功能的综合, 并提供额外的功能
其他:
增加可视化提示,可视化追踪Debug
问题:


- 团队工作时,可视化脚本很难合并
可视化脚本通常被存储为二进制文件
即使使用合并工具,手动的排序脚本图形效率也非常低,而且容易出错 - 当面对复杂的逻辑时,图形会变得非常繁杂
需要统一的规范避免蓝图过于复杂,比如函数拆分
Info
从可视化脚本的设计需求和用途来看 引擎就是提升生产力的工具
Visual Script和脚本本质上没区别
Gameplay 开发中的3C - character、control、camera

角色

简单定义

角色移动包含需要要处理的细节


角色存在多种交互状态,需要和环境进行各种互动。

伴随物理的更真实的角色控制
Info
在引擎层面需要让允许各种系统的状态可以互相通讯,并开放给设计师去定义具体逻辑
控制
将不同输入设备的输入转换为对应的 game play

一个好的控制系统需要处理很多细节

这里是一个射击瞄准的Gameplay控制
- 进入瞄准时 -- 需要拉近、拉远镜头达到一个很好的聚焦效果
- 需要瞄准辅助 -- 吸附系统,要判断那些东西是目标,吸附是一个过程
- 工具和脚本都需要提供数据编辑功能交给艺术家
- 增加反馈 -- 力反馈(手柄震动)、光效(环境变化)
- 场景感知 -- 在不同的场景同一个按钮输入产生不同的效果
这个是游戏和电影区别,游戏存在玩家行为的互动
Chords 和 Key Sequeneces

Info
control系统决定了游戏中手感的体验
摄像机
相机并不是绑死在角色身后的,而是应当随着角色跑动、走动发生变化。当靠近墙时,保证相机不穿墙。

相机很多时候是和角色绑定到一起,但是并不会固定到一个相对位置
角色行为会带来相机的变化

上图展示了弹簧臂和聚焦时摄像机根据曲线拉近的处理

在不同游戏模式下,相机的各种参数和位置都需要精心设计


相机效果:例如打击感就运用了很多相机效果,例如相机震动,相机滤色(例如残血屏幕变红)等
相机管理:在游戏中可能需要对很多不同的相机视角进行切换,需要一个相机切换的管理系统。


相机在游戏中表达的是人非常强的主观感受
比如恐惧时瞳孔会放大,同时FOV缩小更聚焦敌人
比如让角色移动感觉更真实(从摄像机的角度)
Camera系统是性价比非常高,让游戏有大作感的系统

引擎需要提供用脚本系统对摄像机进行控制
Info
Gameplay的归纳就是Everything is Gameplay,所有系统都为玩法服务。