Upload
bien
View
62
Download
0
Embed Size (px)
DESCRIPTION
切割管道. Niklas Frykholm. 实现亚秒级的迭代时间. Bitsquid 公司. 再次回到这里. 做出改变. 检查结果. 出口. 寻找实体. 汇编数据. 载入关卡. 重启游戏. 迭代运动循环. 分钟(小时)直到一个改变能够在游戏里看见. 为什么需要更快速的迭代时间?. 生产效率 等待构建所失去的时间 质量 更多的调整 在游戏机上游戏内测试的资产 注意:本次演讲是关于优化管线 延迟 而不是 吞吐量 更新单个不正确的资源所要求的时间. 《 汉密尔顿的大冒险 》 ( Hamilton ’ s Great Adventure ). - PowerPoint PPT Presentation
Citation preview
切割管道实现亚秒级的迭代时间
Niklas FrykholmBitsquid 公司
迭代运动循环分钟(小时)直到一个改变能够在游戏里看见
再次回到这里做出改变
出口
汇编数据
重启游戏
载入关卡
寻找实体
检查结果
为什么需要更快速的迭代时间?
• 生产效率等待构建所失去的时间
• 质量更多的调整在游戏机上游戏内测试的资产
• 注意:本次演讲是关于优化管线延迟而不是吞吐量更新单个不正确的资源所要求的时间
《汉密尔顿的大冒险》( Hamilton’s Great
Adventure )50 多个关卡
《末日余痕》( Krater )《玫瑰战争》
( War of the Roses )
《星际虚空》( Starvoid )《阿比逃亡记》高清版本
( Abe’s Oddysee HD )
雄心勃勃的目标“ 立刻”看到改变( <1 秒钟)
没有欺骗!目标硬件 + 目标帧速率
30 赫兹30 赫兹
实时编辑怎么样?我们是否甚至需要一个管线?
改变模型 ...改变素材 ...改变纹理 ...新增刚体 ...编辑脚本 ...
改变模型 ...改变素材 ...改变纹理 ...新增刚体 ...编辑脚本 ...
重启游戏
载入关卡
发现实体
检查结果
编译数据
出口
做出改变
再次回到这里
实时编辑的问题
• 游戏并不总是最好的编辑器
• 如果游戏数据是生动的二进制图像,那么版本管理是很棘手的
协同工作和融合变化也是棘手的
• 跨平台?在 PS3 上编辑 ? X360? iOS?
• 适合编辑的数据格式不具备最佳的运行时间性能
快速的游戏
• 二进制资源
• 就地载入
• 无需寻找时间
快速的工作流程
• 较短的编译时间
• 热重载
• 立即的反馈
快速迭代:两全其美
进攻策略尽可能快的进行编译并使用重载代替重启
再次回到这里做出改变
出口
汇编数据
重启游戏
载入关卡
寻找实体
检查结果
重载加速这一项
替换这些
分而治之
• 重新编译和重新加载所有数据( >1 GB )永远不可能足够快
• 我们必须使用更小的模块来工作把游戏数据当成是一些个别资源的一个集合,这些资源每个都能够
单独进行编译,接着在游戏运行时重新加载
个别的资源
• 通过类型 + 名称进行识别
• 二者都是唯一的字符串标识符(进行散列处理)名字来源于一个路径,但是我们把它当做一个标识 ID
(只能通过平等比较)
类型:名称:
源文件:
纹理纹理 /植物 /草地
纹理 /植物 /草地 .texture
编译资源• 每一个资源编译到一个特定平台的运行时间通过二进制大对象( blob )进行优化
• 通过名字散列进行识别
草地 .texture
(游戏内的资源管理器)
ff379215ff379215
(数据编译)
加载资源• 资源被组合成包用来加载
• 资源包通过一个后台线程进行流式处理。
• 在开发过程当中,资源都被储存在由散列命名的单独文件当中。
• 在最终版本中,资源包里的文件被捆绑在一起用于线性加载
ff379215edf123b
22345e791
b3d42677
123
ff379215edf123b
22345e791 b3d42677
123
boss_level.package
重新加载资源• 运行游戏侦听 TCP/IP端口
信息是 JSON structs 结构数据
• 来自我们的工具的典型命令
激活性能抬头显示设备 HUD
显示调试线Lua REPL命令 ( 读取 - 评价 - 打印 -
循环 )
重新加载资源
• 也用于我们所有工具的可视化
> 重新加载 纹理 植物 /草地
> 重新加载 纹理 植物 /草地
重新加载资源(详情)
• 载入新的资源
• 基于类型通知游戏系统
指针指向旧的和新的资源
• 游戏系统决定做什么
删除实例(声音)停止和启动实例(粒子)保持实例,对它进行更新(纹理)
• 破坏 /卸载旧的资源
ff379215
ff379215
O ff379215
ff379215
ff379215
ff379215
示例:资源的重新加载
if (type == unit_type) {for (unsigned j=0; j<app().worlds().size(); ++j)
app().worlds()[j].reload_units(old_resource, new_resource);}
if (type == unit_type) {for (unsigned j=0; j<app().worlds().size(); ++j)
app().worlds()[j].reload_units(old_resource, new_resource);}
void World::reload_units(UnitResource *old_ur, UnitResource *new_ur){ for (unsigned i=0; i<_units.size(); ++i) { if (_units[i]->resource() == old_ur) _units[i]->reload(new_ur); }}void World::reload_units(UnitResource *old_ur, UnitResource *new_ur){ for (unsigned i=0; i<_units.size(); ++i) { if (_units[i]->resource() == old_ur) _units[i]->reload(new_ur); }}
void Unit::reload(const UnitResource *ur){ Matrix4x4 m = _scene_graph.world(0); destroy_objects();_resource = ur; create_objects(m);}
void Unit::reload(const UnitResource *ur){ Matrix4x4 m = _scene_graph.world(0); destroy_objects();_resource = ur; create_objects(m);}
疑难问题
• 将数据配置到游戏机
• 处理大型资源
• 编译很慢的资源
• 重载代码
问题:配置到游戏机
• 将数据配置到游戏机上可能会很缓慢
文件传输程序并不适用亚秒级的迭代
• 解决方法:运行电脑上的文件伺服器 – 游戏机从那里读取文件
透明的文件系统后端
文件伺服器
获得 < 路径> < 数据 >
问题:大型资源
• 非常大的资源( >100 MB )永远无法快速地进行编译和加载
• 找到合适的资源颗粒度不要把所有的几何体放到单个文件中把几何实体放到到不同的文件中让关卡对象参照它所使用的实体
问题:很慢的资源
• 冗长的编译使得快速迭代不可能实现光照贴图, navmeshes文件,等等。
• 将烘焙( baking )从编译中独立出来烘焙始终是一个明确的步骤:“现在只做光照贴图”(编辑按钮)烘焙过的数据被保存在资源数据里并检查到存储库接着像往常一样编译(从原始的纹理到压缩平台)
问题:重载代码• 要加载的最棘手的资源
• 四种代码
着色器( Cg着色器、高级着色器语言 HLSL )
流程(可视化脚本)Lua 脚本语言C++语言
• 流程和着色器当做正常的资源
只是是二进制数据
物理碰撞埃及拱
被触摸的执行者触摸执行者触摸单元位置正常开始触摸结束触摸
颗粒效果
视觉特效 fx/火
单元位置
创建破坏流程脚本
实时重载的 Lua语言确保在重载时,变化能够被应用到现有的执行者
( Actor )类别。
Actor = Actor or class()
function Actor:move(dt) self.pos = self.pos + dtend
Actor = Actor or class()
function Actor:move(dt) self.pos = self.pos + dtend
原始版本
Actor = Actor or class()
function Actor:move(dt) self.pos = self.pos + self.v * dtend
Actor = Actor or class()
function Actor:move(dt) self.pos = self.pos + self.v * dtend
更新
ActorActor
my_actormy_actor
self.pos = self.pos + dtself.pos = self.pos + dt
ActorActormovemove
set_velocityset_velocity
my_actormy_actor
self.pos = self.pos + dtself.pos = self.pos + dt
self.pos = self.pos + self.v * dtself.pos = self.pos + self.v * dt
如果没有这个,重载将创建一个新的执行者类别并且现有的执行者对象将不会看到代码改
变。
movemove
set_velocityset_velocity
重载代码: C++
• 工具支持“重启可执行程序( Restart Exe )”
可执行程序文件进行重新加载,但如果你仍然在相同的位置看到相同的对象,那么只要使用新的引擎代码
状态通过工具保持
• 无法达到 <1 秒的目标,但还是非常有用的
小型的可执行程序大小会有帮助
快速的编译
再次回到这里
做出改变
出口
编译数据
重新载入
检查结果
小技巧:使用分析程序, Luke你的工具也想要一些表现出沉迷热爱的东西
渐进式编译
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
• 找到上一次编译以来所有被修改过的资源数据
• 确定依赖于这些文件的运行时间数据
• 对需要的部分进行重新编译
• 重要的是这个过程是坚如磐石的信任很难获得却很容易失去“ 最安全的做法是进行一次完全编译”
挑战:依赖关系• 基本着色器资源( base.shader_source )包括 常
见着色器资源( common.shader_source )
如果 common.shader_source 改变就需要重新进行编译
• 没有读取每个文件我们要如何知道是否改变了?
• 解决方法:一个编译数据库
从前一次运行储存信息在启动时打开,在关闭时保存更新
• 当一个文件进行编译时,储存它的依赖关系到数据库当中。
通过跟踪 open_file()自动确定它们
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
挑战:二进制版本• 如果纹理资源的二进制格式改变了,那么每一个纹理都需要进行编译
• 解决方法:重用数据库:在数据库中储存每一个编译资源的二进制版本在数据编译器中再度检查当前版本如果有一个不匹配就重新编译
• 我们使用相同的代码库(甚至是相同的可执行程序exe文件)在数据编译器和运行时间上,所以二进制版本总是在 sync系统中。
仍然有许多开销用于编译单个文件
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
触摸盘 , ~2 秒
遍历整个源代码数来检查修改时间触摸盘,与项目规模成正比, 5-20 秒
读取和保存数据库, ~1 秒
与修改过的文件数量成正比好吧,这是需要解决的必要工作
启动 & 关闭• 启动和关闭编译器过程需要花费数秒的
时间
• 解决方法:重新使用这一流程!作为伺服器运行通过 TCP/IP端口接收编译器请求
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
编译伺服器
result = successresult = success
result = failureerror = Animation ”run” used by
state ”run_state” not found
result = failureerror = Animation ”run” used by
state ”run_state” not found
source = projectdest = project_win32platform = win32
source = projectdest = project_win32platform = win32
扫描资源
• 缓慢:检查每个项目文件的修改时间( mtime )
• 脆弱:取决于日期
如果一个备份副本受到了恢复,我们可以有 mtime(file) < mtime(dest)
在编写的 dest 很糟糕时就会崩溃信任很重要:我们从未想要强制进行一次完全编译
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
foreach (file in source)dest = destination_file(file)if mtime(file) > mtime(dest)
compile(file)
foreach (file in source)dest = destination_file(file)if mtime(file) > mtime(dest)
compile(file)
想法:明确的编译列表• 工具发送一个它想要重新编译的文件列表
• 工具持续跟踪那些应改变的文件
纹理编辑器知道所有用户改变过的纹理
• 快速
• 脆弱:在工具之外不起作用
svn/git/hg 更新在 Photoshop 里编辑纹理在文本编辑器里编辑 Lua文件
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
解决方法:目录观察程序• 在伺服器启动时进行一次完整的扫描
• 在最初的扫描之后,使用目录观察程序检测变化
ReadDirectoryChangesW(...) 命令
• 不需要进一步的扫描
• 使用数据库来避免脆弱性
从上一次成功的编译里储存修改时间( mtime )到数据库里
如果扫描时修改时间或者文件大小改变了 – 重新编译如果目录观测程序通知我们有改变发生 – 重新编译
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
目录观察程序争用条件我们不知道收到时间会有多长
1. 文件改变了
require ”stuff”
function f() print(”f”)end
2. 用户按下编译按钮
C
source = projectdest = project_win32platform = win32
source = projectdest = project_win32platform = win32
3. 请求到达编译伺服器
4. 伺服器获知改变的文件
争用条件技巧使用临时文件作为“栅栏”
1. 文件改变了
require ”stuff”
function f() print(”f”)end
2. 用户按下编译按钮
C
3. 请求到达编译伺服器。伺服器创建
一个临时文件
4. 伺服器获知改变的文件
5. 伺服器获知新的临时文件
source = projectdest = project_win32platform = win32
source = projectdest = project_win32platform = win32
依赖关系
• 因为我们没有破坏进程,我们可以把依赖关系数据库保存在内存里
只需要在伺服器启动时从磁盘读取
• 我们可以保存数据库到磁盘作为背景进程
当我们要求进行一次重新编译时,我们不需要等待保存数据库
当编译器处于空闲状态时,数据库就会在稍后进行保存
启动 Exe文件
扫描资源
依赖关系
重新编译
关闭
最后的进程
• 磁盘访问只会在处理请求是下列项时发生:编译修改过的文件创建目录观察程序“栅栏”文件
• 否则一切都发生在内存当中
启动 Exe文件
启动观察程序
启动伺服器
查找修改
依赖关系
读取数据库
扫描资源
分析清秋
编译
发送回复
保存数据库
关闭
结果
项目 规模 零编译 最小变化《汉密尔顿》
( Hamilton ) 7 600 个文件 17 毫秒 20毫秒
《玫瑰战争》( War of the
Roses )19 900 个文
件 33毫秒 38毫秒
《末日余痕》( Krater )
27 300 个文件 75毫秒 83毫秒
测试项目 100 000 个文件 222毫秒 285毫秒
高兴的内容创造者 !!!
结果
通用规则• 考虑资源颗粒度
为个别编译 /重载选择合理的大小
• TCP/IP端口是你的朋友
倾向于通过网络访问磁盘来进行工作把进程当成伺服器运行来避免启动时间
• 使用数据库 + 目录观察应用程序来跟踪文件的系统状态
数据库也可以在编译器运行之间缓存其他文件保存在内存中,反映到背景磁盘上
有什么问题吗niklasfrykholm
www.bitsquid.se