第一部分 架构与封装
第1章 UI交互 (已看)
第2章 玩法底层 (已看)
第3章 辅助系统
第二部分 艺术资源
第4章 资源分类
第5章 后处理效果
第6章 资源工作流
第三部分 底层核心
第7章 渲染原理
第8章 3D数学基础
第9章 寻路算法
第四部分 自定义扩展
第10章 调试工具
第11章 日志工具
第12章 快捷功能
第13章 后台服务
第五部分 独立游戏
第14章 角色分工
参考文献
第1章 UI交互
1.1 绑定事件响应
请列出为UI控件绑定事件响应的方法, 并分析不同方法之间的优劣
[总结]
绑定响应的方式有两种: 在组件中添加和在代码中添加. 它们的核心相同, 都是通过代理来回调自己的函数. 组件绑定的操作更友好,但容易被人更改. 代码绑定的优点是更稳定,而且能更灵活地控制监听的事件点,缺点是逻辑编译在代码中, 最好有健全的框架支持, 以防止出现使用问题.例如, 重复绑定的处理函数, 忘记移除的绑定,等等
[扩展问题]
1. 有没有办法克服代码绑定的缺点?
2. 既然大家都倾向于使用代码绑定, 那么组件绑定的存在有意义吗?
1.2 事件传递流程
请从结构的角度概述,控件的消息模型包含哪些部分, 并解释UGUI中点击事件与响应调用是如何关联在一起的
[事件体系]
首先说明, 事件体系的基础是设计模式中的观察者模式, 因此按照标准的Subject和Observer来解读没有任何问题. 但在这里, 笔者更希望以功能为导向, 将事件体系划分成更易于理解的模块. 按这种划分方式, 事件体系由四部分组成, 分别是:
监测器(Monitor)
采集器(Collector)
派发器(Dispathcer)
响应器(Receiver)
其中用户的操作被监视器驱动的采集器捕获, 接着监视器将反馈的信息通知到派发器中,最后通过派发器将事件传播出去
[Unity3D 实现]
在Unity3D中, 功能模块的每个部分都有对应的实现类
监测器 (Monitor)
采集器 (Collector)
派发器 (Dispatcher)
响应器 (Receiver)
[类图]
[总结]
[扩展问题]
1. 请结合设计模式中的观察者模式, 分析Unity事件框架的优劣
2. 如何利用EventSystem来降低事件的频率
控制可以控制采集器中的采集频率,或派发器中派发事件的频率。理论上StandaloneInputModule组件中有InputActionPreSecond这个变量可用来控制消息输入,但查阅源码,只发现它对导航按钮做了限制,未实现其他类型的控制,代码版本为5.5。
1.3 事件响应接口
通常对于事件传递, 绑定消息处理时都要些很多相同的框架级别方法,例如:
m_btn1.onClick.AddListener(OnBtnClick1);
m_btn2.onClick.AddListener(OnBtnClick2);
m_btn3.onClick.AddListener(OnBtnClick3);
public void OnBtnClick1() { }
public void OnBtnClick2() { }
public void OnBtnClick3() { }
View Code
请问是否有方法对其简化, 减轻开发编码量?
[扩展问题]
1. 有办法对各种控件添加统一的响应接口吗?
2. 有时候界面中会有大量不显示的按钮, 这会影响我们的封装吗?
如果存在大量的隐藏按钮,会导致界面中加载很多无用监听,降低游戏运行效率。为了解决这个问题,可以将绑定函数放到OnEnable中,并在OnDisable中解绑
第2章 玩法底层
2.1 游戏循环
请简述Unity 3D中常用的生命周期函数, 并以此为基础简述游戏循环的设计要点
[问题分析]
一般来说, 在大型商业项目中, 我们需要自制一套游戏循环, 来满足多变的游戏玩法, 那些现有的声明周期函数可以使用?那些需要被替换掉?这些取舍贯穿了整个游戏开发过程
[生命周期]
深入理解这张图, 可以应对大多数的时序问题. 在Initialization, Disable/enable, Decommissioning区域的函数只会调用有限次数, 而其他部分都是受控重复或循环调用, 其中
Awake函数在构造脚本对象时调用
OnEnable/OnDisable在每次激活对象时调用
Start在第一次触发脚本时调用
OnDestroy在销毁对象时调用
主循环分为物理模拟, 游戏逻辑, 渲染绘制三个子循环. 从Unity实现的方式来看, 这三个循环是在同一个线程中. 不过自Unity 3D的5.x版本中, 引擎加入了多线程渲染的选项, 即把第三步循环放在另一个线程中进行, 这样就可以在固定帧率下降低逻辑循环的压力, 减少掉帧现象的发生. 至于物理模拟循环, 笔者觉得没必要拆出到另一个线程.毕竟线程间通信也需要性能,而物理模拟循环运行的速度一般要快于游戏逻辑, 如果出现数据访问冲突时还需要加锁,就得不偿失了
[重写模板脚本]
using UnityEngine;
using System.Collections;
public class #SCRIPTNAME#: MonoBehavior {
#region Public Attributes
#endregion
#region Private Attributes
#endregion
#region Unity Message
// void Awake() {
// }
// void OnEanble() {
// }
// void Start() {
// }
// void Update() {
// }
// void OnDisable() {
// }
// void OnDestroy() {
// }
#endregion
#region Public Methods
#endregion
#region Override Methods
#endregion
#region Private Methods
#endregion
#region Inner
#endregion
}
View Code
[游戏循环]
while (true) {
Input();
Update();
Render();
}
View Code
从这个层面看, 游戏循环由三部分组成, 分别是
1. 非阻塞的用户输入
2. 更新游戏逻辑状态
3. 渲染游戏画面
每次循环完成后, 会更新一次画面的绘制, 这个过程也被称为帧(Frame). 帧率(FPS)可以标定游戏循环的速率与真实时间的映射关系. 帧率值越小, 意味着游戏越"卡". 游戏在PC上, 通常为60帧/秒, 在手机上为30帧/秒. 另一方面,帧率的倒数即为每帧所占用的时长, 单位通常为毫秒. 影响帧率的主要因素是每帧需要做的工作. 例如, 复杂的物理计算, 游戏逻辑的处理, 图形细节控制等, 这些都会占据CPU与GPU. 如果处理操作的时长超过帧率的倒数,那么就会拖慢帧率,这种现象称为"掉帧"
[固定帧率模式]
while (true) {
double start = getCurrentTime();
Input();
Update();
Render();
sleep(start + 1/FPS - getCurrentTime());
}
Application.targetFrameRate = FPS;
View Code
[追赶模式]
double preFrameTime = getCurrentTime();
double lag = 0.0;
while (true) {
double current = getCurrentTime();
double elapsed = current - preFrameTime;
preFrameTiem = current;
lag += elapsed;
Input();
while (lag >= 1/FPS) {
Update();
lag -= 1/FPS;
}
Render();
}
View Code
[总结]
Unity 3D作为完整的引擎, 常见的生命周期函数与游戏循环模式都已具备. 但作为特定的游戏, 通常有自己的特点. 例如, 竞速类游戏与MMO网游在游戏循环的设计上就有很大的差别. 竞速类游戏对实时反馈的要求很高, 如果采用追赶模式, 则抽帧造成的体验就会很差. 在掉帧方面, MMO网游面临的则是角色在场景中漫游时, 其他玩家的模型加载与位置同步造成的卡顿. 在这种情况下, 可能会使用分帧加载, AOI(Area Of Interset)等处理方法来保障游戏的流畅
[扩展问题]
1. 请尝试分析多线程渲染的实现原理
在执行Render时,会在另外一个专门的线程中进行。好处就是可以利用多核的优势,减少了等待时间
2.2 时间记录
请问在Unity 3D中常见的时间分为哪几类, 获取和记录它们的方法有哪些? 它们的适用场景分别是什么?
[真实时间线]
真实时间线指的是, 以真实时间的流逝作为标准,进行时间统计的时间线.从重要程度上讲, 它代表着绝对的时间流逝, 是一切时间的参考,不同设备得到的结果一定是相同的,这也是整个时间记录的基石
Time.realtimeSinceStartup
View Code
[游戏时间线]
游戏时间线是应用更广泛的时间计量方式.它以游戏世界的时间流逝速度作为标准度量时间.很多情况下,它与真实时间线不同.
[间隔时间]
间隔时间的计算方法为计算两帧之间的差时.
Time.timeScale
Time.deltaTime
Time.unscaledDeltaTime
Time.smoothDeltaTime
View Code
[扩展问题]
1. 现在有两种情况引起游戏间隔时间变长: 一种是目标机器性能差, 需要追赶补偿效果; 另一种是断点调试, 不希望影响逻辑. 请问有没有办法在设计时间线时, 过滤掉断点调试的情况
一般说来,目标机器卡顿与断点调试的延迟不是一个数量级的,可以通过时长的判断,例如是否大于0.1s,来区分这两种情况
2.3 动画事件
请简述动画事件的制作与逻辑处理方法, 如果有多种备选方案, 请对比它们的优劣
[问题分析]
所谓动画事件, 指的是动画进行到某个时刻触发的事件
[基于动画的事件]
编辑器里编辑
[基于时间的事件]
另一种常见的事件是逻辑型事件. 虽然它也与动画的状态紧密相关, 但这些事件种包含着逻辑处理. 例如, 角色在攻击动作的过程种触发伤害: 或者在攻击出拳的动作过程中向前移动,遇到敌人就停止移动. 这些事件与逻辑紧密相关, 不仅取决于美术的表现效果,更多的是数值与逻辑的合理性
对于这种事件, 采用基于时间的动画事件就更为合理. 在制作事件时, 我们记录的是局部时间线的偏移值, 并将它以独立文件的形式存储. 在上一节中, 我们知道游戏会存在一条游戏时间线. 当动作开始时, 我们会将这些事件时间偏移量并入游戏时间, 并在游戏时间线的对应时间添加触发事件的回调, 这样事件触发就完全脱离了模型动画
这样做的另一个优势是显示与逻辑的分离. 在同屏多人的游戏中, 有时会不播放角色动画, 只产生技能触发的需求. 在这种事件框架下, 就可以只运行基于时间的事件而不去加载模型与动画. 因为伤害触发与计算不需要依赖于动画, 所以一切逻辑都可以正常运行. 另外, 在有异步数据影响的场景中, 这种事件控制方法也有优势, 它可以预先做好逻辑计算, 再对动画做插值处理, 数据优先于动画直接进行同步, 将网络延迟对游戏的影响降到最低
[扩展问题]
1. 在后期优化时发现同屏特效过多, 在处理基于动画的特效事件时存在性能瓶颈, 请问该如何优化
对于这种情况,制作流程可以保持不变。但在播放特效时,调用统一的代码接口,在接口函数中根据场景中的特效数量进行抉择与控制
2.4 游戏同步
请问在游戏同步方面有哪些分类方式?帧同步与状态同步有何优劣?
[游戏模式]
根据游戏中玩家之间的互动方式, 可以将多人交互类型的游戏大体上分为两个类别: MO与MMO. 这两种典型的模式有着完全不同的侧重点和技术架构
多人在线游戏(Multiplayer Online, MO)
大型多人在线游戏(Massively Multiplayer Online, MMO)
MO与MMO在游戏内容上有很大差别, 所需的技术也截然不同, 但一个游戏重同时存在两种模式并不冲突. 常见的做法是, 将MO类型的游戏以副本的形式嵌到MMO中, 并分别采用独立架构对游戏逻辑提供支持. 这样就可以兼顾MMO的玩法多样性与MO对战的乐趣性. 在WoW中, 野外战斗与大部分副本都是以MMO架构实现的,而少数在短时间内需要玩家紧密配合, 历尽艰难才能通关的副本则是通过响应更快的MO架构实现的
[通信方式]
1. C/S架构
从通信结构上看, 同步可以分为C/S与P2P两种. C/S是Client/Server的缩写,指的是客户端与服务器连接的消息传递方式. 在这种框架下, 所有消息都通过服务器转发.它的游戏在于可以很好地控制非法行为. 服务器知道一切数据, 通常也会对数据进行模拟计算, 因此, 作弊行为很难出现, 另一方面客户端也可以只做表现, 完全由服务器的数据驱动. 这种方式常用在不需要, 或很少需要预表现的游戏中
这种结构的最主要弊端是网络延迟较大, 通常会控制在200ms以下. 另一方面, 中央服务器上大量的运算也会增加服务器的设备成本. 在商业项目上线之前,通常会进行压力测试, 主要关注点在带宽, 连接数, 响应速度等. 通过模拟大量玩家的操作, 来测试服务器硬件是否能够应付预期数量的玩家
2. P2P架构
与C/S架构不同, P2P架构是两个客户端之间直接连接. 它与C/S结构的优劣正好完全倒置, 它有低延迟的优点, 也可以节省大量的服务器运算, 却很难避免作弊的行为. 例如, DOTA中的全图外挂无法禁掉, 根本原因就是客户端拥有其他玩家的数据
[数据模式]
1. 全网状数据模式
全网状数据模式指的是, 每个客户端需要给网络中的每个设备广播自己的数据. 因此每个设备上有完整的数据, 各个终端之间传递的只是控制设备的输入信息. 可以预想到, 随着网络中终端数量的增加, 信息量会急剧增长. 由于消息量大, 因此不同客户端之间通常只会传递用户输入的信息, 这就需要保证每个客户端的初始数据与每次操作的运算结果必须完全相同, 否则就会表现不一致
2. 星型数据模式
另一种情况是网络中的成员并不平等, 所有终端都要依从中央终端的数据. 这个终端在C/S架构中被称为中央服务器, 在P2P架构中被称为主机, 这里为了方便说明, 暂称为服务器. 在标准的星型结构中, 服务器在收集完所有设备发送的数据之前, 一直处于等待状态, 接收完成后再将数据广播出去, 因此它是将输入数据暂时集中到服务器上, 这一点与全网状数据模式完全不同.这种结构避免了终端过多导致的数据暴涨, 但也会引起数据传输速度的下降, 有利有弊, 需要根据自己项目的实际情况做取舍
[同步方式]
1. 状态同步
本节中提到的状态同步和帧同步都是广义上的, 虽然它们的底层实现都可以基于上面提到的模式, 但从广义讲, 它们的同步思路不同
状态同步指的是将其他玩家的状态行为同步, 例如, 怪物的AI控制, 角色技能释放, 战斗伤害计算等等. 纯粹的状态同步模式下, 这些内容都由服务器运算, 只是将结果下发给客户端, 客户端根据得到的数据驱动显示而已. 这种模式的缺点是流量消耗比较大, 消耗的流量取决于场景中需要转发数据的人数和内容. 另外, 先天的结构导致响应速度存在问题, 无法做到高频交互的流畅体验. 最明显的特点就是"拉扯"现象. 它表现为某个角色会突然出现在某个位置, 或某个技能效果突然出现, 甚至角色忽然死亡等. 引发这个现象的原因是, 网络波动导致数据未能及时送达, 而客户端进行了某种程度的预表现或航位推测
对于大多数实时性要求不高, 交互简单的游戏, 一般会使用状态同步. 通常来说, 常规MMO类型的游戏只能使用状态同步
2. 帧同步
帧同步是指客户端只同步用户的输入指令, 例如, 向前走, 按下哪些技能等, 不同的客户端各自计算自己的结果. 由于消耗的流量只取决于指令数, 因此会大大减少消息. 另外由于消息结构的原因, 帧同步的速度要比状态同步更快, 因此适用于一些高频交互的游戏
不过由于每个客户端都要独立计算, 因此需要保证计算结果的一致性. 理论上讲, 相同的时机, 输入相同的内容, 会得到相同的结果. 不过从实际情况看, 做到完全相同还是有些难度的. 以Unity 引擎为例, 一方面, 各个脚本之间Start, Update等生命周期函数的调用顺序不确定: 另一方面, 使用Physic物理系统也不保证是确定性模拟. 比如一个单位的坐标偏差了0.01导致技能未能命中目标, 那么这个目标的血量判断就会受到影响. 如果这个目标的行为受到血量的影响, 那么最终的结果会完全不同. 因此让每个客户端计算结果完全相同不是一件容易的事, 一些常见的注意事项如下:
1. 不使用浮点数, 用整数代替
2. 不同客户的同步频率要保证一致
3. 随机种子相同, 并自定义接口防止其他公用系统干扰
4. 使用排序容器, 保证遍历顺序
5. 逻辑显示分离
6. 使用补间过渡, 调整速率, 掩盖卡顿
除了一致性的难点, 帧同步还需要解决流畅度的问题, 由于通过网络传输过来的数据一定会慢于本地, 而我们又希望在相同的时刻输入信息, 因此就会引发等待, 反映到用户体验就是不流畅
优化方法有很多, 例如, 在帧同步游戏中, 由于广播的频率非常高, 因此每次广播的数据就要足够小, 这样可以节省很多消息处理的时间. 对于消息, 可以将需要所有客户端同时发生的内容提前广播给其他用户, 采用时钟同步. 客户端逻辑先行, 显示通过平滑追赶的方式处理. 很多改进是体验优化的范畴, 需要结合具体游戏进行
在传输层, 移动端的同步建议使用UDP作为传输协议. TCP为了保证传输的可信性, 很多机制不太适合波动较大的移动网络. 在弱网络环境下, UDP的RTT几乎不受影响, 而TCP的RTT波动比较大, 特别是在丢包重发时影响比较明显. 虽然使用UDP会引入丢包,丢包的问题.但可以通过冗余的方式来解决这个问题. 比如每帧三个数据包, 实际上是包含了过去两帧的数据,也就是每次发三帧的数据来对抗丢包
[扩展问题]
1. 请问帧同步中的锁帧(Lockstep)是指什么?
锁帧指的是如果未收到消息,就不应该继续向下执行的同步模式,它是帧同步能同步执行的核心。但锁帧会影响流畅性,因此通常会做些定制化的更改。
第3章 辅助系统
3.1 有限状态机
3.2 脚本系统
第4章 资源分类
4.1 贴图种类
4.2 材质效果
4.3 动画分类
4.4 流动效果
第5章 后处理效果
5.1 模糊效果
5.2 泛光效果
5.3 辉光效果
5.4 景深
第6章 资源工作流
6.1 图片格式更改
6.2 动画抽取
6.3 文件移动检测
第7章 渲染原理
7.1 渲染管线
7.2 渲染顺序
第8章 3D数学基础
8.1 点和向量
8.2 向量的运算
8.3 区域检测
8.4 平面移动
第9章 寻路算法
9.1 寻路这件事
9.2 A*算法
9.3 Navigation系统
9.4 任务调配
第10章 调试工具
10.1 GM命令
10.2 绘制曲线
10.3 指示绘制
第11章 日志工具
11.1 出错暂停
11.2 日志接口优化
11.3 频道化日志
11.4 崩溃日志上报
第12章 快捷功能
12.1 自定义菜单
12.2 定制UI
12.3 回退操作
第13章 后台服务
13.1 编辑器服务
13.2 自动注册框架
13.3 遍历文件
第14章 角色分工
14.1 产品策划
14.2 美术设计
14.3 运营知识
14.3.1 用户规模数据
14.3.2 用户价值数据
14.4 总结
参考文献
1. 游艺网教育部. 次世代游戏开发基础. 北京: 清华大学出版社, 2015.
2. 游艺网教育部. 次世代游戏角色制作. 北京: 清华大学出版社, 2015.
3. 李瑞森, 张卫亮, 王星儒. 网络游戏场景设计与制作实战. 北京: 电子工业出版社, 2015
4. Mike Bailey, Steve Cunningham. Graph shaders(Second Edition). 刘鹏, 译. 图形着色器----理论与实践(第二版). 北京: 清华大学出版社, 2012.
5. Rick Parent. Computer Animation algorithms and Techniques, Second Edition. 刘祎, 译. 计算机动画算法与技术(第二版). 北京: 清华大学出版社, 2012.
6. Robert Nystrom. Game Programming Patterns. GPP翻译组, 译. 游戏设计与开发. 北京: 人民邮电出版社, 2016.
7. 中嶋谦互 网络游戏核心技术与实战. 北京: 人民邮电出版社, 2014.
8. Scott Rogers. The Guide To Great Video Game Design(Second Edition). 孙懿, 高济润, 译. 通关! 游戏设计之道(第二版). 北京人民邮电出版社, 2017
9. Eric Lengyel. Mathematics for 3D Game Programming and Computer Graphics, Third Edition. 詹海生, 译. 3D游戏与计算机图形学中的数学方法(第三版). 北京: 清华大学出版社, 2016
10. 冯乐乐. Unity Shader入门精要. 北京: 人民邮电出版社, 2016
11. Sanjay Madhav. Game Programming Algorithms and Techniques. 刘瀚阳,译. 游戏编程算法与技巧. 北京: 电子工业出版社, 2016
12. 王睿杰等. 创世学说: 游戏系统设计指南. 北京: 电子工业出版社, 2016.
13. 于洋, 余敏雄, 吴娜, 师胜柱. 游戏数据分析的艺术. 北京: 机械工业出版社, 2015