文章目录
- 0,游戏性课程框架
- 一,事件机制
-
- [1.1 事件的定义](#1.1 事件的定义)
- [1.2 callback的注册](#1.2 callback的注册)
- [1.3 事件的分发系统](#1.3 事件的分发系统)
- 二,游戏逻辑与脚本系统
-
- [2.1 特点和常见脚本语言](#2.1 特点和常见脚本语言)
- [2.2 脚本语言的GO管理](#2.2 脚本语言的GO管理)
- [2.3 脚本语言的架构](#2.3 脚本语言的架构)
- [2.4 可视化脚本](#2.4 可视化脚本)
- [三,Gameplay 开发中的3C :Character、Control、Camera](#三,Gameplay 开发中的3C :Character、Control、Camera)
-
- [3.1 角色](#3.1 角色)
- [3.2 控制](#3.2 控制)
- [3.3 相机](#3.3 相机)
- QA
0,游戏性课程框架
- Gameplay 的挑战:
- 玩法系统要和动画、特效、UI、声效、Audio、外设等多个系统共同合作,因此岗位涉及面很多
- 同一个游戏也有很多不同的玩法,比如巫师三和昆特牌,对玩法系统的可拓展性要求高
- 玩法的快速迭代
一,事件机制
事件机制就是让GameObject 互相交互的机制,比如走到哪里发生爆炸。这种机制如果全都使用ifelse语句写死条件,系统会崩溃掉,所以一般使用event或者message的形式来实现。
想应对比较复杂的游戏机制时,事件机制一般采用发布-订阅模式(Publish-subscribe Pattern):不同的发布者发送不同类型的事件给事件调度者(event dispatcher),可以理解为一个调度公司,事件调度者把事件告诉对应的订阅者(不知道发布者是谁),然后订阅者作出相应的动作(callback)
- 该模式的三个关键点分别是 事件的定义、callback、事件的分发系统
1.1 事件的定义
Event Type + Event Argument(配置)
Event虽然可以使用基类去继承, 但如果不断加入新的事件类型和对应编辑性,则每次都需要重新编译(与上节课的代码反射相关),这时常见解决方法有:
- 将新事件新编译成的C++代码当成 DLL,加入到系统中(UE)
- 上层采用 C# 语言,方便动态挂接和扩展
- 用脚本语言
1.2 callback的注册
回调函数或触发函数(invoke)。回调函数的注册和触发有一个时间差,在这段时间有可能发生意外,比如订阅者被销毁,因此对象的生命周期和回调函数的安全性是非常重要的。
可以使用c++11中类似智能指针的技术解决:
- 强引用:使订阅者在有事件注册时不能销毁。但是会导致系统内存越来越大,所以很少这么用,只有部分情况,比如物体之间有父子关系时使用。
- 弱引用:在执行回调函数前,判断订阅者是否存在即可。但是性能会下降一些。
1.3 事件的分发系统
如果分发系统把所有的消息都从头到尾扫一遍的分发效率很低,需要专门的管理。
- 立即执行:消息来了马上就分发。(但是可能会在线程中打断父函数的调用)
问题:
- 当发生连锁反应时,比如手雷引爆其他手雷,会造成回调函数的层数过深的问题。
- 如果事件回调本身很耗时,比如粒子系统,帧率会骤降
- 调用一层套一层,很难并行
- 事件队列:把事件存储到Queue中,后续统一处理;并且需要序列化后存储,用到时候再反序列化(c++反射)---跨进程夸网络需要
具体实现:
- 使用循环队列Ring Buffer来管理内存(申请一次内存即可)
- 使用Batching分批管理:将事件分类,比如网络事件、动画事件、战斗事件等,每一类别分别采用一个队列。
问题:
- 不能保证消息执行的顺序(比如先动画后物理等逻辑关系),所以为了保持系统健壮性需要同时支持 PreTick、ImmediateTick,PostTick(这块是bug高发地)
- 是one-frame delays系统,会导致一帧左右的延迟(及时性差),因此在需要及时性的地方,比如战斗打击,就要hardcode一下专门处理
二,游戏逻辑与脚本系统
-
早期游戏逻辑都是直接用c++写的,但这样不可持续,因为:
- 每次对游戏逻辑进行修改都要重新编译;
- 当遇到错误代码时,很容易导致程序崩溃;
- 当已发布游戏遇到 bug 时,难以进行热更新。(现在游戏遇到bug,官方更新一个补丁就能解决,就是用到热更新)
- 玩法基本是由设计师负责的,不会写代码,而用脚本语言更容易。
-
脚本语言运行时会先被编译为bytecode,再到虚拟机上运行,代价是速度慢一些
2.1 特点和常见脚本语言
-
脚本热更新:可以快速迭代、线上解决bug--其实就是更新函数指针(但需要考虑工程鲁棒性)
-
脚本语言优点:
- 好学易上手
- 热更新方便
- 脚本语言一般运行在虚拟机沙盒上,可以自己crash而不会导致系统crash,更稳定(大型游戏一旦某个功能crash掉,会立马重新启动脚本重新接入)
- 可以快速迭代
-
脚本语言的问题:
- 慢,比如lua就慢很多。可以使用JIT优化---一边解释一边编译
- 需要虚拟机去运行脚本语言
- 调试不方便
-
几个受欢迎的脚本语言:
- Lua(魔兽世界),轻量内存少,但内库少需要自己写
- Python,库强大丰富,不轻量,占用内存多
- C#(Unity、暴雪),学习成本低,本身就是编译语言,内存占用中等
2.2 脚本语言的GO管理
脚本语言汇总最难的事对象的管理,但是由谁管理 GO的生命周期呢,是代码还是脚本呢?
- 引擎代码管理
引擎生命周期管理器--更严谨也有没写好内存泄漏的可能
当脚本涉及到本地对象的时候不安全
当玩法复杂需要创建对象时,在脚本中创建更简单。
- 脚本管理
GO的生命周期可以被脚本的GC系统(Garbage Collection垃圾回收器)自动管理-----省事但慢,甚至用到10%时间
对象释放的时间不可控(GC 控制)
脚本中如果引用关系过于复杂容易发生内存泄漏
- 因此一般大型单机都是引擎直接管理,而玩法复杂的比如mmorpg一般用脚本管理GO
2.3 脚本语言的架构
- 引擎包脚本:主要玩法写在native code里,把组件扩展成脚本
- 脚本包引擎:主要玩法写脚本里,把引擎当成 SDK库提供各种服务。(迭代快,需要认真设计接口,但现在用的少---弹幕说网易的自研引擎就是这样)
2.4 可视化脚本
- 比如UE的蓝图,Unity的Visual Scripting、Shader Graph就是可视化脚本,这种形式对艺术家友好,并且可视化更不容易产生错误。
- 并且可视化脚本debug十分方便,每个节点的输出都可以看到
- 可视化脚本的问题:
- 团队工作时,可视化脚本很难合并,会丧失语义且效率低下(因此很多团队前期用可视化脚本,成熟后会翻译为代码)
- 有时候可读性很差,一团乱麻
- 可以把可视化脚本翻译为脚本,然后再执行(再一次应征可视化脚本就是脚本)
三,Gameplay 开发中的3C :Character、Control、Camera
- 3C系统是游戏体验的核心
- 要了解什么是3C系统,《双人成行》可以作为最好的案例。
3.1 角色
- 角色移动:障碍、上下坡、起步和停下等,要处理非常多细节和复杂状态,比如滑行、滑冰、飞等
- 与环境互动:雪地系统、音效、粒子等等
- 真实物理互动:状态机控制下的各种状态
3.2 控制
控制系统是游戏丝滑手感的由来
- 输入设备控制:鼠标键盘、手柄、方向盘、甚至飞行控制器等的控制,比如自动吸附、调整相机等(瞄准辅助也是类似)
- 反馈感:比如振动或力反馈,键盘背光也算吧
- 按键组合:拳皇
3.3 相机
- 相机并不是固定在角色身后的,而是随着角色跑动、走动等状态变化,相机远近大小都跟着变化,或者Camera Track(模仿电影视角更有代入感)
- Spring Arm:当靠近墙时,保证相机不穿墙。
- 相机效果:互动抖动、滤镜、各种后处理等
- 视角变化:第一、三人称切换、武器不同变化、载具不同变化,这个变化还需要插值
- 除了上述要求,还需要提供一个可视化脚本系统,让设计师能对相机参数进行调整设计。
QA
- 接入可视化脚本是不是就不需要传统脚本了?不是,很多时候传统脚本效率更高,一般两者都需要
- 未来平台会不会趋向禁止热更?现在确实一些平台比如ios、ps不喜欢热更,未来会不会变化不能确定
- 会不会用逻辑和表现分离的模式开发?实际上大部分是分离的,比如一些修仙类游戏的表现比较简单,表现和逻辑可以分的非常开,这样团队规模也可以做得很大,开发效率也会高。但现在更多玩家喜欢互动更多的3A游戏,这样逻辑和表现联系就更紧密,需要糅合去做,对开发者要求也更多。