You are on page 1of 280

腾讯游戏学院

TENCENT INSTITUTE OF GAMES


成就 戏梦想
讯 戏 成立于2016年12月,致力于 造 戏知识分 和
业 平台,专 于推动专业 养、 戏 研究和发展、开发
态建设。 过与国 校合作推动 戏 、 术研究和各类
事,组织 业 及开发 扶持 动,提 戏类专业课程等,为
戏 创造更 可 。
讯 戏开发精粹

讯 戏 

工业出版社

Publishing House of Electronics Industry


北 ·BEIJING
内容简介
《 讯 戏开发精粹》是 讯 戏研发 技术 晶, 10
名 讯 戏资 技术专 成, 了 主 戏研发
路上积累沉 技术 案,具有较强 性及 性, 涵盖
戏 本系 及开发工具、 和 、计算机图形、 工智 与后台
架构等。
未经许可,不得以 式 制或 本书之 分或 。
版权所有, 权必究。
图书在版编目(CIP)数据
讯 戏开发精粹/ 讯 戏 .—北 : 工业出版社,
2019.9
ISBN 978-7-121-36602-4
Ⅰ.① … Ⅱ.① … Ⅲ.① 戏程序—程序设计— 集 
Ⅳ.①TP317.6-53
中国版本图书 CIP 核字(2019)第096804号
责 辑:张春
印刷:
订:
出版发 : 工业出版社 北 市 区万 路173 箱
:100036
开本:787×980 1/16
印张:18.25
字 :419千字
版次:2019年9月第1版
印次:2019年9月第1次印刷
价:79.00
凡所购买 工业出版社图书有 问 ,请向购买书店调 。
书店售 ,请与本社发 系, 系及 购 话 : ( 010 )
88254888,88258888。
质量投诉请发 件至zlts@phei.com.cn,盗版 权举报请发
件至dbqq@phei.com.cn。
本书咨询 系 式:tencentgamesgems@tencent.com。
编委会
顾问:崔晓春       
主编:叶劲峰
编委:  智  刘   匡 尼
柏    杨   沙 
审校:董 磊  刘 雅   毅
推荐序
前不久,一位 中 师,也是我 同 同 妈妈,主动
问我这几年新开设 戏设计和 竞技专业是否 得报 ,
对制作 戏 常有兴趣。听到这个咨询,我 了一口气: 戏正
逐 越来越 认可。
我 知 , 戏是“第九 术”,是各种 术和技术 力
集 成 。记得 10 年前,我参与开发 讯第一款 研 戏《QQ
幻想》 ,随着项 进展,我 到,开发出一款 质量
MMORPG 戏 很强 、 合 技术 力。比 一 技术细 :
最 度 制瞬移 挂 情 下, 弱 络环 下 移动
和同步?怎样 实现 于决策树 各种NPC AI, 得NPC
为更贴近 和有趣? 戏 经 系 平 ,各种 之 力
平 ,还有上百万 同 线 稳 性、 更新、 切 ……
诸 此类, 当 技术 景下,是颇为严峻 战。
我 也知 ,知识只有分 更有价 。站 巨 肩膀上,
看得更远;有了前 经 加持,也 成 得更快。《 讯 戏开发
精粹》这本书汇集了 讯 戏 戏开发中 分精华,从客 端到
服务器端,从 引擎到工具 ,从图形 到AI,各领 有 证
过 决 案呈现。 讯是一 具有 度社 责 感 业, 愿意
这 力和经 私奉 给 ,为 业 发展 己 一 力
量。互相 习,一起进步,是我 希望。
我 还知 , 戏 征之一是互动, 桌、 房 、
……我 可以看到各种 戏情景 开心愉快, 已经融 我
日常 之中。 乐是 天性, 让 更 ,是我 每一
个 戏从业 命。从 世 到现实 ,再从现实 到
世 , 技术 段来 变 ,未来就 了!

腾讯互动娱乐 公共 发运营体系负责人
编者序
戏开发对于一 软件开发 来说,总 上了一层神秘 纱。
这可 是 个原 造成 。 先, 戏开发 技术 比较广,一
技术 计算机图形 、 模 、实 络同步等比较 应 一
软件开发中。其次, 戏开发属于创意工业,对各类 戏 求
有很 区别,不 技术没有形成标 ,各 技术 案、工作 程等
也 有不 差异。最后,公司之 至公司之 也可 有技术 垒,
影响知识和技术 。这 情 不利于有兴趣 朋友进 此 业,
从业 进步也 受 , 远影响 业 发展, 以 对
竞争。
20世纪90年代 ,互 未普及之前,只 过BBS
集一 国 “漂 ”过来 戏开发技术 档, 《德 总
3D》 三 景渲染及纹 贴图技术、 标 Mode
X 去 VGA 256 双 冲区渲染等。 个资讯匮乏 年代,每次
到新技术 档, 兴奋得 至宝。
国 戏开发 “ ”, 概 过千禧年代 《
戏 程精粹(Game Programming Gems)》系列丛书。这 丛书影响
了一 代 开发 ,让我 一窥世 各 戏开发 各种秘技,
决 戏开发中 到 各种 同问 ,同 可以 发 感,研发比
书中更 决 案。
进 互 息 年代,我 上 博客、问
答等 息,可以更快速 知悉各种新技术。 同 , 上 息相对于
出版来说, 常较为 ,品质参差不齐。从业 也 于 原
,不 随 公开一 戏开发中 到 新技术。
本书受《 戏 程精粹》系列丛书 启发,希望鼓励 讯 戏
工程师与业 同 分 一 实际应 戏里 技术,与 业 。
过 审核及 辑等机制,尽量筛选可对 公开、 品质 章,
也 证技术具有一 性及 性。对国 业 ,希望这本
书 成为一 步, 进更开 未来,提升 技术水平。
本书从提案到出版 达一年半 , 了 各位作 忙碌
开发 务中 空 ,还必须感谢 讯 戏
力 持,也 感谢 讯 戏 董磊、刘雅和 毅 项 成功推
进。我也 心感谢本书 智、刘 、匡 尼、 柏 、 杨
和沙 ( 名不分先后), 是 讯 戏各个 技术专
,悉心为 章 关。也 常感谢 工业出版社 张春 和
葛 协助出版事宜。
最后,希望本书 对读 有所帮助, 有 意见请不吝 过
件反 给我 :tencentgamesgems@tencent.com,期望 篇再
见。
叶劲
《腾讯 发 》主编
腾讯互动娱乐 魔 作 群
目 录
封面
腾讯游戏学院
扉页
版权信息
编委会
推荐序
编者序
第一部分 游戏数学
第1章 基于SDF的摇杆移动
摘要
1.1 引言
1.2 有号距离场(SDF)
1.3 利用栅格数据预计算SDF
1.4 SDF的碰撞检测与碰撞响应
1.5 避免往返
1.6 利用多边形数据预计算SDF
1.7 其他需求
1.7.1 如何将角色从障碍区域中移出
1.7.2 角色不能越过障碍物的远距离移动
1.8 动态障碍物
1.9 AI寻路
1.10 动态地图
1.11 总结
参考文献
第2章 高性能的定点数实现方案
摘要
2.1 引言
2.1.1 浮点数简介
2.1.2 32位浮点数(单精度)表示原理
2.2 基于整数的二进制表示的定点数原理
2.2.1 32位定点数表示原理
2.2.2 64位定点数表示原理
2.3 定点数的四则运算
2.3.1 加法与减法
2.3.2 乘法
2.3.3 除法
2.4 定点数开方与超越函数实现方法
2.4.1 多项式拟合
2.4.2 正弦/余弦函数
2.4.3 指数函数
2.4.4 对数函数
2.4.5 开方运算
2.4.6 开方求倒数
2.4.7 为什么不用查表法
2.5 定点数的误差对比与性能测试
2.5.1 超越函数及开方的误差测试
2.5.2 性能测试
2.6 总结
参考文献
第二部分 游戏物理
第3章 一种高效的弧长参数化路径系统
摘要
3.1 引言
3.2 端点间二次样条的构建
3.3 路径的构建
3.4 曲线的弧长参数化[3]
3.5 曲线上的简单运动
3.5.1 跑动
3.5.2 跳跃
3.5.3 相邻路径的切换
3.5.4 曲线上的旋转插值
3.6 总结
参考文献
第4章 船的物理模拟及同步设计
摘要
4.1 浮力系统
4.1.1 浮力
4.1.2 升力
4.1.3 拉力
4.1.4 拍击力
4.1.5 阻力上限
4.2 引擎系统
4.2.1 移动、转向模拟
4.2.2 向心力计算
4.3 Entity-Component及同步概览
4.4 浮力系统物理更新机制
4.5 总结
参考文献
第5章 3D游戏碰撞之体素内存、效率优化
摘要
5.1 背景介绍
5.2 体素生成
5.3 体素内存优化
5.3.1 体素合并的原理
5.3.2 体素合并的算法
5.3.3 地面处理
5.3.4 水的处理
5.3.5 范围控制
5.3.6 内存自管理
5.3.7 体素内存优化算法的效果
5.3.8 体素效率优化
5.4 NavMesh生成
5.4.1 体素生成NavMesh
5.4.2 获取地面高度
5.4.3 后台阻挡图
5.4.4 前台优先级NavMesh
5.4.5 锯齿
5.5 行走、轻功、摄像机碰撞
5.5.1 行走
5.5.2 轻功
5.5.3 摄像机碰撞
参考文献
第三部分 计算机图形
第6章 移动端体育类写实模型优化
摘要
6.1 引言
6.2 方案设计思路
6.2.1 角色统一与差异元素分析
6.2.2 角色表现=人体+服饰
6.2.3 角色资源整理
6.2.4 资源制作与实现
6.3 具体实现
6.3.1 实现流程
6.3.2 CPU逻辑
6.3.3 GPU渲染
6.4 效果收益、性能分析和结语
6.4.1 方案优劣势
6.4.2 方案补充
6.4.3 应用场景
参考文献
第7章 大规模3D模型数据的优化压缩与精细渐进加载
摘要
7.1 引言
7.2 顶点数据优化
7.2.1 顶点数据合并去重
7.2.2 索引数据合并
7.2.3 顶点数据排序
7.2.4 子网格的拆分与合并
7.2.5 顶点数据编码压缩
7.3 有利于渐进加载的数据组织方式
7.4 总结
参考文献
第四部分 人工智能及后台架构
第8章 游戏AI开发框架组件behaviac和元编程
摘要
8.1 behaviac的工作原理
8.1.1 类型信息
8.1.2 什么是行为树
8.1.3 例子1
8.1.4 执行说明
8.1.5 进阶
8.1.6 例子2
8.1.7 再进阶
8.1.8 总结
8.2 元编程在behaviac中的应用
8.2.1 模板特化
8.2.2 加载中的特例化
8.2.3 运行中的特例化
第9章 跳点搜索算法的效率、内存、路径优化方法
摘要
9.1 引言
9.2 JPS算法
9.2.1 算法介绍
9.2.2 A*算法流程
9.2.3 JPS算法流程
9.2.4 JPS算法的“两个定义、三个规则”
9.2.5 算法举例
9.3 JPS算法优化
9.3.1 JPS效率优化算法
9.3.2 JPS内存优化
9.3.3 路径优化
9.4 GPPC比赛解读
9.4.1 GPPC比赛与地图数据集
9.4.2 GPPC的评价体系
9.4.3 GPPC参赛算法及其比较
参考文献
第10章 优化MMORPG开发效率及性能的有限多线程模型
摘要
10.1 引言
10.1.1 多进程单线程模型
10.1.2 单进程多线程模型
10.1.3 单进程单线程模型
10.2 有限多线程模型
10.3 使用OpenMP框架快速实现有限多线程模型
10.4 控制多线程逻辑代码
10.5 异步化解决数据安全问题
10.6 对“不安全”访问的防范
10.7 拆解大锁
10.8 其他建议
参考文献
第五部分 游戏脚本系统
第11章 Lua翻译工具——C#转Lua
摘要
11.1 设计初衷
11.2 实现原理
11.2.1 参考对比行业内类似的解决方案
11.2.2 翻译原理
11.2.3 翻译流程
11.3 翻译示例
11.4 实现细节
11.4.1 连续赋值
11.4.2 switch
11.4.3 continue
11.4.4 不定参数
11.4.5 条件表达式
11.5 运行性能
11.6 TKLua翻译蓝图
11.6.1 类关系
11.6.2 类成员
11.6.3 方法体
11.7 发展方向
11.8 总结
参考文献
第12章 Unreal Engine 4集成Lua
摘要
12.1 引言
12.2 UE4 元信息
12.2.1 介绍
12.2.2 Lua通过元信息与UE4交互
12.2.3 读写成员变量
12.2.4 函数调用
12.2.5 C++调用Lua
12.2.6 小结
12.3 通过模板元编程生成“胶水”代码
12.3.1 接口设计
12.3.2 实现
12.3.3 读写成员变量
12.3.4 引用类型
12.3.5 导出函数
12.3.6 默认实参
12.3.7 默认生成的函数
12.3.8 C++调用Lua
12.3.9 小结
12.4 优化
12.4.1 UObject指针与Table
12.4.2 结构体
12.4.3 运行时热加载
第六部分 开发工具
第13章 使用FASTBuild助力Unreal Engine 4
摘要
13.1 引言
13.2 UE4分布式工具
13.2.1 Derived Data Cache(DDC)
13.2.2 Swarm
13.2.3 IncrediBuild
13.2.4 FASTBuild
13.3 在Windows系统下搭建FASTBuild工作环境
13.3.1 网络架构
13.3.2 搭建基本环境
13.3.3 可用性验证
13.4 使用FASTBuild分布式编译UE4代码和项目代码
13.4.1 准备工作
13.4.2 部署多机FASTBuild环境
13.4.3 编译UE4代码及对比测试
13.4.4 优化FASTBuild
13.4.5 再次测试分布式编译UE4代码
13.5 “秒”编UE4着色器
13.5.1 准备工作
13.5.2 大规模着色器编译测试
13.5.3 材质编辑器内着色器编译测试
13.6 总结
第14章 一种高效的帧同步全过程日志输出方案
摘要
14.1 引言
14.2 帧同步的基础理论
14.2.1 基本原理
14.2.2 系统抽象
14.3 本方案最终解决的问题
14.4 全日志的自动插入
14.4.1 在函数第一行代码之前自动插入日志代码
14.4.2 处理手动插入的日志代码
14.4.3 对每行日志代码进行唯一编码
14.4.4 构建版本
14.4.5 整体工具流程及代码清单
14.4.6 为什么不采用IL注入
14.5 运行时的日志收集
14.5.1 整体业务流程
14.5.2 高效的存储格式
14.5.3 高性能的日志输出
14.5.4 正确选择合适的校验算法
14.6 导出可读性日志信息
14.7 本方案思路的可移植性
14.8 总结
第15章 基于解析符号表,使用注入的方式进行Profiler采样的技术
摘要
15.1 进行测量之前的准备工作
15.1.1 注入的简单例子
15.1.2 注入额外的代码
15.1.3 注入的注意事项
15.2 性能的测量
15.2.1 时间的统计方法
15.2.2 针对函数的采样
15.2.3 测量实战
15.3 总结
第一部分 游戏数学

第1章 基于SDF的摇杆移动[1]
作者:郑贵荣、叶劲

摘要
当前主 MOBA 杆移动,为了有更 , 杆移动 决 到 碍 后
碍 问 , 此提 一种 于SDF 杆移动 决 案。
SDF(Signed Distance Field)即有号距离 , 示空 中 到形 最短距离,一 正
示形 , 负 示形 。
为SDF 成较为 , 此 计算 成。顶 MOBA 戏只 二 SDF计算,为
存 量,先栅格化 图, 过 到 边形( 碍 ) 距离离线计算栅格顶 有号距离,从
成SDF 。运 双线性过 样可以 得 图 意 有号距离 ,与 碰 半径比较判 是
否和 碍 发 碰 ,检 过程只 查 和进 乘法计算, 杂度为O(1)。
SDF 梯度 向代 最 变化 向, 此可以 梯度算 作为边 法线,当 与 碍 发 碰
后可沿着法线 直 向 ,同样可以根 梯度 向快速迭代来 MOBA 戏中击 后“卡” 碍
中 问 。对于瞬 位移(比 现)且不 穿越 碍 求,可以 投 ,以有号距离作为
迭代步 。对于AI寻路,SDF也可以 过 索 (判 有号距离与碰 半径 )来实现,且可
以 碰 半径 索贴近或远离 碍 路径, 破寻路对称性。
前 讲到 SDF是离线 成 , 么对于MOBA 戏中动态 碍 ,可以 程序式SDF和
CSG运算来实现。不过,SDF 提 同 也存 着存 空 、较 动态更新( 形发 变
化) 问 。

1.1 引言
当前 MOBA 中,移动 式 杆移动, 杆移动 先 决 问 是与 碍 碰
检 ,以及发 碰 后 走(碰 后直 止 常糟糕)。根 图 不同, 杆移动
碰 检 式有 种。
(1) 碰 式:直 (或 )与 边形进 碰 检 , 后 边形 边移动。
(2)NavMesh 式:同样 (或 )与 边形碰 检 , 后 边形 边移动。
(3)栅格 式:检 是否 栅格 ,或 与 栅格 距离,碰 后移动 向不 确 。
这里提 一种更为 且更为 决其 移动相关 求 案,即: 于SDF 杆移动。
1.2 有号距离场(SDF)
先简 一下有号距离 概念。有号距离 (Signed Distance Field,SDF) 示空 中
到形 (比 碍 ) 最短距离(纯量 ),一 距离 负 示形 , 正 示形
, 图1.1所示。

图1.1 SDF

公式 示, 先 义φ: 对于一个形 集S,有

检 某 x是否 形 ( 碍 )之 示为:φ(x)≤0, 果 先知 每个 有号距离


φ(x), 么碰 检 只 一次查 即可。

1.3 利用栅格数据预计算SDF
SDF记录 是 到 碍 距离,核心思想即空 ; 果动态计算 x 有号距离φ(x),
么 杂度跟 碰 检 案没 么区别。 此,我 计算得到 张 图 SDF , 为不可
存 图上所有 , 根 碍精度对 图进 栅格化,比 主 MOBA 戏 5v5 图可以
256 × 256 栅格。
先 绍一种 于栅格 SDF 计算 法。
根 景 碍 成 图1.2所示 栅格 图, 示 ,白 示可 走区 。 Meijster算
法[1]计算栅格中 意格 (x,y)到栅格 区中最近格 距离:
从 得到一张栅格 图 SDF , 图1.3所示。

图1.2 栅格 图

图1.3 SDF 越 ,φ 越

果 2字 示每个格 SDF,256 × 256 栅 格 图 存 为 256 × 256× 2


=128(KB)。 得到栅格 图 SDF 后, 检 (图1.3中 )与 碍 发 了碰 呢?
发 碰 后 又该 移动呢?

1.4 SDF的碰撞检测与碰撞响应
前 提到φ(x)≤0 示 x 碍 , 么碰 检 只 得到 φ , 后与碰 半径r比较
即可,φ(x)≤r 示 与 碍 发 了碰 。 于栅格 图 SDF 是离 存 , 移动是
连 ,不 一个栅格 意位 φ 等同于栅格顶 ,否则 栅格边 巨变。 此,
移动是连 情 下, 法直 查 取 所 位 φ , 图1.4所示 心位 , 根
周边栅格顶 φ 样 取。

图1.4 计算 心 φ

为距离本身是线性 ,可以 双线性过 (Bilinear Filtering) 样 位 φ ,根


所 栅格 个顶 线性 可得到 景 意 φ , 图1.5所示。
φ(x,y)=(1−x)(1−y)φ(0,0)+x(1−y)φ(1,0)+(1−x)yφ(0,1)+xyφ(1,1)

图1.5 双线性过 示意图


此 成SDF 碰 检 ,只 查 和乘法计算, 杂度为O(1)。以下为 得 景 意
SD 代码。

当前几乎所有 MOBA 杆移动过程中,碰到 碍 之后 是 着 碍 , 不是直


止, 为 止 实 很糟糕。 么SDF 发 碰 后 碍 问 呢?
图1.6所示,v 示 杆 向( 原 移动 向),当与 碍 发 碰 后 沿着v′ 向 ,
v′和v 关系是

上式中,n为碰 法线, 取呢?

图1.6

SDF 为纯量 ,纯量 中某一 上 梯度(Gradient) 向纯量 最快 向, 此可以利


SDF 梯度作为碰 法线:同 ,φ(x)几乎随 可导,可以 有 差分法(Finite Difference)
求出x 梯度:
从 得到碰 法线n,求出 向实现碰 后 碍 。以下为求梯度 向 代码。

至此,得到当 杆 向移动 实际移动 向代码。


1.5 避免往返
SDF 很 决 碍 问 , 实际 中 到凹形 碍 ,则 出现
碍 不 往返 情 。
图1.7所示,实线箭 示 杆 向, 线箭 示 到 碍 后 碍 向, 果
杆 向一直 持不变,则 A 向右下 ,到达B 后又 向右上 ,从 导致 凹 槽
A、B 不 往返走不出来。 么,当前后 向相差 于90度 止 动,重新拨动 杆 再次移
动。

图1.7 往返
1.6 利用多边形数据预计算SDF
实际 中存 另一个问 是, 碍 有明显 抖动感, 期望 果是平 。
到之前 SDF 成 程中,先离 栅格化 图, 后根 栅格 计算 成SDF , 图1.8所
示。

图1.8 抖动

( 形)半径r=0.5, 碍 轨迹呈 齿 , 为SDF 本身就呈明显 齿


。 期望 果 图1.9所示。

图1.9 平

此 优化SDF 计算。前 SDF计算是栅格顶 到 栅格 距离, 区 本身是 边形


构成 , 么实际 应该是 着 边形 边 直线移动 , 此可 到 边形 距离来计算栅格顶
φ , 边形 距离 为负, 为正,得到 图1.10所示 SDF 。

图1.10 根 到 边形 距离计算φ

过双线性过 法,可得 半径r=0.5 A、B、C三 φ 为0.5, 此 碍


可以 到沿边平 移动。

1.7 其他需求
1.7.1 如何将角色从障碍区域中移出
MOBA 戏中有 量 击 、击退等技 , 击中 “卡” 碍区 中,对于这种情
, 快速 移动到最近 合适位 。 为梯度 示最 变化 向,所以可以 梯度快速
查找合适位 :
x′= x+∇φ(x)(r avatar−φ(x))

图1.11所示,n为梯度 向,r=0.5为 半径,实线 位于 碍 , 心φ=−0.1; 线 位


于合适位 , 心φ=0.6,0.6= r −(−0.1)。
当 碍是凸区 一次迭代就 找到合适位 ,是 凸区 一次迭代可 法找到合适位 ,
MOBA 戏 图 是 凸区 , 此 次迭代,直到φ(x′)≥r 止。以下为迭代计算合适位
代码。
图1.11 查找合适位 示意图

1.7.2 角色不能越过障碍物的远距离移动
当 进 瞬 远距离移动,比 现, 又 求不 越过 碍 ( 越过 碍 情 则 上
法) ,就 进 连 碰 检 来 穿越 碍 情 。建设 投 (Disk Casting)
进 , 于SDF 投 优势 于迭代步进可以 当前位 φ , 图1.12所示,这
了迭代次 。
以下为 投 计算位 代码。
图1.12 白线为最 冲刺距离, 线为实际移动距离

1.8 动态障碍物
以上 绍 建立 成SDF 础上, MOBA 戏中 技 带有 果是 常常见 ,
运 成动态 碍 。前 提到 成 张 图SDF 计算量 , 此根 动态 碍 重新 成 张 图
SDF 不现实。 决动态 碍 情 ,我 先来看一下 SDF CSG运算 则。
● 集:φA∩B=max(φA,φB)
● 并集:φA∪B=min(φA,φB)

● 集:

么对于动态 碍 情 ,可以 成 态 图SDF和动态 碍SDF 叠加,即取二


集。同 动态 碍SDF可以直 程序 示[2],比 常 图1.13所示 SDF和图1.14所示 矩形
SDF。
SDF:
φ(x)=‖x −c‖ −r

图1.13 SDF
图1.14 矩形SDF

以下为计算 SDF 代码。


矩形SDF:

以下为计算矩形SDF 代码。

为 不可 走 动态 ,对于 可以 走 环形 则可 为 对 集, 图
1.15所示。

图1.15 环形SDF可 为 对 集

对于 分 技 等跨越 碍 功 , 过 集运算即可实现, 常 易。
至此, 过SDF 运算 则很 决了动态 碍 问 , 程序SDF 计算量 比 态SDF查
计算量 , 此不适合具有 量动态 碍 情 。 果 量动态 碍 分布稀 ,则可以 过空
分割管 动态 碍 ,从 程序SDF 计算次 。

1.9 AI寻路
MOBA 戏 兵、野怪、 AI 到寻路,SDF也 很 这个问 。寻路算法可
经典 AStar或 JPS, 过 索 ,以SDF 成可 走 即可。 判 索 位
是否可以 走,只 判 其是否 足φ(x)≥r就 。
对于可 走对象寻 路径之后 进过程中 到动态 碍 情 , 果 已寻路径中 杆移动
式从当前 向下一个 走,则 动 碍 , 须重新寻路。 到前 提到 凹形 碍
中走不出来(即前后位 变化) 情 ,再进 一次寻路即可。
于SDF AStar寻路,还 过 φ加 代价评 中,从 常 易 破对称性, 过 走对
象 半径r实现远离或 贴近 碍 。

1.10 动态地图
计算得到 SDF 图较 实现动态更新, 为重新计算SDF比较 。 么 实现动态
图 求呢?对于 殊 戏类 , 戏(Rouge-like)中 图,本身就是 匀 格所组成 ,
我 可以为其输 , 每一个 格 看作一个矩形,可以 上 中提到 矩形SDF公式来 示单个矩
形。
匀 格 图上,当 一帧 走 距离不 超过单个 格 ,可以 过检 每一帧与
所 格相 8个 格 碰 来实现 碍 功 。 图1.16所示,当 走之后位于 格
4 ,图中最右边 代 ,此 我 只 检 格6、0、5与 最近距离来进 碰

图1.16 匀 格 图上移动

个过程 代码 下:
SDF 是 过读取 格 图中 可 过标记来决 这个 格是否参与计算 , 此就可以实现动态
匀 格 图,可以 运 标记某个 格 过性。
可以 过取距离 梯度得到朝向向量,对于简单 几 图形,可以 过几 法求出,比 形:

对于矩形, 设 标系原 矩形中心,矩形 个象 是相互 ,则可 此问 退化为 一个


象 求 :
刚 法是不 图中 出现 图1.17所示 碍 卡住 情 。

图1.17 碍 卡住 情

决此问 ,只 进 次迭代求出最终 正 位 即可。 当 移动步幅较 , 现等,


进 连 碰 检 。与 于 计算 离 SDF 有所不同 是, 匀 格 SDF 是以 计
算 精度连 , 此计算 法与前 稍有不同。
景中 其 碍 , 较 汽车、其 等,则可 过矩形、 形 SDF 来 示,并
果与 格 图取出 SDF 集操作。

1.11 总结
于SDF 杆移动,利 空 ,以较 存 来O(1) 碰 检 ,且 利 梯
度 实现 从 碍 移动到可 走边 , 不越过 碍 瞬 远距离移动。 过SDF CSG运
算 则 很 动态 碍 问 , 对AI寻路 比较 易 到 破对称性,寻出远离或贴近 碍
路径。
当 ,SDF也有不足 ,即较 实现 图 动态变更, 量动态更新 程序SDF,从 导致
加了计算量。 对于天 匀 格 图 戏来说,也可以 实现运 格 可 过性,来
实现 图 动态变更。
参考文献
[1]Meijster,Arnold,Jos BTM Roerdink,and Wim H Hesselink.A General Algorithm for
Computing Distance Transforms in Linear Time.In Mathematical Morphology and Its
Applications to Image and Signal Processing.Springer.331–40,2002.
[2]Quilez,Inigo.Modeling with Distance
Functions,2008.http://iquilezles.org/www/articles/distfunctions/distfunctions.htm.
第2章 高性能的定点数实现方案
作者:周轩、 如冰

摘要
现代 戏程序 常 来 示实 , 各个软硬件平台并没有严格 标 IEEE
754,导致 运算 果 不同平台上 以 到严格一致,也就是 运算 果具有不确 性。
这对于 赖计算确 性 锁步同步(帧同步) 戏有严重影响, 为微 误差 累积,导致不同平台上
模 巨 分歧。
本章 绍 ,可以 证 不同平台上计算 果 一致性,从 决了这个问 。本章 绍了
示原 ,以及 则运算、开 、超越 等运算 实现。 运算 于 运算实现, 此
不同 软硬件平台上可以实现一致 果,适合对确 性有 求 技术 案, 确 性 库、
引擎,以及上层 戏逻辑。 持常 运算后, 起来就可以 一样简单、
。本章还对 运算 精度、性 和原 进 了比较。本章 绍 案最 本书主 叶劲
峰实现,已应 于War Song、《 战 潮》和《线条 作战》等 戏中。

2.1 引言
2.1.1 浮点数简介
,顾名思义, 位 是 动 ,跟随最 位变动, 此 绝对精度也是变动 。
CPU架构中, 运算 常 FPU(CPU 协 器)来 , 令集参 IEEE 754实现, 是实践中
不一 严格 这个标 。 : 同一个 CPU 里 FPU和SSE 令得到 果不一致, 此对确
性有 求 程序不 直 来计算。 运算是确 ,本章 运算 础上,实现了
一种简单且 运算 案。
2.1.2 32位浮点数(单精度)表示原理
戏开发中, 果 ,则 常 32位 单精度 。 2.1所示为32位
构。

2.1 32位 构

公式为
其中,S为符号位,S=0 为正 ,S=1 为负 。E为阶码, 示0~255 ,对应2 幂次
−127~128。M 示尾 , 示 为0~8 388 607,对应1.0~1.99999988。
这里, 精度是不 ,有 位 为7~8位。

2.2 基于整数的二进制表示的定点数原理
二进制 示中较低 n位 为 分, 么一个 二进制形式 示 其实就
是这个 以2n。设a为 ,f(a)为这个 对应 ,即 f(a)是一个 ,a是
所 示 , 么有

对应 可以实现为 类 成员变量。
2.2.1 32位定点数表示原理
下为32位 实现 法。

2.2所示为32位 构。

2.2 32位 构

原 示 式为32位 , 22.10 格式,即22位有符号 ,10位 。可 示精


度为

上 为
[−2 21,221−2 10]

实际 为
−2097152.0~2097151.990234375
2.2.2 64位定点数表示原理
上一 讲 于32位 案最 本书主 叶劲峰实现,之后 导下 笔 充实
现为 于64位 版本。 为64位 有着更 精度,所以可以实现更 杂 超越 运算。
下为64位 实现 法。

2.3所示为64位 构。

2.3 64位 构

原 示 式为64位 , 32.32 格式,即32位有符号 ,32位 。可 示精


度为

上 为
[−2 31,231−2 32]

实际 为
− 2147483648.0~2147483647.9999999997671693563461
其中,32位 选择了10位 ,64位 选择了32位 。也可以根 实际情 , 所 精
度来确 位 。

2.3 定点数的四则运算
两个 a,b 则运算, 对应 分别是 f(a),f(b),暂不 出等问 。

这意味着两个 和/差,就是这两个 对应 和/差 示 。


其中,f(a)f(b)是两个 对应 乘积,2-n (2 -nf(a)f(b))是这个乘积再 以2n得
到 所 示 。

其中, 是两个 对应 商, 是这个商再乘以2n得到 所 示

。 乘以、 以2n 运算可以 过移位操作来实现。


2.3.1 加法与减法
加 先对齐阶码(相当对齐 ), 后再相加 。 是对齐 ,
我 之前讲 原 ,可以直 对应 加 法。
2.3.2 乘法
乘法是 过 分相乘、阶码相加实现 。
对于32位 , 前 所讲 原 可以实现为

这里 右移44.20(64)→32.10(32) 存 息丢 和 出 可 。 意这一

对于64位 ,直 两个 64位 相乘, 一个128位 示
果。
对于GCC,可以 _int128_t 128位类 , 直 128位乘法。

对于VC, 前还没有128位 类 ,可以 intrinsic function _mull128来计算,并 两


个std::int64_t来存 果。

第三个参 中 64位变量 ,即可以得出计算 果 64位。


可以 义简单 128位 来存 64位 相乘 果。 :
2.3.3 除法
前 所讲 原 , 法运算 乘以232,为 出 引 128位 运算。对于GCC可以直
128位 法:

对于VC就不 了, intrinsic function中没有128位 法。 是可以 汇 代码来实现。


C++中 明:

这里 a_low 给 存器rcx, a_high 给rdx, b 给r8, ret 给r9。


汇 代码实现:

意 是, 出来 商不 超过64位,否则CPU 报出异常。
审稿阶段,笔 发现最新版本 Visual Studio 2019已经提 了128位 法 intrinsic
function _div128(),与汇 版本 功 一致。

2.4 定点数开方与超越函数实现方法
计算机 实现不 过有 次 则运算计算 , 项式/有 式 合法、迭代法和查
法。 为 法性 消 较 ,所以这里我 主 项式 合。当 ,也 讨其 法 优
劣。
2.4.1 多项式拟合
区 [a,b]上可以 n次 项式P n(x)近 求 我 求 f(x)。 项式P n(x)和
求 f(x) 误差绝对 也是一个 |Pn(x)−f(x)|。 项式P n(x)有 种选择,
勒展开式中截取前n+1项:

就是一个 。 勒展开式 误差是

可以看出,x 果离a很远 话,误差也 很 。


本书 主 叶劲峰提出了下 这种 法,误差绝对 区 [a,b]上有最 :

对于给 n,我 标就是求出Pn 系 , 得误差绝对 最 最 :

这里 软件Mathematica求 合 项式 系 。 Mathematica中 次输 下命令:

其中,{x,{1,2},4,0} 示 区 [1,2]上逼近, 分 4次 项式、分母0次 项式 有 式进 逼


近。得到 果为

意思是 区 [1,2]上,
ex ≈1.17974+0.380661x+1.32348x 2−0.350357x 3+0.184801x 4
乘以232再取 就是为了 系 示为 形式。具 其 法请参 软件 帮助 档。
再次强调 是, MiniMaxApproximation求 出 项式 只是 区 上 近我 求
。所以, 果 求 这个区 以 , 么就 利 性质转 成这个区 。
2.4.2 正弦/余弦函数
我 可以利 正弦/ 弦 周期性、对称性等, 一 度x转 成 区 度进 计
算:

我 可以让k1对8取 :
k=k1m od 8
么 度 终边就 [kπ/4,(k+1)π/4] 。 之,我 平 分成八 , 确认
度 终边 哪一 。 下来 思路就是求 终边和最近 标轴所 直线 正弦 和 弦 ,再
转 为我 求 度 正弦 和 弦 。 意:这里强调 是和 标轴所 直线 , 和 标
轴 , 为和 标轴 一 是和 标轴正向所成 度, 和 标轴所 直线 是
于或等于直 个 度。这里记 终边和最近 标轴所 直线 为θ。 果k是偶 , 么
θ=x 1; 果k是 , 么θ=π/4−x 1 。我 利 合 项式求出θ/2 正弦 和 弦 ,再利 二
公式得到sinθ,cosθ:

后根 终边所 位 ,我 就可以得到所 求 三 和sinθ,cosθ之 关系。比


k1=5, 终边 [5π/4,3π/2] , 么就有
cosx = −sinθ
sinx =−cosθ
图2.1所示是 与 译器 带 正弦 相对误差(这里x 常 ,所以图中 了
对 刻度)。
图2.1 与 译器 带 正弦 相对误差

2.4.3 指数函数
常迅速,幸 我 可以 输 x 拆分成 分和 分:

其中,e [x]是e 次 ,很 易求 。 {x}属于[0,1]区 , 这个区 可以


项式 合。
图2.2所示是 与 译器 带 相对误差, 这种 法求出Exp 误差对比(这
里x 比较 ,图中 了 匀刻度)。
图2.2 与 译器 带 相对误差

2.4.4 对数函数
为这里 是 于 二进制 示 ,所以求 以2为底 对 比较 ,一 对 也
可以 底公式进 转 :

x可以约化为


log2x =n+log2y
为这里 是 于 二进制实现 ,所以知 最 位 1 位 就可以求出n。 下 分
可以 合 项式求 。
2.4.5 开方运算
3D运算中,有 量 求向量 度 运算,其中 到开 。开 有很 法, 下所示。
(1) 顿迭代法:后 一项可以 过前 一项简单 运算得出。对于 运算来说,这里有
法,不是 想 计算 式。
(2) 开 运算:已知 y,我 求 x 得x2=y。根 之前对 运算和
对应 运算 关系,有:
(f(x))2=2n f(y)
其中,f(x),f(y)分别是 x,y 对应 ,我 可以先 2nf(y)转 为 开根
号再取 , 么 f(x)只 取 附近一 。我 选取平 后和2nf(y)最 近

一项, 分情 下比较 两项即可( 上说只 是这两项之一, 开

根号运算也不是 精确 )。
(3) 开 法:可以根 之前论述 和 对应 之 关系,对其对应
开 。 开 可以参 Jack W.Crenshaw 论 Integer Square Roots [1] , 为一个2 幂
次 项式, 类 于 法 段,过程类 于 算开根号(日常 算开根号 法是 于 十进
制 示 ), 杂度直 和 位 相关。
(4)本章提倡 MinMax 项式 合法。
2.4.6 开方求倒数
3D运算中,有 量 向量单位化 运算,其中 到开 求 。以向量V(x,y,z)为 :

其归一化(单位化) 法是V 乘以向量x,y,z分量平 和 开 。引擎 常提 一个


InvSqrt()。还记得卡 实现 版本吗?先求一个近 , 后 过 顿迭代法再计算一次,这样得
到一个相对精确 。这里我 先 4次 项式模 找到一个较精确 (最 误差为0.001%), 图
2.3所示。
图2.3 InvSqrt: 项式 合

后 过 顿迭代法再计算一次(相对误差 到0.00001%以 ), 图2.4所示。


图2.4 InvSqrt: 项式 合+ 顿迭代一次

2.4.7 为什么不用查表法
意 是, 查 法 , 中 是 ,最 其 代码中,或 过 加载, 不
过初 运算再转 得来。只有 没有较 项式 合 法 选择 查 法。查
法 优 是实现简单, 有两个:
(1) 常 赖 示,一旦对 示格式 出微 调 , 中所有 更新。
(2) 进 集 运算 ,查 法对 速 存利 ;对于其 常 情 ,则 易 到 存
命中 败 情 。

2.5 定点数的误差对比与性能测试
2.5.1 超越函数及开方的误差测试
以double(双精度) 运算 作为 试 ,对比float(单精度)和fixedpoint(32.32)
计算误差,其中 试 对比 有标 库 std::sqrt、std::pow、std::log、std::exp、std::sin
和std::asin,两 误差列于 2.4中。

2.4 超越 及开 误差比较

fixedpoint(32.32) 示 实 ,对比 double(双精度) 标 库 实现,平 误


差 于万分之一。
2.5.2 性能测试
我 两个平台上 试 (单精度)与 (32.32) 性 ,每个 试量度运算1 000 000
次 总 。 试 硬件 2.5所示。性 果列于 2.6中。

2.5 试 硬件

2.6 性 果(运算 计,单位为ms)


其中, 法两 差距 ,其 同一个量级 , 分情 下, add、sin,
运算还快一 。

2.6 总结
本章从最 础 计算机 示和运算开 , 绍了一种 于 运算 案,并和单精度
计算性 和精度进 了比较。 过 原 64位 运算,最 度 利 了CPU 运算
力, 此比较 。其性 跟硬件实现 (单精度) 近( 本 1 以 )。本章 绍
案 常适合于 确 性(决 性)、绝对精度不变 应 合。

参考文献
[1]Crenshaw,Jack W..Integer Square
Roots,1998.https://www.embedded.com/electronics-blogs/programmer-s-
toolbox/4219659/Integer-Square-Roots.

[1]本章相关 已 请技术专利。
第二部分 游戏物理

第3章 一种高效的弧长参数化路径系统
作者:

摘要
2015年 某跑 类 戏中想 实现一 创意 法, 一个路径系 , 移动 路径引导,并
且 路径上有简单 运动(走、跑、跳和碰 反 )。当 了一系列Unity与路径相关 件,
不 足 求。 别是弧 参 化 性,即令曲线 参 t 与曲线 度L为线性关系,从
参 t 线性变化映 到 度 线性变化上,实现曲线上 匀线速度运动,这一 是实现路径上 运
动 础[1]。个别 件实现了类 功 , 是其原 为暴力 线性 ,根 设 精度 曲线拆成
线段组成 查找 , 序列化和 存 量 ,对 存和包量并不友 。
该路径系 主 程 下:
(1) 最 二乘法, 项式 合路径曲线 度 反 ,利 合 反 实现弧 参
化,这样只 存 项式系 ,运 对 项式求 即可, 须 存一个巨 查找 和进
查 操作, 可以直 求 。
(2)根 实际 求,没有 常见 三次 项式曲线, 是构造了一条二次 项式样条曲线,
是简化各种曲线 求 计算,同 持C1连 。
(3) 弧 参 化 础上, 普 运动计算映 到曲线上,以曲线 局 切空 标架为 实
现了曲线上 简单 运动, 曲线上也有可 运动 现。

3.1 引言
2015年参与 某跑 类 戏中,策划 不 足于普 、平直 跑动 景,想 试一 有趣
、弯曲 跑动 景, 轨、过山车跑 等。对于当 跑 类 戏 ,跑动实际是沿着一条
一 直线路径移动 ,跳跃等 直 向 移动也是以这个路径为 , 水平 向左右移动可以 看
作是路径 切 , 么 想到 曲线路径代替直线路径来引导 跑动,并 简单 运动转
到曲线路径上。
合 戏 求,对这样一个曲线路径系 有 下 求:
(1)路径布 简单,最直观 就是布 路 。
(2) 具有局 性, 一个路 只 影响上、下 。
(3)曲线至 具有C1连 性, 足 本 光 求。
(4)两个路 曲线可以是异 曲线,等同于可以 控制 路 曲线 向。
(5)与曲线相关 计算 尽量简单,尽量 进 迭代计算。
求(1)意味着曲线 过每一个控制 ,所以 了 Bezier 曲线。 求(2)则 了Natural
曲线这类 影响 局 曲线,剩下可选 曲线有Catmull-Rom[2]。 求(5)中,希望曲线 计算
尽量简单, 别是 度 计算和与平 求 计算,这两类计算和各种逻辑操作相关, 曲线形式简单也
让一 迭代 计算量变 。所以最终并没有 三次曲线, 是选择 两个控制 之 成二次样条,
两条二次曲线 程确 ,并不 工 辑。另 , 路径 辑 过程中,模 Catmull-Rom
引 了Cardinal类曲线 构造 式以 操作。

3.2 端点间二次样条的构建
两个路 之 成曲线,并且 求两个路 可以 控制位 和朝向(切线 向) , 单一
一段二次曲线 到 度不 问 。这里构造了 下两条 二次曲线来 决这个问 (见图
3.1)。
给 起 P0、起 切线T0、终 P1和终 切线T1,有二次曲线 f1(t)和f2(t),令其 足 下条
件:
f1(0)=P0
f1′(0)=T0
f2(1)P1
f2′(1)=T1

图3.1 二次样条曲线示意图

对于 f1(1)和f2(0), 设有一动 Pm, 该 曲线 足:


f1(1)=f2(0)
f1′(1)=f2′(0)
从 可以得到两条曲线 系 程组:
该 程组 为

则有

可以看到,最终动 Pm不 出现 程中, 为隐含 ,对 是透明 。为了 分段曲线当作一


段曲线 ,还 两段 曲线 参 t归一化到 一 [0,1] 。令fs(t)为参 t归一化后
分段二次曲线,有 f(0) s=P0,f(1) s=P1 。这里 每段 曲线占 曲线 比 来归一化曲线参
。设L1、L2分别为曲线 和 度,则有

类 ,也可以得到曲线 度 程L s(t), 曲线 度 程l1(t)和l2(t) 示 归一化


程:
为此 计算曲线 f1(t)和f2(t) 曲线段 和 度。对于二次曲线 ,曲线 线积分
有 析 (分 积分):

其中:

这 系 可以离线 计算 ( 态路径),或 运 初 化曲线 计算(动态构建路径)。


该公式较为 杂, 是 于曲线归一化 过程中。 果进一步 成了曲线 弧 参 化,
更为简单 线性 度计算。
3.3 路径的构建
路径为路 曲线 , 路 两段曲线 可以 相同 切线以 证C1连 ,也可以设
成 切线, 来 切 了 向 直线路径等。路径系 只负责 和提 路 息,并没有
制曲线 类 ,所以路径系 本身是 持 种类 曲线 。
路径上 每个路 可以设 己 向(切线), , 每一个 工 ,也 比较
琐,所以 切线 设 上模 了 Catmull-Rom 这类 Cardinal 曲线 法,即路 i 切线 路
i−1和路 i+1 位 Pi 1和P i+1决 :

τ为切线 (张弛 ), 图3.2所示,这样 分情 下,只 路径两个端


路 切线,中 路 只 调 端 位 来影响路 切线 向,调 来影响曲线 弯曲程
度。

图3.2 Cardinal曲线切线 设

实现上,路径上 路 路 息构建曲线 ,总是 路 转 到 己 局 标


系下,即 路 Pi Pi+1构建 曲线中,Pi 于 标 (0,0,0)且 转为(0,0,0)。所以, 路
径 ,每一段曲线 计算 果还 一次局 标系到世 标系 转 。这样 是路径作为一
个 不受刚 变 影响, 别适合 戏中 景动态 求。

3.4 曲线的弧长参数化[3]
有了路径,还 路径上匀速运动,其 运动可以 匀速运动变化和 合得到。从朴素
求 度出发,我 希望曲线 参 t与曲线 度L为线性关系:
L(t)=Lt,t∈[0,1]
此 ,我 可以 过参 t 匀速变化得到 曲线上 匀速移动。显 易见,这个关系只有直线
成立。
对于 于一次 项式曲线,这里 下 式。
设 f(t)为连 曲线,其 度为L;另设参 u,建立和曲线 度 线性关
系:

即 参 u 达 曲线 度计算 ;设一个参 u到参 t 变 t(u),且 足t(0)=0,t(1)


=1。 变 带 曲线参 程中,可以得到一个新 曲线参 程:
g(u)= f(t(u))
于g(u) 度 为 , 此变 下对应 度 应有

从 变 t(u)为

为得到变 t(u), 求出L(t) 反 t=L-1(l)。


于L-1(l)没有 析 ,这里 最 二乘法进 合求近 。

(1) [0,1]区 进 等曲线 度 划分,得到n条等 曲线段 ,对应 参

区 为 。

(2)对每一条曲线段 , 样m个L(t) 该区 ,得到 集

,其中 , 。

(3)以 度为 变量, 最 二乘法 合 足 样 项

式 程,得到L-1(l) 区 近 i i+1。

(4) 剩 区 重 步 2和步 3,求出L-1(l) 所有区 近 ,则 为 组

成 分段 。
步 1划分等曲线 度区 是消 查找分段 历操作。当 三次 项
式作为 合 ,其系 可以 Vector4 存,则 分段 可以 存为Vector4 组
Vector4[n] 。 对 于 给 度l, 等曲线 度 划分下,其对应 分段 系 索引为
Clamp((int)((l/L)*n),0,n−1)。 果是其 形式 不 足等曲线 度 划分,则
记录每个划分 度, 历Vector4[n]找到l对应 分段 系 。
步 1 等曲线 度区 划分 , 于此 未求出,只 迭代法寻找等 划分 。这
里 Newton-Raphson法[4]来求 程:
其迭代形式为

其中,A、B、C 参见“端 二次样条 构建”。L(t)为严格单调 ,适合 Newton-


Raphson法。可以 同样划分 量下同序号 等参 作为迭代 初 t i(0)以加速 。
构造 计算量分析 下:
(1)寻找等曲线 度 划分 ,默认最 迭代次 为10, 实际计算过程中到不了这个次 就
。n个划分 求 n−1个划分 ,总迭代次 ≤10(n−1)。
(2)对每条曲线段进 m次L(t) 样, 两个已经计算 端 ,总 L(t)调 次 为
n(m−2)。
(3) 最 二乘法 合 分,设 Kp 为 合 项式 阶 ,则构造 程系 矩阵 杂度为
, 斯消 法求 分 杂度为 。
(4) 计算不受刚 变 影响,不 对刚 变 重新计算。
项 中,n=20,m=11,Kp =3,连 两个路 曲线 80个 示。 果 运 动态构
建曲线,为 计算量过 造成卡顿,可以 分帧计算 法 计算量分摊到 帧。 意到每段路径之
没有相互 赖,只 路 息,也可以 线程并 计算。另 ,根 项 情 也可以适当 低分
段 和 样 。
示 t(u)带 原曲线 程中,得到新 归一化曲线 程:

其对应 度计算为

对于 弧 参 化后 曲线 程gs(u),其求 计算比原 曲线 程 fs(t) 一次 项式


求 计算(具 , 三次 项式 合, 了3次加法、5次乘法、1次 法), 是对应 度计
算 幅简化,只 1次乘法。
弧 参 化后 曲线 程gs(u)与原 曲线 程fs(t) 等参取 对比 图3.3所示。
图3.3 弧 参 化后 曲线与原 曲线等参 对比

图中上 为原 曲线,下 为弧 参 化后 曲线,重参 化后,等参对应曲线上 等距。

3.5 曲线上的简单运动
对于曲线上 意 P, 该 法平 上 一个向量作为法向量, 后 合该 切向量 过
向量 积得到副法向量, 此可以建立一个局 标系。这里 法向量 N 为 up向量,代 直 轴
向;副法向量B为right向量,代 左右轴向;切向量T为forward向量,代 前后轴向。
曲线引导 运动,这里 义为不“ 离”曲线 运动,即 运动必须 有对应 曲线上 线速
度vs。 先 该线速度计算出 曲线上向前(或向后, 速度 符号 )经过 t后移动到达 新 P及
其局 标系(T,B,N), 后以此作为 运动位 新 ,并且 证 运动后 位 位于 P
法平 上,所以 意 刻 运动位 总是 曲线上某 法平 上,即曲线 “ ”。我 意到,
这种运动计算 式只关心当前曲线上 及其局 标系,以及经过 t 后 曲线上 及其局
标系, 和局 标系是否 曲线得来并不重 ,所以 实现 模 可以和具 曲线
耦, 以 及其局 标系 作为 模 输 。
具 实 上,这里只讨论 实际 戏中实现 跑动和跳跃( )两种运动。 这两种运动中,曲
线还有 作 , 止 不 下 ; 局 标系 up向量 负 向为重力 向。
3.5.1 跑动
曲线上 跑动 悬空( )问 ,以及碰到 碍 问 。
悬空主 有两种情 ,一种是 到较 向下 ,一种是走出 撑 沿,二 可以 一
式 。
设当前 和局 标系为( ),经过 t 后新 和局 标系为(
),当前对象位 为Loci,相对于新 差为
当 到较 向下 或 出曲线 撑 上移动 ,有H >0,此 先 试 对象移动到
PH:

后从该位 沿着−Ni+1 向 FH 试是否存 碰 可以作为 撑 : 果存


撑 , ,则沿着−Ni+1 向移动到和 撑 位 , ,则直 移动到曲线上
Pi+1; 果不存 撑 , ,则从跑动 态转为 态, 束本帧 , ,则
移动到曲线上 P i+1。对于H≤0 情 ,曲线为平直 或上 ,可以直 对象移动到 Pi+1。
移动对象 过程中,可 碰上带有碰 碍 。根 位移向量和碰 法线 关系以及
位 可以分为两类:一类是 法沿着 移动或 过 碍 ,导致当前 运动发 截 ,
位 上, 止对象 位移;另一类是可以沿着 动或 过 碍 ,这种情 法
对象移动到 期 位 , 是可以持 移动到 法平 上。一 商业引擎对这样 情 有
, Unity3D CharacterController。
3.5.2 跳跃
跳跃 态 发来 跑动 态 转 ( )和 (主动跳跃)。
一 合下 跳跃 分成水平和 直两个 向分别 ,曲线上 跳跃也 相同 ,其中水平速
度分量 终为曲线上 线速度,是一种曲线上投影速度恒 法,与对象实际经过 位移过程 关。所
以,同 运动且具有相同水平速度 两个对象, 论实际运动路径 ,总是 相同 法平
上。
同跑动,设当前 和局 标系为( ),经过 t 后新 和局 标系为(
),当前对象位 为Loci。 Loci和Ni可以构建一个平 :

该平 和直线
N(t)=Ni+1t+Pi+1
记为PNN ,显 PNN Pi+1 法平 上。这里 对象移动到PNN,位移向量

和Pi 切平 平 ,可以看作是原水平运动 向 Ti 引导曲线 变了 向, Ni发 了 转,


其 向新 Pi+1 N轴。
对于 直 向,根 重力加速度更新速度 标量 :
新 Pi+1 −Ni+1向量作为重力 向计算 直 向 位移向量:

最终 位 为

下 过程中 果 到可以作为 撑 碰 ,则转为跑动 态。其 碰 式类 于跑


动。
3.5.3 相邻路径的切换
跑 类 戏有切 跑 操作。得 于 计算与具 曲线 耦,路径 切 也可以对 计算透
明。
设当前路径曲线为g1(u),切 标路径曲线为g2(u), 发切 路径 ,设当前 g1(u)上
为( ),其法平 为

计算该法平 与 曲 线 g2 ( u ) Pg2 , 该 对应 曲线参 为 ,局 标系为(


)。该 为g1(u) Pg1 g2(u)上 等位 , 此 路径切 到g2(u),且从等位
ug2开 替 g1(u)上 移动计算。路径 切 不 瞬 成, 一个 对象从路径g1(u)移动
到路径g2(u) 过程。为此, ( )变 到g2(u)上 局 标系下,这里设其
变 后 为( ), 对象 移动到路径 g2 (u)之前, ( )和
( O,X,Y,Z ) ( O 为 局 标系 原 ( 0,0,0 ) ,X,Y,Z 为 局 标系 三个 标 轴 ( 1,0,0 ) ,
(0,1,0),(0,0,1)) 果转 到世 标系,作为当前 及关 局 标系输 给
模 。 ( )变 到g2(u)上 局 标系下, 是路径切 一旦开 ,就只
与 标路径相关, 路径 变化不 影响切 过程, , 路径 切 过程中两条路径走向了不同
向。图3.4展示了路径切 过程,其中 线为实际 移动路径, ( )和
(O,X,Y,Z) 来;橙 线 示转 到g2(u)局 标系后发 路径切 g1(u) (
)。
图3.4 路径切 轨迹

3.5.4 曲线上的旋转插值
每一个路 有位 和 转属性。 转可以分 出三个 标轴,所以 计算所 赖 局
标系(T,B,N)可以 过分 转 得, 转 过对两个路 转 得。
根 不同 求,还可以选择 得到 转是否对齐到曲线 切线。 转 对比 图3.5所示。

图3.5 转 对比

对于二次曲线构成 路径 , 意相机 转最 不 和曲线 切线相关 ( 对齐到


曲线上), 是 立计算相机 运动。 于二次曲线只有C1连 ,相机 朝向和曲线 切线 向关 令
朝向 变化 眼可 觉 硬感。

3.6 总结
本章实现了一种简单、 路径系 ,其优 下:
● 计算简单,运 性 ,弧 参 化后也只 加了 量计算。
● 持 路径上进 线性 位,相关 计算 量 ,并且适当 造也可以 合 性 运
构建。
● 曲线参 路 局 标系构建, 持动态 。
● 实现了路径引导 简单 运动,且计算过程与具 曲线类 耦。
该路径系 并不追求强一致性和连 性,适当 弃一 连 性条件可以实现不同类 曲线 。我
项 中只 了二次曲线, 是其框架也同样适 于三次曲线。

参考文献
[1]Cook,John D.Unit Speed Curve
Parameterization,2018.https://www.johndcook.com/blog/2018/07/15/unit-speed-curve/.
[2]Parametric Curves and Splines; Cubic Splines:Hermite,Cardinal,Catmull-
Rom.http://web.eecs.umich.edu/ ~ sugih/courses/eecs487/syllabus.html; University of
Michigan.
[3]Length and Natural
Parametrization.https://en.wikipedia.org/wiki/Differential_geometry_of_curves#Length_
[4]Newton's Method.https://en.wikipedia.org/wiki/Newton%27s_method; wikipedia.
第4章 船的物理模拟及同步设计[1]
作者:周茗

摘要
只模 戏中比较常见, 论是帆 、汽 还是 , 模 上 可以简化成动力、 力和水
力 叠加。 果是匀速 平 上 ,则可以进一步忽 水 力。 近 计算这
力,从 引擎 模 下有逼真 现,是一个 。
最常见 法是 底离 化成 干 样 ,分别 样水 度, 后积分 水 度,得到 力。这
种 法忽 了水中其 力 存 ,得到 果不 真实。另一种 法是利 力 公式,计算 水中
受 力。这种 法则过于 杂,不适合应 于对实 性 求 ,比 戏。本章从一个 新
, 水中受到 力分 成与 水 积相关 力、与速度相关 升力、与 水 积变化相关
力, 顾了 与真实度, 常适合 戏中 。
本章 算法 《 法则》中 , 适应各种天气 水 ,也 充分发 各种类 只 。
本章 第一 绍了 力系 ,包 水后受到 力、移动中 升力以及水 两种 力:
力和 击力;第二 绍了引擎系 ,包 模 只 动力和向心力;第三 绍了 前两
应 到实际工程中,包 类 设计思路以及为 么 这样设计;第 绍了怎样更新 力, 得
戏中第一 (本 )和第三 ( 本 ) 有 、合 现;最后一 则对本章
进 了总 。

4.1 浮力系统
本章所讲 力是 过经典 米德原 计算得到 ,计算 身 力 到 水 积, 此计
算 杂度是正比于 身几 顶 个 。 实际应 中,推 顶 个 于25个,这样 可以 不
同几 征,又 计算上尽可 。另 , 于 身是动态刚 (Dynamic Rigidbody),
分 引擎 求是凸 边形(Convex) 。这两个前提 得 于 模 身刚 形 不 精
确,我 法是另 一个动力 动态刚 (Kinematic Rigidbody) 击碰 ,专 于 精确
击检 ,并且每帧根 身刚 位 动态更新。
图4.1分别展示了两种 刚 。图4.1(a)和图4.1(b)是 于计算 力 动态刚 ,一 称
为“移动碰 ”,顾名思义,是 来参与 引擎动态模 刚 , 单个凸 组成。可以看
出,不同 有着不同形 动态刚 。 是动态刚 重心,重心 常并不是几 中心,这符合真实
世 设 。
图4.1(c)和图4.1(d)是 来 击检 动力 动态刚 ,一 称为“ 击碰 ”, 个
形 组成。这 形 可以是 (凸或 凸), 图中 身 分; 础 碰 形 ( 、
、 囊 ), 图中 栏杆 分。 于不参与 引擎 模 , 跟随“移动碰 ”移动, 此对
性 影响很低。
图4.1 刚 示

4.1.1 浮力
根 米德原 , 力(Buoyancy) 可以 水 积计算得到:

其中,ρ代 水 度,g 是重力加速度,V 代 水 积。 力 向是竖直 ,作 位 水


积 中心。当 水 上 止 ,其受到 重力和 力 相同, 向相反。 于 水 积 中心并不
总是和 质心位 相同,所以 力力矩。 力力矩计算 下:

其中,Fb代 力,来 上一个公式,r 是质心到 水 积 中心 向量。 戏 实际计算中,我


发现 力计算力矩 很不稳 , 别是 有 情 下。 此, 实际计算 ,我 展为下
公式:

同 ,为了更 调试 力 感,我 提 了 力力矩参 ParamTb,最终 力计算公式为


这里 R是 转矩阵, 转矩阵是正 , 计算中直 其转 矩阵代替求逆,以计算
模 空 力力矩。
有了 力计算 础公式,下一步 决 问 就是 计算 水 积V以及 积 中心。Randy
Gaul 博客[1]中提出了一种计算 水 积 法,本章 其 础上稍加 进。
先 绍 是求 积 公式。取一个参 P, 得 每一个三 和 P连成一
个 。 图4.2所示,每个 积 可以 下 公式算出来:

当u,v,w三个向量逆 列 , P 三 ABC 正 ,V是 于0 ;反之, P 三 ABC


反 ,V是 于0 。 此,计算所有 和 P形成 积之和,就 得到 积了。
下 绍 是求 几 中心 公式。

图4.2 力计算 示

以 积作为权重,可以得到 个 几 中心。 个 积和几 中心 计算公式 下:


有了 积和几 中心 求 公式,下一个 决 问 是 水 积。还是先从三 出发,一
个三 只可 于三种 态中 一种: 水、 出水、 分 水。
下来分别分析三种情 。
情 一,三 水: 是A,B,C三个 低于水 度,三 可以和 P形成 为
水 积。 是 意, P必须 水 上。
情 二,三 出水: 是A,B,C三个 于水 度,丢弃该三 。
情 三,三 分 水,这又分成两种可 :两个 水或 一个 水。
● 两个 水, 图4.3所示。根 和水 ,ABC 分成了3个三 形:X符合情 二,丢弃;
Y、Z符合情 一,计算 水 积。
● 一个 水, 图4.4所示。根 和水 ,ABC 分成了3个三 形:X符合情 一,计算
水 积;Y、Z符合情 二,丢弃。

图4.3 三 两个 水 情

图4.4 三 一个 水 情

此, 水 积求 为两个问 。
● 求一个 积,上 已经 了 绍。
● 已知三 形 顶 A,B,C,求与水 问 。 法简单, 此不 述了。
对于 意一个凸 ,我 可以根 三 和水 度 关系得到所有 水三 列 , 下来是
合 设 参 P,以计算 水 积。
我 中 水 上 投影 作为P ,这种 法 意以下问 。
● 简化了水 模 : 设从 边 到 中心 水 连线是一条直线。 果 法 受这种精
度 丢 ,则可以 从三 向水 投影出一个上下 不平 棱柱,计算 积。 图4.5所示,DFE
平 是水平 ,棱柱 上下平 并不平 。

图4.5 杂 水 下 水 积 计算

● 计算出 V可以 于 积或 为负 。
4.1.2 升力
设计 底 , 让水 其周 形成环 ,这个 最终 形成升力(Lift)。当 移动
, 身分成两段:一段贴近 身 动,称为上 (Upstream);一段 上 层,远离
身 动,称为下 (Downstream)。 于水 是有 度 ,下 更底层 水 ,移动更慢。这
样就 尾形成一个 ,上 则 充这个 ,从 形成环 。这个环 底板上形成升力。
我 升力简化为两 分:
第一 分是Wilson [2]提出 ,升力 公式为

其中,ρ是 度,C 是升力系 ,和 形 以及朝向有关,S 是浸没 水中 积,


v是离 很远 水 相对速度。 实际运 中,我 水是没有 速 ,v是 速 。S正比于
水 积,我 水 度乘以一个 来 示。
第二 分和 水 度 平 成正比,这么 让 水中开起来以后迅速 得足 升力。
合第一 分和第二 分,升力 最终公式为
公式中 ParamL1和ParamL2分别对应了第一项和第二项中 常量项,可以 。l是 水 和
尾 水 之 距离,可以 计算 水 积 , 所有 水三 标转 到 模 空 ,计
算得出。公式 义了升力 ,升力 朝向是竖直向上 。
升力同样有力矩。我 计算力矩 , 力 义为 模 空 沿着负 舷 向 单位向量。
戏中,+z是模 前 , 么可以得到:

公式中 ParamT1 是升力力矩系 ,R是 转矩阵,massCenter是 质心。

4.1.3 拉力
水中运动 受到水 力(Drag),根 力 中 经典 力 程:

中受到 力为 F,ρ是 度,C 是 力系 ,S 是参 积,一 义为运动 向


上 正 投影 积,v是速度。和计算升力 一样,我 水 速为0,v等于 速 。S 正比
于 。
水 力还和 水 积 变化有关,当 水 积加 , 出更 水;当 水 积 ,
有水 。上 公式是根 每帧 水情 算出 力,下 公式则是根 两帧 变化 算
出 力, 之 是相互 充 。

公式中Vtotal是 积,m是 重量,v是速度,Vinc和Vdec分别代 加 积和


积,Param inc和Param dec则分别是两个开 系 。 计算 加和 积 ,不是 总 水
积计算 , 是 每个三 形成 分别计算 。
本 绍 两个公式 合起来,就得到最终 力公式, 力 向和速度 向相反:

其中,Paramd是本 第一个公式中 常量项,即 ;Vimmerse是 水 积。


力同样有力矩, 力 力矩是相对于 速度 , 义为

这里 ParamT d是一个与 有关 常量。

4.1.4 拍击力
有了以上几种力, 水中 运动已经 常 近真实世 了。 是 从空中 水中 没有明显
速, 此我 加了 击力(Slam)。与 加/ 积类 , 计算每个三 对应 水 积
,还 跟踪新 积。
了新 积 ,还 计算新 积三 朝向。以新 积作为权重,求和各个三
法线,得到新 积 法向量。我 计算 击力 , 投影带法向量 速度分量。 为水
是作 ,每当一个 从空中进 水里 ,受到 力一 是 直该 。比 于
尾 水了, 是 于该 和速度 向 于90°,并不 力;反之, 水
了,则应该 平 于法向量、 正比于速度 力。

公式中 Params 是调 击力影响 常量。Vnew 是 对每个三 形成 分别计算再

求和 。只有当 水 积从0变成 0 算作Vnew,否则算作Vinc。

4.1.5 阻力上限
力和 击力 属于 力, 只 让速度和 速度 ,却不 反向,所以 力控制 一

设 ,我 证:

● 对向量 每个分量 足。之所以 对每个分量分别检 ,是 为Fr 朝向和v 朝

向是不同 。这里 m是质量。

● 两 模 相等。之所以只 模 相等,是 为Tr 是正比于w 。这里 I

是惯性张量, 戏中一 为一个三 向量。R和前 中 义相同,是 转矩阵。


图4.6所示是 台 下受力示意图。 A和Fb分别代 力 作 和 力 , 向是竖直
向上 ; B和F1( 常 )分别代 升力 作 和 , 向也是竖直向上 ;Fd代 水 力,Fs
代 击力,这两个力合称 力, 向与4.1.3 和4.1.4 中 绍 相同。 于发 刚从空中
水 ,所以升力较 , 击力 常 。 果 平 水 上移动, 击力则 常 。

图4.6 台 下受力示意图

4.2 引擎系统
只引擎系 主 决 移动和转向模 ,并且提 了向心力。 先我 判 引擎是否 水
下,以决 移动/转向是否响应。移动、转向、向心力 计算最终 是以力和力矩 形式 加 刚 上
。 绍具 算法前,先 绍一下 到 符号及其含义,详见 4.1。

4.1 符号及其含义

4.2.1 移动、转向模拟
当有输 且引擎 水 下 ,可以移动和转向。 移动算法 模 功 和档位,这不是本章 重
, 此不进 述。移动是以力 形式 加 刚 上 , 设已经算出功 P,P为负 代 退,为正

代 前进,我 可以得到移动 力: 。转向 求出转向力矩, 设已经算出转向力

矩 T,T 为负 代 朝左转,为正 代 朝右转,我 可以得到转向 力矩: 。


当没有输 或 引擎 水 上 , 于 水中受到水 力 速。 果发现 速 果不 ,就
调 4.1 中 绍 水中 力参 了。
4.2.2 向心力计算
只 转弯 , 身 向转弯 向 。为了有操控感,我 并没有 加向心力,只是以力矩 形
式 加 刚 上。计算向心力 第一步是求出模 空 下 速度v local和 速度wlocal , 可以直
速度和 速度与 转矩阵 转 矩阵相乘得到。向心力 计算公式为

Paramc是一个控制向心力 常量,和 质量成正比。这里 vlocal.fwd和wlocal.up分别是 v

local向量中代 前 分量和w local 向量中代 上 分量, 为一个 。比 Unity中,y是up


向,z是forward 向,则上 公式变为

这里 力 是 半 , 此向心力力矩为

4.3 Entity-Component及同步概览
本 绍 力系 和引擎系 是 集成 ,还 对第三 同步组件进 简单说明。本 并不是实
践 ECS 系 , 中 Component 并不是纯 。类 于 Unity,Entity 持有一组Component,并且
每帧 tick 这 Component 。 力系 ( BuoyancyComponent ) 、 引 擎 系
(EngineComponent)和第三 同步组件(SyncComponent) 是一个Component,可以挂 Entity
上。
图4.7所示, 同步系 中, 控制 单位叫第一 , 其 控制 单位叫第三 。第一
EngineComponent 和 BuoyancyComponent , 第 三 SyncComponent 和
BuoyancyComponent。
带有动态刚 第三 同步中, 论是 dead-reckoning 还是影 跟随, 先 逻辑层算 每
帧 应该 位 , 后再设 属性,当 模 束后, 正确 位 。
图4.7 第一 和第三 Component示

第三 位 同步 法有三种。
(1)设 位 :这种 法 造成 瞬移,并且 模 上不 逼真。不推 这种 法。
(2)设 速度: 过计算位移差求出速度, 引擎进 模 前应 到动态刚 上。这种
法比较顺 ,适合绝 情 。有 为位 差造成第三 其 刚 卡住,此 就 设
位 。
(3)设 力:这种 法 加了一层 性,控制 度加 。
这里 SyncComponent 是第二种 法,每次 引擎进 模 前 更新第三 速
度。另 ,让不同客 端 水 度一样, 别是 有 波 情 下,是 常 , 常我 许客
端 水 度不一致,这 两个客 端 只 运 力模 ,以适应不同 水 度。
这 就出现问 了:第三 同步组件 每帧更新刚 速度, 力组件则 每帧更新刚 受到
力,就 造成位 不一致。 ,第三 刚 本来应该以速度 v 移动到位 X, 是 力组件 速度
向上 加了 力、 力和升力,从 系 算出 速度 和v有 差,最终导致 模 束
位 不是X。

4.4 浮力系统物理更新机制
本 绍一种 力系 更新机制,可以 决第三 同步 速度设 问 。正 之前提到
,第三 过设 速度来移动 , 此第三 力系 也应该设 刚 速度,并且只 盖 分速
度分量, 顾同步 精确和水中 真实感。 力系 根 是否是第一 选择最终计算 果是力还是速度。
4.1 绍 力 ,算出来 是力和力矩,我 不 变所有计算公式 情 下实现这种机制。
图4.8展示了Component 更新 过程。第一 过引擎组件(EngineComponent)计算出 动
力, 过 力组件(BuoyancyComponent)计算出 力, 这 力 引擎进 模 之前 一
加 刚 组件(PhyComponent)上;第三 过同步组件(SyncComponent)计算出下一帧 同步速
度, 过 力组件(BuoyancyComponent)计算出 力推算出下一帧 力速度, 这两个速度糅合
后, 引擎进 模 之前设 刚 组件(PhyComponent)上。
图4.8 Component 更新 过程

更新机制 核心是BuoyancyUpdate类, 有以下三个 口。


(1)AddForce:累计力到变量linearMomentumChange中。
(2)AddTorque:累计力矩到angularMomentumChange中。
(3)Apply:根 模 ,计算速度/力,并 加 动态刚 上。
linearMomentumChange和angularMomentumChange对应了力和力矩 累计 , 为矢量,每帧
空, 力计算 成后 所有累计 力和力矩 一设 给 系 。
● 果是第一 ,则直 累计 力和力矩输出。这样 是 合刚 本身 尼, 现
更 。同 和EngineComponent以力 式输出 移动力、转向力矩和向心力力矩 持一致。
● 果是第三 ,则利 公式FΔt=mΔv更新速度vnew:

公式中 m代 质量。
类 ,更新 速度wnew:

其中,R代 转矩阵,I是惯性张量。
为了让第三 有合 现,我 BuoyancyUpdate 分 盖同步速度。 先,为了贴
合水 度, 了 ,也就是 了速度中代 上 分量;其次,为了贴合水 上波动 感觉,
了 和 , 速度中 up分量对应了 戏里 yaw,应该 持同步。
我 近水 计算 力,从 低了CPU开销。

4.5 总结
本章 绍了一种 只模 算法,可以模 意凸 水中 受力,又 根 第一 和第三 提
令 意 现。
过 水中 力分 成与 水 积正相关 力、与 水/出水 积相关 击力、与速度相关
力、简化 力 升力,以及简单 引擎系 ,当 只 水中 可以提 加速度和转向功 ,同 根
是否转向 加了向心力。我 只 水中受力 计算 力组件中, 操控和向心力计算
组件中, 过 组件,可以让 意 实 有 力。
对于第三 同步问 ,我 分本 模 果, 盖 分 ; 第一 又 最真实
,我 根 是否是第一 力计算 果 算成力或 速度, 合同步组件算出 第三 速度,最
终 加于 系 ,得到 合 果。

参考文献
[1]Randy Gaul.Buoyancy,Rigid Bodies and Water Surface[EB/OL].[2014-02-
14].http://www.randygaul.net/2014/02/14/buoyancyrigid-bodies-and-water-surface/.
[2]Wilson Ryan M.The physics of sailing[J].JILA and Department of
Physics,Colorado.University of Colorado,Boulder,2010:1998-2010.
第5章 3D游戏碰撞之体素内存、效率优化[2]
作者:

摘要
本章 绍3D 戏 素 存、 优化,适 于前端、后台。 素是3D空 最 示单位,为了
述 , 直 向 干 素合并后 仍称为 素。 素 戏中可 于 走、 、 机等
碰 检 , 素也 为 存过 , 以普及。 前3D 戏碰 检 普 法是 Bounding
Volume Hierarchy(层次包 ), 这种 于Tree 检索 并不是太 ,尤其是对后台压力很 。
本章致力于 素 存、性 优化。 存上, 过 素合并、 省 下 度、水不 成 素、控
制 素 成 、 存 管 等 式优化,优化后18个 景,平 为800m×800m, 素总 为
124MB,最 景占 15MB,最 景占 1MB,平 占 6.9MB,客 端 每个 景中只 加载当前
景 素,所以客 端运 于 素带来 存 加最 为15MB;后台 加载所有 景 素,所以
后台 于 素 存 加124MB。 上, 为 管 存,查找某个位 所有 素, 杂度为
O(1);再 过 计算每个 素可到 居 哪个 素,从当前 素移动到 居 素, 杂度近 于
O(1)。 素 系下,本章讲 有关 取 度、后台 图、前台优先级NavMesh、 齿平 、
走、轻功、 机碰 等功 实现 法。

5.1 背景介绍
MMORPG 戏已经 本进 3D 代,3D 戏 法不 上,还包 空中 、战 等。 前
3D 戏有三种 法:
(1) 素, 讯最先 《天涯明月 》项 组提出[1]。
(2) 边形 格, 是查找 较低,后台性 压力 。
(3)分层, 是 以上很 划分层 线, 且 杂 建筑层 太 , 存太 。
素是3D空 最 示单位,类比于2D空 素, 图5.1所示为汽车 素模 ,图5.2所示
为 景原图,图5.3所示为原图 素化后 模 (树叶不 成 素)。

图5.1 汽车 素模
图5.2 景原图

本章 绍3D 戏 素 存、 优化,适 于前端、后台。2014年《天涯明月 》项 组曾分


了 素 存、性 优化, 为 素 存占 之 ,很 项 望 却步。 过本章 绍 优化,18
个 景平 为800m×800m, 素总 为124MB,最 景占 15MB,最 景占 1MB,平 占
6.9MB,客 端 每个 景中只 加载当前 景 素,所以客 端运 于 素带来 存 加最
为15MB;后台 加载所有 景 素,所以后台 于 素 存 加124MB。 过 管 存, 查找
杂度 低为O(1)。后 绍 存优化、 优化所 景 为 800m、 800m、 400m。

图5.3 原图 素化后 模

5.2 体素生成
素 成受开 项 Recast Navigation[2] 启发,该项 Mesh经过 素化、 区 成、轮廓
成、 边形 格 成、 度细 成等步 成NavMesh, 此 Recast Navigation对Mesh 素化
代码 出来即可 成 素。

5.3 体素内存优化
5.3.1 体素合并的原理
每个 素 是一个 , 示有实 占 了该 (比 后 图5.6所示为石 素模 ,
该 素模 成,每个 上、下 为正 形, 、 为0.5m)。
设 (x,y) 有两个 素,第一个 素 上、下 度分别为4m和3m;第二个 素 上、下
度分别为10m和5m。第一个 素 上 和第二个 素 下 之 空 为1m, 身 为
1.8m,该空 法 过, 此这两个 素可以合并成一个 素,上、下 度分别为10m和3m。
果第二个 素 下 度为6m,第一个 素 上 和第二个 素 下 之 空 为2m,可以
过, 此不 合并。 过这种 式可以 景 素 从44MB 到36MB。
景中, 很 模 是 闭 , 为 Mesh 只 盖 , 成 素是空心 。 图5.4所
示 石 ,该石 是 闭 , 成 素是空心 , 图5.5所示。石 上、下 两个
素 示, 这两个 素可以合并成一个, 为 法进 石 。 过连 区 索,可以标记出石
和 不连 ,不连 区 上、下 素可以合并,合并后 素模 图5.6所示。 过这种
式可以 素 从36MB 到31MB。

图5.4 景中 石
图5.5 合并前 素模

图5.6 合并后 素模

这里就 对 术有 求:所有 进不去 空 , 成 闭 。 图5.7箭 所示,建筑 座


下 是不可进去 , 座 和 是连 , 图5.8所示,这样 座 成 素也 是空心
,造成 存 费。
图5.7 建筑 座

图5.8 建筑 空心 座

5.3.2 体素合并的算法
先选择一个种 , 后 过种 向前、后、左、右 , 法 到 空 ,即为不连 ,
合并上、下 素。 实现层 ,直观 想法是构造一个三 矩阵, 后 三 矩阵里 度优先
索( 度优先一 栈 出)。 这里 素 精度是 、 分 别 为 0.5m , 三 矩阵 为
1600×1600×4000, 存占10GB, 度 索一次 20min 成。本章提出一种 索 法, 1s 成
索。
先,构建原 素 反 素,原 素 示 实 占 空 ,反 素则 示没有 实 占 空 。
spans记录 景 素集合,antiSpans则记录 景 反 素集合。spans[x+y*w]为一个 ,该
存(x,y) 所有 素。height为 个 景最 度, 果spans[x+y*w]为空, 示(x,y)
没有 素,则该位 以上没有实 , 此(x,y) 反 素只有一个,上、下 度分别为height、
0。lastmax记录(x,y) 上一个 历 素 上 度, 果下一个 历 素 下 度smin
和lastmax之差 于 度,则可以 (x,y) 加 一个反 素,上、下 度分别为smin、
lastmax;否则, 须加 反 素, 为没有 实 占 空 度 于 度, 法 过。 果
历到最后一个 素, 景最 度height和lastmax 差 于 度,则 (x,y) 加 一个
反 素,上、下 度分别为height、lastmax。
其次,标记连 区。 antiSpans中 度优先 索, 先 列中 一个可 走区上 反
素,从该反 素开 标记 个 景 连 反 素。取出 顶反 素frontSpan,从前、后、左、右 个
向 历 居,找出frontSpan和 居反 素s 集(min,max), 果max和min之差 于 度,则
示可以从frontSpan到达s, s flag标记为1, 示连 ,并 s压 列中,直到 列为空,则标
记 所有连 区。
最后, antiSpans 中 删 所 有 flag 为 0 连 反 素,并根 antiSpans 重 新 构 建
spans。 过这种 式,合并 素 算法 存 低为10MB左右, 1s以 。
5.3.3 地面处理
个 景中 素最 , 不可 到 以下, 此 素 下 度省 , 一认为
0, 素 存从31MB 到21MB。这里 设 景 第一层 素即为 (并不 确, 后 中 有详细说
明)。 景中某 建筑 边沿下没有 , 此, 认为建筑边沿 下 度为0, 也 法到建
筑边沿下。 此, 建筑边沿下, 景最低可达 度之下1m位 ,加一个 片,该 片 成 素。
景中不显示该 片,该 片 成 素没有下 度。建筑边沿 为不是第一层 素, 此 有下
度,同 又 为该 片 最低可达 度以下, 此 也 法到达该 片。 图5.9所示,建筑下有
一 分是悬空 ,下 也没有 , 此该 分 成 素省 下 度, 一认为下 度为0,
素 图5.10所示。建筑下 素 住,建筑下 石 也 法 过去, 加 片 式,新
成 素 图5.11所示。建筑下 没有 , 素 成正常。
图5.9 1

图5.10 2
图5.11 3

5.3.4 水的处理
戏中,个别 景有水, 果水 成 素,则 标记该 素为水,每个 素就 带一个标记。
,只有个别 景有水, 果为所有 景 素 加一个标记,并不合 。所以,这里对水并不 成
素,水 度单 记录。最直观 法是开 二 矩阵, 每个(x,y) 记录水 度, 果 景没有
水,则不开 该二 矩阵。 这种 式, 造成 存 极 费, 为绝 分区 没有水。 此,
对水 分 存 式,每一 为100×100,再开 一个二 组, 组 每个 素 为 ,
果该区 有水,则 向100×100二 组;否则,该 为空。 查找(172,567) 水
度,则 先检查二 组 (1,5) 是否为空, 果为空,则 水; 果不为空,则代 有
水, 二 组 (1,5) 向 100×100二 组里查找(72,67) 度。查找
杂度仍 为O(1), 存 空 却 了。
5.3.5 范围控制
很 景 很 , 实际 动 区 并没有 么 。 图5.12所示,箭 1所 框为
景 ,箭 2所 框为 许 实际 动 区 。 为 法到 框以 , 此 框以 须
成 素。 框 制了 图边 , 图 前、后、左、右、上边 是 框 边 , 下边 比 框边
2m, 为 框边 以下 不 成 素。为了 止出现建筑悬空、建筑以下 情 , 框
边 以上1m 位 加 片,该 片 成 素。 为了 止 到 片上, 图下边 比 片 ,
此 图下边 比 框边 2m。 果 框边 以下 形 杂, 为 框边 以下不 成 素,则也可
省一 分 存。
图5.12 控制

5.3.6 内存自管理
前 存中存 素有三种 案。
(1) 存 , 以上分 存 ,类 于水 存 式。 为 以上 素较 ,分 存
可省掉 分没有 素 区 。 ,该 案仍 有相当 存 费, 先, 向每个 ;
其次, 每个 也有相当 区 没有 素。
(2) 叉树:该 案 不 给没有 素 区 开 空 , 查找 杂度为O(logn), 且
到 。
(3) 每个(x,y) new出来一个 组, 组 存 (x,y) 所有 素 息。 new出来
1600×1600个 组, 所占空 为1600×1600×4byte, 果 Unity、C# 64位机器上 占
8byte,则 所占空 为20MB。另 , 此 new操作,必 带来 量 存碎片, 且查找 居 素
导致cache miss, 为(x,y)和(x,y) 居 向 空 存上并不连 。
以上三种 案或 或 存和性 上 有问 。
本章 管 素 存组织 式:一 组spanArr记录所有 素 息,spanArr 每个 素 为
short类 , 为 素 度以0.1m为粒度, 果short 为64,则代 度为6.4m;一 组 indexArr
记录(x,y) 素 spanArr 中 位 。 景中 所有 素 度 顺序 spanArr中,(x,y)
第一个 素只 上 度, 第一个 素上、下 度 。查找(x,y) 所有 素,
先 indexArr 中 查 找 ( x,y ) 素 spanArr 中 起 位 start 和 个 count,start 即 为
indexArr (x,y) 素,count 即为 indexArr (x,y+1) 素 去 (x,y) 素
加1 以2。 为(x,y) 第一个 素省 下 度,所以 果(x,y) 有两个 素,第一个 素
上、下 度分别为4、3,第二个 素上、下 度分别为10、7,则 spanArr (x,y) 记录
三个 素4、7、10。 此, 素 为(3+1)/2,即为2。 下来,根 start和count spanArr中找
出(x,y) 所有 素。这种存 式,就是 上 中 案3 管 , 了 量 new操作
和空 不连 ,以及cache miss。
果项 中 景不 杂,则可以 下述优化 式。 (x,y)和(x,y+1)、(x+1,y)、
(x+1,y+1) 有k个 素,且k个 素上、下 度 相同,则 spanArr中存 (x,y) 第一
个 素 , short上加个标记, 示其三个 居 素和(x,y)相同,且(x,y+1)、(x+1,y)、
(x+1,y+1)三个 居 素不 spanArr。这样 查找(x+1,y) 素 , 先查找(x,y) 第一
个 素 标记, 果标记为真,则 示(x+1,y) 素和(x,y) 相同,直 从spanArr取
(x,y) 素即可; 果标记为 ,再从 indexArr 中查找(x+1,y) 素 spanArr 中 位
和 量, 后 spanArr中取(x+1,y) 素。 景不 杂 情 下, 存 显 。
5.3.7 体素内存优化算法的效果
素 存优化算法 果 5.1所示。 优化 占 存44MB, 素合并后 存为31MB,不记录
下 度 存为21MB,控制 后 存为13MB, 存 管 后 存为8MB。

5.1 素 存优化算法 果

5.3.8 体素效率优化
过上 中 绍 存 管 ,查找(x,y) 所有 素, 杂度是 O(1)。 设(x,y)
有两个 素,第一个 素上、下 度分别为4、0,第二个 素上、下 度分别为10、7;
(x,y+1) 有三个 素,其上、下 度分别为1、0,6、5和10、9。 设 站 (x,y) 第二个
素上, 度为10m,往(x,y+1) 向走, 历(x,y+1) 三个 素,查找可以 走到
素。 该 中, 可以到(x,y+1) 第三个 素上, 为该 素上 度也为10m。 ,
景建筑 杂 情 下,这种 历(x,y+1) 所有 素 式 消 ,客 端只有 己
素 走,其 走 是 过转发过来 路 , 此对客 端 影响不 , 是后台 对所有
校 , 历 式消 太 。
为(x,y)和其 居 形相差不 ,所以, 果 站 (x,y) 第二个 素上, 么 走
到(x,y+1) 也很可 走到第二个 素上,或 第一个、第三个 素上。 每个 素上再加一个
short,存 8个 向 走到哪个 素上,每个 向两个bit。 ,当前 素为(x,y) 第k个
素 , short 前 两 个 bit 为 00 , 则 代 可 以 走 到 ( x,y+1 ) 第k个 素 上 ; 为 01 代 可以走到
(x,y+1) 第k+1个 素上;为10代 可以走到(x,y+1) 第k−1个 素上;为11代 (x,y+1)
k−1、k、k+1三个 素 不可走, 历(x,y+1) 所有 素。经 计,两个 bit 为11
情 不到5%, 此只有极 情 历 居 所有 素, 素之 走 杂度近 为
O(1), 提升近10 ,每个 景 平 素 6.9MB 到9.1MB。

5.4 NavMesh生成
5.4.1 体素生成NavMesh
NavMesh(导 格)是3D 戏中 于实现 动寻路 一种技术, 戏中 杂 构组织关系简
化为带有一 息 格, 这 格 础上 过一系列计算来实现 动寻路。NavMesh是 Mesh经过
素化、 区 成、轮廓 成、 边形 格 成、 度细 成等步 成 , 后 导 格上 过A*
或D*等算法 成连 边形列 ,并从列 中找出最优路径。
这里客 端寻路 NavMesh,后台寻路 Jump Point Search。 为客 端只有 己寻
路,性 没有压力, 是 路径必须 ,不 贴边走,分级NavMesh可以 路中 标记为 优先级,
路边 标记为低优先级,这样寻出来 路径 可以 贴边问 。后台 对 量NPC寻路,所以后台
寻路重 快,Jump Point Search寻路速度是NavMesh 几十 , 足性 求。 Jump Point
Search路径存 贴边问 , 是NPC绝 种 开 带,所以NPC 路径很 有贴边 情 , 此
后台 Jump Point Search寻路。
成 素提取上 Mesh,提 给Unity并 成NavMesh。为了 成精细 NavMesh,这里为
NavMesh 成 素 精 度 为 0.15m×0.15m×0.1m , 碰 到 存及 , 素精度为
0.5m×0.5m×0.1m。
5.4.2 获取地面高度
个 景 素化, 仍 度。 ,策划 为了 位 , NPC出 位 只
水平 标, 度默认为 度。一种 取 度 式是取 NavMesh 度, NavMesh
度不精 ,比 栏杆边 附近。 图5.13所示,左、右两 为不同优先级NavMesh,箭 所 即为
NavMesh 该位 度。 素第一层也不 作为 度, 图5.14所示,箭 1 向 是 板,
第一层 素是箭 3所 位 。这里 法是, 先 成 素提取上 Mesh,提 给Unity并
成NavMesh, 后给 一个 , 素上进 ,为所有 过NavMesh 走到 素 加上标
记,最后删掉 法 走到 素,剩下 素即为 素,这 素 度即为 度。

图5.13 取 度1
图5.14 取 度2

5.4.3 后台阻挡图
后台 图标记哪 区 可 走,从 对客 端 位 进 校 。后台 寻路算法Jump Point
Search也是 图上进 。客 端有NavMesh 是可 走区 ,为了和客 端 持一致,后台根
客 端NavMesh 成 图, 客 端 NavMesh有 层,后台 图只有 一层, 此 成
图 ,客 端 NavMesh 过 。 过上 所述 法 取 素, 素上 Mesh
重新 成NavMesh,重新 成 NavMesh即为可 走区 ,没有NavMesh 即为不可 走区 。
5.4.4 前台优先级NavMesh
前台 NavMesh 给 寻出来 路径 果贴边,则对 不 , 此 对NavMesh分优先
级。路边 NavMesh优先级低,边 费代价 ;路中 NavMesh优先级 ,边 费代价低, 此寻
出来 路径 优先 路中 NavMesh。 图5.15所示, 区 为 优先级NavMesh, 区 为低
优先级NavMesh。Unity 持对NavMesh分优先级,只 提 Mesh标记优先级即可。
先根 图, 过Meijster算法计算SDF(Signed Distance Field),SDF 示当前位
半径 没有 。 过上 所述 法 取 素, 历 素, 果当前位 SDF 于某个
,则该位 素 上 Mesh标记为低优先级;否则,该位 素 上 Mesh标记为 优先级。
后 素标记了优先级 Mesh提 给Unity 成优先级NavMesh。
图5.15 优先级NavMesh

5.4.5 锯齿
贴着 边走 , 易与 边发 碰 , 是碰 不 卡住, 沿着 边 切线 向平 移
动。 图5.16所示, 区 为 , 欲从A到C, D 卡住,此 D 变 向,沿 边
切线 向移动到B。 取切线一 过Mesh计算, Mesh占 空 , 此这里 NavMesh计算切线。

图5.16 碰 模

素 上 Mesh 成 NavMesh 图5.17所示, 为 素 上 Mesh有 齿, 此 成


NavMesh也 有 齿。 果 含有 量 齿 NavMesh计算切线,并对 移动 平 , 果并不 。
此,这里 素 Mesh下 再 一层原 景 Mesh,原 景Mesh 标记为低优先级。 为原 景Mesh
度比 素Mesh 度低, 此有 素Mesh 不 根 原 景 Mesh 成 NavMesh,只有 边
带,没有 素 Mesh, 根 原 景Mesh 成NavMesh。最终 成 NavMesh 图5.18所示,区
2、区 3 NavMesh仍 素Mesh 成, 区 1 NavMesh 原 景Mesh 成,并且NavMesh是平
,可以根 其切线对 移动 平 。为了加速计算,边 区 素上 标记,只有 边 区 对
平 。
图5.17 带 齿 NavMesh

图5.18 消 齿 NavMesh

5.5 行走、轻功、摄像机碰撞
5.5.1 行走
这里 走 素上进 , 每个心跳里,根 移动 向和 算出新水平位 ,并对新位
进 碰 检 。
走 碰 情 有4种:
(1) 居 素太 ,超过向上走 最 度,不可走。
(2) 居 素太低,超过向下走 最低 度,以某个速度 直下 。
(3)走到 居 素 碰 ,根 前 绍, 素合并 证该情 不 发 。
(4)可 走,直 走过去。 , 这种情 下, 为 素 齿 性,导致 走并不平 。
图5.19所示为房顶,其 成 素 图5.20所示。可以看出, 房顶上 走 同走台阶一 ,
此 平 。
图5.19 房顶

图5.20 房顶 成 素

5.5.2 轻功
轻功有水平 向和 直 向两条运动曲线。每个心跳 两条运动曲线上 样,算出 新位 ,
后对新位 进 碰 检 。碰 有6种情 :
(1)向上进 素, 上房顶, 以某个速度 直下 。
(2)水平进 素, 中 , 弃 水平位移,只取 直位移。
(3)向下进 素, ,轻功 束。
(4) 水,水中 素,转 泳 态。
(5) 碰 , 直 到新位 。
(6)超出 图边 , 果是向前、后、左、右、上超出 图边 ,则 以某个速度 直下 ;
果是向下超出 图边 ,则 , 位 是 轻功中以某个较 记录 直向下找到
可 。
5.5.3 摄像机碰撞
机是为了 可见区 ,当 移动 , 机也 移动并检 碰 ,当检 到碰
移动到合适位 。 前普 法是当 移动 , 机根 线 度等计算位 , 后从
到 机 出一条 线, 机 线和 素 第一个 上。 , 和 机 距离一
5m以上,每帧 5m 距离 检 线和 素 比较 。 此,这里只 机进 素,以及
机 旧位 和新位 之 有 , 机 线和 素 第一个 上。检 机是否
素里很快, 机是平 移动 , 机 旧位 和新位 一帧 很 近,所以检查 机 旧位
和新位 之 是否有 素也很快。这一步 是 止 和 机一直 两边,导致看不到

这里 Bresenham算法计算 线和 素 。Bresenham是 来描绘 两 所决 直线 算
法, 算出一条线段 n 光栅上最 近 。这个算法只 到较为快速 加法,常 于绘制
中 直线,是计算机图形 中最先发展出来 算法。
算法:设startPos为 线起 ;endPos为 线终 ;dirInfo 示 x、y、z 向走1m, 线 走
米;maxLength 示startPos和endPos 距离;tMax 示 到立 格 下一个x、y、z边,
从 线 起 位 走 远。 果tMax.x比tMax.y、tMax.z ,则 示沿着 线走,先碰到立
x边; 果tMax.x 于maxLength,则 示startPos和endPos之 没有 素,算法 束。 果当前位
素里,则 示startPos和endPos有 。已知startPos、沿着 线走 距离tMax.x和 向, 此
可以计算出碰 位 ;否则, 示未到endPos且未 到 素,tMax.x 加dirInfo.x。 后重新找出
tMax.x、tMax.y、tMax.z中最 ,决 最先到立 x、y、z哪条边。
图5.21所示为Bresenham算法二 示意图。startPos为(0.5,0),endPos为(4,2), 找到
startPos 到 endPos 到 ,即为黑 格 (3,2)。dirInfo 为(1.1517,2.0155),tMax 为
( 0.5758,2.0155 ) 。 第 一 轮 , tMax.x < tMax.y , 当 前 格 为 ( 1,0 ) , 未 到 , tMax 为
( 1.7275,2.0155 ) ; 第 二 轮 , tMax.x < tMax.y , 当 前 格 为 ( 2,0 ) , 未 到 , tMax 为
(2.8792,2.0155);第三轮,tMax.x>tMax.y,当前格 为(2,1), 到 ,直 返 位 :
(0.5,0)+ 2.0155×(0.8683,0.4962),即(2.25,1),其中(0.8683,0.4962)为startPos到
endPos 向向量。
图5.21 Bresenham算法二 示意图

参考文献
[1] 《 天 涯 明 月 》 服 务 器 端 3D 引 擎 设 计 与 开 发 . 讯 戏 ,
2015.http://gad.qq.com/article/detail/10014.
[2]Recast & Detour.https://github.com/recastnavigation/recastnavigation.

[1]本章相关 已 请技术专利。

[2]本章相关 已 请技术专利。
第三部分 计算机图形

第6章 移动端体育类写实模型优化[1]
作者:冯 、吴

摘要
本章 案 一 干模 、不同 模 以及 一 和动作, 合 GPU 中 顶 位移,
实现了几百 实模 和几千 动作 求。 先,该 案 一 标 身 模 和 , 达几千
动作可以 ;其次, 合 服 贴图和肤 贴图, 盖了绝 分 干 现;第三,对不同
不同 模 、 贴图和个性化 肤贴图( 身)与 干进 组合,达到 现上 差
异化;第 ,根 不同 征对标 进 变化以及GPU中顶 位移,实现了 干
分不同 矮胖瘦 差异化 。另 , 过 一 不同款式服 模 替 原 标 干模 和贴图,
模 /贴图合并,实现了不同服 求。
该 案适 于移动端 戏,可以运 最低 是 卓 机(操作系 Android 4.4, 存2GB,CPU
4/8核1.5GHz)、苹果 机iPhone 5S(iOS 9.0及以上, 存1GB, 器A7+M7)。该 案 制 和
主 于 前移动端硬件, 合 量了移动端 运算性 、 存、 量消 、包 量以及 术制作
护成本与 现上 平 。该 案实现了 身 、 、肤 、 和发 上 素差异化,以 证
对不同 识别,忽 了对区分 影响较 其 素( 短、 等)。对于主机
端或 更真实细致 还原,可以 此 案 础上进一步拓展, 或 位移更 关 ,实现
和 不同 短等。该 案适 于 量 实 格 真实 (该众 为 所广泛认识和辨
识),以及超 量动作(几千 ) 戏, 竞技类 戏。

6.1 引言
类 戏(运动 戏,Sport Game),即以 运动为主 戏,这里 实 格
类 戏,与其 类 戏相比有着明显 质。 类 实 戏( EA公司 Fifa系列、NBA Live
系列和Madden NFL)最显 一个 征是 有 量( 百至上万个)真 实 ,以及超 量(上
千至 万 ) 动作资 ,远超其 类 戏。另 , 类 实 戏中 对应于现实中 运动
员,这 运动员 为 所 知, 戏 常又 常“硬核”,对明星运动员十分 悉, 至追逐和
。 此, 戏 设计制作必须 度符合真实 ,动作专业合 、丰 且真实可 ,这对
还原度和辨识度以及制作、实现 提出了极 求。
对于 移动端实现 类 戏中 实 ,我 主 临下 两个问 。
(1) 类 实 上千至 万 动作资 , 论是出于制作成本还是 存占 ,
进 优化,并尽量 可以 同一 动作资 。另 , 戏 和动作 还原度、辨识度 求不同
存 明显差异。 此, 同一 动作资 础上差异化 现,比 身 、 、肤
等, 常重 。
(2)移动平台相比于PC、主机 存、 器和 量消 上有更 制, 且机 众 ,一 分
低端机 CPU运算和渲染 力 弱, 更 中低端硬件 机 也 常重 ,这 我 对
进 更 优化,来 CPU和GPU 消 。
本章主 这两个问 ,对移动端 实 提出一种优化 制作和实现 案。 中示意所 模
、贴图以及 果展示 来 戏《最强NBA》。

6.2 方案设计思路
我 希望 过对 实 构成 素进 , 影响 辨识 重 程度进 区分,寻找出所有
可以 一 素和差异专属 素, 后 此 划具 资 制作。同 , 这个过程中,尽量
可以 同一 动作资 。
6.2.1 角色统一与差异元素分析
类 戏 实 是现实中 真实 ,并且穿着正常服 , 此我 可以 “ ”归纳
为“ ”和“服 ”两 分。我 对 戏 归纳分析,就具 转化为对 和服 归纳分析。
6.2.2 角色表现=人体+服饰
先,我 和服 再进一步细化成各种 素(不局 于下 所列,我 只 选较为重
素),这 素构成了一个 , 图6.1所示。
图6.1 构成

后,我 进一步分析各 素对区分不同 个 重 程度,以 后 素进 归类, 6.1


和 6.2所示。

6.1 素

( )
6.2 服 素
重 程度 位为中等、不重 ,说明不同 差异较 ,比较相 ,或 觉感受不明显,不足
以影响不同 区分; 重 程度 位为重 ,则说明不同 差异较 ,或 觉感受 常突
出, 就 法区分 。
最后,根 重 程度 不同,提取出不同 一 素和差异 素。
● 一 素: 重 程度 位为中等、不重 素,不足以影响区分不同 征。
● 差异 素: 重 程度 位为重 素, 就 法区分不同 。
另 ,为了简化讨论,同 后 对资 划 不必 制作和 费,我 再对一 位和
素进 一 合简化。
先, “ 属性”过于 象,肤 和 属性 现 各个具 位上,所以我
这 属性具 赋予 相应 位 素, “ 肢”和“ 干”分别 加“ ”和“肤 ” 素。对
于“ 属性”中 和肤 则不再单 进 分析, 身 。
其次, 干 本 服和裤 所 盖,不 看到,所以我 可以忽 干 “肤 ”属
性。 干 比 和 属性,可以 盖 服和裤 现。比 ,脂肪较 、 较胖 ,
服尺码更 ,腹 服也 撑起,强壮 有较 , 服和裤 胸 、 和
撑起。所以,我 可以 “ 服”和“裤 ”来代替或等同于“ 干”, 干 = 服 + 裤 , 干
、比 等属性 服、裤 承。这样,我 只讨论 服和裤 即可,后 也不 制作 干 相
关资 了, 费。
最终 出 果 6.3所示。

6.3 一/差异 素

( )

6.2.3 角色资源整理
出了 一 素和差异 素后,我 就可以开 划 资 了。我 资 ( 戏资
)区分为 和专属两 分。
● 一 素, 划为 资 ,只存 一 ,所有 。
● 差异 素, 划为专属资 , 对每个 单 制作。
另 ,3D 戏资 中 模 、材质、贴图和动作 相互 合 , 此我 合 这 资
相互关系和影响。其中, 为材质对于同类 是 一 ,可以对应 个不同贴图,所以这里忽
材质,我 所讨论 资 主 模 、贴图以及 和动 资 。 包含以下一 性:
● 模 包含几 (空 位 、比 等)。
● 贴图包含 。
● 影响模 几 (空 位 、比 等)。
● 一个模 可以对应不同 贴图。
● 同一个贴图可以给 个不同模 。
● 一个模 包含一 , 此一 只 对应该 动作(动 )资 。
● 不同模 果顶 几 相近,则可以对应同一 ,进 可以 相同 动作资 。
梳 出这 性,可以帮助我 对应 素和资 关系。
● 比 、五 形 , 服和裤 比 、款式形 、 , 肢 比 、 和身 等,
与模 对应, 其几 现。
● 五 、毛发 、 肤, 服和裤 款式设计、 纹 , 肢 肤等,与贴图对
应, 其 现。
● 比 、 服和裤 、 肢、 和身 等受 影响, 此我 可以 来控制
、身 。
● 果所有 模 相近、 度相近, 么就可以 相同 ,进 可以 同一 动作资

下来,我 资 对应到具 素上, 出 果 6.4所示。

6.4 资

注意:体型被赋予到衣 、裤 (躯 )和四肢分 中。
6.2.4 资源制作与实现
根 上一 资 划,我 具 资 制作。这里仍 素逐一对模 、贴图等进
分析。同 , 为 也是资 重 组成 分,并关乎 所有动作 标,所以这里也 对
进 分析。 素相对 杂,我 后 单 分析。
模 和贴图为专属资 ,我 根 每个 不同 形 、五 和毛发,为其单 制作。每
个 贴图中 肤 , 与其 肢 肤贴图(肤 至 有5 肤贴图,详见后 )中
某一 一致。 果该 有专属 肢贴图( : 有 身),则 贴图 肤 与专属 肢贴
图肤 持一致。 模 图6.2所示, 贴图 图6.3所示(为了 重相关权 ,对贴图 了模糊
)。

图6.2 模

图6.3 贴图

我 以俱乐 、 为 , 、 裤 模 和贴图 是 资 。所有 、 裤款式(几 形


)相同,我 制作一 标 模 , 图6.4所示。

图6.4 、 裤模

贴图 分,相同俱乐 、 础贴图一致:每个 制作一 础贴图 资 ;每 、


裤制作一 0~9 字贴图资 ,可根 求进 组合;每个俱乐 、 ,包含主客 各
一 、 裤贴图和一 号码贴图, 图6.5所示。
图6.5 、 裤贴图

日常和 款式 服、裤 ,与 、 裤相 ,对于相同 形款式制作 模 资 ,对于相


同 形款式、不同 纹可以制作不同 贴图。
肢 几 形 和比 属于 资 ,我 制作一 标 模 , 图6.6所示。 肢贴图也属
于 资 ,可以 肤 至 制作5 肤贴图,对于具有 身 征 ,我 为其制作专
属 肤贴图资 , 来代替 贴图, 图6.7所示。

图6.6 肢模

图6.7 肢贴图

我 最初 临 一个主 问 就是尽可 同一 动作资 ,以 省制作成本和 低性 消 。上


我 分析了, 果所有 模 相近、 度相近, 么就可以 同一 ,进 可以 同一 动
作资 。
前 我 已经 划了所有 同一 服、裤 和 肢模 ,这 素已经 一
。我 再进一步 所有 模 一尺寸和 度, 证与 服、裤 模 可以 合,就
不同 也 同一 。 模 也是 此。这样所有 模 具有相同 度,
同一 ,可以 同一 动作资 。 构 图6.8所示。
上 我 确 了所有 有同一 和 度, 此可以设 一个标 身 , 后 过
根 (根 )得到不同 度 。根 计,我 某类 项 运动员 平 身 1.98m,确
为一个标 模 身 ,以尽可 实现其 身 。

图6.8 构

为 干、 肢与身 比 相对线性,我 又 到最 ,所以 这 分 后


觉上 不合 性并不明显,可以 受。 对于 ,一 与标 1.98m身 差异较 ,或 某
真 比 比较 殊 (比 某 与身 相比,明显较 或较 , 且成为该 一个辨
识 征),可 出现 比 与真 明显不相符 情 。我 同一思路, 过 ,
控制 合 比 , 图6.9所示。
图6.9 身 实现

其 服 资 制作与 服、裤 相 ,对于相同 形款式制作 模 资 ,对于相同 形款


式、不同 纹制作不同 贴图。
下来,我 单 分析 素, 类 戏 实 差异具有以下几个 征。
● 运动员 绝 身材 匀、 含量 、脂肪 , 差异较 , 此可以忽
极度肥胖或 极度瘦弱 。
● 我 可以根 含量 分为强壮和瘦弱,根 脂肪含量分为肥胖和纤瘦。 含量和脂肪
含量相互不冲突。
● 对于 , 肩 、胸 、上 脂肪相对 盖 , 强弱 这几个 位 现明显。
● 对于脂肪 , 腹 位脂肪最 易 积, 现也最为明显。
此可以看出, 差异 我 区分不同 位(比 肩 、胸 、腹 等)对模 几
(顶 位 )进 和变化。 来控制模 局 顶 是直 办法, 带来更 动
顶 变 计算消 ,也 给超 量动作 制作 加 杂度,还可 干 动 正常 。所以
我 控制 ,尽可 寻找更优 案—— 过 计算消 , 干 到动作 正常
制作和 。
这 ,GPU 是一个不 选择。GPU 几 阶段可以直 对顶 位 进 控制, 了
计算消 。顶 可以包含顶 法线等很 息,也可以存 我 想 其 息。同 GPU渲染
动 动顶 计算之后,也 了对动作 干 。
我 开 GPU 中实现 : 果 移动一个顶 , 一个向量, 确 向量 向
和 度; 果 移动模 区 一组顶 ,还 有各顶 对应 权重 。 下来我 就去寻找和构
造这 :
● 模 顶 法线为我 提 了最合适 向量 向,正是 所 模 向 凸起或 向
向。
● 对于向量 度, 根 每个 来 制,我 可以根 不同 。
● 对于顶 权重,有较 选择,贴图、顶 切线、UV2、顶 等 可以 我 存 权重,
且 可以存 向量, 我 区分 和脂肪。 贴图存 ,绘制 ,可以 着 器顶 程序中
对贴图进 样 得权重, 顶 纹 取(Vertex Texture Fetch) Shader Model 3.0,我
希望可以 更低端 Shader Model 2.0下运 。 果 顶 UV2、切线 息来存 ,则 了
Shader Model 3.0和对纹 样, 是制作 法 DCC(Digital Content Creation)工具直
绘制, 另 开发工具 持。 此,我 最终选择适应 求低、性 消 又 绘制 顶
来存 权重。
这样,我 就可以 过法线向量权重 ,来得到所期望 顶 移动向量,实现 。
vertexPosition =vertexPosition +normal × weight × value
图6.10所示, 权重记录 顶 中,根 顶 法线 向、权重和 (图中value=20f),
调 顶 位 。
图6.10 法线 出

肢及 服、裤 模 中,我 顶 Red 存 权重, 图6.11所示;


Green 存 脂肪 权重, 图6.12所示。
图6.11 顶 Red 存 权重( 肢及 腹权重)

图6.12 Green 存 脂肪 权重( 腹权重)

权重为[0,1],本应显示黑白 , 是为了 查看图片,权重1.0 分示意为黑 ,0.0 分示


意为白 。
具 实现中,为了优化运 性 ,我 各 位模 合并为一个 格,并 同一种材质
(见后 6.3.2 )。 、 等模 资 不 设 变化(顶 权重为RGB(0f,0f,0f)),
未绘制顶 息 模 计算 着 器默认为RGB(1f,1f,1f), 此,为了 制作 对 量此类
模 绘制权重——顶 RGB(0f,0f,0f) 工作,我 可以 权重与 关系映 为1f-weight,
以反 绘制 服、裤 、 肢 模 顶 中, 后 计算中再纠正 来。图6.13展示了反 后 权
重 。
图6.13 法线 出(反 后 权重 )

图6.14展示了顶 各权重 取反后显示出来 ,以及赋予不同 后 果。从左向右


次为: 肢强壮, 肢瘦弱, 腹肥胖, 腹纤瘦。

图6.14 权重及 果

最终制作 果 下:
● 我 制作了一 标 1.98m 模 , 同一 标 ,所有动作资 可以 不同

● 过组合不同 以及 服、裤 和 模 ,可以得到所有差异化 模 。
● 过组合 、 裤贴图和号码贴图,可以得到所有 所 、 裤贴图(其 服 等制
作相同, 没有号码贴图合并以 )。
● 过选取一 肤 贴图, 专属 贴图,可以得到所有 所 贴图。
● 过根 ,可以得到正确 身 ,再 过 ,得到正确 比 。
上所述, 本 决了我 最初主 临 第一个问 : 类 戏 实 尽量优化 资
,同 戏 还原度和辨识度, 可以 同一 动作资 。同 ,资 、GPU
,也给第二个问 —— 更 中低端硬件 移动设 , 下了一个较 础。 下来
具 实现中,我 进一步 对移动端进 优化。
6.3 具体实现
我 对 、 服、裤 、 肢等模 、贴图资 拆分和 ,以及对同一 动作资 ,
低了制作量和 存 量, 同 也 加了 Drawcall,性 消 加 。为了 Drawcall,提
渲染性 ,我 运 对模 进 合并,赋予同一种材质,并对贴图也进 合并。
6.3.1 实现流程
实现 程 图6.15所示。

图6.15 实现 程图

6.3.2 CPU逻辑
具 实现 ,CPU逻辑包含了模 合并、贴图合并和根 设 相关参 几个阶段。
模 合并 程 下:
(1)合并 、 服、裤 、 肢模 顶 息。
(2) 为后 还 合并贴图,所以这里 重 分 顶 UV 。
图6.16展示了 模 合并前后 Bounding Box(包 )情 (参 Unity3D Editor Scene
图),合并前有六 分,合并后只有两 分。
图6.16 模 合并

贴图合并 程 下:
(1) 服、裤 贴图与号码贴图合并为一张贴图。
(2) 个护具贴图与 肢 肤贴图合并为一张贴图。
(3) 服、裤 、 肢、 贴图合并为一张贴图。
图6.15展示了贴图合并步 。
根 设 材质、身 和 程 下:
(1) 材质赋予模 , 合并后 贴图赋予材质。
(2) 根 ,实现身 。
(3) ,调 比 。
(4)设 材质 和脂肪 ,实现 。
逻辑代码 下:
6.3.3 GPU渲染
GPU渲染阶段主 包含了根 模 顶 权重和CPU所设 参 变 顶 位 标,实现 工
作。
GPU渲染 程 下:
(1)从顶 Red和Green 中 取记录 、脂肪 权重。
(2)纠正资 制作 权重取反 计算。
(3)计算对应 和法线 向,得到 移向量,计算新 顶 位 。
代码 下:
6.4 效果收益、性能分析和结语
图6.17展示了 《最强NBA》 戏中实现 不同 果。本 案 过对 、 服和裤
( 干)、 肢、身 、 、肤 一 素和差异 素进 分析 , 划了一 资 与
专属资 制作 案, 过尽可 模 和贴图资 , 了 戏包 尺寸, 低了 存消 ,同
证了 差异性和还原度。并且,标 模 及相同 , 所有 可以 量 动
作,也 了制作成本,提 了性 , 决了资 制作和 问 。 过 运 对模 和贴图资
合并,又进一步 低了 于资 与拆分造成 Drawcall渲染性 消 。经过 试,合并优化
后 比优化前CPU 低35%,GPU 低30%。最终,本 案可以运 最低 是 卓 机
(操作系 Android 4.4, 存2 GB,CPU 4/8核1.5GHz)、苹果 机iPhone 5S(iOS 9.0及以上,
存1GB, 器A7+M7), 决了对低 硬件移动设 持问 。

图6.17 不同 果

6.4.1 方案优劣势
优势:
● 同一 以及 量动 可以适应不同 。
● 了Drawcall,可以提升性 。
● 低端移动硬件。
劣势:
● 运 进 模 和贴图合并, 带来一 性 和 消 ,延 了进 正式 戏前 等
待 。
● 贴图 合并,带来了 存消 。随着 量 加, 存占 量也 呈线性 。
● 个 同一种材质, 术 果有所 低。
6.4.2 方案补充
● 前本 案只 了模 顶 Red、Green 存 和脂肪 权重,Blue与Alpha
, 更细腻 控制,则可以 这两个 丰 。
● 渲染 顶 程序阶段,顶 位 移动后,模 法线没有 重新计算,光 存 误差。另
, 计算上投影 Pass中仍 原 顶 标,也 出现误差。 为误差较 ,本 案 于性
忽 光影 纠正。 果 更 现,则可以进一步 善此 。
● , 顶 移 了GPU中计算,可以 试 CPU模 合并过程中,并重新计算法
线,得到正确 光 和投影 果。也可以 顶 权重取反纠正 模 合并过程中一并逻辑 ,这样
可以 低GPU 消 ,不过CPU 消 以及 等待 加。
● 本 案忽 了 肢比 (上 与前 比 、 与 比 ) 差异, 果 加此 细
,则可以 试对相应关 进 或 移动 。
● 本 案 础上,可以进一步 或其 服 进 模 与贴图 合并。
6.4.3 应用场景
本章 绍 案不 适 于 实类 戏,也为其 类 戏提 了一 决 案。
以MMORPG 戏为 , 单个 动作 量相对较 , 众 、 各异(比 各种 NPC、
怪 等), 对每个 来制作动作, 法 ,制作量 。 果 本 案, 动作,对
有 对性 , 么就可以实现众 差异性 模 , 且 极 资 制作量。
另 ,对于 乐 类 戏, 身上Avatar 件 量较 , 果 本 案中 合并模 、贴图
法,则 有 低CPU、GPU 消 ,从 持更低端 移动机 , 。
上所述,可以根 具 戏类 来选择本 案 分技术 。

参考文献
[1] 戏类 .https://baike.baidu.com/item/%E6%B8%B8%E6%88%8F%E7%B1%BB%E5%9E%8B.
[2] .https://baike.baidu.com/item/%E8%82%8C%E8%82%89%E7%BE%A4.
[3] 干 .https://baike.baidu.com/item/%E8%BA%AF%E5%B9%B2%E8%82%8C/5899293.
[4]Vertex Texture
Fetch.NVIDIA,2004.https://www.nvidia.com/object/using_vertex_textures.html.
[5]Meijster,Arnold and Roerdink,Jos BTM and Hesselink,Wim H A.K.Peters.Real-Time
Rendering Third Edition.Ltd.Natick,MA,USA,2008.
第7章 大规模3D模型数据的优化压缩与精细渐进加载
作者: 颖

摘要
WebGL 口渲染显示 尺寸max、maya、fbx等3D模 件格式,模 优化是必不可
步 。本章 述了一 mesh、texture 优化压 案, 压 比最低可以达到10%以下,
专 存 组织和 景 构设计,可以实现 3D 模 质量精细 进加载, 主 速条件下
达到 速 览 。
顶 先 空 位 序和条带化, 后再进 三步压 。第一步, 对不同顶 素
(position、normal、color、uv、index等) 征 有 对性 码 案, 所有 一变
到 空 ;第二步,利 顶 位 相近、其 属性也相近 征,对第一步 成 进 差量转
, 分布 较 区 , 后利 算术 码或变 变 进一步压 ,以
省存 空 ;第三步, 一个 zlib压 ,根 压 质量 根误差 求 不同,压
为2%~98%(较原 )不等。
此 , 顶 、纹 存 布局上也 了 进加载 求, 组织 于并 压 、快速显示。
,顶 根 position、index、uv、normal 等 觉重 性 序存 ;所有同类材质纹
干 级mipmap 包 一个 件中,并且 albedo、roughness、normal等贴图对 觉 果 度
序, 优先级分开 输到客 端。这 提 模 输 析 性。

7.1 引言
随着各 览器对WebGL标 持逐步 善,越来越 3D应 迁移到Web端,且有变 变 杂
趋势。其中不乏中 3D 戏和 精 3D模 站,这类应 模、渲染 杂度 不逊于
很 系 原 应 ,这造成了 程序设计模式 不同于普 Web应 ,反 更 近 戏或
桌 程序。 繁 渲染更新、 量 加载、 杂 应 逻辑, 对程序优化提出了更 求。Web前
端程序设计 员 重新 精力 到CPU 、GPU 、Cache、 存、显存、总线以及Internet
络带 等计算机资 划上,还 心 程语 性, 细操作 存分 ……,总之,又 古
底层 程一样 重细 , 证Web端 杂3D应 性和 壮性。
现 主 速为10Mb/s~100Mb/s,远低于硬 和 存 输速 , 3D模 模可达上
GB, 幅度压 原 尺寸, 络 输 ,成为提升 关 。下 就 详细 述一系
列模 顶 和纹 压 优化 案。

7.2 顶点数据优化
3D模 一 3ds Max、Maya等 术制作软件 成, 这 模 格式并不 直 于
Direct3D、OpenGL等常见 3D渲染API, 此顶 转 优化压 是 个3D 渲染工作 第一
步,其 步 致可以分为 下几个 步 。
(1)顶 合并去重。
(2)索引 合并。
(3)顶 序。
(4) 格 拆分与合并。
(5)顶 码压 。
下 先澄 后 中 几个概念。
● 模 (model): 示一个 模 或 景,包含 个或 个几 以及其 3D对象,具有
层级 构。
● 几 (geometry): 示一个 3D 件, 一个或 个 格组成。
● 格(mesh): 一个或 个 格组成。
● 格(sub-mesh): 一个或 个 边形构成。
● 边形(polygon): 3个或3个以上顶 构成 闭合 。
● 三 形(triangle):只包含3条边 边形。
● 顶 (vertex): 示空 中 一 ,是一个或 个属性 集合,至 包含position属性。
● 属性(element): 示顶 空 中 征, 位 (position)、法线(normal)、切线
(tangent)、纹 标(uv)、 (color)、 合权重(blend weight)、 合索引(blend
index)等。
● 索引(index): 来索引顶 顶 冲中 位 (构成 格)或 顶 属性 属性 冲中
位 。
7.2.1 顶点数据合并去重
图7.1至图7.4所示,一个立 包含6个 边形,每个 边形包含2个三 形, 36个顶 ,这
顶 具有8个不同 position属性 、6个不同 normal属性 、18个不同 uv属性 、4个color属性
。 这个立 36个顶 中,实际 有 分顶 是 相同 ,即具有 相同 属性 。 此,顶
合并去重 是顶 第一步。去重算法很 得研究优化, 果 泡对比, 杂度
是O(n2 ),其实可以 分层拆分 法去重, 杂度为O(n),这里就不详细展开讨论了。

图7.1 顶 位
图7.2 顶 法线

图7.3 顶

图7.4 顶 纹 标

7.2.2 索引数据合并
分3D模 制作工具中,很 3D属性 ( 材质、光 组等)是以 为单位组织 ,并
且随机 序。 到GPU渲染性 优化, 某 具有相同3D属性 列到一起临近存 , 又以
索引形式 示, 此此步实际就是索引 序操作。 实际操作中,一 优先 材质种类进
序。
7.2.3 顶点数据排序
索引 材质 序后, 对每个 格进 顶 序是 常重 一步,是后 顶 优化
前 条件。 绝 情 下, 3D 模 中空 位 相近 顶 ,其属性 也比较相近, 于这个
计事实, 顶 存中 位 空 距离 序, 顶 属性 变化呈低 分布,这样
列更有利于压 优化。具 现为,后 差量 码 形成更 ,从 消 更 存 空 ,
也更利于最后 压 , 算术 码或zlib压 。此 ,顶 空 位 序后也 更
利 现代GPU 某 优化机制,比 Post T&L Cache、Early Z等。 上有很 关于顶 序 资
[1],这里就不展开 绍了。
7.2.4 子网格的拆分与合并
经过顶 序后 个 格可 有相同材质, 这 有相同材质 较 格合并
成一个更 格,以 省CPU绘制 令调 开销; 巨 格也有可 拆分成 个较
格,以 开硬件性 制。 格 合并比较简单,只 重新映 索引即可; 格 拆分稍微 杂
, 连 三 形进 拆分,尽可 证 格 空 上 连 , 拆分 过程中还 涉及拆分
顶 制, 细选择拆分边 ,尽可 制操作。 存 上,可以 格 空 计距
离存 同一个 件中,以 省 络 输连 建立和销毁 开销。
7.2.5 顶点数据编码压缩
以上步 成后,未经压 码 原 顶 、索引 就已 毕, 下来 对这 根 其精
度、 分布 征进 有 对性 有 压 或 压 ,以 低 尺寸。其关 于 细权 压 精
度和压 ,得到一个 条件下 意 中 ,同 顾 码算法, 别是 码算法 ,让
前端渲染 有极 。属性 码压 致分为 下几个步 。
(1)根 类 属性 精度、 分布 征, 类 属性 转 为 。此步 根
类 可为有 压 或 压 。
(2) 上一步 进 差量计算,存 差量 :

(3)对差量 进 zigzag和varint 码[2]或 算术 码[3]。此步 为 压 。


(4)进 zlib 压 。
第一步转 式对不同类 顶 属性 法不尽相同,下 详细 绍。
顶 位 码较 调试, 为顶 位 几乎是 意 , 且最低精度也不太 确
,一旦没有控制 ,就 觉上 很 影响。最常见 顶 映 为 式 下:
以上公式是 匀映 , 匀映 最适合 情 就是所有不同 上 匀分布,这样精度只
1/N即可,即 ,N为不同 量。 果轴向上 分布不 匀, 么 论 怎
样 ,映 精度 得不到 。比 ,x∈[0,1000], 500个不同 x , 90%
集中 [0,10]区 ,精度只到1/500, [0,10]区 是 不 。所以,精度 求和顶 位 某
个轴向上不同 分布 度紧 相关。
其中一种优化 案就是分段 匀映 , 划分为 个不等 连 区 , 这 分段区
序列 差尽可 ,再根 每段 不同 量和 差确 Scale 。原 根 其所属分段区
不同 Scale 进 码,这样就形成了一个 匀刻度 “标尺”, 分布 集 刻
度比较 , 分布稀 刻度也比较稀。顶 x、y、z三轴 标分别 各 “标尺”进 有
压 ,得到 果相比 匀精度 码 有较 提 。对第一步得到 进 差量计算, 形成绝
对 较 序列,再对此 序列进 zigzag和varint 码, 低 存 空 , 后进
zlib压 或算术 码得到最终 果。
法线和切线一 3个分量构成,每个分量一 一个float32类 示。 实际应 中,很
精度更低 float16、short 至byte类 来 示。 细观 发现,法线和切线 3个
分量并不是 意 组合, 是 一个关系 约束着 。也就是说,所有可 x、y、z
一个半径为1 上, 为 意 (x,y,z)∈[−1,1], 是分布 一个半边 为1 立 ,
即 只 一个byte类 来 示法线或切线 每一个分量,也 费很 空 。 此, 果先 法
线 或切线 映 到 标, 可 分布 上[4], 么, 相同精度下就 得到更 压

实际上,法线和切线可以 极 标 示为 (φ)和 位 (θ)形式, 果 经纬度分为
Nθ和Nφ等分, 么我 可以 意法线 或切线 映 成 j和k,j 示单位向量 经度 向上 索引,k
示单位向量 纬度 向上 索引, 图7.5所示。
分为 j× k , 上 意一 可以近 成一个矩形, 图7.6所示。

图7.5 分割样式

图7.6 分 误差

这个矩形 所有 映 为相同 j 和k , 压 阶段 恢 成 向中 红 向
量,可以看到压 后 误差最 是 向4个 向量, 这个误差可以 过和中 向量进 乘运算
得到两个向量之 , 此可以得到一个关系, 足一个最 误差 最 j 和k 是 。
上 这个公式就是给 一个最 误差、 等分 ,计算出每等 形 位 最 划
分成 等 。根 试,最 误差ε取 1.05°,Nφ取 120 ,Nθ 足最 误差 最 为248,
分为18778 ,为比较 想 ,Nφ 最 位可以作为切向量空 左右 标志。 实际操作 ,可以
Nθ(j) 存 组NTHETA_SERIES中, 码和 码法线和切线 繁计算 杂公式。 码代
码 下:
经过以上步 ,法线或切线 码成包含两个分量 向量(j,k),每个分量为1字 符号
。 于顶 顺序已经 位 列, 此相 法线或切线 标上有很 概 近或相等, 码后
成 (j,k)向量与前一个分别进 各分量 差量计算(第二步),得到 差 有 相同性,
比 匀分布 法线 码后 (j,k)差 是相等 ,这样 常有利于最后 zlib压
(第 步)。
uv 一 具有 下两个 征:
● 有 。一 u,v∈[0,1],即 平 (tiling), 也不 别 ,可以 计 格纹 标
[u min ,u max]和[vmin,vmax]。

● 精度有 。 格所关 材质 纹 尺寸有 ,一 不超过8196 素,uv 来索引 素,一 求


只 半 素精度即可, 此 最 尺寸 两 作为uv Scale。
先可以利 下公式 uv 从double或float类 映 为unsigned int类 :

其中,Scaleu,Scalev取纹 尺寸 两 。
后对 UV 进 差量计算。同 ,相 顶 UV 是相近 ,差量 分布 较 区
。 下来进 zigzag和varint 码, 较 较 存进 存 ,最后进 zlib压 。
顶 是很有可 极度压 顶 属性, 为 情 下,顶 于着 不带albedo贴
图 格,所以 单个 格上顶 一 变化较 , 图7.7所示。

图7.7 格顶

可以发现其 格顶 是单 ,或 种类较 , 这种情 下,可以利 调 板 码代替逐


存 ,这样 就变成了调 板索引, 越 ,压 越 。比 调 板包含256种 ,压
为25%。 种类较 情 下, 调 板 码可 不 带来 期 果。是否 调 板模式,
可以 下 式判 :
判 调 板 码不 带来更 压 情 下,可以 有 压 式, 证一
水平 础上 低 精度。 低精度一 有两种途径:
(1) 持 空 不变, 低 码精度。比 R8G8B8变为R5G6B5,直 sRGB或Linear
RGB空 成。
(2)变 到新 空 ,并 低 码精度。比 从 sRGB 空 变 到 YUV比或 HSB
空 , 后根 眼感知 性,对不同 分量进 精度 , 持或 量 度分量, 幅度 低
差分量, 果 细调 码算法,这种 式带来 上 可 更 。
当 码 成后, 分索引已经是单字 度了,可以直 zlib压 成操作。 合权重
具有 下 :
● 分情 下0 较 。一 只有1/4~1/2 顶 具有两个以上不为0 权重。
● 对精度 求不是 别 。经 试, 于 览查看 合,精度为1/1000 顶 合 果已经可
以 受。
戏中,顶 合一 化权重,即每个顶 所有权重分量和为1。一 每个顶 存 3
个 合权重分量,第4个分量可以 下 公式计算得出:

合权重优化 第一步是 下公式进 变 压 :

根 精度 求,Scalew一 取 [256,2047]。
变 毕后,第二步是分别对3个权重分量进 差量计算, 后逐分量进 zigzag和varint
码,最后进 zlib压 或 算术 码。 果 到 合权重0 比 情 ,压 果 常显

顶 合索引 一 4个分量构成, 常为unsigned int类 , 示影响顶 索引,
合索引 必须是精确 ,所以 法进 有 压 。 是 实际模 中索引 一 不 很 , 为
单个模 中起作 是有 , 太 影响动 更新 。 此, 戏这种 常追求 3D
应 ,其 术制作 严格 制 量, 别是单 格 量,这样单 格 顶 索引 就
具有 下两个 计 征:
● 索引 不 别 , 分情 下 上 于2048。
● 单 格中顶 合索引不同 量较 。比 一个 格 顶 索引 为{0,128,256,2048},
为[0,2048], 种类很 ,一 4种。
对以上两个 计 征,可以先对 合索引 进 映 操作,即 格中所有可 索引 映 为从
0开 紧 列 ,比 {0,128,256,2048}映 为{0,1,2,3}, 这个映 关系存 起来。 为
不同索引 量 单 格 是很 ,所以这个映 关系并不 占 太 存 空 (后 映
于着 器 矩阵 组 索引,从CPU更新 变 矩阵到正确 位 )。映 毕后再分别对4个 合
索引分量 差量计算,得到 列 常有利于进 zlib压 或算术 码。
顶 索引只 进 压 。 于三 形 是 空 位 进 序 ,索引 紧 相连,经过差量
操作后 zigzag变 压 ,即可 unsigned short或unsigned int转 为unsigned char,
后再进 zlib压 或算术 码 就 得到可观 压 比。

7.3 有利于渐进加载的数据组织方式
上 绍 顶 各类属性 码压 后, 下来 绍 存 组织这 压 。 本 循两个原
则:
● 尽可 之 赖,提 压 码并 度, 恢 。
● 对 觉影响最 优先 输, 其尽 开 压 码。
模 格拆分就 很 决 格并 压 问 , 为 格之 是互相 立 ,并没有逻辑上
赖关系, 且顶 不同 属性 是分开压 存 , 压 实际上也可以并 。 过实际应
发现,一 并 粒度拆 到 格和属性就已经 了,对于相同属性 分段压 存 求并不是
别强 , 为经过 格拆分后,不 存 顶 量 别 格。
意 是,利 线程并 析 幅度提升 ,短 量 GPU 资 创建 求,这
创建 求 匀分 到后 程序逻辑中;否则,单帧进 太 操作 前端 明显 卡
顿。
重 优先 输到客 端, 先 根 求 义这 优先级。根 3D 模 展示应
征, 顶 属性 优先级 列 下:
position=index>uv>blend weight=blend index>normal>color>tangent
这个优先级 列只是个 经 总 ,不同 应 可 不尽相同。 格最重 是空 形 ,其次是
材质, 此position和index 了uv之前;纹 作 于光 , 此 uv normal之前;顶
和法线纹 对 观 影响属于细 层 , 此切线( 来组成切向量空 ) 最后。 存 组织也
循此 则,区别于 几 存 件,即一个 件存 一个几 所有 息,这里 同种类
一起存 式,比 , 模 不同 格 顶 位 属性 存 同一个 件中, 顶 法线 存
另一个 件中,这样所有 格 位 优先 输 毕。当 ,也 件 量和尺寸,
络并发连 和连 利 达到 平 , 件太 太碎 络连 利 低, 件过 ,又
件尾 格 等待较 加载 析显示。
纹 也 循重 优先 输 原则,以PBR材质为 ,不同纹 对 觉 优先级 下:
albedo>roughness>normal>metalness>specularF 0
为了快速显示模 材质 观, 白模, 模 所有材质中同类纹 级mipmap 存
一个 件中,比 , 所有albedo贴图 级mipmap 存 一个 件中, 所有法线 级
mipmap 存 另一个 件中。这样 主 是让模 快速呈现 材质 观,细 可以随后
加载逐 加。根 实际 络 计速度决 纹 mipmap 包 件 ,一 控制 300KB之 ,
这样 分 络条件下,1~5s 即可 成 输,推 至 包到64×64分辨 ,以 证初 观 渲
染质量不 太低。单个mipmap 包 件最 可包含100张左右 常见压 格式纹 (pvrtc、etc、dxtc
等),这对于 分3D模 已经足 了。 尺寸纹 可以存 分开 单个图片 件中。

7.4 总结
本章 绍 优化 案利 实际模 精度、 分布等 征, 证一 水平 前提下,
达到了很 优化压 果。 不同 分布 性和实 可 受误差 ,各顶 分量 压 果一
图7.1 所示。

7.1 顶 分量压

该 案 算法设计上 顾了 和 性, 存 组织上 了 线程并 压 和 式 输


征。 此,该 案并不局 于Web环 ,对其 计算资 受 或追求极致 3D应 景也具有参
意义。

参考文献
[1]Pedro V.Sander,Diego Nehab,Joshua Barczak.Fast Triangle Reordering for Vertex
Locality and Reduced Overdraw.ACM Transactions on Graphics.Proc.SIGGRAPH,2007.
[2]Varint and ZigZag
Encoding.Google,2018.https://developers.google.com/protocol-buffers/docs/encoding.
[3]Arithmetic
Coding,Wikipdia,2018.https://en.wikipedia.org/wiki/Arithmetic_coding.
[4]J.Smith,G.Petrova,S.Schaefer.Encoding Normal Vectors using Optimized
Spherical Coordinates.Computers & Graphics.36,2012.
[1]本章相关 已 请技术专利。
第四部分 人工智能及后台架构

第8章 游戏AI开发框架组件behaviac和元编程
作者: 勇刚

摘要
behaviac是 戏AI 开发框架组件,也是 戏原 快速设计工具。 持 平台,适 于客 端
和 服 务 器 。 behaviac 实 现 了 为 树 ( Behavior Tree,BT ) 和 有 态 机 ( Finite State
Machine,FSM),以及分层 务 (Hierarchical Task Network,HTN)。
其中 为树 实现最为 , 过 优化 运 ,不 持序列、选择、并 、 器、动作等
,更 过附件 形式 持前 、后 、事件等功 。此 ,还提 了选择
( SelectorMonitor ) 等 。 behaviac 运 有 C++ 和 C# 两 个 版 本 。 运 反 技术
(Reflection),给 提 了最 性和可 展性, 不 展 就可以 达
各种 求; 过 成最终代码 形式 了 反 所 加 负担,从 提 了最 。C++版本
运 于 程(Meta Programming),提 了一 利 反 合 程 案。
读 可以直 behaviac作为 戏AI 开发框架组件来 ,也可以参 behaviac提 对 BT、
FSM、HTN 实现和优化思路,还可以 习到 于 程 反 技术。所有代码,包 辑器和运
开 。
本章 先概述了behaviac 工作原 、 为树 核心概念和behaviac对 为树 优化, 后从类
息 剖析了 程 behaviac中 。

8.1 behaviac的工作原理
behaviac 组件分为 辑器和运 , 辑器是 立运 程序,运 库 合到 己 项
中,各模 关系 图8.1所示。
图8.1 各模 关系图

● 工作区 于管 个项 ,包 类 息和 为树 件等。
● 类 息包 Agent类及其成员属性、成员 法和实 等,以及枚举和 构 类 。
● 为树描述了 Agent类 为,利 各种 和类 息来创建 为树。
● 运 端根 辑器导出 类 息,加载和 辑器导出 为树。
8.1.1 类型信息
类 息 来描述类 属性和 法。 辑器中可以 过 创建类 ,该类 息作为 本 语
法单位 来创建 为树。 下以 XML 格式描述了一个 Agent 类 息,可以 behaviac 中查看更
细 。类 息中 属性 来描述类 相应属性,比 康 、 击 等, 法 来 示类
力, 远程 击、逃 、跳跃等。
有了类 息,就可以 该类 息来描述 为了。
8.1.2 什么是行为树
为树(BT)是 为 组成 树 构, 图8.2所示。

图8.2 为树

对于 FSM,每个 示一个 态; 对于 BT,每个 示一个 为。同样是 连


成 ,BT有 么优势呢?
BT 中, 是有层次(Hierarchical) , 其 来控制。每个 有一
个 果(成功Success、 败Failure或运 Running),每个 果 其 来管 ,从
决 下来 么, 类 决 了不同 控制类 。 不 护向其 转 ,
模 性(Modularity) 强了。实际上, BT中, 于 不再有转 , 不再是 态
(State), 是 为(Behavior)。
此可见,BT 主 优势之一就是其更 性和模 性,让 戏逻辑更直观,开发 不
杂 连线 晕。
8.1.3 例子1
图8.3所示,3号Sequence 有3个 ,分别是4号Condition 、5号Action 和6号
Wait 。 3号 是2号Loop 。

图8.3 1

下 先 绍一下各 类 逻辑。
● Sequence(序列) :顺序 所有 返 “成功”, 果某个 败,则返
“ 败”。
● Loop(循环) :循环 到 次 后返 “成功”, 果循环次 为−1,则 循
环。
● Condition(条件) :根 条件 比较 果,返 “成功”或“ 败”。
● Action(动作) :根 动作 果返 “成功”、“ 败”或“运 ”。
● Wait(等待) :返 “运 ”,一直到 过去后返 “成功”。
8.1.4 执行说明
说明 下:
● 果4号条件 果是“成功”,其 3号序列 则 5号动作 , 果5
号 返 “成功”,则 6号等待 , 果6号 返 “成功”,则3号 毕且 返
“成功”, 么2号循环 下一个迭代。
● 果4号条件 果是“ 败”,其 3号序列 则返 “ 败”,不再
,并且2号循环 下一个迭代。
8.1.5 进阶
上 中只讲了成功或 败 情 , 果动作 持 一段 ,比 5号动作 ,Fire
持 一段 呢?
● 果可以是“成功”、“ 败”或“运 ”。
● 对于持 运 一段 Fire动作,其 果持 返 “运 ”, 束 返 “成功”。
● 对于持 运 一段 Wait 动作,其 果持 返 “运 ”,当等待 到达 返
“成功”。
当 持 返 “运 ” ,BT “知 ”该 是 持 “运 ” ,从 后 过
程中“直 ” 该 , 不 从 开 ,直到该运 态 返 “成功”或“
败”,从 后 。从 看,就 “ ” 了 个“运 ” 上,其 不再管
, 一直等运 束 ,其 再次 管一样。
请 意,这一段说明只是从概念上讲 , 概念上可以这样 ,实际上,即 于运 态
每次 也是 返 ,只是其返 是“运 ”,其 对于返 是运 态 , 其
,所以看上去 不再管 一样。
8.1.6 例子2
图8.4所示, 为 了 晰 说明运 态,我 再 来看一个 。 这个 中,Condition、
Action1和Action3是3个 。
● 0号 是一个Loop ,循环3次。
● 1号 是一个Sequence 。
● 2号 模 一个条件,直 返 “成功”。
● 3号 Action1是一个动作,直 返 “成功”。
● 4号 Action3同样是一个动作,返 3次“运 ”, 后返 “成功”。

图8.4 2

其代码 下:
该BT C++代码 下:

上 为树 代码就 同 戏更新 分。status = g_player->btexec() 戏 更新


(update或tick)中, 每帧 调 。
别 ,对于运 态,即 从概念上讲运 态是“ ” 了 上, 每帧 调
btexec。也就是说,其 是每帧 运 ,只是下一帧是 上一帧 ,从 现 是运
态, 其 束之前,其 不 控制转移给其 后 。这里 “ ”并 真 ,并 后
代码(上 其 代码 分)不 。status = g_player->btexec()后 果有代码,则

果 是 么样 呢?
果 图8.5所示。
第1帧: 图8.6所示,2号Condition 返 “成功”, 3号Action1 ,同样返 “成
功”, 4号Action3 ,返 “运 ”。

图8.5 果

图8.6 果1

第2帧: 图8.7所示, 于上一帧4号Action3 返 “运 ”,所以直 4号Action3



第3帧: 图8.8所示, 于上一帧4号Action3 返 “运 ”,所以直 4号Action3

意 是 , 2 号 Condition 不再 。 且 , 本 次 Action3 返 “ 成 功 ”,1 号
Sequence 返 “成功”。0号Loop 束第1次迭代。
第4帧: 图8.9所示,Loop 第2次迭代开 ,就 第1帧 一样。

图8.7 果2

图8.8 果3

图8.9 果4

8.1.7 再进阶
持 返 运 态 优化了 , 其 果就 “ ”了 BT 一样, 果发 了
其 “重 ” 事情 怎么办?
behaviac中有 种 法,比 可以 前 附件、并 、选择 控 和事件 树等
法来 和响应事件,具 可以参 https://github.com/Tencent/behaviac/上提 章、 程
和示 。
8.1.8 总结
为树 本概念总 下:
● 每个 有一个 果(成功、 败或运 )。
● 果 其 控制和管 。
● 返 运 果 作 于运 态, 于运 态 持 ,一直到其返
束(成功或 败)。 其 束前,其 不 控制转移到后 。
其中 运 态是 为树 关 ,也是 为树 关 。
下BehaviorTask::exec代码是更新 为树 核心代码。
● 当 次 , 为 态是BT_INVALID, onenter_action,只有当onenter_action返
true ,否则直 返 。
● 后 update_current, 果 update_current 返 BT_RUNNING,则意味着该
于运 态,下一帧 ,所以该 过 SetCurrentTask 记录下来,从 后 直

● 否则, 果 update_current 返 不 是 BT_RUNNING , 则 意 味 着 该 束,
onexit_action 。
● update_current 是 ,不同类 有其 殊 实现, 别
BranchTask::update_current 合 SetCurrentTask 来直 于运 态 。具 细 请
参 https://github.com/Tencent/behaviac/。

8.2 元编程在behaviac中的应用
运 库 来加载 辑器导出 类 息和 为树,从 和 为树逻辑。以C++版本为
, 辑器根 类 息 成相应 C++代码, 下所示。

同 成类 于下 册代码, 类 息 册到AgentMeta这样 类 息库中,从 后 可以


加载 为树以及 为树。
加载 XML 格式 为树 ,有类 于下 代码, 过类 名字、属性以及 法 名字,从
AgentMeta这样 类 息库中 取相应 义来加载属性和 法。

此可知,每一个Agent类 有一个相应 AgentMeta来存 其类 息, 过该Agent 名字,


每一个属性 名字、类 ,每一个 法 名字、参 、返 等 息, 为树和具 类 合起
来,从 加载 过名字 类 。
8.2.1 模板特化
C++中模板分为 模板和类模板。
模板是一种 象 义, 代 一类同构 ;类模板是一种更 层次 象 类 义。
所谓 化,就是 泛 模板具 化,为已有 模板参 进 一 其 殊化 , 得以前不受
约束 模板参 ,或 受到 ( ,const 身一变成为了 之类 , 至是经过其 模
板类包 之后 模板类 ),或 下来。
8.2.2 加载中的特例化
下所示, 加载 为树 ,StringUtils::ParseString 来从一个字符串中读取相应类

ParseString 义 下:

这里广泛 了模板 化(Template Specialization),当模板 译 , 动根 类 是否


是枚举类 、是否包含FromString来选取合适 化。
么,怎么判 一个类 是否是Enum呢? 上 代码中 到了Meta::IsEnum,其具 实现 下:
果 T 是一个 Enum, 么 IsEnum::Result 等于 true,否则等于 false。 对于 Meta::Has
FromString,则只是直 到了类 化,HasFromString 泛 下:

也就是说, 省情 下,对于所有类 ,HasFromString::Result 等于 0; 对于 Agent类


, 为其具有FromString,所以对该类 提 下:

IsEnum、HasFromString这样 模板类 称为type traits类 ,behaviac 过type traits类


, 合 模 板 实 现 了 众 有 趣 功 。 更 实 际 示 , 请 参
https://github.com/Tencent/behaviac/。
8.2.3 运行中的特例化
代码 下:
FirstAgent有一个int类 属性p1和一个 Say,behaviac 成上 册代码和下 化
代码。

当 为树调 Say ,下 代码 ,同样利 了 化。


总之, behaviac 中 广 泛 了类 模板 化,更 实际 示 ,请参
https://github.com/Tencent/behaviac/。
第9章 跳点搜索算法的效率、内存、路径优化方法[1]
作者:

摘要
本章 绍跳 索(JPS)算法 、 线程、 存、路径等优化 法,JPS 过拓展跳 不是
每个 居来寻路, 为跳 远比 居 ,所以寻路速度远快于A*。
本章 绍 优化算法 来加速跳 寻找,或 拓展 跳 。JPS-Bit 位运算加
速跳 寻找, 图 每个格 码为1个bit, 此1个int可以存 32个格 。 后利 CPU 令
__builtin_clz找到32个格 里 跳 , 此寻找跳 速度比 历32个格 快几十 。JPS-Prune利
剪枝剪掉 必 “中 跳 ”,“中 跳 ” 拓展中只具有简单 承 作 ,不具 拓展价
,剪枝“中 跳 ”可以 拓展 跳 ,从 加速寻路。 为“中 跳 ”是路径中沿对
线 向 拐 ,找 路径后 路径中 计算“中 跳 ”,构成 路径。JPS-Pre利 提
前计算每个格 上、下、左、右、左上、右上、左下、右下 8个 向 最 step,step为走到最近跳
、 、边 距离。JPS-Pre 须沿各 向寻找跳 , 是根 step快速确 跳 ,从 加
速寻路。
三种优化算法可以组合 ,实 中 , JPS-Bit 、 JPS-BitPrune 、 JPS-BitPre 、 JPS-
BitPrunePre 寻 路 速 度 分 别 为 A* 算 法 81 、 110 、 130 、 273 。另 , 变量 明为
thread_local可 持 线程寻路, 每个线程 有一个thread_local变量, 导致 存 量显
加, 过分层、 存池等 法优化 存。JPS 寻找 路径 现上并不是最优 路径, 过
后 对路径进 优化。
JPS算法可以 应 2D、3D 戏中 和NPC 寻路上。2D 戏可以直 应 JPS算法,很 3D
戏也 2D寻路, 此也可以直 应 该算法。

9.1 引言
寻路算法 戏和 图中有 种 途。A*算法已经众所周知,对于其优化也是层出不穷 , 性
并没有取得突破性进展。本章 绍JPS 、 线程、 存、路径等优化算法。 性 实 中设 寻路
景, 得起 和终 差距200个格 , 计寻路10000次 总 。
不同算法 寻路 9.1所示,A* 费260.740s; 础版 JPS 费17.037s;位运算优化
JPS(JPS-Bit) 费3.236s;位运算和剪枝优化 JPS(JPS-BitPrune) 费2.37s;位运算和
JPS(JPS-BitPre) 费2.004s;位运算、剪枝和 JPS(JPS-BitPrunePre) 费0.954s。
本章 绍 JPS-Bit和JPS-BitPrune 持动态 。本章 决了绝 分开 JPS算法存
Bug:穿越 (比 图9.2中,从H走到K ,穿越H右边 )。

9.1 不同算法 寻路
实 中 , JPS 算 法 5个版本,平 费 分 别 约 为 1.7ms 、 0.32ms 、 0.23ms 、 0.2ms 、
0.095ms,寻路速度分别约为A*算法 15 、81 、110 、130 、273 。 2012—2014年举办 三
届( 前为止只有三届) 于 Grid 格 寻 路 比 赛 ( The Grid-Based Path Planning
Competition,GPPC)中,JPS已经 证明是 于 权重格 , 没有 情 下寻路最快 算法。
下来 绍JPS 、 线程、 存、路径等优化算法。 后 读GPPC 2014,从寻路 、路
径 度、消 存、 败 等 比较22种参赛寻路算法 优劣。

9.2 JPS算法
9.2.1 算法介绍
JPS(Jump Point Search,跳 索)算法是2011年提出 于Grid 格 寻路算法[1] 。JPS算
法 A*算法框架 同 ,优化了A*算法寻找后 操作。 图9.1所示为A*和JPS算法 程对
比,不同于A*算法中直 取当前 所有 关闭 可达 居 来进 拓展 策 ,JPS 算法根
当前 向、 于 索跳 策 来 展后 , 循“两个 义、三个 则”(两个 义确 强
迫 居、跳 ,三个 则确 ) 拓展策 。
9.2.2 A*算法流程
A*算法 程 下。
(1) 起 start加 开启 集合openset中。
(2)重 以下工作:
① 当openset为空 ,则 束程序,此 没有路径。
② 寻找openset中F 最 ,设为当前 current。
③ 从openset中移出当前 current。
④ 关闭 集合closedset中加 当前 current。
⑤ current为 标 goal,则 束程序, goal 开 逐级追溯路径上每一个 x
parent(x),直至 溯到起 start,此 溯 各 即为路径。
⑥ 对于 current 8个 向 每一个 居 neighbor: 果 neighbor 不可 过或 已经
closedset 中,则 过; 果 neighbor 不 openset 中,则加 openset 中; 果 neighbor
openset中, 此路径G 比之前路径 ,则neighbor 更新为current,并更新G 、F ,G
示从起 到当前 路径代价,H 示不 不可 过区 ,从当前 到终 路径代价,且
F=G+H。
图9.1 A*和JPS算法 程对比

各术语参 下。
● current:当前 。
● openset:开启 集合,集合 有待进一步拓展。
● closedset:关闭 集合,集合 不再拓展。
● neighbor:当前 居。
● parent(x): x 。
9.2.3 JPS算法流程
JPS算法 程 下。
(1) current当前 向是直线 向:
① 果current左后 不可走且左 可走(即左 是强迫 居),则沿current左前 和左 寻找
不 closedset中 跳 。
② 果current当前 向可走,则沿current当前 向寻找不 closedset中 跳 。
③ 果current右后 不可走且右 可走(右 是强迫 居),则沿current右前 和右 寻找不
closedset中 跳 。
(2) current当前 向为对 线 向:
① 果current当前 向 水平分量可走( ,current当前 向为东北 向,则水平分量为
东, 直分量为北),则沿current当前 向 水平分量寻找不 closedset中 跳 。
② 果current当前 向可走,则沿current当前 向寻找不 closedset中 跳 。
③ 果 current 当 前 向 直 分 量 可 走 , 则 沿 current 当 前 向 直分量寻找不
closedset中 跳 。
9.2.4 JPS算法的“两个定义、三个规则”
义一:强迫 居(Forced Neighbour)。
果 n是x 居,且 n 居有 ,并且parent(x),x,n 路径 度比其 从
parent(x)到n且不经过x 路径短,其中parent(x)为路径中x 前一个 ,则n为x 强迫 居,x为
n 跳 。 , 图9.2中,寻找从S到E 路径 ,K为I 强迫 居,I为K 跳 。这里不认为从H到K
走,否则 走进H右边 区, 分JPS开 代码认为H到K 直 到达,所以存 穿越 情
。 果 H到K可走,则K为H 强迫 居,H为K 跳 。
图9.2 寻路问 示 景(5×5 格)

义二:跳 (Jump Point)。


① 果 y是起 或 标 ,则y是跳 。 , 图9.2中,S是起 也是跳 ,E是 标
也是跳 。
② 果 y有强迫 居,则y是跳 , I是跳 。
③ 果从parent(y)到y为对 线移动,并且y经过水平或 直 向移动可以到达跳 ,则y是跳
。 , 图9.2中,G是跳 。 为parent(G)为S,从S到G为对 线移动,从G到跳 I为 直 向
移动,I是跳 ,所以G也是跳 。
则一:
JPST算法 索跳 , 果直线 向(为了和对 线区分,直线 向代 水平 向和 直 向,
且不包 对 线等 线 向,下 所说 直线 为水平 向和 直 向)、对 线 向 可以移动, 么
先 直线 向 索跳 , 后再 对 线 向 索跳 。
则二:
① 果从parent(x)到x为直线移动,n是x 居, 有从parent(x)到n且不经过x 路径,且
路径 度 于或等于从parent(x)经过x到n 路径,则走到x后下一个 不 走到n。
② 果从parent(x)到x为对 线移动,n是x 居, 有从parent(x)到n且不经过x 路径,
且路径 度 于从parent(x)经过x到n 路径,则走到x后下一个 不 走到n。
则三:
只有跳 加 openset中,最后寻找出来 路径 也 是跳 。
9.2.5 算法举例
图9.2所示,5×5 格,黑 代 区,S为起 ,E为终 ,JPS算法寻找从S到E 最短路
径。
先 起 S加 openset 中 。 从 openset 中 取 出 F 最 S , 并 从 openset 中 删 ,加
closedset中。S 当前 向为空,则沿8个 向寻找跳 , 该图中从S出发只有下、右、右下3个 向
可走, 向下 索到D 到边 ,向右 索到F 到 , 此 没有找到跳 。 后沿右下 向寻找跳
, G ,根 上 中“ 义二” 第3 ,parent(G)为S,从parent(G)到S为对 线移动,并且
G经过 直 向移动(向下移动)可以到达跳 I, 此G为跳 并加 openset中。
从openset中取出F 最 G,并从openset中删 ,加 closedset中。G 当前 向为对 线
向(从S到G 向),沿右(当前 向水平分量)、下(当前 向 直分量)、右下(当前 向)3个
向寻找跳 。 G 只有向下可走, 此向下寻找跳 ,找到跳 I并加 openset中(根 上 中“
义二” 第2 )。
从openset中取出F 最 I,并从openset中删 ,加 closedset中。I 当前 向为直线
向(从G到I 向), I I 左后 不可走且左 、前 可走, 此沿左 、左前 、前 寻找跳
, 左前 、前 到边 ,只有向左 寻找到跳 Q并加 openset中(根 上 中“ 义二”
第2 )。
从openset中取出F 最 Q,并从openset中删 ,加 closedset中,Q 当前 向为直线
向,Q 左后 不可走且左 、前 可走, 此沿左 、左前 、前 寻找跳 , 左前 、前 到
边 ,只有向左 寻找到跳 E并加 openset中(根 上 中“ 义二” 第1 )。
从openset中取出F 最 E,E是 标 ,寻路 束,路径是S,G,I,Q,E。
注意:这里不考虑从H能走到K 况,因为 角线 向 阻 ,如 需要H到K能 到达,则路
S,G,H,K,M,P,E,修 跳 计 即可,但在 中如 H到K能 到达,则会 越H右边 阻 。
上述JPS算法 寻路 是明显快于A*算法 , 从S到A沿 直 向寻路 , A , 果 A*算
法, F、G、B、H 加 openset中, 是 JPS算法中,这4个 不 加 openset中。 为S,A,F
路径 度比S,F路径 ,所以从S到F 最短路径不是S,A,F。同 ,S,A,G也不是最短路径,根 上
中“ 则二” 第1 ,走到A后不 走到F、G,所以F、G不 加 openset中。 S,A,H是从S到H 最
短路径, 是 为存 S,G,H 最短路径且不经过A,根 上 中“ 则二” 第1 ,从S走到A后,下一
个走 不 是H, 此H也不 加 openset中。根 上 中 “ 则三”,B不是跳 ,也不 加
openset中。实际上, 从S到E 寻路过程中,进 openset中 只有S、G、I、Q、E。
图9.3所示为 A*和JPS 算法 寻路消 中 对比,其中 D.Age:Origins、D.Age2、StarCraft
分别代 戏《龙 世纪:起 》《龙 世纪2》《星际争 》 景 图 集 合 ;M.Time 示操作
openset和closedset ;G.Time 示 索后 。可见 A*算法 约有58% 操
作openset和closedset,42% 索后 ; JPS算法 约有14% 操作openset和
closedset,86% 索后 。 openset中加 太 ,从 过 护最 (
、删 杂度 为O(logn)),是JPS算法比A*快 原 。

图9.3 A*和JPS算法 寻路消 对比

9.3 JPS算法优化
9.3.1 JPS效率优化算法
JPS-Bit 过位运算加速寻找跳 。JPS-Bit和JPS-BitPrune 持动态 ,当动态 出现
, 格 标记为 ;当动态 消 , 格 标记为 。 图9.4所示,黑 分为 ,
设当前位 为I,当前 向为右,1代 不可走,0代 可走,则I当前 B 8个格 可 8个
bit:00000100 示,I 上一 B− 为 00000000,I 下一 B+ 为 00110000 。 CPU 令
__builtin_clz(B)(返 前导0 个 ) B 寻找 位 ,可得当前 第5个位 (从0开
)。 __builtin_clz(((B−>>1)&& !B−)||((B+>>1)&& !B+))寻找B 跳 , ,
本 中 ( B+>>1 ) && !B+ 为 ( 00110000 >> 1 ) && 11001111 , 即 00001000, ( B−>>1 ) &&!B 为
00000000,__builtin_clz ( ( ( B−>>1 ) && !B− ) || ( ( B+>>1 ) && !B+ ) ) 为
__builtin_clz(00001000)为4,所以跳 为第4个位 M。

图9.4 寻路问 示 景(3×8 格)

JPS-BitPrune JPS-Bit 础上 剪枝优化,剪掉不必 中 跳 (见上 中“ 义二” 第3


)。中 跳 拓展过程中只具有承 作 ,不具 拓展价 , 中 跳 加 openset中 加
拓展 次 , 此JPS-BitPrune 中 跳 删 ,并 中 跳 后 跳 跳 为中 跳
跳 。
JPS-BitPrune 找到 路径中加 拐 (中 跳 ), 得每两个相 路径 之 是
直、水平、对 线 向可达 。 设 前找到 路径为start(jp1),jp2,jp3,…,jpk,end(jpn),对
于每两个相 跳 jpi、jpi+1:
① 果jpi、jpi+1 x 标或 y 标相等,则说明这两个跳 同一个水平 向或 直 向,可以
直线到达, 须 这两个跳 之 加 拐 。
② 果jpi、jpi+1 x 标和y 标 不相等, 么:
● 果x 标 差dx(即jpi x 标 去jpi+1 x 标)和y 标 差dy 绝对 相等,则说明这两
个跳 对 线 向直线可达, 须 这两个跳 之 加 中 跳 。
● 果dx和dy 绝对 不等,则说明这两个跳 对 线 向不 直线可达,此 jpi、jpi+1之
就 加 中 跳 ,即jpi沿对 线 向走min(dx,dy)到达 。
图9.5所示,起 为S(1,1), 1、4、6 为中 跳 —— 为 2、3是 足“ 义二”
跳 ,所以 1是为了到达 2、3 中 跳 ;同 , 4、6也为中 跳 。 剪枝中 跳 之
前, 中 跳 后 调 为该中 跳 。 1 后 跳 为 2、3、4,
其中 4也为中 跳 ,删掉中 跳 1后, 2、3 跳 1 为 S;删 中 跳
4后, 4 后 跳 5 跳 4 为 S( 4 跳 为 1, 1已经 删
, 此 溯到 S);删 中 跳 6后, 6 后 跳 7 跳 6 为 S( 6
跳 为 4, 4已经 删 , 4 跳 1也 删 了, 此 溯到 S)。

图9.5 JPS-BitPrune 剪枝优化示

寻路中,从 S寻找跳 , 先找到中 跳 1, 后 水平 向和 直 向寻找到跳


2、3, 2、3 跳 设为 S; 沿对 线 向寻找跳 ,走到 4后,沿水平 向和 直
向寻找到跳 5, 5 跳 设为 S; 沿对 线 向寻找跳 ,走到 6后,沿水
平 向和 直 向寻找到跳 7, 跳 7 跳 设为 S。 此 , JPS-BitPrune 得路径
S(1,1)、 7(4,6)。 为路径中S(1,1) 法沿 直 向、水平 向、对 线 向走到
7(4,6), 加 中 拐 。根 上述 拐 加策 , 6(4,4)可作为中 拐 。 此,JPS-
BitPrune构建 路径为S(1,1)、 6(4,4)、 7(4,6)。
下 过对比剪枝前后从 S到 7 寻路过程,来说明剪枝 优化 。
不剪枝中 跳 :
(1)从 S 索跳 ,找到跳 1,此 openset中只有 1。
(2)从openset中取出F 最 跳 1,并 索 1 后 跳 ,沿水平 向和 直 向找到
跳 2、3,沿对 线 向找到跳 4,此 openset中有 2、3、4。
(3)从openset中取出F 最 跳 4,并 索 4 后 跳 ,沿水平 向和 直 向找到
跳 5,沿对 线 向找到跳 6,此 openset中有 2、3、5、6。
(4)从openset中取出F 最 跳 6,沿 直 向找到跳 7,此 openset中有 2、3、
5、7。
(5)从openset中取出F 最 跳 7,为 , 索 束, 此 路径为
S(1,1)、 1(2,2)、 4(3,3)、 6(4,4)、 7(4,6)。
剪枝中 跳 :
(1)从 S寻找跳 , 先找到中 跳 1, 后沿水平 向和 直 向寻找到跳 2、
3, 2、3 跳 设为 S; 沿对 线 向寻找跳 ,走到 4后,沿水平 向和 直 向
寻找到跳 5, 5 跳 设为 S; 沿对 线 向寻找跳 ,走到 6后,沿水平
向和 直 向寻找到跳 7, 跳 7 跳 设为 S; 沿对 线 向寻找跳 , 到 , 索
终止,此 openset中有 2、3、5、7。
( 2 ) 从 openset 中 取 出 F 最 跳 7,为 , 索 束,此 得 路径为
S(1,1)、 7(4,6)。不同于 剪枝 JPS算法 拓展中 跳 1、4、6, JPS-BitPrune中,
1、4、6作为中 跳 剪枝,有 了 拓展,寻路 得到 提升。
JPS-BitPre 旧 JPS-Bit中 位运算, 其中 则是对每个 存 8个 向最 走 步
step 。 果 图 是N × N,每个 向最 走 步 short 示,则存 空 为
N×N×8×16bit, 果N 为1024,则存 空 为16MB。 于存 空 占 较 , JPS-BitPre
权 是否以空 。另 ,1024× 1024个格 图 1s ,2048× 2048 图
为1 左右。JPS-BitPre和JPS-BitPrunePre 不 持动态 , 为动态 导致8个 向
最 走 步 发 变化。
step 跳 、 、边 等决 , 果 到跳 ,则 step 为走到跳 步 ;否则,step为走
到 或边 步 。 图9.6中 N ,向上最 走到 8,step为2;向下最 走到 4,step为
4;向左最 走到 6,step为3;向右最 走到 2( 2是 足“ 义二”第2 跳 ),step为
5; 向 左 上 最 走到 7,step 为 2; 向 右 上 最 走到 1( 1是 足“ 义二”第3 跳
),step为1;向左下最 走到 5,step为3;向右下最 走到 3( 3是 足“ 义二”第3
跳 ),step为3。
下 过对比 前后从 N到 T 寻路过程,来说明 优化 。

图9.6 JPS-BitPre寻路 景示

JPS-Bit:
(1)从openset中取出 N,沿8个 向寻找跳 , 1、3、11是 足“ 义二”第3 跳
,加 openset中; 2是 足“ 义二”第2 跳 ,加 openset中。
(2)从openset中取出F 最 11,沿 直 向找到跳 T,加 openset中。
(3)从 openset 中取出 F 最 T,为 , 索 束,路径为 N(4,5)、
11(3,4)、 T(3,3)。
JPS-BitPre:
(1)从openset中取出 N,沿8个 向寻找跳 ,根 得到 各 向 step,可以快速确
8个 向最远 到达 {1,2,3,4,5,6,7,8}, 图9.6所示, 1、2、3 为 足“ 义二”
跳 ,直 加 openset中。 后判 终 T位于以N为中心 下 、左下 、左 、左上 、上 哪
分, 为T位于左下 ,只有 5位于左下 , 此 4、6、7、8直 过。 从N到5 向上,
step 为 3 , N和T x 标差绝对 dx 为 1,y 标差绝对 dy 为 2 , 从 N到 5 向上走
min(dx,dy),得到 11,加 openset中。
(2)从openset中取出F 最 11,沿 直 向找到跳 T,加 openset中。
(3)从 openset 中取出 F 最 T,为 , 索 束,路径为 N(4,5)、
11(3,4)、 T(3,3)。
过对比发现,JPS-BitPre和JPS-Bit找到 路径是一样 。 , 于JPS-BitPre 须 每一步
拓展过程中 沿着各 向寻找跳 , 是根 step 快速确 openset 选 ,从 提
了寻路 。
图9.7所示,寻路算法 法找到从S到E 路径, 败寻路 费 远 于成功寻路 费 ,
为 败情 下 历所有 路径。为了 这种情 , 每次寻路之前, 先判 起 和终 是否
可达: 果起 和终 同一个连 区 ,则起 和终 可达,否则不可达。只有起 和终 可达,
去寻路。

图9.7 不可达 两 S、E

先计算Grid 格 连 区 ,算法 下:
只 度优先 索, 度优先 索 递归层次太 , 导致栈 出。 图9.7所示 S、1、2
连 区 号 为1, 3、4、E 连 区 号 为2,S、E连 区 号不同, 此S、E不 同一个
连 区 ,不 寻找路径。
计算连 区 算法 下。
(1) 当前连 区 号num初 化为0。
(2)对Grid 格 每个 current重 以下工作:
① num++。
② 果current是 ,则跳过。
③ 果current 访问过,则跳过。
④ current 连 区 号记为num,标记已访问过。
⑤ 度优先 索和current 连 所有 ,连 区 号 记为num,并标记已访问过。
openset 最 实现,最 底层 构是一个 组,最 、删 、查找 杂
度 为O(logn)。JPS算法 繁 openset和closedset中判 跳 是否存 , 此这里 以空
法对最 查找进 优化, 查找 杂度 为O(1)。
对于1km × 1km 图,构建2000× 2000 二 组matrix, 组 每个 素pnode 为一个
, 对象类 包 ID、是否 展过(expanded,即是否 closedset中)、G 、F 、 跳
parent、 最 中 索引index等12个字 。 果 图(x,y) 是 索到 跳 , 么 先检
查 matrix(x,y) 是否为空, 果为空,则 示该跳 之前未 索过,从 存池中new出一个
跳 , 加到最 openset中,并 shift up、shift down之后,matrix(x,y).index记
录跳 最 中 索引; 果不为空,则 示该跳 之前 索过, 先检查expanded标记, 果标记
为 真 , 则 示 closedset 中 , 直 跳 过 该 跳 ; 否 则 , 果 matrix ( x,y ) 和
openset(matrix(x,y).index) 相等,则 示 openset中。 戏服务器普 单进程 线
程架构,为了 持 线程JPS寻路, 一 变量 明为线程 有thread_local。 ,上 中提到
为了优化openset和closedset 查找速度,构建 二 跳 组matrix。该 组必须为线程
有;否则,不同线程 寻路 , matrix 素 向 跳 , 导致寻路 误。 ,A线程
展 跳 后, expanded标记为真,B线程再试图 展该跳 ,发现已经 展过,就直 跳过。
9.3.2 JPS内存优化
果 0.5m × 0.5m 格 粒度,每个格 占1bit,则1km × 1km 图占 存 约为
2000× 2000/8字 ,即0.5MB。为了 上、下两个 向也 过取32位 得32个格 息,
存 图 转90°后 息。上 中不可达两 提前判 , 存 连 息, 设连 区
最 为15个,则 存 为2000× 2000/2字 ,即2MB。 么,总 存 为:原 图 息
0.5MB、 转 图 息0.5MB、连 息2MB,即3MB。
另 ,为了优化openset和closedset 查找速度,构建二 跳 组matrix, 为2000 ×
2000×4字 ,即16MB。为了 持 线程,该matrix 组必须为thread_local,16个线程 存
为16× 16 MB即256MB, 存空 太 , 此 优化这 分 存。
先 2000× 2000 分 成 20× 20 个 ,每 为 100 × 100 。 20× 20 个 为第一层 组
firLayerMatrix,100 × 100为第二层 组secLayerMatrix。firLayerMatrix 400个 素为400个
,每个 初 化为空,当 历到 跳 属于 firLayerMatrix(x,y) ,则从 存池中 new
出100 × 100 secLayerMatrix,secLayerMatrix 每个 素也是一个 , 向从 存池中new出
一个跳 。
, 索 2000× 2000 个 格 图 , ( 231,671 ) 位 找到一个跳 , 先检查
firLayerMatrix ( 2,6 ) 位 是否为空, 果为空,则 new 出 100 × 100
secLayerMatrix。 secLayerMatrix(31,71) 检查跳 是否为空, 果为空,则从
存池中 new 出跳 ,加 openset中;否则,检查跳 expanded标记, 果标记为真,则 示
closedset中,直 跳过该 ;否则 示 openset中。
戏 中 NPC 寻 路 为短距离寻路, 此可以 JPS 寻 路 区 制 为 80× 80 , 一 个
secLayerMatrix是100 × 100, 此JPS寻路区 可 一个secLayerMatrix 示。 么,两层matrix
为:20× 20 ×4字 +100 ×100×4字 ,即0.04MB。 16个线程下,总 存 为:原 图
息0.5MB、 转 图 息0.5MB、连 息2MB、两层matrix 0.04MB× 16, 3.64MB。 戏中
景最 不到20个,所有 景JPS总 存 不到72.8MB。
寻路 ,每次 一个跳 加 openset中, new出对应 跳 对象, 跳 对象中存
ID、 、寻路消 等 12个字 。为了 存碎片,以及 低 繁new 消 , 管
存池。每次new 对象 , 从 存池中 请,为了 止 存池 过 , 制 索步 。 存
池是 真正 存之前,先 请分 一 量 、 相等(一 情 下) 存 作 。当有新
存 求 ,就从 存池中分出一 分 存 , 存 不 再 请新 存。
这里 存池 有两个:
(1)跳 存池,初 为800个跳 ,当new出 跳 超出800个 ,即 止寻路。 设
NPC 寻路上 距离是20m,则寻路区 积是40m × 40m,格 为80× 80即6400个,经 计跳
占所有格 比 不到1/10,即跳 于640个, 此800个跳 足 了, 占 存
800字 × 12,即9.6KB,忽 不计。
( 2 ) secLayerMatrix 向 100×100×4 字 存池, 为每次寻路 至 一个
secLayerMatrix , 果每次寻路 重新 请,寻路 后再 ,则 造成开销。 此,
secLayerMatrix 向 100×100×4 字 空 也 存 池 中 , secLayerMatrix 存池占 存
0.04MB。
9.3.3 路径优化
图9.8所示,A为起 ,C为终 ,B为跳 ,实线为JPS 索出来 路径, 线为 索过程。可以看
出,从A到C可以直线到达, JPS 索出来 路径却 转 一次, 戏 现上, 显得比较 怪。
此, JPS 索出来路径后, 现上对路径进 优化。比 JPS 索出来 路径有A、B、C、D、E、
F、G、H 8个 ,走到A , 样检查A、C是否直线可达, 果A、C直线可达,再检查A、D是否直线
可达, 果A、D直线可达,则 检查A、E, 果A、E直线不可达,则路径优化为A,D,E,F,G,H;走到D
,再检查D、F是否直线可达, 果D、F直线可达,则 检查D、G, 果D、G直线不可达,则路径优
化为A,D,F,G,H。 此类推,直到走到H。 为 样检查 速度很快, 约占JPS寻路 1/5, 且只
有当走到一个路 后, 样检查该路 之后 路 是否可以合并, 样 消 平摊 走 过程中,
此 样 消 可以忽 。
图9.8 路径优化案

9.4 GPPC比赛解读
9.4.1 GPPC比赛与地图数据集
于Grid 格 寻路一直是 广泛研究 问 ,也有很 已经发 算法, 是这 算法没有
相互比较过, 此 辨优劣, 选择算法也有很 。为了 决这个问 , 国丹佛
Nathan R.Sturtevant 授创办了 于 Grid 格 寻 路 比 赛 : The Grid-Based Path Planning
Competition,简称GPPC, 前已经 2012年、2013年、2014年举办过3次 [2] ,下 主 讨论GPPC
2014。
GPPC 比赛 图集 9.2所示, 图 主 分为 戏 景 图和 造 图。其中来 戏
景 图 有3类:Starcraft(《星际争 》)、Dragon Age 2(《龙 世纪2》)、Dragon
Age:Origins(《龙 世纪:起 》),3类 戏分别提 11、57、27张 图和29 970、54 360、44
414个寻路问 。

9.2 GPPC比赛 图集
来 造 图 也有3类:Maze(迷 )、Random(随机)、Room(房 ),这3类 分别提
和18、18、18张 图和145 976、32 228、27 130个寻路问 。6类 提 149张 图和334 078
个寻路问 。图9.9给出了3类 戏 景 图示 ,图9.10给出了3类 造 图示 ,其中黑 代
区,白 代 可 走区。 图 从100 × 100个格 到1550× 1550个格 ,6类 图 分布 图
9.11所示,横 标是格 ,纵 标是 图 ,最 图来 Dragon Age:Origins,最 图来
Starcraft和 造 。

图9.9 GPPC 3类 戏 景 图示

图9.10 GPPC 3类 造 图示
图9.11 GPPC 6类 图 分布

9.4.2 GPPC的评价体系
GPPC 相同 下运 参赛算法,其中CPU 是Xeon E5620, 核 器,2.4GHz主 ,
12GB 存。为了消 误差,GPPC 求对每种参赛 寻路算法 334 078个寻路问 上运 5 , 寻路
334 078×5,即1 670 390次,所以下 绍 总运 等 标 是寻路1 670 390次 果。其中运
包 加载 和寻路 , 并不计算 运 。
GPPC 义 下13个 标来评价寻路算法(其中,路径 示从起 到终 路径)。
● Total(s):寻路1 670 390次所 费 总 。
● Avg(ms):寻路1 670 390次 平 。
● 20 Step(ms):寻找到路径 前20步所 费 平 。该 标 量最快 久可以跟随路径,
实 互 戏中,该 标很重 。
● Max Segment(ms):每条路径最 段 寻路平 。该 标 量 实 互中,寻路算法
最差情 下 现。
● Avg Len:路径 平 度。 果A寻路算法 路径上 现 , 短路径上 现不 ;B寻路算法
路径上 现不 , 短路径上 现 ,则A 该 标优于B 标, 为Avg Len 加主 来 路
径。该 标 向于 路径上 现 寻路算法。
● Avg Sub-Opt:寻找到 路径 度/最优路径 度 平 。 果A寻路算法 路径上 现 ,
短路径上 现不 ;B 寻路算法 路径上 现不 , 短路径上 现 ,则B 该 标优于A
标, 为1 670 390次寻路 路径 是短路径。该 标 向于 短路径上 现 寻路算法。
● Num Solved: 1 670 390次寻路中,成功 。
● Num Invalid: 1 670 390次寻路中,返 误路径 。 误路径是 路径 相 路
法直线到达。
● Num Unsolved: 1 670 390次寻路中,没有寻找到路径 。
● RAM(before)(MB):寻路算法 加载 后,寻路之前占 存 。
● RAM(after)(MB):寻路1 670 390次后占 存 ,包 所有寻路 果占 存

● Storage: 占 硬 。
● Pre-cmpt(min): 费 ,图9.12中该列 字之前 “+” 示 并 计算进

图9.12 参加GPPC比赛 22种算法 果对比

9.4.3 GPPC参赛算法及其比较
到 前为止,参加GPPC比赛 算法 有22种,其中参加GPPC 2014 有14种,可 致分为 下4类:
(1)对A* 进, Relaxed A*(RA*)和A* Bucket。
(2)利 格 算法, Jump Point Search(JPS)和SubGoal Graphs。
(3) 先 成 意两 第一个路 压 库, SRC。
(4) 于 优先级 算法, Contraction Hierarchy(CH)。
图9.12给出了参加GPPC比赛 22种算法 果对比,其中前14种为参加GPPC 2014 算法。第1列
(Entry列)为算法名,其后13列给出了每种算法 13个 标上 现。第1列中 加粗 算法 示该算
法 某 标上达到帕累 最优,该算法所 加粗 标, 示帕累 最优 标。帕累 最优
示:没有其 算法 帕累 最优 标上 优于当前算法。 ,JPS(2012)帕累 最优 标为第6
个 标Avg Sub-Opt和第12个 标Storage, 示没有其 算法 这两个 标上 优于JPS(2012)。22
种算法没有严格 优劣关系,只是 不同 标上 现各有优势, 可 于对不同 标 具 求来
选择适合 己 算法。
下 给出所有 GPPC比赛中 得帕累 最优 算法,本章 绍 JPS算法位列其中。
● RA*(2014):第10个 标RAM(before)和第12个 标Storage帕累 最优。
● BLJPS(2014):第2个 标Avg、第6个 标Avg Sub-Opt和第12个 标Storage帕累 最优。
● BLJPS2(2014):第2个 标Avg、第6个 标Avg Sub-Opt和第12个 标Storage帕累 最优。
● RA-Subgoal(2014):第2个 标Avg和第12个 标Storage帕累 最优。
● NSubgoal(2014):第2个 标Avg、第6个 标Avg Sub-Opt和第12个 标Storage帕累 最
优。
● CH(2014):第2个 标Avg、第6个 标Avg Sub-Opt和第12个 标Storage帕累 最优。
● SRC-dfs-i(2014):第3个 标20 Step和第4个 标Max Segment帕累 最优。
● SRC-dfs(2014):第2个 标Avg和第6个 标Avg Sub-Opt帕累 最优。
● JPS(2012):第6个 标Avg Sub-Opt和第12个 标Storage帕累 最优。本章 主 JPS 未
算法中Avg Sub-Opt 现最优。
● PDH(2012):第3个 标20 Step和第12个 标Storage帕累 最优。
● Tree(2013):第2个 标Avg帕累 最优。

参考文献
[1]Daniel Damir Harabor,Alban Grastien.Online Graph Pruning for Pathfinding on
Grid Maps.ResearchGate,January 2011.
[2]Nathan R.Sturtevant.The Grid-Based Path Planning Competition.Ai
Magazine.35(3):66-69,September 2014.
第10章 优化MMORPG开发效率及性能的有限多线程模型[2]
作者: 鲁

摘要
MMORPG 为涉及 量 野 感知, 别是 对有帮战、国战 法 品类,80%以上 性 消
Obj 移动、战 、AI、属性同步等有 野感知 模 上, 低于20% 剩下所有 逻辑,后
开发成本却占 个开发成本 80%以上,这是MMORPG性 分布 性和开发成本 性 “二八法则”。
单线程单进程模 不足以承载过 , 是开发 最 ; 进程单线程模 设计有很
展性, 是 同步以及异步调 带来很 开发和调试成本; 线程模 实际 MMORPG开发
中,也 涉及加锁以及 量异步 调,开发和调试成本也不低。
有 线程模 期可以 决以上问 ,平 开发成本和性 问 。其核心设计思想是 后台 一次
tick严格区分成 上不 重叠 两个阶段:单线程阶段和 线程阶段。单线程阶段 绝
分常 请求,比 加 友、 易等, 是可以跨 景随意访问 ,不 线程 问 , 单
线程一样 程序即可; 线程阶段 景耦合度低 消 操作,比 AI、移动、战 、属性同
步等。

10.1 引言
论是端 还是 ,MMORPG 仍旧占 着 常 市 ,很 MMORPG 戏运 已经超过10
年,成功 端 也 移植到了 。 后 台服务器架构 上, 期 端 服务器 于硬件 制,
GameServer(为了后 叙述 , 戏 核心逻辑服务器 一称为GameServer) 了 进程
分布式设计, 至 进程 线程 设计,往往 投 开发精力也很 。本章 简单 绍 前 讯平台
主 MMORPG 戏架构设计以及对开发成本 影响。
10.1.1 多进程单线程模型
讯 研 MMORPG 戏 GameServer上一 了单线程设计,以《御龙 天》、《天涯明月 》
和《 》为 , 是 进程单线程 架构,这和 讯后台设计 系里 “ 系 ” 思想有渊
。这种架构 很明显: 先是 离,单GameServer宕机不 影响 服 ,单线程 操作让
开发 也 须 心 ;其次是 展性强, 过分布式 ,可以 到几万 同 线,比 《御龙
天》单服承载可以达到4万 以上。 憾之一是为了充分利 核CPU,往往一台 机 个
GameServer , 为进程 存 ,实际 机器 存 很 ; 憾之二是异步逻辑过 ,
GameServer往往负责“否” 判 ,很 “是” 判 和 必须到中控Server上 ,并且还
同步到各个GameServer,即 引 了coroutine等来简化异步逻辑, 同步和 量 异步调 也
仍 让开发成本很 。
10.1.2 单进程多线程模型
讯引 MMORPG 戏, 和 系 本 了单进程 线程 架构,比 《天龙八 》和
《 世 》两款 ,以及云 Skynet, 没有 存 弊端,也 充分利 核优势,不
中控Server同步 。这种模式也有不 之 , 先,为了操作简单 异步调 ,很 互 法
约束了 必须站 同一个 景上,给策划加以 制;其次,为了 加锁,又 证 性,有
跨线程调 了很 消息 , 过Service 式提 服务,这种异步 法太 ,仍旧比较 杂;
最后, 戏开发和 软件开发区别很 , 求随着市 持 迭代 动, 代码设计之初模 划分
晰,经历 干次 后耦合度越来越 , 线程 加出 概 。 于以上原 ,开发 仍 是其弊
端,往往涉及 力 。
10.1.3 单进程单线程模型
对MMORPG 优化 本 操作展开, 过性 分析工具,一 位到 操作 着 野
感知展开(性 随着 野 加呈几 级 提升),其中包 : 野算法本身 优化、 野 剪
低运算和 络 量 量级。 过极致 优化,单进程单线程也 足 分MMORPG 求,很 ,
承载 求只 开服 前几个 。从运 度看, 为单服导量有 和 服策 ,可 并不
过 PCU 撑,比 《剑侠情 》架构设计也只 足2500 线即可, 《 》单
进程单线程跑 Windows机器上,承载压 可达4000 , 讯 研 《仙剑 侠 online》同样也是单
进程单线程模 , 单服也 承载3500 线。单进程单线程有一个最 优势,就是开发和调试成
本低,易于 护,适 于 制紧张 。 是,优化到一 阶段,到达性 期以后,再有较 突
破 出很 优化成本去提升。

10.2 有限多线程模型
过和业 朋友 ,发现MMORPG有一个 同 ,就是80%以上 开发成本消 正常 逻辑
上, 80%以上 性 消 和 野有关 模 上。比 《御龙 天》,移动包和技 包 CPU消
上 占比之和 30%以上;战 得比较 《天涯明月 》 战 , 技 逻辑消 就 50%以上;
另一款 讯 研 MMORPG, 为有后台寻路、 素判 、 为树 义 杂AI以及分段技 设计,CPU
消 比同类 品 , 2018年10月 试期 , 得一 天CPU 分布, 图10.1所示。
图10.1 MMORPG 一 天CPU 分布

MMORPG后台主 有两 动力,一是消息 动,包含 上 协议 动和其 Server 消息


动,这 分 主 来 是战 请求包和移动请求包,移动和战 占这一 分 80%左右 性 消 ;
二是 器,包含各 系 心跳逻辑以及各个Obj 心跳逻辑, 承载5000个 线 ,怪 和NPC
往往 达到10万个之 , 此 器 主 来 是 景心跳(AI、CD检查、 敌等),这 分占
个CPU 75%左右。这两 分组成了 区 ,累计占比 达90%, 同 是有很 跨
景操作,以及 量 公 模 访问(比 件、帮 )。 另 10%是UI上 各种请求操作,以及
挂、帮 己 心跳逻辑等,代码量极 ,耦合度很 。 么,有没有一种可 ,让 区 线程
并 起来, 又不影响其 区 代码 杂度呢? 于以上 设提出一个“有 线程模 ”,其核心原
很简单,就是 GameServer 每一帧 分成 上不重叠 两个阶段,即单线程阶段和 线
程阶段,单线程 耦合度 、计算量 代码, 线程并 计算量 ,耦合度低 代码, 图
10.2所示。

图10.2 有 线程模 线程阶段


从 论上我 看看有 线程模 想情 下对性 影响, 原来单线程设计 性 消 ,
设 离给 线程并 计算 占比为mt,单线程 占比为st, 8核下 锁运 ,性 随着
st 短 提升, 么 公式:
e=1/((1−st)/8+st)
果st=20%,性 可以提升333%; 果st=10%,性 可以提升470%。后 我 从实际运 果上
看, 性 峰 刻,st占比平 远 于20%, 至可以弱化到5%以 ,MMORPG 这种 殊性给有
线程带来很 提升空 , 图10.3所示。

图10.3 有 线程模 性价比

10.3 使用OpenMP框架快速实现有限多线程模型
于以上 绍 原 ,有经 程序员 须 费太 精力,可以 己实现这么一 框架。 果想先
过几 代码 证一下,则推 OpenMP试试。OpenMP 一种可移植、可 模 ,提 给 程
一个简单 口来开发并 应 , 持 平台 存 C、C++、Fortran 器 程,可以
运 绝 器架构和操作系 上,包 Solaris、AIX、HP-UX、GNU/Linux、Mac OS X和
Windows平台。 译器 令集、库 和环 变量组成,影响运 为[1]。 果以前没有 过
OpenMP,建议先快速看看 OpenMP 快速 章[2],这里不 述。
OpenMP 框 架 图 10.4 所 示 , 这 是 一 个 OpenMP 经 典 运 模 ,一 从 一 个 主 线 程 ( Master
Thread)开 运 单线程模式下, 到 barrier(比 从左到右第1个)后, 进 线程模
式, barrier(比 从左到右第2个)是等待所有 线程 务 成后, 再次进 单线程阶段
。这个设计思路和有 线程模 度匹 , 过几个 令就可以构建有 线程模 。
图10.4 OpenMP框架

OpenMP里最经典 for语句这里并没有 ,这是 为for语句 每次循环 有线程以及线程私有


请和销毁 开销, 概是毫秒级别,for 语句不适 于每秒 有几十次循环 MMORPG 戏服务器架
构, 此选择了task模式[3]。我 一次 景 心跳分成两 分,一 分运 单线程模式下,命名为
TickSingle;另一 分运 线程模式下,命名为TickMulti。一旦运 线程阶段,就 一个
景 TickMulti 当作一个 task, 给OpenMP进 线程并 计算。模 一下 戏 循环,
每次tick至 客 端请求以及各个 景 心跳逻辑。 以下为有 线程实现 核心 码中,尽
量标 所 到 OpenMP标识, 对业务逻辑 了尽量简化,可以看到真正 动就几个以#pragma开
令。
● parallel 令: 义并 区 ,#pragma omp parallel[clauses], 持 个或 个 句。
● num_threads 句:parallel 令 句, 义线程 。
● master 令: 只有主线程 程序 分。
● task 令: 义 务,线程组 某一个线程 下来 每一个 务。
● taskwait:设 barrier,这个 令 证前 task 已经 成, 后 往下

于以上代码,所有可以 线程 逻辑 Scene::TickMulti() 即可,以 景
TickMulti 为task切分是最适合MMORPG ,OpenMP 证一个task只 一个线程中 , 不 再
次 切分。当各个 译器有 集成 , 译 , 加选项-fopenmp,不 引 库
件即可 译成功。 有 机上, 繁忙 可 出现CPU 升, 启动之前设 环 变量,让单
线程阶段其 CPU 策 是“ 动 息”即可。我 可以 启动 本中 加 下语句:

得一提 是,我 分消息也延后给了 线程阶段,对这 分消息有两个 求:一是逻辑简单,


没有跨 景和 局变量 操作;二是 受 序 乱,这个消息包和其 分没有过 赖关系。
之前 ,我 很 想 选择移动包和技 包延后到 线程阶段 ,更细致 建议可以参 下一 。

10.4 控制多线程逻辑代码
以上实现看上去很简单, 下来我 分析哪 事情可以 到 线程阶段去 ,哪 可以 到单线程阶
段去 ,以下是建议 单线程阶段 逻辑。
● 对于 局变量(global、static和单件 )有 尽量 单线程阶段,比 发 件、组 相
关、 变帮 等。
● 跨 景有 互 尽量 单线程阶段,比 加 伍成员后同步 息给所有 员、两个可 不
一起 私 等。
● 代码逻辑很 杂、未来 展性很强 功 ,比 录、切 景等。
● 其 尽量 代码 单线程阶段, 有较 性 消 ,再去 线程。
最后一条 常重 ,是有 线程模 “有 ”所 ,我 还是 尽量 证代码 简单, 为 线
程 得越 , 决 问 就越 , 开发成本和性 上就 。 笔 业务中, 线程阶
段 主 是以下 分,且 是 于压 决 , 这 分 性 消 已经 于90%。
● 动逻辑只包含移动和技 。
● 移动和技 心跳逻辑、AI逻辑。
● Obj 属性计算和更新。
● 为了 决 录 tps, 分 录 事情延后到了 线程阶段,比 战力计算、阵 计算等。
● 为了 决切 景 消 ,切 景后 野更新延后到了 线程阶段。
● 以性 压 为 础,出现 top20 模 中且和跨 景、 局 关 ,其 不建议
线程阶段 。

10.5 异步化解决数据安全问题
一个 线程进程 同步和 本身就是一个课 ,有 线程模 也有 线程阶段,这个问 就
不 , 杂问 尽量让单线程阶段 管是简化 主意。 果 是一个MMORPG 从业 ,
可 现 就 出,代码耦合度太 , 法 证 线程阶段 不 调 到“不 ” 。举一个
易碰到 :技 可 发 死 , 死 调 OnDie() 口,这个 口是开
性 , 系 可 这里新 或 调 ,比 可 让单件实 件系 发一 件,
造成 件存 局变量 变( 设 件系 没有 象成 立 服务), 这种情 试图 线程
程序中 变 局变量,这是不 。以下代码可以说明这个 。

为了 加锁有 逻辑, 线程程序往往 消息 列异步化, 给一个 线程去


变 。有 线程不是 刻运 ,当前 景这次心跳 A线程,下次可 B线程,不 简单 发
消息给对应线程, 是可以延后到下次单线程 阶段去 。这种情 过一个 消息 , 合
C++ 11 Lambda 达式, 常 易实现。以下代码 示了 果 线程阶段 到发 件请求,则延后到
单线程阶段去 过程。 得 意 是,这个 不 过于 繁, 止性 消 消息 上, 性
占比上也应该是很 分。
10.6 对“不安全”访问的防范
果某 不是线程 ,并且 造起来又有较 成本,则应该明确拒绝 运 线程阶
段,让 出异常。以 加 伍为 ,这个操作 后 很 杂,我 就 明确让加 伍
只 运 单线程阶段, 有 担心万一从 线程阶段调 过来怎么办? 此 有一种 和检查
机制, 式是提 一个名为CheckSingleThread 进 检查,一旦发现 线程阶段调 ,后
台开发 员就 到短 或 微 提 ,很快就可以根 栈 息 异步化。以下 码 示了 异常且
告警 过程。
同样,有 运 线程阶段, 是我 不希望 其 线程调 过来,比 景 Obj上
, 为可 引起 冲突。同 ,也可以 加一个对 线程 和检查 CheckMutilThread,
标识 只 运 线程上。以 身上 属性设 底层 口为 ,这个 口可 变 、 、速度
等各种 身上 变量,我 不希望 有跨 景 访问,就可以明确 出来。以下代码 示了 设
属性 , 果有 法跨线程 调 就 出异常且告警 过程。

CheckSingleThread和CheckMutilThread对我 帮助很 ,适当 让我 常快 找到了


应该 ,并且 过SingleDo延后到单线程阶段, 访问冲突,以告警来 动 ,
很 。笔 项 试阶段 毕后,引 试后没有 为 线程访问导致 冲突告警 发
,控制 线程 代码量越 , 就越 。

10.7 拆解大锁
加锁是不建议 , 是不 绝对 开,比 底层发包 口、底层日志 口, 持 线程调 ,
么就很可 有 量 加锁 。提到加锁,是很 疼 问 ,正 为 戏逻辑 杂、 求 不稳
,很 易造成死锁,以及对 担心。有 线程不是 量 线程,只有 量代码 线程阶
段,其实是比较 易控制 。
先,建议 先 一 锁( 看上去是不 成 为),所有 加锁逻辑 ,先不
,优先 快速让代码跑起来,并且至 不 死锁。这 锁 具有 建 计,记录冲突日志和冲突
,后 我 根 冲突 计优先级,逐个 锁去掉或 分 ,这样至 证 版本 造期 是
稳 ,不 为锁导致 工 。 锁 代码很简单,先 试上锁, 果有冲突则记录冲突 ,CPU
实现参 [4]。以下 码简单 示了记录锁冲突 过程。

下来就是根 实际 优先级来去锁化,我 标应该是每一帧 不 为锁冲突有明显


( 线程阶段 占比千分之一以 )实际 ,后 分析几个锁 经 。
利 线程局 变量 加锁,这种 式 起来往往最简单,比 之前一个返 static变量
,我 只 简单 这个变量 明为thread_local变量即可。对于C++ 11以下 版本, 果没
有这个标识符,OpenMP本身也提 了强 private 句可以 变量进 线程私有化,并且还 提 诸
初 化 机、退出并 区 是否恢 等操作[1],这里不再 述。
以下 码可 是程序原来存 一个 ,之所以有 static 变量, 计是为了 繁
构造和析构,也可 是为了 单线程下调 。 是 果这个 运 线程下就 出问 ,可
个线程同 读 , 成thread_local static MyStruct s_mydata 后,就不 存 线程冲突
问 了。
另 一种更常见 情 是,我 希望 线程阶段读操作是 , 么就 尽量 操作 单线
程阶段,比 伍和帮 。 线程阶段可以 制 读, 有 线程 情 下,确实 证绝 分
操作本身就 单线程阶段, 仍旧 有 ,我 就 尽量 操作 加上CheckSingleThread检查。
这里举一个更典 ,来说明 根 具 情 去分析。比 当两个帮 宣战后, 记录
计, 为帮 战 同 发 个 景中, 此有跨线程访问 可 ,所以最初 代码可 先加锁
证 。 码 下:

是,这是一个 事件, 过锁 计发现这里冲突 较 , 对map和set 红黑树 实现,


我 知 果 不 ,读也不 , 果 加锁, 线程读也同样 加锁。 个 延后到单线
程阶段是可 , 是 操作 异步化本身也有不 开销,看 很 平 。 果只 有树 新 操作
延后到单线程阶段, 么 不 变树 构 情 下, 线程阶段是可以 读 , 造成 下代码后,
没有锁操作,又 异步化 为了低 操作。
以上两个简单 是想说明对于锁, 果有 证明 可 影响 , 么就 过具 分析尽量
到去锁化或 加锁 。另 , 果 确 和业务逻辑关 不 , 么就可以 拆分成 锁。

10.8 其他建议
LuaJIT 为有2GB 存 制(截至 前, 最新版本对64位 持是默认关闭 ,不建议
Release阶段 ), 果线程 量较 ,则有可 出现 存不 情 。 果 移动、技 、AI
上没有过 Lua, 么建议还是 LuaJIT 持 。 果 戏 线程逻辑过于 赖Lua, 么
原 Lua 持 线程 运 也是不 选择。
以 景 心跳为一个task,每个task 并不平 , 线程阶段 出现 线程负载不 情
, 想 标是线程平 分担,这 让 线程阶段 短。task 调度 GCC 模式下 本是先
来先服务(FIFO),调度 式比较 变, 是可以决 哪个 task 先给OpenMP, 给 线程之前,我
对 task 上一帧 进 序 列,这样就 证 操作先 ,以最 可 不让
个线程末期等待一个 task 成, 进 单线程阶段 ,从 提 了帧 。

参考文献
[1]OpenMP.https://www.openmp.org.
[2]OpenMP 总 .https://www.cnblogs.com/pdev/p/10559045.html.
[3]OpenMP.https://computing.llnl.gov/tutorials/openMP/#Task.
[4]RDTSC 令 实 现 纳 秒 级 计 器 ,
2010.https://blog.csdn.net/gonxi/article/details/6104842.

[1]本章相关 已 请技术专利。

[2]本章相关 已 请技术专利。
第五部分 游戏脚本系统

第11章 Lua翻译工具——C#转Lua
作者:罗 华、陈 钢

摘要
本章 绍一种C#代码转Lua代码 译 案,简称TKLua 译 案。 TKLua 译 案, 开发项
可以 C#语 进 开发, 发布项 C#代码 译成Lua代码。 项 开发 顾C# 开发
, 项 发布后又 受Lua动态语 利,适 于有代码 更新诉求 Unity 机 戏。
TKLua 译 案具有一 性,与 译 案不同, 了 译程序集 译 代码
形式。该 案利 标 C# 译器 译 果 成了 级语 性 分析, 幅度 低了 译 度。其 译
原 是:
(1)利 Mono Cecil[1]库分析程序集中 类、字段、 法签名, 后 其 译成对应 Lua所模
类 构。
(2) 过ILSpy [2]工具分析IL 令集,重建 语句 达式组成 AST( 象语法树),并 译成对
应 Lua 法 。
(3) Lua类 构与Lua 法 合并成 Lua代码。
同样 原 ,还可以 C#代码 译成其 语 , JavaScript,以快速移植到微 戏等平
台,从 实现 同一 代码 译到 个语 平台, 了重 开发工作。

11.1 设计初衷
机 络 戏客 端对代码 更新有很强 诉求, Lua开发是实现代码 更新 常 案。
果项 模比较 ,往往 开发 练 Lua,设计 常合 码 , 输出 质
量 Lua代码。 于Lua是弱类 语 ,运 前 以 态分析(这也是TypeScript[3] 决 核心问
),导致出现下列影响开发 问 。
● 绝 分情 下,IDE 以提 成员 动 提示,这不是IDE设计 问 , 是 弱类 语
性决 。
● Lua 译期所 检查 误 常有 ,许 问 运 暴 出来。
● 没有 试 盖 情 下项 以重构, 对于 戏客 端来说, 试 盖成本相对比
较 。
本章 绍 TKLua是一种极低成本 译 案, 设计初 是让开发过程对Lua 感知,开发
C#开发,运 和发布 ,一 C#代码 译成Lua代码。程序员 受C# 强类 、类 推导、类
检查带来 利性,又 受 译后Lua动态语 更新 优 。

11.2 实现原理
11.2.1 参考对比行业内类似的解决方案
业 已经有一 决类 问 成 案,列举 下。
● Haxe[4] :一种新 程语 ,其 译器可以 Haxe语 译成其 语 ,比
JavaScript、ActionScript、PHP、C++等。
● Bridge.NET[5]:这是一个开 项 , C#语 译成JavaScript。
● TypeScript[3]:微软公司出品,是 JavaScript 一个严格超集。 是 加了 态类 和 于
类 向对象 程,语 格 常 C#。其设计 标是开发 应 , 后转译成JavaScript。
以上 决 案 是从 码开 ,经过词法分析、语法分析等常 译过程, 合 工程 杂度较

TKLua 译 案是从标 译后 程序集Assembly[6] (本章 称“程序集”)开 ,相比
码,程序集 构 简单得 。 助程序集分析工具,可以很 提取和 程序 构和 令 息,进
转 输出逻辑 达和原程序集一致 Lua代码。
11.2.2 翻译原理
TKLua 译 案 了 译程序集 形式, 直 译 代码。该 案利 标 译器 译 成
了 级语 性 分析, 幅度 低了 译 度。
译器 主 原 是利 两个成 开 库,即 Mono.Cecil和ILSpy。其中 Mono.Cecil负责从程
序集中提取类、字段、 法;ILSpy则负责分析 法 令序列。
TKLua 构 图11.1所示,底层有两个开 库Mono.Cecil和ILSpy(ILSpy 于Mono.Cecil),
译器 分 析 器 ( Analyze ) 、 类 成 器 ( Type Generator ) 和 达式 成 器 ( Expression
Generator)组成。

图11.1 TKLua 构图

● Analyze:分析程序集参 和 程序集关系。
● Type Generator:分析程序集中 类、字段、 法, 成对应 Lua 构。
● Expression Generator:分析 法 ,利 ILSpy重建 AST( 象语法树), 成对应 Lua
达式。
11.2.3 翻译流程
当C# 代码经过 译得到程序集之后, 译 程上经过三步对程序集进 分析和 成, 图11.2
所示。
(1)类 构 译, 过Mono.Cecil分析程序集中包含 所有类,以及类中 义 字段和 法,
集到这 息后,就可以 成Lua对应 类 和 构及 法 义了。 意 是,此 所得到 法
义只包含 法签名, 法得到 法 。
(2) 法 译,利 ILSpy 法 中 IL 令序列重建成AST, 译工具 AST转 成Lua语句
和 达式,形成Lua 法 。
(3) 第一步输出 Lua类 构与第二步输出 Lua 法 合成 Lua 件,从 实现了C#到
Lua 译过程。

图11.2 译 程图

第一步:类 构 译, 图11.3所示。

图11.3 类 构 译

● 图左边是C# 代码, 义了一个类Demo,其包含x和y两个成员变量,以及一个成员 Foo。


● 代码经过 译之后, 过Mono.Cecil分析程序集得到图中 Cecil 构, 构 包含了Demo
类 、x和y字段,以及Foo 法 义。
● 过对Cecil 构 译, 成图右边 Lua Demo类 和Foo 法 义 输出。 得 意 是,
此刻 法还只是 法签名,没有 法 。 于 Lua 是弱类 语 ,x和y字段 须 义。
第二步: 法 译, 图11.4所示。
图11.4 法 译

● 图左边是C# 代码, 义了int x=32; int y=18; return x+y;三条语句。


● 代码经过 译之后,形成图中 IL 令 。IL 是 于栈 令,图中含义是 32存
0号栈空 , 18存 1号栈空 , 后 add 令,并返 运算 果。
● 过ILSpy分析上述IL 令 , 成ILSpy对应 AST。
● 分 析 AST , 并 查 找 对 应 符号 ,最后 译 成 Lua 对 应 语 句 local x=32; local
y=18;return(x+y)。
第三步: Lua类 构和Lua 法 合成 Lua 件,从 实现了C#到Lua 译过程。
性能优化: 为 代码 译之后, 对字符串、常量、枚举、计算等进 一系列优化,比 删
代码、 各种字符串、 运 开销等,这种优化也对最终 Lua代码 成 优
化 果。可以 为,TKLua 译代码是经过 译优化之后 代码,对性 提升 常有帮助。

11.3 翻译示例
本 详细 绍TKLua 译示 。
译示 C# 代码 下:
译示 成 Lua 代码 下:
C# 代码经过上述 译过程之后得到Lua 代码。可以看到, 这两 代码中 找到以下一一对应关
系。
● C#中调 类 base.Awake(), 译之后变成Lua中 Awake(self)。
● 泛 binding<…>(), 译之后泛 参 变成 法参 binding(…)。
● seats.Add, 译之后变成Upvalue 存 F_Add。
● C#迭代器, 译之后对应Lua迭代器。
● C# 格 , 译之后对应Lua 格 。
注意: 于在 Lua 中 员访问 通过 table(字典部分) ,其 一 Hash表,
访问都 一 Hash ,而不像编译型语言一 在编译 能 员函 和变量地址(虚函 除外)。
以,Lua 员访问 一 额外 耗,一般 优化经验 人员在编写Lua代 会 不变化 函 缓
存到Upvalue。因 , 处F_Add调 翻译器优化 结 。虚函 Awake和Start则 进行这
缓存优化。

11.4 实现细节
本 详细 绍 译过程中 一 典 细 及优化 法, Lua不 持连 赋 、不 参 、
continue等问 决 案。
11.4.1 连续赋值
Lua中赋 是没有返 , 此 法对变量进 连 赋 。TKLua 案是拆 达式,
图 11.5 所 示 , y=x=foo ( ) 拆 成两次 立赋 ,利 临 变 量 csl_0 作 为 中 存 :
csl_0=foo(); x=csl_0; y=csl_0。
注意:也能设计 采 闭包 现连续赋值, 如:y=(function()x = foo(); return x
end)(),但 运行 能 会变 多。

图11.5 决连 赋 问 ——拆 达式

11.4.2 switch
于 Lua中没有switch语句,所以 译过程中 其 语句来模 。TKLua 案是 if
条件判 和repeat循环来模 switch语句。
● switch中case判 , if…else…模 判 。
● switch中break跳转, repeat…break模 跳转。
译 果 下所示。
C# switch代码 下:

译后Lua下 switch代码 下:
注意:在Lua中if 件判 和repeat 环分别只需要一 令, 能不受 响。如 采 table表
switch 功能,则需要 别 表 创 和销 销,避免运行 能变 。
11.4.3 continue
于 Lua中没有continue语句,所以 译过程中 其 语句来模 。 一个循环 中可 同
存 意 量 continue和break,TKLua 译 是 嵌repeat循环和break模 :
● 加 嵌repeat循环层。
● 原C#中一个continue, 译成Lua中一个break, 跳出 层循环。
● 原C#中一个break, 译成Lua中两个break,跳出两层循环。
C# while continue 下:

Lua下 while continue 下:


11.4.4 不定参数
C# 不 参 译后是 组参 , 代码中 不 参 息 译器丢弃, 译输出
其当作 组参 输出。这样 常 利, 是 以还原成更为 Lua不 参 。
C# 参 代码 下:

Lua 参 代码 下:
11.4.5 条件表达式
Lua可以 过与或 式模 问号 达式, 下所示。

是此 法有一个 ,当x为nil或 false 不符合条件 达式 C#语意。为此,有两种 决


案。
(1)闭包 案: if语句包 进闭包 里,可以 达式 征,也 决上述 问 。示

(2)wrap&unwrap 案: 一个wrap 包 x 达式,当x为nil或 false 返 标


记,最后 unwrap 还原。示 :

相比 ,闭包 性 更 , 是 GC对象; wrap相对稳 一 , 到GC对 戏


影响, 译器选择wrap机制。

11.5 运行性能
本 对比了TKLua 案和Unity原 案(IL2CPP) 性 ,以此来说明TKLua 案 适

TKLua 案 译后 Lua代码 Lua 机上,相比Unity原 案 有 机 开销。
图11.6所示是 iPhone 7 Plus设 上TKLua 案输出 代码和Unity原 案运 性 对比
。 过对比我 发现,TKLua 案 译后 Lua代码 所消 是Unity原 案 5~8 。

图11.6 性 对比

可见TKLua 案 可以 C# 戏代码,并且较 持 更新, 是运 性 相比Unity


原 案 弱得 ,比较适合CPU消 较 戏, 棋 戏、卡 戏等。
对于性 感 戏,则可以 CPU消 较 逻辑,直 C++或 C# 并导出给TKLua调
, 这 分逻辑不 更新, 是可以 持C#或 C++原有 性 。

11.6 TKLua翻译蓝图
本 绍 TKLua已经实现 译 图,是为了 译 性,列举了C# 各种 级 性。 译
图一 分为三 分 , 图11.7所示。
图11.7 TKLua 译 图

11.6.1 类关系
各类 绍 下。
● partial类: 译后, 动合并 具 类, 标 译器 成工作。
● 匿名类: 译后, 成具 实名类, 标 译器 成工作。
● 嵌 类: 成Lua形式 嵌 关系, 译工具 成工作。
● 承类: 成 承关系 类 , 译工具 成工作。
● 泛 类: 分实现,后 可 充实现。
11.6.2 类成员
这 分 下。
● 字段初 化: 译后, 初 化 中 成赋 过程, 标 译器 成工作。
● 属性: 译后, 加set/get具 , 标 译器 成工作。
● 索引器: 译后,索引对应 过程, 标 译器 成工作。
● 展 法: 译后,为类 展 法变成 态 调 , 标 译器 成工作。
● 运算符重载: 译后,运算符重载变成具 调 , 标 译器 成工作。
● 匿名 : 译后,匿名 动变成实名 , 标 译器 成工作。
● 字段:弱类 语 字段, 须 别 义, 译工具 成工作。
● 法: 成对应 常 Lua 法, 译工具 成工作。
● 构造 : 成对应 Lua初 化 , 译工具 成工作。
● 态 法: 成对应 Lua 局 , 译工具 成工作。
● 泛 :泛 参 变成 参 , 成对应 Lua , 译工具 成工作。
● 匿名构造 和类成员初 化:标 译器 动合并到构造 中, 译工具输出。
● 匿名 态构造 和 态成员初 化:标 译器 动 成语句, 译工具输出。
● 可选参 : 译后,未 参 动 默认 充, 标 译器 成工作。
● 参 : 译后,等价于 组参 , 标 译器 成工作。
11.6.3 方法体
这 分 下。
● Lambda 达式: 译后, 达式展开为具 调 , 标 译器 成工作。
● 常量: 译后,常量名 替 为具 常量 , 标 译器 成工作。
● 枚举: 译后,枚举 替 为 , 标 译器 成工作。
● 引 关系: 译后,引 关系变成类 之 相互调 , 标 译器 成工作。
● Typeof: 译后,替 成具 类 , 标 译器 成工作。
● 泛 构造: 译后,泛 参 实 化, 标 译器 成工作。
● 赋 : 成Lua赋 ,连 赋 拆 , 译工具 成工作。
● 局 变量: 成Lua 局 变量local, 译工具 成工作。
● 循环语句:反 译后,所有 循环 变成单一 Loop 构, 译工具 成Lua for循环。
● 条件语句: 成Lua if条件, 译工具 成工作。
● switch语句: if条件判 和repeat循环组合模 , 译工具 成工作。
● 集合初 化(Collection Initializer):标 译器 成 构化 令, 译工具 成工
作。
● 对象初 化(Object Initializer):标 译器 成 构化 令, 译工具 成工作。
● try…catch语句: 成Lua xpcall, 译工具 成工作。
● 问号 达式: 成等价 “或与 达式”。
● 其 : 本直 译, 译工具 成工作。
注意:图11.7中“编译器” 列举 高级 翻译, 准编译器编译 ,大 降低了翻译
复 。

11.7 发展方向
随着微 戏 兴起,越来越 戏 开 关 H5 戏 开发。 果 戏 想 原有
戏移植到H5 戏平台,则 临着 戏功 JS重 一 ,工程量比较浩 可 情 。 是, 原
有 C#工程简单重构后进 译,或许可以 幅度 低重 开发成本。
上述章 详尽 绍了 C#代码 译成Lua代码, 么利 相同 原 ,也 C#代码 译成JS代
码, 图11.8所示。
图11.8 TKLua JavaScript 向

于这样 思路,TKLua 译 案 加实现了C#代码转JS代码 译功 ,为 戏快速移植到微


戏平台提 了一种 案。
C#代码 下:
译器 成 JS代码 下:
果 戏已经有Unity C#版本, 么 过上述 译过程或许可以快速输出H5版本,以 移植微
戏、 Q 一 、Facebook Instant Games。

11.8 总结
TKLua 译原 是 对程序集, 不是 代码进 译 。程序集是经过 译器 译以及充分优化
,许 语法糖 译期就 合成常 构,所以 幅度 低了 译 度。这就是TKLua 译模式
业中 优势所 。
另一 来说,从程序集开 译器 丢 一 码层 息。 程序集 逻辑 达上是
, 是有 这 息 丢 让 译工具 更优 输出选择,比 不 参 和Lambda 。
TKLua 案最终运 还是Lua 本,所以运 性 比Unity原 案(IL2CPP)弱一 。 实际
项 中可以 对性 求 、功 较稳 逻辑只 C#或 C++来实现,其 逻辑则可以 TKLua
案转 成Lua,这样就可以 证性 同 , 戏 随 更新。

参考文献
[1]Mono.Cecil.Mono,2017.https://www.mono-
project.com/docs/tools+libraries/libraries/Mono.Cecil/.
[2]ILSpy.ICSharpCode,2019.https://github.com/icsharpcode/ILSpy/tree/v2.4.
[3]TypeScript.Microsoft,2013.https://www.typescriptlang.org/.
[4]Haxe.Foundation,Haxe,2017.https://haxe.org/.
[5]Bridge.NET.Object.NET,2017.https://bridge.net/.
[6]Assembly,2017.https://docs.microsoft.com/en-us/dotnet/framework/app-
domains/assemblies-in-the-common-language-runtime.
第12章 Unreal Engine 4集成Lua
作者:陆

摘要
Lua 作为一种轻量 嵌 本语 , 戏开发中得到了广泛应 ,提 了 戏业务 开发
。 对不同 戏引擎, Unity和Cocos2d,现 市 上 找到不 Lua集成 案,本章主
绍 Lua集成到UE4中, 得可以 Lua开发UE4 戏。本章 分为三个 分:
(1)为了 持C++反 ,UE4为 分C++代码 成了 息。本章 绍 利 UE4 息来实现
Lua与UE4 互,包 Lua与C++、Lua与 图之 互。
(2)对于没有 息 分,本章 绍 利 模板 程 C++类导出到Lua中。现代 译器本身
已足 强 ,利 模板技术操纵 译器可以为我 成足 “ 水”代码,省去了利 第三 析工
具 麻 。这 分 思想也可以应 到其 C++项 中。
(3)本章 绍 进一步提 Lua UE4中 开发 和运 , 合Lua Table与C++
设计、优化 构 GC、运 加载技术等。

12.1 引言
Lua中模 C++ 类系 ,一 是 C++ 过userdata Lua 机中,并为这个userdata
一个 , 后重 该 __index 来实现对C++ 调 和成员变量 读取,重
__newindex 来实现对成员变量 。
为C++是 态类 语 , Lua是动态类 语 ,Lua调 C++ 先调 中 实参从Lua栈
取出, 后再调 标 ,这个中 (也称为“ 水” ) 译前就确 下来, 且对不
同 标 不同 中 , 下 代码所示。
为每个Lua关心 C++ “ 水” 是一件重 枯燥 事情, 下来主 讨论 利 UE4
息和模板 程 轻这 分 工作量。

12.2 UE4 元信息


UE4为 己 图 本 动 成了类 中 , Lua 法直 调 这 中 , 可以利
UE4为C++ 成 息来实现调 ,最终调 到真正关心 C++代码。为了 持 图 本和C++ 一
致, 图类对象也 成同样 息,所以也可以实现Lua与 图 互。
12.2.1 介绍
普 C++类代码,经过 译器 译之后 丢 很 关于类本身 息, 类 名字、类里有 么成
员 和成员变量、这 成员 是 么类 等,这 息就 称为 息。 常 程序运 是 法 取
到 息 ,为了 持序列化C++对象等 辑器 础功 ,这 息必不可 。UE4提 了几个 ,
反 功 C++代码类里,每次 译工程 UE4 先对 代码进 一次分析, 过标记 集
反 类 息,并 则 成 代码。这 代码也一起参与最终 译,并且 引擎启动 ,
对于不同 C++类实 化出对应 息对象,这 对象 存了C++类 描述 息,即 息。
可以 成 息 类有两种:
(1) 承 UObject 类,这种类 有所有反 功 , 实 存是 UE4 系 管
,有 性 开销和 存开销。
(2)普 构 类, 存开销,也不 承 类, 可以 管 其 存,
是 只有成员变量可以 成 息,所以只 访问 成员变量, 法调 成员 。
不同 息类负责描述不同类 息,主 分为4类。
(1)描述一个变量类 类是UProperty, 成员变量或 形参。
(2)描述 类 类是UFunction, 包含 个UProperty,负责描述 参 和返 类
息。
(3)描述一个普 构 类 类是 UStruct, 有 个 UProperty,描述 个成员变量
息,没有包含UFunction。
(4)描述一个UObject 类 类是UClass,UClass 承 UStruct,一个UClass可以包含 个
UFunction和UProperty,对应于一个C++类包含 个成员 和 个成员变量。
关系 图12.1所示。

图12.1 类 关系图

12.2.2 Lua通过元信息与UE4交互
不管是UClass、UStruct还是 UFunction, 同 是 包含了 UProperty。UProperty 反
中 了 常重 , 记录了变量 器中 存 移 , 器 是一 存。当 于描述
UClass或 UStruct 成员变量 ,这个 移 代 该成员变量 类 存布局中 移 。利 这个 移
,再 器 起 ,UE4就实现了读 器中 成员变量 。
了 移 , 对不同类 成员变量,UProperty 出了不同 类, 图12.2所示。比 ,
描述一个int类 变量 类是UIntProperty。这 类更详细 记录了变量 息,并且重载了
类 读 , 证对不同类 变量 读 正确进 。 ,当变量 类 是 ,往 器中
则是 果 从 器 移 开 往后4字 存里;当变量 类 是 构 ,描述
UProperty 类是UStructProperty, 了 构 贝 存,可 还 调 构 变量 赋
操作。
图12.2 UProperty 出 类

12.2.3 读写成员变量
对不同成员变量 读 所 息 UE4 UnrealHeaderTool工具分析出来, 存 一个
个UProperty实 中,这 实 又 存 相应 类 息UStruct实 里。UStruct可以根 成员
变量名找到对应 UProperty,只 提 了 构 实 ,再 合UProperty记录 成员变量 类 存
布局中 移 , 么就 计算出该成员变量 存 , 后根 UProperty 类 进 读 就可以
了。 为UClass 承 UStruct,所以 描述 UObject 类,也可以 同样 式读 成员变量。
12.2.4 函数调用
过 息去调 C++ , 先 决 参 问 。UE4为每一个 反 成
了一个中 ,这个中 一个buffer 作为参 , 主 逻辑就是 一个个实参
从buffer里取出来,赋 给对应 临 变量,再 这 临 变量去调 真正 C++ 。对于每个
有一个UFunction实 来描述 ,UFunction里 UProperty记录了每个实参 这个buffer里 移
,读 buffer里参 法与上一 读 类成员 式一样。当想 Lua中调 C++类 某个 ,
先 过类名找到 UClass实 ,再 过 名找到 标 UFunction实 , 后 过这个
UFunction提 息 存中分 出一 buffer, UProperty 类 从Lua栈中取出实参 ,并
移 buffer中, 充buffer 毕后就可以 去调 中 了,中 再负责从buffer里
取出实参去调 真正 标C++ 。
,下 这个 :

UFUNCTION()就是 UE4提 给 ,标记一个 成 息和中 。这个 息


UFunction 实 存 两 个 UProperty : 第 一 个 是 UIntProperty , 存 移 为 0; 第 二 个 是
UFloatProperty, 存 移 为4。 buffer 构 图12.3所示。
图12.3 buffer 构

当 调 ,先根 UIntProperty从Lua栈中取出一个 , 充到buffer 0字 ,再根


UFloatPropery从Lua栈中取出一个 , 充到buffer 4字 。 充 毕后,去调 UObject类
ProcessEvent 法就可以了, 图12.4所示。

图12.4 调 示意图

上 是比较简单 情形, 可 还有返 和引 类 ,这 息也 记录 息中,


根 进 即可。
12.2.5 C++调用Lua
UE4 可 以 调 图 , 先 明一个 ,并 UFUNCTION 加上
BlueprintImplementEvent 标记,UnrealHeaderTool就 动为这个 成实现代码。 实现代
码中,主 逻辑是 实参构造出一 buffer,再 过 名找到 图 UFunction, 后 这个
buffer去发起调 。
UE4 这个实参buffer加上UFunction 签名等上下 息构造一个 调 调 栈 ,
UE4里是 FFrame实 来 存这个调 栈 , 后 这个FFrame实 递给 图 机, 图从
该FFrame中读取实参变量,计算 束 果 FFrame实 中。 该FFrame 递到 己 义
里,根 则 实参从FFrame实 中取出 Lua 机进 计算,并 果 FFrame中, 么
就 C++调 图变成C++调 Lua。
UFunction 提 了这个功 ,让我 递 FFrame 这个 ,该 过 存
UFuntion Func成员变量中,该 4.20版本中 签名为typedef void(*FNativeFuncPtr)
(UObject*,FFrame&,RESULT_DECL)。下 是实现调 Lua 致代码:
有了 签名FuncToCall,我 就 从Buffer中取出实参,压 Lua栈,与上一 根 签名从
Lua 栈中取出实参 充 Buffer 程正 相反,其 本原 类 。 下来 原来UFunction里
Func设 为这个 义 。
上 中说到,对于每一个 C++类, 有一个 UClass 实 存 反 息,查找对应 UClass实
法可以 过 名字,UClass* Class = FindObject(ANY_PACKAGE,*ClassName), 引擎初
化之后, 反 息 局唯一并且只存 一 。UClass中包含了所有 UFunction 息,只
戏代码 前 对应 UFunction即可, :

当C++调 这个标记了BlueprintImplementEvent ,就不再去 图 逻辑, 是去


我 义 逻辑。 图里调 Lua 也可以 过该 式实现,不同 是C++调 , 调 到
义 CallLua , 递进来 是 调 FFrame实 。 图调 递过来 FFrame 实 是
调 ,不是 调 , 代码构建出 调 FFrame实 ,代码可以直 参
ScriptCore.cpp CallFunction 码,或 直 从调 FFrame实 中找出实参,性 更优。
12.2.6 小结
利 UE4 息, 须 成Lua“ 水”代码,就可以读 类成员变量和调 C++ 。利
图 机 令 式,C++也可以调 Lua 中 。 为 图类也有一样 息,所以也就
实现了Lua与 图 互。

12.3 通过模板元编程生成“胶水”代码
上一 绍了 过 息来实现Lua与C++ 互。 是这种 式有比较 制,UE4 了只
有 类 变量 成 息,其 类 则不 , 模板类。还有一个 是, 反 调 过程
中, 类 判 来读 Lua栈里 变量,这 带来一 性 。此 , 实际项 中,有 可
想 本中调 引擎 类 某 法, 加上UFUNCTION 就 达到 , 这 引擎
码,一 情 下应当 。 到这 素,有必 实现一种 离UE4反 系,也可以实现Lua与C++
互 技术。
离UE4反 系,就 求我 己 对每个 导出 “ 水” ,负责 从Lua栈
里读出, 后去调 标 。
对每个不同 “ 水” 带来巨 重 性劳动, 前市 上有一 工具可以帮助
成“ 水”代码。 swig和tolua++, 我 一 类 于C++ 件 口 件, 工具
析 成C++代码。还有, 助Clang C++语法分析工具,可以直 分析C++ 代码来 成“ 水”代码,
实际项 中一 还得 一 件,告诉工具该为哪 类、哪 和成员变量 成“ 水”代
码。
一 来说,第三 代码 成工具 度更 ,功 也可以 得更 善, 是总 带来 麻
, 习起来更 。现代C++ 译器已经越来越强 ,模板 程提 了可 程 式来控制 译器
力,再 合 代码 成 力,最终实现 件 到C++ 代码中,利 译器为我 成“
水”代码并直 参与 译,所有 误 译期 发现。这 代码本身也是合法 C++代码, 习
起来更 易,和项 合得更紧 ,也不 运 工具 成代码。
12.3.1 接口设计
为了 口导出到Lua中,这 代码 易于书 、格式 并且直观,只 必
息就 动 成“ 水”代码。 此 础上 为绝 分 C++类 导出,实现Lua调 C++ 功
尽量 善,希望 持 性有:读 public成员变量,导出不同 构造 ,导出成员 、
static 成员 、static 、 ,还 持 重载功 。以下是一个典 代码

代码主 几个 来 成,比 每个 导出到 Lua 中 类 LUA_GLUE_BEGIN和
LUA_GLUE_END 包起来, 导出 成员变量 LUA_GLUE_PROPERTY , 导出 成员 或
static类 LUA_GLUE_FUNCTION , 须 成员变量 类 或 成员 类 。
默认构造 根 类 动导出, 果想导出不同参 构造 ,则 LUA_GLUE_CTOR ,代码
下:

果 导出重载 ,则 LUA_GLUE_OVERLOAD ,并提 重载 签名,代码 下:

对于有默认实参 ,所有 导出 持 后 上默认实参 ,该 可以与 签名里


默认实参 不同。
对于有 承关系 类,可以 LUA_GLUE_END()中 下 类 类 , 持 个 类。

这 可以 义 .cpp 件中,也可以 义 .h 件中,即 该.h 件 次包含也不 造


成重 义,这里利 了 译器对模板类 去重功 (后 绍)。 可以与类 义 不同 件中,
成 代码对 导出 类是有 赖 , #include 相关 件。这样可以跨模 口导出到
Lua中, 戏模 里导出引擎 类, 不必去 引擎 码。
这 上尽量 到了简 , 须了 Lua与C++ 互 知识。并且 代码只 译成
功,就 证导出成功。
12.3.2 实现
中 册进Lua 机 机,必须 于Lua代码 ,这里利 是static变量 初 化
机制, 下 代码:
当包含这段代码 动态 库 加载 ,就 调 RegisterFuncToLua::StaticVar 构造
。LUA_GLUE_BEGIN和LUA_GLUE_END 主 就是 来 成这样 构 ,LUA_GLUE_FUNCTION和
LUA_GLUE_PROPERTY 来 成具 中 , 这 中 {} 包 起 来 就 可 以 成一个
initializer_list,可以作为实参初 化一个 组 器,该 组再 框架 ,框架 历该 组 中
册进Lua 机。
为了不引起重 义, 根 类 名 出不同 构 , LUA_GLUE_BEGIN(A) 成 是
struct RegisterFuncToLua__A,LUA_GLUE_BEGIN(B) 成 是struct RegisterFuncToLua__B。
是 果 这段代码 .h 件中,并且 个.cpp 件包含, 么同一个类 态成员变量 义代
码还是 重 义 误 。
为了 决这个问 ,我 成模板类,代码 下:

当 个.cpp 件包含 ,也不 出现 态成员 重 义 误, 为C++ 器 重 化类


去重。这里举一个 成该 构 :
类名和所包含 调 doRegisterWork进 册, 册 果是 Lua 机中 成了一个
Table,里 包含了可调 中 ,并为其 类名。LUA_GLUE_BEGIN 还包含一段 化模板类代
码, 来 C++中 取导出 类 名。代码 下:

当 构 变量压 Lua栈后,调 该变量类 化 模板类找到名字,再 过名字就 Lua


机中找到对应 了。
12.3.3 读写成员变量
C++和Lua 类 系 有着显 区别,Lua读 C++中 不同类 变量 不同 读 式。
C++中可以 意 义新类 , 是仍可以 分成4类对待:
(1) 础类 , int、float、string等,读 贝形式。
(2)UObject ,UObject类比较 殊,单 ,下 有 绍。
(3) UObject 构 ,读 过 。
(4) 义读 操作 类 , 常是一 模板类 , UE4 TArray。
默认情 下,LUA_GLUE_BEGIN 导出 构 当作第3类来对待, 果有 殊 求,则可以 对
类 化读 操作,就可以 控制 读 。这里以压 Lua栈为 :

THas_PushValueToLuaStack模板类 来 试类 T是否具有 义压 操作,没有 话就 调 上


化版本。CustomTypeToLua 模板类相当于一个适 器, 果 对 T 化CustomTypeToLua 模板
类,并给这个 化类 加一个 PushValueToLuaStack 法, 么THas_PushValueToLuaStack就
试 过,就 进 下 化版本。
Lua中读取成员变量 , 去调 中 , 先从Lua栈中读取实 , 后 成员变量
引 到负责读取 模板 ,模板 根 变量类 选择对应 化版本,最后 对应 压 Lua
栈 操作。 成员变量也是类 ,根 类 对应 读取Lua栈 操作。
12.3.4 引用类型
果 参 中存 引 类 ,则分为两种情 来讨论。第一种情 是 础类 引 , 、
、字符串等, 础类 变量 Lua中是 过 递 ,所以当C++ 实参 , 论 也 法
影响到Lua 机中变量 。 到Lua调 可以 个返 ,所以 调 C++ 后,
引 变量再次压 Lua栈,当成返 递 Lua。第二种情 就是 构 引 ,Lua 中持有 是
构 ,所以取出该 , 后 递给引 调 。当C++ 实参 , 直 Lua中
这个 构 变量,达到引 递 。之后为了与 础类 引 式 持一致, 调 束
,也 实参当成返 压 Lua调 栈。
到 实参引 ,所以 一个临 变量来 存Lua栈里 实参 , 调 后,再
该临 变量压 Lua栈。这样 可 有 个,所以 一个 器来 存。 签名千变万化, 法
直 某一个 类 器,最后 Tuple 模板为每个不同 成对应 器。这里可以进
优化,为了 构 贝,不管 签名中 构 参 类 是 么,Tuple 器对应位 实参
类 是 构 类 , 调 标 调 。这样 后, 器中成员 类 是 础类
, Tuple 器也不 带来 构造析构开销,经 试与纯 工 “ 水”代码 性 一样。
Lua调 C++ 过程 图12.5所示。

图12.5 Lua调 C++ 过程

当调 束后,再 Tuple 器中 引 压 Lua调 栈, 为 构 FVector 过 递,所


以Lua中原来 变量已经发 了 变, 为了 持一致,也 当成返 压 Lua栈, 图12.6所示。

图12.6 Lua调 C++ 束后 引 参 压栈

12.3.5 导出函数
Tuple是C++模板 程中很重 一个 器, 可以根 类 列 成一个 构 , 这个
构 里 对 每个类 成一个成员变量,并 下标读 成员变量 ,下标 是第N个类
对应 成员变量。 之前 讨论,我 主 利 Tuple来实现对C++ 调 ,其 致逻辑是先根
导出 签名,利 性萃取技术(Traits)去 历形参列 ,并为此 成一个Tuple 器。
下 以调 一个成员 为 来进 ,代码 下:

有 法直 根 形参列 成Tuple 器, ,当参 中有引 类 ,Tuple不


引 类 作为成员变量 类 ,所以 const、引 &、右 引 &&等 符去掉。还有一种情 是之
前讨论过 构 ,对于 形参中 构 类 ,不管是 、引 还是 类 ,我 转 成
类 Tuple 器中。上 代码中 TraitTupleInerType模板就是 来 这个逻辑 。
当有了这个Tuple 器之后, 下来 逻辑就是 实参 类 从Lua栈中取出 器中, 后
Tuple 器 包去调 标C++ ,最后 返 和引 实参压 Lua栈。代码 下:

为 构 是 当成 存 Tuple 器里 , 标 可 不是 构 ,这里
ConvertStructPointBack负责 。 助这个模板 ,就可以实现Lua对C++类成员
调 , 须关心具 成员 类 。其 类 调 程是类 ,核心 是 Tuple
器作为 。
最终LUA_GLUE_FUNCTION 义 下:
一个[](lua_State*inL)->int32{}中 册到Lua中,当其 调 就 Lua栈和
对应 成员 ,调 模板 LuaPopAndCallAndReturn 成真正 调 逻辑。
12.3.6 默认实参
对于C++中 可以 加默认实参,实参 是 译期实现 ,所以运 法 得某个参
默认实参,为此 代码 工 加默认实参,默认实参也是 存 Tuple 器中 。当Lua栈中
参 不足 ,这 默认实参 充到临 变量 Tuple 器中。图12.7(a)展示了Lua栈提 足 参
,图12.7(b)展示了当参 不足 ,从默认实参 Tuple 器中读取实参 。

图12.7 默认实参示

充临 Tuple变量 代码 下所示,带默认实参相比不带默认实参, 了一 逻辑, 于判 Lua


栈中 实参个 是否充足,所以 性 上也 差一 。
12.3.7 默认生成的函数
一 类 有默认构造 ,operator==、operator+等 础 , 省情 下 动导出这
,省去了为这 LUA_GLUE_FUNCTION 工作量。 有 类 这 删 了, 动为
成 代码就 调 到不存 ,从 译 败。
以默认构造 为 ,对于类 T,为 动 成 中 去调 new T(), 这个类 T
默认构造 删 了或 了private 符下, 么就 译报 。
为了 决这个问 ,可以 殊 义一个LUA_GLUE_BEGIN_XXX ,专 对没有构造 类。 这
种 式有两个 ,一是 加 量,提 了 杂度;二是 了默认构造 ,还默认导
出其 , 析构 ,这 问 又暴 出来了,必须为没有析构 类 ,或 干脆
一个 么 不 动导出 , 么 己 加。
么, 不 加 量 情 下,又尽可 导出 省 呢? 对于某个类不存 这
,让导出 代码 译不报 即可,这里可以利 模板 SFINAE 性,对于不符合条件 ,让
译器不去 成 误代码。下 以默认构造 为 :
Lua代码不再直 调 new T(), 是调 模板 TSafeCtor(), 果类 T没有构造 ,
么就 调 下 化版本,返 一个空 ; 果类 T有默认构造 , 么就 调 上 化版
本,并返 新构造 实 。对其 动导出 也 类 , 证 译没有问 。
12.3.8 C++调用Lua
为已经实现了根 不同变量类 变量压 Lua栈 模板 ,事情就变得很简单了。 先 过
名找到Lua , 后 所有 变量压 Lua栈,当发起调 后,再从Lua栈中取 返 即可。

12.3.9 小结
利 和模板 程,可以 成Lua调 C++ “ 水” ,这种“ 水” 标
签名 变量出栈、 栈,省去了运 期 类 判 ,性 优越,并且可以给 Lua导出几乎所有 类
, 制较 ,可 展性强。

12.4 优化
12.4.1 UObject指针与Table
UObject 类 UE4 逻辑框架中举足轻重, 只 过工厂 创建实 并返 ,对UObject
实 所有操作 是 过 进 ,所以可以 对UObject 一 优化。当初次 一个 UObject
Lua 机 ,为 创建 userdata 后, 一个字典 UObject 和这个userdata 关
起来,下次不管从哪里 这个UObject , 先 字典里查询 之前是否 过,是则 之前创
建 userdata,这样 是UObject userdata Lua 机里就具 了唯一性,可以 相等性判
, 了userdata 创建, 且当作为Table 索引 ,不 同 存 两个相同 UObject 。
更进一步,可以 UObject 和Lua中 一个Table 起来,这样 Lua 机中取该 ,取
到 是 Table, 图12.8所示。

图12.8 C++向Lua 递 替 为对应 Table

当这个Table 给C++ ,又可以转变 真正 UObject , 图12.9所示。

图12.9 Lua向C++ 递Table 替 为对应

这个Table可以 展UObject Lua 机中 力, 为其 存一 变量、重载 C++


等, 加了开发 利性。
12.4.2 结构体
为C++ 返 之后栈上 存就 了, 果返 是 构 类 , 么一 存中
分 一个 构 ,并 返 制给这个 存中 实 , 后 这个实 返 给Lua 机。 果
调 这样 , 每帧 查询Actor Pos, 久了, Lua 机中就 存 量 存中
构 引 ,一旦启动 就 引发卡顿。另 ,这 构 实 常也只 当 上下 中
一次,本身没有必 存 上。
我 对返 构 了优化, 对每一种 构 类 创建了一个 RingBuffer,只 同 存
量该类 实 , 30个,当 返 ,从RingBuffer中取出一个实 , 返 制给 ,再
返 Lua中。RingBuffer是常 存 , 了 存 分 和 开销, 且Lua不 管
命周期,对 不 造成 影响。同 , Lua中 递 构 变量给C++ ,也 须
上分 存,可以从RingBuffer中取出实 。
意 是,RingBuffer 存 循环利 , 法持久化 存一个 构 变量。当 持久化
, RingBuffer 实 制给一个 上 实 即可。
经过简单 试, 100万个 构 , i7 器上 0.5s左右,经过优化后为0.001s,可
忽 不计。
12.4.3 运行时热加载
本语 本质上是根 “字符串”来 一 逻辑 ,当 机运 这 “字符串”,并告
知 机 , 机就 新 逻辑。这样 戏进程不 关闭就 运 到新 代码, 开发调试。
为了 证程序 加载前后 态 一致性,只有 逻辑可以 加载, 以 持不
变。 Lua中 常 require语法来加载Lua 件,所以以 件为 本单位 加载。 件里
代码更新了, 么就重新加载该 件, 新加载 件 成 来替 原来 。
意 是,重新加载 件, 件 代码。 代码里 有一 语句是对 以 进
了操作, 么一 导致 乱。上 说 件 加载到了一个匿名 中, 么就可以 这
个匿名 关 环 ,给 创造一个沙 环 ,让 与 相关 代码不对真实 环
作 。 一 情 下,这个沙 环 导致这 代码 败, 为其逻辑 去查询 真实环 中存
, 新 加 沙 环 中不存 ,进 加载 败, 下 代码所示。

真实环 中 制到沙 环 中代价太 ,所以我 给沙 环 关 了一个 殊 。这个


对所有 查询 返 一个“万 ” ,这个“万 ” 也对所有查询 返 “万 ” ,这
“万 ” 可以进 尽量 操作 不 报 , 可以调 不存 等,尽可 让 件 代码
成功。这主 过Lua 机制 __index和__newindex 法来实现,其中__index对所有索引
返 “万 ” ,newindex对所有赋 操作 。
当成功重新加载 件后, 得到一 新 ,为了 证 一致性, 对应 旧
upvalue 制到新 上,之后 历 个Lua 机, 原来 向旧 项 成 向新 ,这样就
成了以 件为粒度 加载。
第六部分 开发工具

第13章 使用FASTBuild助力Unreal Engine 4


作者: 育

摘要
本章 提 UE4项 开发 绍一 决 案和相关工具,主 绍 FASTBuild这
款开 分布式 译工具来提 UE4项 C++代码 译和材质着 器(Shader) 译 。
本章还 详细 绍相关 试过程,下 先简单列出主 试 果。
● 代码 译 极 试:重 译UE4代码,只 单机资 50分 左右; FASTBuild分布式
译工具可以 低到15分 左右;经过进一步优化 低到11分 左右, 提升了4~5 。
● 着 器 译 极 试:重 译4万 着 器,只 单机资 1 50分 左右; 分布
式可以 低到5分 左右, 提升了20 左右。
● 材质 辑器 一 试: 某材质 后,从“应 ”到看到 览 果,只 单机资 1
分 左右, 分布式可以 低到8秒 ,让材质 辑器 “实 览”功 变得更加 “实 ”。
当 , 分布式之后还有其 , 开Unreal Editor(Unreal 辑器),新 景 速度
也 提 , 为其中 着 器 译 提 了,本章 于篇幅就不一一 绍了。 果 对 提
UE4 项 开发 感兴趣, 么请 读本章 。 说明 是, 中所有 、截图和代码
于Unreal Engine 4.20.3和FASTBuild 0.95,不同版本 引擎代码 译 、为实现分布式 进
代码 等 有所区别。另 , 中所展示 试 果 于后 绍到 试 和 络 ,
根 不同 硬件环 ,分布式工具所 起到 果也 有所不同,请 意区分。

13.1 引言
UE4 其开 、开 、不 更新和追求最新技术 性, 较 硬件性 , 取更真实
果,也具有 善 戏模 ,以及丰 件和工具 持等 ,可以适 于几乎 品类和平台
戏开发,广受国 开发商 欢迎, UE4开发 戏 作也比较 。
谈 起 UE4 , 也普 提起“重”这个字,相对 , 觉 得 Unity3D 引 擎
更“轻”。“重” 代 “功 强 , 习曲线比较 峭”“模 比较 ,想 不太 易”等
观 以 ,也隐含了一 开发 担忧。也许正 为UE4开 ,开发商想 “更快、更
” 往往 不 觉 想到 代码, 加或 善 分功 或 追求想 果、 等, 常
译引擎 代码 工作; 开发UE4 C++项 程语 性, 运 速度更快, 是 译速
度较慢;更 模 , 豪华 材质 辑、层次 构 材质 持等, 让逻辑更 晰、功 更强 ,
也有 一发 动 身 问 ,往往 累 ——比 某个材质,想 览或 景中看
到 果,往往 等待一 段 ; 善 模 、 件和工具也 得 戏 更加丰 , 戏 积
也更 , 开Editor或 景变得更慢。
提 UE4项 开发 呢?为每个开发 员 一台 级工作站, 可以提 , 不
加成本 情 下,有办法提 吗?这 就 想到了“分布式”。

13.2 UE4分布式工具
UE4 身也很重 问 , 开发或引进了一 “分布式” 决 案,下 绍UE4现有 分
布式技术。
13.2.1 Derived Data Cache(DDC)
档:https://docs.unrealengine.com/en-us/Engine/Basics/DerivedDataCache。
Derived Data Cache分本 Cache和 络Cache,前 于存 本 过 资 ,
态模 和 模 、材质和 局着 器、 、 形碰 息( 度图)、寻路碰 息、 度 、
贴图、UV动 、 等,几乎包含了 分 UE4 辑器直 ;后 跟前 性质相同,
是存 络(比 局 )上 录里 ,可 于 个 。
开发 员 Unreal Editor 开项 或 新 景 ,Editor 优先从本 DDC里加载所 ,
这 是上次运 Editor 过后 存 本 ,这就 了重 相同 ; 果某
本 DDC里并不存 ,则 试从 络DDC里 取, 果存 则 制到本 DDC且加载到Editor
中; 果 没找到,则实 件( 材质着 器), 毕后 存到本 DDC里 下次 。
可以想象一下, 设 有很 开发 员 Editor 辑 景, 果没有 络DDC, 么对于每
个资 每个 实 一次; 果有 络DDC,则 分 有可 只 一次。这本身
就是一种“集中计算,分布 ” 经典应 景, 提 个 工作 。
于 根 实际情 设 一 DDC 息, 络路径 息,所以默认并没有启
络DDC, 根 档 具 项 件中设 实现该功 。
13.2.2 Swarm
档 : https://docs.unrealengine.com/en-
us/Engine/Rendering/LightingAndShadows/Lightmass/UnrealSwarmOverview。
Swarm主 于分布式计算光 、 影等光 贴图, 态 和 态/ 光较 景中
发 很 作 ,加快光 贴图 成。当某个 成员 成光 贴图 , 分 务到其 空 客
机(可以是其 成员 )帮忙计算一 分,分 比 越 , 计算 务 成速度就越快。这是
典 “分布式计算” 景。
就 上 所说 , 态 和 态/ 光较 景中,Swarm 发 很 作 , 移动
端 戏项 、 运 低端 PC 上 戏项 等建议 Swarm; 追求真实 果, 分 是实
光 项 中,Swarm 作 很 。所以应根 进 设 , 设 请参 档。
13.2.3 IncrediBuild
IncrediBuild 是第三 公司 商业 决 案,是一种较为成 分布式 译工具。 持C++项
分布式 译,也 持分布式运 立 开发工具等, 各 对应 授权,单个功 授权费还算
宜, 果 到 功 较 ,授权费则比较贵。
UE4 集成了该软件 口,包含Unreal Build Tool(UBT)和ShaderCompiler两 口
集成。 果项 成员 上 有 IncrediBuild 软 件 , 设 相应 络环 和对应
license, 译UE4代码 UBT检 到本 有IncrediBuild,则 优先 进 分布式代码
译; 启动 Unreal Editor 初 化 , 检 本 件是否 许和 了IncrediBuild, 果检
过,则 Editor 过程中一旦有 着 器 译 求,就 进 分布式 译。
下来 绍本章 主 ——开 分布式 译工具FASTBuild。
13.2.4 FASTBuild
:http://www.fastbuild.org/。
语 : 持C、C++、Objective-C和C#等。
译 器 : VisualStudio ( MSVC ) 、 VisualStudio.Net ( C# , 前 持本 译 ) 、 GCC 、
Clang、SNC、Intel compiler、GreenHills、CodeWarrior等。
标平台:Windows、Linux、Mac OS X、PS 3、PS 4、Xbox 360、Xbox One、Wii、WiiU、
Android和iOS。
工作系 :Windows、Linux、Mac OS X。
以上 绍来 FASTBuild 。这是一款开 软件, 于类 MIT协议, 度较 。
FASTBuild 持 么 标平台、 么 语 ,看上去比IncrediBuild 一 ,是怎么实现 呢?原
来 利 了现代 译器 持 “Preprocessor”,即 机制。 ,C++语 译 有两个
主 过程, 先 析语义,对一 #if、#include 等语义关 字进 ,同 还进 替 、 束符
替 、 号 等,这个过程就称为“ ”, 后 进一步 译成 标对象 件。现代 持
语 及其 译器, 持显式 过程,即可以 过参 控制进 ,先 成 立
中 件,不再 赖其 件等第三 件。FASTBuild 就利 了这一 , 常轻松 实现了 持
么 语 、 么 标平台,以及可以 三 主 平台上工作。
另 ,FASTBuild也跟IncrediBuild一样 持 程语 分布式 译,也 持第三 立程序
分布式运 ,这是怎么 到 呢?原来 设计了 己 本——.bff 件, 档中可以查看到具
本格式和语法等。以 程语 译为 , 一个项 上想 FASTBuild 实现分布式 译,
就 对该项 所 程语 和 译工具版本等 性, .bff 件中详细记录。 C++项 ,
译器cl.exe 哪里, 又 赖哪 .dll 件, 这个项 里有 个.cpp 件 译(即
个 译 务),分别是 么,又各 有 么 件 引 ,各 了哪 译参 , 译 果 是
么,等等。
还是以 程语 分布式 译为 ,我 看看 FASTBuild 工作 程是怎样 。 根 .bff
件描述 息, 本 对所有 译 务进 成中 件; 下来 中 件 译成真正 obj对象
, 根 本 CPU空 程度分 该 译 务是 本 还是发布到远程 , 果是后 ,则 连
到可 远程 后, 先 译器程序发送过去,再 译 务 后 中 件发送过去( 果
同一次 译中同一台远程 先后分 了 个 务,则每个 务本身 中 件 单 发送, 一开
发送 译器程序则不 重 发送)。远程 FASTBuild主程序 到 译 务后,直 运 之前
译器程序, 该中 件 成 标对象 件;最后 标对象 件 发起 务 。当 ,这
只是主 工作 程,实际 工作 程 杂一 , 分 译 务 远程 中很久 没有 成怎么
,远程 译出 或 有警告怎么 , “ 译” 务(一 称为分布式计算 务)
等,有兴趣 读 可以 了 , 于篇幅本章就不再进一步展开了。
么FASTBuild跟IncrediBuild 础工作 程上有 么区别,以及 此 哪 具 不同
呢? 明 一 下 , IncrediBuild 是 闭 商业软件,与其工作原 相关 档较 , 此以下与
IncrediBuild相关 分析, 是作 本 根 工作经 一 个 总 , 参 。
还是以 程语 分布式 译为 。IncrediBuild也 译器程序发送到远程 , 不对
件进 ,直 根 本 CPU 空 程度分 译 务是否 发布到远程 , 果是,则直
发送 件和 译参 过去,远程 IncrediBuild客 端 到 译器程序直 译该
件,当 到某 赖 件 ,再向 务发起 索 该 赖 件,并 该 件存 到本 存中,下一次
果有其 译 务也 该 件,则直 从 存中读取, 不 向 务发起 索 。 译 成后,远
程 果 件发送 务发起 。
这个 程也描述得比较简单, 正是两 主 区 别 ——FASTBuild ,
IncrediBuild不 。后 这 了比较 工作,实现起来比较 杂, 得 也较 。下
简单 列两 各 有 么利弊, 13.1所示。

13.1 FASTBuild与IncrediBuild对比

过这 简单对比可以看到,IncrediBuild还是 常有优势 ,进 分布式代码 译 , 相同硬


件和 络环 中 一 比FASTBuild 一 。 FASTBuild也有其优势,我 正确对待:即
进 代码 译 ,FASTBuild也可以提 ; 进 “ 代码 译” , 后 试 着 器
译, 名字中有“ 译”, 实际上是分布式运 立工具程序进 分布式计算 务, 就
须“ ”,此 应该相差 几。当 , 果想 比较“ ” 分布式工具,从这个
度来看,FASTBuild有比较 优势。

13.3 在Windows系统下搭建FASTBuild工作环境
13.3.1 网络架构
作为开 软件,FASTBuild 身也比较崇 , 社区里看到别 提问,为 不 IncrediBuild
样提 一个服务器程序来实现 分控制功 呢? 员直 “去中心化”来 ,所以FASTBuild
是没有中心(即服务器程序) 。
所有 同一个局 , 可以 某一个 同 录路径作为“Broker”,当某 启
动FASTBuild主程序(FBuildWorker) ,该程序 名称作为 件名 这个Broker 录里创建一
个空 件,并且 历该 录下 其 类 件,从 得知 络上 有哪 ,当 发起分布式 务
,就可以 过这个列 去 试连 ;当本机中其 程序(不含FASTBuild主程序,以及协助其
分布式 务 运 所有 程序) CPU占 超过一 (20%)或退出主程序 ,从该 录下
删 代 本机 件,这样其 就不 利 该 帮助 分布式 务了。
过上 简单 描述,我 对FASTBuild 络组成和各 工作有了 致 了 。 下来 两
台 A和B来 示, A( 后 截图中IP 尾号为79)上 建 译环 和发起 译,B(IP 尾
号为97) 协同 译,展示 建工作环 。
13.3.2 搭建基本环境
A上 正常 译环 , Visual Studio 2017 Community; B上只 正常
Windows系 环 即可, 须 译器。
实际上这两台 是平等 ,下 称为 Worker—— 果 B 上也 了 译环 , 么B也可
以发起 译,此 A就可以和B互 。所以,下 提到 “Worker”就是 这种情 下 意
(举一反三,推导到两台 以上 情 )。
关于FASTBuild本身 和 , FASTBuild 上有比较详尽 档 绍。主 有两步:
和 。
从 下载最新版本程序压 包, 压 到本 录下。 版本只包含FBuildWorker.exe和
FBuild.exe 这两个可 件,前 是 (UI)程序,也是 FASTBuild 主程序;后 是命令 程
序, 发起分布式 务 , 该命令 程序 合不同 参 来 。另 ,推 下载FBDashboard
这个第三 控程序,可以从FASTBuild 下载页 中找到 。该程序 第三 提 ,本身也
是 于MIT协议 开 软件。 后 绍 UE4实际应 中,我 了该程序。
简单 “ ”之后, 进 一 , 步 本 跟系 环 变量设 有关。
先 压 录路径加 系 环 变量“Path”中,这样就可以 访问FBuild.exe
分布式 务了。
后设 Broker , 系 环 变量中 加FASTBUILD_BROKERAGE_PATH = xxx即可,xxx是一
个所有Worker 读 络路径。请 意,FASTBuild 前还不 持Unicode路径, 果 路径确实
有Unicode字符,则可以先 该路径映 到每个Worker 本 符,这样就可以 含ASCII字符 短
路径 Broker 了。
另 , 果 Cache 功 ,则 设 Cache , 系 环 变量中 加
FASTBUILD_CACHE_PATH = yyy即可,yyy可以是 络路径,也可以是本 路径,请根 设 。
Cache路径也 含有ASCII字符。 意:Cache 件总量 递 ,所以请根 实际 持空
足 。另 , MSVC 有一 简单 制( /Z7,不 /clr), 档中有说明。当 ,也
可以不设 这个环 变量, FASTBuild 译 本中也有 设 具有相同作 变量,请 档中
查看,比较简单。
上述 和设 步 所有Worker上 进 一 ,到此为止, 本环 设 就 成了。
意 是,环 变量 变并不 直 应 到已经 开 程序上, CMD、Visual Studio IDE等, 有
请重启这 程序。
13.3.3 可用性验证
初次 建FASTBuild工作环 ,有可 络策 、 或 录权 等 素不 正常工
作,所以我 过简单 实 来 证 否正常。
先, 每台机器上 运 FASTBuild主程序(FBuildWorker.exe), 图13.1所示,推
参 -cpus=−1,-mode=idle来运 。

图13.1 FASTBuild主程序

上 息 绍 下。
● 标 栏:显示当前程序名称和版本号;当前运 是x64位程序;机器名以及 连 了这
个“Worker”( 么 没 ,所以是0 Connections)。
● 工具栏:当前 态设 为“Work For Others When Idle”(参 -mode=idle),两台 分
别 了7个逻辑核CPU(参 -cpus=−1,即最 逻辑核 1)。
● 主窗口:展示每一个逻辑核 当前 态,分别是第几个逻辑核; 主机名( 果此 别
发起分布式 译,这台 参与了协同 译,就 这里展示每一个连 到这台 主机名);这台
每个逻辑核上参与协同 译正 事情,当前没有参与 务,且 足“空 ” 态判 策
,所以显示Idle 态(参与协同 译后, 显示正 帮助“ 译xxx.cpp”等)。
该程序运 后, 果该机器 正常 读 权 访问Broker 录,且 足“空 ” 态判 策
(本机其 进程 CPU 20%以下,则认为是“空 ” ), 往Broker 录里 一个以本
机器名为 件名 空 件;当不 足“空 ” 态判 策 或 退出该程序之前, 从Broker
录里删 该 件。所有Worker 可以 过 历这个Broker 录里 这类 件 取当前可 络端其
Worker 息。
后, A上发起 译。
档 口,很 易找到一个“Hello World”示 , 绍 FASTBuild 本 件
(.bff 件)。实际上这个示 太 了,并不 展示分布式 译 功 。所以我 直 以FASTBuild
代码作为示 ,可以 下载页 中下载到最新 代码。
压 代码到本 意 录下,可以 code 录下找到其主 .bff 本 件,请 本 辑
器 开 ,根 实际 译器和 路径 (主 是Visual Studio 路径、Windows
SDK 路径等)。
命令 窗口中进 该 code 录, fbuild.exe -showtargets 查看所有 译 标参
,其中有一个 solution 标参 ,可以 成 Visual Studio Solution 件, 查看和
FASTBuild 代码;也可以 其 译类 target直 译FASTBuild 代码。详细命令 操作 式,
可以运 fbuild.exe -help查看所有命令 参 ,或 查看 相关 档。
这里 -dist -clean参 译All-x64-Release , 图13.2所示。
从 译输出中可以看到,本机从 Broker 中发现了一个 Worker, 是实际上后 所有 译
成.obj 件 只提示“<LOCAL>”,并没有远程B机器名 提示出现,也就是说,并没有 到远程
B,问 出 哪里呢?
经过一番分析和 试,总 出这里主 有4个可 出现 问 决。
(1) 所有Worker 上开 Windows 白名单。 络应 程序 不开对 Windows
,一 有 包 软件 动 册 白名单, FASTBuild “ ”过于简单,
所以 动为FBuildWorker.exe和FBuild.exe设 白名单。

图13.2 译输出 果1

(2)确 FASTBuild所 TCP端口是开 。 分公司 有管 策 ,默认屏 所有 必 端


口。 果是这种情 ,就 开 FASTBuild 端口号 权 ,默认端口号是31264,也可以根
实际情 代码中 成一 不 其 程序占 端口号。
(3)检查Broker 录 访问权 ,确 所有Worker机器 从中正常读 件。
(4)有 一 络环 下( 某 跨 段环 等),机器 法直 过主机名 析 IP 进
相互访问,此 就 FASTBuild 代码,直 本机IP 册Broker,以 其 机器可以直
取远程 IP 列 。下 这段是实现这个意图 主 代码。另 ,还 该 明和调
,比较简单,请 充。

成 译后, 所 有 Worker 上替 原来“ ” 件 ( FBuild.exe 和


FBuildWorker.exe),重新运 主程序。此 主程序窗口 标 栏中已经显示了本机IP ( 图
13.3所示);当 足“空 ” 态 , Broker 录中也 该IP 进 记( 图13.4所示)。
图13.3 标 栏中显示IP

图13.4 Broker 录里出现以本机IP 命名 空 件

过对上述几个问 查和 决,再次 试 译,发现一切正常,出现了 分 译发 远程


Worker上 记录, 图13.5所示。
图13.5 译输出 果2

至此,Windows系 中 FASTBuild分布式工作环 已经 建 毕,并且 过了可 性 证。下 我


进一步 绍 UE4代码 译和材质着 器 译中 FASTBuild,为UE4 上 翅膀!

13.4 使用FASTBuild分布式编译UE4代码和项目代码
13.4.1 准备工作
前 绍了IncrediBuild已经 UE4 引擎 译工具Unreal Build Tool(UBT),所以
IncrediBuild 分布式 译 UE4 代码 是不 进 ( IncrediBuild
机环 即可), 是FASTBuild并没有跟UE4发 集, 进 分布式 译UE4 代码之
前, 我 己 动 UBT 代码, 加对FASTBuild 持功 。
其主 思路 前 讲FASTBuild工作原 已经 绍过,即:只 当前 译器主程序(
cl.exe)、 赖 DLL 件,项 中 设 Include、Library等公 录,Windows SDK路径等
息,以及UBT本次 译 所有 译 务 息( 件、 赖 件、 译参 等)、 息等
FASTBuild 本语法 .bff 件中即可。
说起来比较简单, 实际动 还是有相当 度 。还 FASTBuild是开 软件, 有一个
开 社区,有很 开 为 了周边相关程序, 动UBT以 持UE4和项 代码 译也有公开
第三 , 至可以 FASTBuild 下载页 中找到相关 , 可 存 更新不及 问 ,
找到相关程序后,只 根 实际 UE4版本调 相应代码即可。
论 ,这一步是 FASTBuild加速UE4 代码 译 前提,可 一 调试UBT, 相
还是 得 。
另 ,前 提到开 社区还有一个比较 软件FBDashBoard,可以 来可 化 控当前 务
态。 运 FBDashboard程序后,该程序 隐 原有 FBuildWorker程序系 栏图标,并出现 己
图标, 图13.6所示。

图13.6 FBDashboard程序图标

实际 试 ,发现 可以很 控分布式 译 务 情 , 也有不 正常工作


情 ,与UBT 代码类 ,也可 存 更新不及 或 合不 问 ,此 只 重启该程序一
就可以 决。
13.4.2 部署多机FASTBuild环境
前 绍了 Windows系 下 建FASTBuild工作环 ,我 该 绍 实际项 成员
计算机上 了环 ,并引 了开 社区 FBDashboard程序。这样一来,我 就有了 概15台
组成 FASTBuild分布式 络,后 绍 试就是 该分布式 络下进 。
13.4.3 编译UE4代码及对比测试
为了展示 FASTBuild分布式 译工具 实际 果,我 进 了对比 试。
● 硬件和 络: 试 有i7 7700 CPU,32GB 存,512GB SSD,NVIDIA 1060显卡; 络是
千兆局 ,加 分布式 络 计算机是其 成员 开发机,总 量有15台左右,一 相差
几,其中有一台是属于性 较 工作站。
● 试对象:相同 两 Unreal Engine 4.20引擎代码,其中一 为原 版本,只 身 计
算机资 进 译;另一 是 过UBT可以 FASTBuild进 分布式 译 版本。 试 同一台
试机上对 分别重 译UE4代码 “Development Editor” 项(x64平台),观 其中 最
项 “UE4” 成 译所消 。
先只 试 本机资 译UE4代码,以下是Visual Studio 输出截图, 图13.7所示,
50分 左右 成了UE4这个主 项 译。
图13.7 输出截图1

着 试 FASTBuild UE4,同样进 译。 图13.8所示,输出 Log格式已经发 了变化


——前 没有了 务 和总 记录,有 译 务后 有远程机器提示。

图13.8 输出截图2

译 成后,FASTBuild还 有详细 汇总 息,包含Cache Hit、Miss和Store ( 试分


布式 译 果 没有启 Cache功 ,所以这几个 一片空白),以及 译 成 (有实际 和
CPU逻辑核心 概 等), 图13.9所示,最终只 15分 左右就 成了UE4引擎代码
译,不到单机 译所 三分之一。
图13.9 FASTBuild 汇总 息

再看一下FBDashboard 截图,其比较类 IncrediBuild 控模式,可以 常直观 查看


到本次分布式 译 务中有哪 远程机器协助 了哪 务, 图13.10所示。
为是 络分布式工具,所以 本次 试过程中我 也 意观 了一下 络 量情 , 图13.11所
示。
这是 发起 务 上选择比较有代 性 络 量 峰 段截图,请 意波形,其中 线是发送
量,实线是 量,纵轴 每一个 格代 100Mbps 。 从 图 中 发 现 平 量超过了
300Mbps,也就是说, 果 百兆局 , 么 络 成为 ;即 是千兆局 , 果 同
发起分布式 译,且 相同 段 达到 络 量 峰,此 也 存 总 络 。
图13.10 FBDashboard 截图
图13.11 发起 译 机器 络 量情

13.4.4 优化FASTBuild
图13.11还隐含了一个可 细心观 发现 问 ——“发送” 量和“ ” 量不成比
,“发送”比“ ” 波形 本 低很 。 存 络 , 么是否可以 这 一 优化
呢?
过详细 读FASTBuild 代码发现,原来 发起分布式 务 ,发起 LZ4快速压 算法
经过 中 件压 后 送到远程 ; 是 远程 成 译, 成.obj 件后 到发
起 ,并没有经过压 。为了 证这一发现,我 再次 过 FBuild.exe -forceremote参 (强
制所有 译 务 远程 上 ) 双机环 中重 一个 项 , UnrealHeadTool项 ,观
到 远程机器 卡 态中得到 发送字 总 ,跟最终 成 .obj 件总 本一致,进一步
证了 果 没有压 这一 论。 果对这 Obj 件进 动 7z 快速压 ,发现可以压 掉
65% 总 件 ,这说明具有很 优化空 。
下 是优化代码。
● Tools/FBuild/FBuildCore/WorkerPool/JobQueueRemote.cpp
● Tools/FBuild/FBuildCore/Protocol/Server.cpp

● Tools/FBuild/FBuildCore/Protocol/Client.cpp
13.4.5 再次测试分布式编译UE4代码
优化后 成 FASTBuild程序替 掉“ ” 录里 原有程序,重新进 上述分布式 译UE4代
码 试。
果 图13.12所示, FBDashboard (右下 )可以看到本次 试 11分 左右(该软件
右下 提示 ),比优化前15分 左右有了不 进步。
图13.12 FBDashboard 控

优化后 络 量情 图13.13所示,“发送”和“ ” 波形不再相差 么 ,平 量


本 100Mbps左右。此次优化不 提 了FASTBuild分布式 译 , 且前 提到 “ 同 发起
分布式 译可 发总 络 量 ”也 得到 善。
图13.13 优化后 络 量情

13.5 “秒”编UE4着色器
络 DDC可以提升 工作 ,并且 也包含材质相关着 器(Shader)和 局着 器
存, 并不 决所有着 器 问 。 ,对于日常 Unreal Editor 比较 繁
操作——材质 辑,UE4提 了功 强 式材质 辑器,并且 持实 览, 每次 开
某个材质 立即实 译一次所有相关着 器, 意 后 击“应 ” (或“实
览”功 开启 ),也 实 译一次所有相关着 器。稍微 杂一 材质涉及到 着 器总 可
有几百或上千,每次 译 这 几分 不等, 越 杂 材质,想 调试出最终 果越 重
干次 、 览 操作,每次 等Editor右下 “ 译着 器”提示 束 看到 果,
累加起来 等待 常可观。
下来 绍 FASTBuild提 材质 辑 , 决材质 辑、调试 痛 “等待”问 ,
实现“秒” !
13.5.1 准备工作
跟 译 UE4代码类 ,也 动 UE4 代码着 器 译相关 分,以实现 FASTBuild进
分布式着 器 译。
代码 思路跟UBT类 ,最终 成一个 循FASTBuild 本语法 .bff 件。与UBT不同
是,这种应 其实属于“ 立程序”分布式 式, 须 循 么 程语 则, 且可以
UE4 代码中关于 IncrediBuild 译着 器 分 ( ShaderCompilerXGE.cpp ) 实 现
FASTBuild 译着 器, 主 对象类FShaderCompileFASTBuildThreadRunnable,以及 实 化
等,所以 简单一 。 论 么简单,代码还是很 ,所以这里只简单 展示 分核心代码,其
分请 发 动 力吧!
下 是一 与初 化相关 代码。初 化 从引擎 件 BaseEngine.ini 中读取
息“是否 许 FASTBuild 分 布 式 译着 器 ”; 果 许,则还 查找看本 是否存
FBuild.exe 程序;最后检 所有着 器 件是否 一个根 录下(引擎着 器和项 录下 着
器), 发送到远程 后,这 着 器 件 远程 FASTBuild临 录下存 , 之 相
对路径关系必须没有发 变,否则 译 赖关系 破 。 同 足这三个条件, 可以
FASTBuild分布式 译着 器。
下 是 成.bff 件 件 息、 义 Compiler 息、 赖 件列 (ExtraFiles)。
赖 件,就是 分布式 译 ,先发送给远程机器其 译所 工具、着 器 件等。有了这 件,远
程机器一 就可以正常 成协助工作。
下 CompilingLoop 实际 译 务,根 实际着 器列 成.bff 件 务列
,并运 FBuild.exe进 分布式 译。
过上 代码 本设 CompilerOptions = '\"\" %d %d \"%%1\" \"%%2\" -xge_xml
%s'\r\n " 可 以 发 现 , 我 这里取(偷)了个巧(懒), Unreal Editor 调
ShaderCompileWorker 这 个 工 具 译 着 器 , 了 IncrediBuild 参 , 这 样
ShaderCompileWorker工具就 当成正 IncrediBuild来运 不 这个工具软件 代
码了。跟UBT一样,这一步是实现分布式着 器 译 关 ,所以请 一 调试一下,确
正常工作。
下来我 进 两项 试: 模着 器 译 试和材质 辑器 着 器 译 试。这两项 试分
别对应 局着 器,尤其是 .ush 件 可 到 情形,以及一 工作中常见 调试材质
果 情形, 证FASTBuild是否对着 器 译 有所提升。 试机器仍旧是前 绍引擎代码分布式
译 机器, 络 机环 也一致。
13.5.2 大规模着色器编译测试
为了 客观 评 FASTBuild分布式 译着 器 带来 , 先进 一次“极 ”
试,即 模着 器 译 试,模 局着 器 件Common.ush后 临 情形。单机 译着 器
操作步 概 下:
(1)禁 FASTBuild分布式 译着 器。
(2) 开Editor, 开Demo 景。
(3) Common.ush,随 加几个空格 存。
(4) Editor中 “Ctrl+Shift+.”快 译着 器,观 Editor各个 译阶段以及“ 译着
器” 量 提示。
为 译着 器跟 译代码不一样, 没有最终 汇总报告,所以 试着 器 译 了
Windows 计 器 式来记录最终 。这里只给出 译开 、显示 译着 器 量 提示和 译
束 截图, 图13.14至图13.16所示。

图13.14 译开 截图
图13.15 显示 译着 器 量 提示截图

图13.16 译 束截图

从上述图中可以看到,实际 译 着 器 量有44000 个,从开 译,到最终提示“ 译着


器”消 ,总 1 50分左右。
分布式 译着 器 操作步 与之类 ,只是一开 BaseEngine.ini 中启 FASTBuild进
分布式着 器 译,重新运 Unreal Editor并且 Common.ush后进 同样 操作, 图13.17至
图13.19所示。

图13.17 分布式着 器 译1

图13.18 分布式着 器 译2
译 着 器总 是一样 (上 图是从录屏 里截取 ,中 显示 着 器总 有细微区别,
是 于暂 机不 么精确造成 ,实际上总 是一样 ),只 5分 左右就 成了所有着
器 译。
极 试 果让我 很惊讶,经过了较 果检查和 觉 果对比, 确认着 器 译 果
是正确 , FASTBuild分布式 译着 器对 提升确实 常明显。

图13.19 分布式着 器 译3

13.5.3 材质编辑器内着色器编译测试
平 开发 , 有 局着 器 情形, 这种操作一 比较 ,与着 器 译更 切 操
作是 材质 辑器 和调试材质 果。所以我 又 Unreal Editor 材质 辑器进 了进一步
试。 试 选择了一个 微 杂一 材质,也进 了单机 译和 FASTBuild分布式 译 两次对
比 试。 试步 很简单,只简单 了材质某个相同 ,观 从 击“应 ”到实际看到 览
果 。
单机着 器 译 图13.20至图13.22所示。
图13.20 单机着 器 译1

图13.21 单机着 器 译2
图13.22 单机着 器 译3

从上述图中可以发现,只进 了 其中一个“Color 红 输出到 光 ”这么简单 ,


后应 后提示有413个着 器 译,直到 成 译,总 1分 。
启 FASTBuild分布式着 器 译 试 图13.23至图13.25所示。

图13.23 分布式着 器 译( 材质)1


图13.24 分布式着 器 译( 材质)2

我 进 了相同 操作,应 后,8秒不到就 成了。材质作为主 现 段之一,


UE4项 中进 材质 辑也是日常工作中 重 。 过本项 试可以看到,FASTBuild分布式 译着 器
也可以帮助我 提 这 工作 ,有 短 繁调试材质 果带来 等待 。此 ,UE4材质
辑器 “实 览”功 也 更加“实 ”和实 , 每次调试 必须 击 查看 果, 得
调试材质这项工作更加 。

图13.25 分布式着 器 译( 材质)3


13.6 总结
过上 绍 分布式 译UE4着 器 可以看到, 利 了FASTBuild 持 立工具程序
ShaderCompileWorker.exe 分布式运 来实现 提升,这进一步证实了FASTBuild并不 是一
个分布式 译代码 工具, 还可以实现很 分布式计算 务, 这一 上 可以发 想象力——希
望本章 起到 砖引 作 ,让 发现更 分布式应 景,提 开发 。
第14章 一种高效的帧同步全过程日志输出方案[1]
作者:唐声

摘要
分情 下, 帧同步技术 案 戏,其所有逻辑 运 客 端,服务器只 于 转
发和校 。一旦客 端之 逻辑出现了不一致,服务器就 法对其进 正了,从 造成当前这一单局
法 救 后果。为了 决这个问 , 此提 一种 于 位客 端之 不一致Bug 决 案。
过程日志,是 个 戏逻辑中每一次 调 日志。该日志包含 名字、所 件名、
件 以及 实参 息。可以 见到这样 过程日志,其 量一 是 常 ,并且输出这
日志所 CPU开销也是 常 。
么,就 利 巧 压 案来 量,以及巧 优化 案来提升性 。最后 达到
果是, 盲 中,当日志开关一直 开 , 感觉不到与日志开关未 开 有 区别。并且 低端机
( 龙625) 量化 试中,对fps 影响 1%左右; 中 端机 中, 为 终是 帧(30fps)运
,所以 不出 差别。

14.1 引言
现代 戏引擎 带日志系 ,并且功 强 实 , 是 到 戏引擎具有一 性,
日志系 先也是 于 性 设计 ,对性 反 其次。正是 为 此, 实际 应
中,App 常 设 一个日志开关, 于 App Release版中 重 级别不 日志关掉。 是 果
到Bug,没有足 日志 息,则可 法 确 位Bug 逻辑 发 ;同样 为 此,日志 法
盖所有 相关逻辑,可 正 没有 盖到 Bug 逻辑 发 ,于是,即 App Debug版中 到
Bug,也可 确 位Bug 日志 息。
果有一个 性 日志系 ,情 就 不同了。我 可以 每一个 口、每一个逻辑分
, 至每一个我 认为关 逻辑 输出日志;并且, App Release版中,我 也可以 日志
开关 开, 不 担心当Bug发 日志 息 问 。
不同 具 应 景下,对于 性 日志系 具 实现有所不同。本章 绍一种已经成功应
于一个 于 Unity 帧同步 戏项 具 实现,并且该日志 案 实现思路也 可以 到其
应 景中。

14.2 帧同步的基础理论
于帧同步 戏项 是本 案 一种 常典 应 景。 绍本 案 具 实现之前,有必
先 绍一下帧同步 相关知识。 果读 对帧同步已经有所了 ,则可以跳过这一 。
14.2.1 基本原理
一 来讲, 同步 式可以分为三种:P2P、C/S和帧同步。P2P已经很 出现 现代 中了;
C/S 是最为普 和 一种 络同步 式,现代 戏引擎 带 络同步 式 是 于 C/S 模式 ;
帧同步是相对 异化 一种 络同步 式, 是 以下情 下具有 C/S模式 法比 优势:
● 一致性。
● 开发周期短。
图14.1所示,Server 逻辑 切分为一个个等 逻辑 片,每一个 片对应一个
Frame,每一个Frame有一个代 逻辑 序号。Client 输 模 到 输 命令(Cmd)
后,并没有 Cmd直 给 戏逻辑, 是发送给了Server。 果Server 同一个 片(比 Frame
3) 周期里, 到了Client A与Client B 输 , 么就 这两个Client 输 以Cmd列 形式
包含 Frame 3里。等到Frame 3 周期 束 ,就 Frame 3分别发送给Client A和Client B。
么,两个Client 相同 逻辑 里,就 到 相同 Cmd列 。

图14.1 帧同步原 示意图

果Client A与Client B 逻辑也 相同, 么对于 相同 Cmd输 , 论上, 计算得


出 相同 逻辑 果,从 实现了 一致性 络同步。
于帧同步 ,可以 种RUDP 案来适应弱 络,并且达到 常 实 性。 于
Server不包含具 戏逻辑,所以可以 短开发周期。
14.2.2 系统抽象
可以进一步 上述帧同步 戏逻辑模 象为 图14.2所示。
图14.2 象计算模

个模 可以 象为“输 ->过程+运算->输出”。 果 到:当输 A与输 B 一致 ,输出


A与输出B也 一致—— 论上,只 过程A与过程B 一致,并且运算A与运算B也 一致,即
Client逻辑代码 一致即可。
,事情并没有这么简单,随着模 模越来越 , 赖和 赖 关系越来越 杂,总是
意中引 一 Bug,从 造成不一致 问 出现。出现这 不一致问 可 原 可以归纳为 14.1
和 14.2所示。

14.1 运算不一致 可 原

14.2 过程不一致 可 原

可以看出,有一 分原 ,比 不同硬件上 精度不一样所造成 计算 果不一致等,可以


研发过程中 过一 技术 段进 控制。 对于相对可控 原 ,比 态变量未重 等所造成
Bug,最终 现 后 逻辑过程中。 实际项 中, 为架构设计 原 ,核心逻辑层 提
一 给 (或 现)逻辑正常调 , 是 这 调 里, 果 变了核心逻辑 态,就有可
造成Bug。
这个 , 果每次逻辑过程 调 有对应 日志 成, 么,对于 确 位Bug 是有着巨
帮助 。 别 , 项 中后期,以及上线运 期 , 个系 越来越稳 ,不一致问 出现也越
来越随机和偶现。这个 ,就 常 赖日志系 来帮助 位和 决问 。

14.3 本方案最终解决的问题
对于帧同步项 ,本 案最终 决 问 是,当两个客 端出现不一致 ,怎么快速 确
位造成不一致 Bug 发 。
一 位过程 图14.3所示。 于 分日志系 性 消 较 ,所以日志开关一 是关闭 。
当发现了一个Bug , 重新 开日志开关, 后重现Bug,这个 取到日志进 分析。同样
于 分日志系 性 问 ,日志 法 盖所有 逻辑过程,所以有可 法从现有 日志中发
现线索,或 发现 线索 法明确 位到问 。于是,又 加日志代码,重新构建版本,再重现一次
Bug。随着项 越来越成 ,Bug 出现概 越来越低, 么重现Bug 成本 越来越 。 此循
环反 , 位一个Bug 十分低下。

图14.3 一 位过程

想中 位过程 图14.4所示。 有一个 性 日志系 ,可以 得日志开关是常开 ,并


且可以 盖 个系 逻辑过程。 么,一旦发现了不一致, 过对比两个Client “ 过程日
志”,找到日志中不同 一 , 过日志所附带 息,就可以 确 快速 位到 不一致 个

图14.4 想 位过程

图14.5所示,展示了不一致 两个过程对比示 。

图14.5 不一致 两个过程对比示


从 论上, 过对比过程A与过程B “ 过程名+参 ”日志序列,就可以 位到造成不一致 Bug
所 逻辑 。
为了尽可 精确 位到出问 逻辑 ,日志 盖 尽可 。 果上述 过程是一个 ,
么就 调 每一个 输出日志。 果上述 过程是 中 一个逻辑分 语句 , 么就
这个语句 输出日志。
果 个系 量 常 , 么就 一种 法 动 第一 代码之前 日志代
码。同 ,为了实现 逻辑 输出日志,还 持以 动 式 位
日志代码。
为了实现 性 日志输出, 对每一 日志代码进 Hash 计算,得到一个 示这一 日志代
码 唯一ID, 日志 件中只 存该 日志 ID以及日志参 , 不 存其 于提 日志可
读性 本 息。
析日志 , 过日志ID从LogPdb(一个 存了日志ID对应 可读性 息 库 件) 件中
取该 日志对应 可读性 本 息,与日志参 合起来显示可读性 日志 本。

14.4 全日志的自动插入
14.4.1 在函数第一行代码之前自动插入日志代码
为了尽可 提 日志 逻辑 盖 ,不可 动 每个 第一 代码之前 日志代码。这里
正则 达式来识别出所有 ( + )。
以下正则 达式可以识别出C# 码中绝 分 代码(根 不同开发 员 码习惯,有
喜欢 static等 符 public等 符前 ,只 调 正则 达式即可。以下正则 达式只是
一个实际 示 , 可以根 项 实际情 合适 正则 达式。 果 项 是 于C++ , 么
也可以 合适 正则 达式识别出 代码):

以下正则 达式可以 代码 础上识别出 ,并以此分析出 名和形参列 :

过已经识别出 和形参列 ,可以 动 对应 日志代码。 , 为

么对应 日志代码为

其中,FSPDebuger.LogTrack是本 案中 于输出日志 ,其第一个参 是这 日志 唯一


ID,后 对日志进 唯一 码 充这个参 ;后 参 是 参 列 , 是有一个细 ,
arg2 为不是 础 类 , 法直 参与 运算,从 不输出到日志中。 果arg2可 影响
运算 果, 么可以 适当 位 动 日志代码( 实际 帧同步系 中,这种情 并不
见)。FuncName 作为日志代码 义“ ”,这里有一个细 , 没有 常C#或 C++
语法:/* */。这是为了让 译器 法 译这 代码, 为 后 程中 到这段 义
“ ”。
当 , 果发现 已经存 日志代码了,或 FSPDebuger.IgnoreTrack()标识为不
日志代码,则不进 。 么 么情 下可以标识为不 日志代码呢?比 一 纯粹
Getter ,或 一 开发 员万分确 不 有问 ,并且没有 出 。
14.4.2 处理手动插入的日志代码
为了 善 动 日志代码 法 盖到 一 逻辑分 不足,可以 动 位
日志代码。比 :

其与 动 日志代码唯一 区别是,后 “可读性 本 息” 是符合当前语法 格


式。
14.4.3 对每行日志代码进行唯一编码
有 种算法可以 于计算一 日志代码 唯一 码。比 , 过该 日志代码 “ 件路径+代码
号”字符串来计算其 HashCode,以 HashCode 作为该 日志代码 唯一 码。 是一 ,字符串
HashCode 有一 概 重 ;另一 ,字符串 HashCode 是一个32位 Int类 ,可以 示2
147 483 647 日志代码, 实际 项 中是不可 有这么 日志代码 。
根 实际情 ,我 项 核心模 中有2 400 日志代码, 一个Short类 足 了,
运 可以 省很 日志 存(当 , 果经过 计项 日志代码 确实 法 Short类 来 示,
么就不得不 Int类 )。
于是,我 了一种比较简单 算法来计算日志代码 唯一 码。
先,为了尽量不去 上次 已经 码过 日志代码, 这一次正式 码前,先 历当前所有
日志代码,找出已经 码过 日志代码,从上一次 LogPdb 中 取该 日志代码 分 息( 论
上,也可以不 赖上一次 LogPdb, 是直 分析该 日志代码及其上下 代码以 取所有 息。 么
赖上一次 LogPdb 是为了 一 程度上提 工具 性 ,比 , 果 个项 代码没有 动,
么 赖上一次 LogPdb 可以 量 正则 达式 ), 合该 日志代码 “ 件路径+代码
号+ 参 个 ”存 这一次 LogPdb中。
其次,再一次 历所有 日志代码,找出未 码过 日志代码,从ID=1开 试从LogPdb中查询该
ID是否已经 占 。 果找到ID=N未 占 ,则N 于该 日志代码 码,并存 LogPdb中。 后从
ID=N+1开 查找下一 日志 ID,直到所有未 码过 日志代码 码。 存 LogPdb中 ,还 存
该 日志代码后 :/#FuncName#/或 /*可读性 本 息*/。其中, 过/# #/和/* */来区
别后 是否为 名。最后 /#FuncName#/删 ,不 法 译 过。
14.4.4 构建版本
意 是,以上操作 每一次构建版本之前 进 ,所 成 LogPdb 与此次构建 版本
号关 。所以,每一个版本 有一个与之对应 LogPdb 件,不同版本 LogPdb 件 有可 不
同。
14.4.5 整体工具流程及代码清单
图14.6所示为 工具 程。

图14.6 工具 程

关 代码 下:
● 日志代码。
● 动 日志代码。
● 对每 日志代码进 唯一 码。
14.4.6 为什么不采用IL注入
对于C#, 过 译 成IL 件。 于 代码 习惯 种 样, IL 格式是 一 , 么
于 IL 比 于 代码 正则 达式识别更加 。 是,为 么不 IL 呢?原 下:
● 果 IL 话, 法 取可读性 息。
● 正则 达式分析 代码,可以很 移植到C++项 中。

14.5 运行时的日志收集
14.5.1 整体业务流程
图14.7所示,展示了运 集日志,发现两个客 端不一致,并且上 日志进 对比 业
务 程。
图14.7 业务 程

到帧同步系 ,当前帧 逻辑 果=上一帧 逻辑 果+(当前帧 逻辑过程+运算),一


旦发现了逻辑 果不一致,并不 对比从第1帧开 日志。 论上,只 对比当前帧(或 上
一帧, 校 是 帧 束 计算 ,还是 帧开 计算 ) 日志即可。
是 于 络 输 ( 图14.7中 【1】与【2】所示),以及不同 校 计算 案可
不同程度 延迟发现问 (比 为了性 ,有 不 对所有 态计算校 , 是计算关 态 校
,或 其 案计算校 ), 此客 端 存最近50帧(或 100帧) 日志,这个
可以根 平 络延 以及逻辑帧 进 动态调 。
于上述 绍,我 义一个FSPDebuger类, 图14.8所示。

图14.8 FSPDebuger类

FSPDebuger.LogTrack()重载了 个实现, 于 持不同 量 参 。LogTrackLoopQueue实现


了一个循环 列, 于 存最近 50帧日志。LogTrackFrame 于 存一帧 日志 ,其中items和
args分别 于存 日志 ID和参 列 。 到 系 List类, 一 GC,所以还 另
一个LogTrackList。
关 代码 下:
● 当进 一帧 , 于切 LogTrackFrame。

● 于输出一条日志(hash,即日志ID)。

14.5.2 高效的存储格式
为了最 可 省 存, 常紧 构来存 运 日志 。
图14.9所示, 一个List<ushort>来存 日志ID, 一个List<int>来存 日志参 。没有 费
一个字 存 空 , 此其实 存 是最 (不 离线压 话)。
图14.9日志 存 格式

这是本 案 核心思路之一。
14.5.3 高性能的日志输出
于日志 参 为 (相对于 日志系 ,为了提 日志可读性 字符串参 ),并
且最终也不 进 日志 本 , 是直 存 线性 组中,毋庸 ,其性 有了 量级 提升。
助于 LogPdb 存 , 最终导出日志 , 可以 得与 日志系 相同 可读性。
这也是本 案 思路核心之一。
14.5.4 正确选择合适的校验算法
图中,不同 校 算法对 个业务 程 一 影响。一 有 下几种 案。
(1) ,对所有 戏对象 所有 态计算Hash 。 于所 计算 态 量巨 , 法 到
每一帧 进 计算。当服务器 过该Hash 判 出不一致 ,可 真正发 不一致 已经过去很
帧了。
(2) ,对常 或 关 态计算 Hash ,可以 到每一帧 进 计算。 是,当服务器
判 出该Hash 不一致 ,可 这之前 干帧有 常 态不一致了。
(3) 过累加和校 ,每一次对 态赋 , 该 态 累加到校 上。 于累加计算 性
是 常 ,并且 一帧里不 每一个 态 出现赋 变化,所以 性 也比 态 Hash计
算 很 ,可以 到每一帧 上报校 。 是其检 力一 , 于位 制和算法 关系,计算
果可 出现碰 ,导致校 果 。并且这 态离 分布 戏 各个 , 校 很 不

(4) 过对每条日志 (即日志 ID 与参 )进 累加和计算校 。该 案正 与最终
过对比日志序列来 位Bug 法 相呼应,从 实现逻辑 洽。 是该校 算法 存 一 碰
概 ,可以 过连 每帧校 来 低概 ,也可以 类 MD5这种本身碰 概 就极低 算法。
实际 应 中,可以根 项 实际情 来选择不同 校 案和算法。

14.6 导出可读性日志信息
于日志 存 常紧 ,必须 合LogPdb 还原出可读性日志 息。
图14.10所示,从“日志ID序列”中 历日志ID,从LogPdb中查询到该 日志代码 参 个
ArgCnt, 后 过 ArgCnt 从“日志参 序列”中 取对应个 参 ,最后 合LogPdb中该 日志
代码 其 息( 件名、代码 、 名、 ) 成可读性日志 息。
图14.10 从LogPdb中导出可读性日志 息

14.7 本方案思路的可移植性
本 案 思路也可以 应 于 帧同步项 中。从 案 实现中可以得出 论: 论是 存 还
是日志输出 性 上, 已经比 日志系 更具优势了。
本 案 思路也可以 应 于C++项 中。 C++项 中习惯 来输出日志, Release版中
可以 过重 义 来 剔 不 日志,这样 个逻辑 性 是提升了, 是同样 临着一 常偶
现 Bug—— 为当 没有日志—— 法 位Bug 问 。本 案 有一 日志性 消 ,
是从 案 实现来看,其性 消 已经 到日志系 中最低了,并且 实际项 中得到 证,其性 影响
几乎可以忽 。 果该C++项 也是 于帧同步 项 , 么 移植本 案就变得更加有必 了。

14.8 总结
本章 绍了一种已经实现应 于帧同步项 日志输出 案, 于 存 和输出性 上极为
出 ,已经可以 到 输出 个帧同步系 调 过程日志 前提下, 日志开关常开,以此来极
提 当帧同步系 出现Bug , 位Bug 工作 。
我 即 上线 一个项 中,平 每帧约输出1 750条日志,平 每条日志带有1个参 ,每帧
日志 量 约为10KB, 论上 存50帧 量为500KB。 是,为了更加极致 优化性 , 繁
存分 ,我 为每帧 先分 10 240条日志ID和10 240个参 量。所以,我 项 个
LogTrack实际占 存3MB左右,压 后 日志 件约为50KB,可以快速 上 至日志服务器。
希望本 案及其思路,可以给读 带来一 启发。
第15章 基于解析符号表,使用注入的方式进行Profiler采样
的技术
作者:

摘要
,当 工程中 到一 量性 求 ,我 可以选择 工具,也可以选择 代
码中 一段 来 计运 殊代码来实现。 环 中, 工具 VTune 等进 性
量 比较 , 是 运 环 中,或 一个 很 得 分性 环 中, 工具
就不 么 了。于是,可以选择 代码中 一 性 计代码 法。 是, 殊代码来
计 息 法有着极 局 性。 先,必须 业务代码中加 或 或 性 计代码,破 了原先代
码 性。其次,对 计代码 每一次 , 必须进 一次 译和发布 过程,这样 得周期变得
很 ,更严重 是很 易 过偶现和意 负载 。最后, 运 环 中, 局 性 是 常
,不 先埋 ,还 经常 护,否则 后,项 量 性 计 所淹没。
论是 工具还是 代码 法来进 性 计, 有着不可 局 性,为了 决以
上问 ,我 索看是否有一种 法 又 进 性 计。于是就有了本章中提到
法。
本章 绍一种 于 技巧 运 代码 二进制 来实现 性 计 法。为了
更 得 标程序 二进制 , 助于符号 。最终实现了一个 性 量工具,
和运 环 中帮助我 更 了 工程 运 并进 优化。本章所讨论 技术和平台、语 息息相
关, 是 于篇幅, 本章中出现 所有代码和 以x86平台Windows 7操作系 下 Visual
Studio 2015环 为 ,其 环 有 干差异, 思想是一致 。
实践中比较 赖可控 运 环 , 果 不可控 客 环 下,该 法可 杀毒软件当作 毒
。 过去 实践中,往往 服务器应 或开发环 下 。

15.1 进行测量之前的准备工作
正式进 量之前,先 述 运 代码 为,并实现 量 样代码 技术,下
讲讲 进 二进制代码 。
我 知 ,最终 PC 上运 程序 是二进制 , 论 么语 , 最终运 是二进制形
式 。 果 直 二进制代码 , 么就可以实 变程序 为。 这种 法 很
就已经 计算机 毒和软件破 中 繁 了, 命名为“ ”,为了区分SQL ,这里加上前
, 命名为“二进制代码 ”。
15.1.1 注入的简单例子
讲二进制代码 之前,我 先 一 段代码来 :
这个最简单 HelloWorld 代码 译后 样 是:

果 011D20F8上 更 为:

么相应 输出也 跟着更 。这样就实现了一个最简单 。从这个 中可以看到,


技术 过 正 运 程序 二进制 来 变程序 运 。运 技术,可以 标程序
造成我 希望 样 ,包 新 代码、记录所 。
15.1.2 注入额外的代码
上进 正 , 一个 远程进程中,示 下:
以上 中,核心 为VirutalAllocEx和WriteProcessMemory两个API 调 ,分别 远程进
程中创建了一个 存空 ,以及 对应 二进制代码 远程进程中。最后调 CreateRemoteThread创
建了一个 线程,来实现 远程进程中 运 。
15.1.3 注入的注意事项
对字符串 是 过程中很 易 误之一。

就 中所说明 , 代码中对字符串 存 一 是一个字符串 ,该 向 段中


具 ,当跨进程空 后, 向 存 是 意义 ,所以我 不 代码中 直
字符串 义。
对字符串 操作 别 慎 ,还 别 意 调 。

本质是 跳转。总 以上两个问 ,凡是涉及 和 存 问 , 代码


中 出现跨进程空 后 向 存 意义 问 。
别提示: VS Debug 模 式 下 成 代码 果 开 启 了 /RTC 开 关 进 栈检 ,则 成与
__RTC_CheckEsp 相关 代码,这 分代码 代码变得不可 。所以,当 译 来
,记得关闭/RTC开关。
么, 决上 所提及 字符串 和 调 问 呢?看以下 :
这个 中,核心 于 过 得 Kernel32.dll 中 来 远程 中 ,以及 远
程进程中创建一个 存空 , 字符串 制过去。其思想是 分 楚 过代码 译出来
哪 是 向 存 ,哪 是 译成二进制 ,凡是 向 存 ,

15.2 性能的测量
符号 就先 绍到这里, 下来进 另一个主 :性 量。这个是主 ,
对 标进程进 运 量,并且 试 合适 式呈现出来。
15.2.1 时间的统计方法
计CPU开销 ,比较 向于 计 法是 开 位 记录一个 ,并且 束位 取
和开 相 得到 差 为运 开销。
实践过程中, 量 法 精度,以及 量 法对 量本身 干 。先举几个 :

最常 Windows API,可以 得毫秒 精度, 并不推 作为 量 。 为其精度并


不 ,并且属于操作系 级别 ,其本身 开销足以影响到 量本身。
推 __rdtscp ,该 只有一条CPU 令,精度为CPU运 周期。 属于CPU 持 ,
论是精度还是本身对 量 干 属于最优 选择。
15.2.2 针对函数的采样
对 样, 先 二进制 件中找到 样 , 后 进 和离开
两个位 进 样 代码 。进 样 所有 为发 之前进 ,所以我 选
择 调 位 。 离开 样比较 杂, 为不 确认 只有一个出口,一旦调
ret后, 离开这个 , 是并没有 硬性 则 证只 有一个ret存 ,所以 于此原 ,并
不 过进 样 法 位 代码。 此,必须 一个 技巧来 证 退出 调
退出 样 。
1. 得 样
下 一个简单 来说明 符号 得对应变量和 。
远程进程 代码:

试 得远程进程中TargetFunc :
以上 中,先关 符号 操作 一系列 ,这 DbgHelp.h中。 该 中以Sym关 字
开 一系列 属于符号 操作 API, SymInitialize和SymRefreshModuleList根 进程句柄
来初 化符号 ,SymFromName 是核心操作, 过命名来 取 标进程中 。当 取 后,
下来就是 段, 代码段 该 对应位 进 量 过程了。
2.进 样
这个步 是 原有代码中 一段 制 代码, 不影响原有代码运 前提下 记录下
来。当 , 果 记录更 息也是没问 , 这里就不 篇幅来重 讲述了。下 为远程进程
中 原有代码。

我 希望 最开 一段代码来记录这个 调 次 。 么 代码 下:

该 中,事先 0x120000 存 中 请了一个 dword 变量,当运 到这段代码


,就可以 变量+1。 这段代码 到上一个 push ebp之前运 。 为已经 译 二进制
件中 是确 ,所以不 随意 移和 充 。这就 另 请一 存来存 代
码,并且 jmp 令跳转过去运 ,并跳转 来。

于是 有了这么一段代码, 下来 是 这段代码 到 标位 。 计jmp 令 代码总 占


6字 , 么就 标代码中 掉6字 位 来 jmp 令,这 导致原来占着 位 令
出来 到新 请 存段中运 。一条汇 令 是不 截 ,所以当6字 截 原有
令 ,则 原有 令 个 出来。于是,总 从 标位 出了9字 到新 请
存段中,最后还 6字 跳 到原有代码中,最终 请21字 存 新 代码。 后 是下
样 :
至此, 中 到了凡是调 口为0x000817B0 ,就 得 为0x120000 计 器 计
加1。
3.离开 样
前 提到, 为 法 证一个 只有一个ret离开 ,所以没有办法 ret 位 对应 样
代码来 取 束。这 可以引 一个 技巧:Return Call。
所谓 Return Call,就是 A 中 册一个B 调 , 束A ,B 调 , 后
再返 到调 A 上层 栈。下 简单 两 汇 代码实现了Return Call。

以上简单 两 汇 代码可以让ret后跳转到eax所 向 。这里 分析ret 为。ret相当


于jmp到栈顶所存 , 后 一次pop操作, 并不 去管到底是不是这个 调 过来 。
这个 性让程序有了很 操作空 ,只 开 ,所有 令 还没有 , 调
Return Call 栈即可。当 束 程后, ret之前 护栈平 ,所有 中发
栈 变化 恢 到 调 之前,此 栈顶 是之前push进去 Return Call ,
ret后进 样 。当进 样 ,栈顶 是调 上层返 , 样 操作
必须 护栈平 , 样 束后再ret就可以 到调 原 。
果 参 ,则可以先 参 栈,再 栈,最后 Return Call 中 参 出
栈即可 护栈平 。
4. 正 运
果当前程序正 于运 中, 么不 样 正 样 上运 ,有极
概 正 运 到 令段中。 恢 环 ,也有概 正 运 样 , 果 这
上进 或 样 话,则 引起程序崩 。所以, 操作中 这一 发 。
以上 实现了两个 ,暂 与对应进程 ID 相关 所有线程,并判 线程 EIP 是否
护 代码段中,以及恢 与对应进程ID相关 所有线程。这样就可以选择一个不 上运
刻进 操作。
15.2.3 测量实战
上 提到 两个 技巧,就可以实现 一个 开 开 计 , 束 计
消 ,以及 更 事情。 果 进 更 杂 计,则可以 计 DLL中, 代码 是
对DLL 加载和调 。以下 是一个 :
该 中,总 成了3段二进制代码,分别对应于 量 退出 、 开
和 原有 进 跳转 。并且 中着重提到 判 标进程当前运 位 不 再 操
作 分,否则 导致 标进程出 。 还原 步 中, 意这个细 。 别 意
是,对 请 代码段 ,也 标进程并没有运 该代码段 进 。
当 成后,就可以

来读取 果了, 中 量 法,读出来 是rtdscp 返 。当 更 杂 量 ,


可以 一个 构来存 希望得到 果。

15.3 总结
过 符号 帮助我 得到详细 标进程 息后,并辅助以二进制代码 段来
标进程 为,实现了对 标进程中 性 量,并且可以得到所有希望得到 程序员
开发中 。 于只 了 量 分 代码,并且 不 再去 量 可以 恢 原 ,所以 量
对性 影响微乎其微。 不 重新 译,也不 工具辅助,只 开发 环 下对应 工
具就可以进 量, 常适合 运 环 中 ,对突发 性 量 求有 常 持。
同 ,该 法并不 于 量,其应 可以 盖到二进制 更新、随 随 加log 输出、
杂 代码调试等 , 于篇幅有 这里就不提了,不过思想是相 。

[1]本章相关 已 请技术专利。

You might also like