资源的引用管理是个有趣的话题,最近我在代码里实践了一种做法,可以在某些方面简化资源的管理,完成之后简单记录在这里。这篇文章先介绍传统的各种方式,然后简单说明一下,这个实践在传统方式的基础上做了哪些改善,解决了什么问题。

引子

游戏开发中的资源管理,通常是指针对游戏中的各类资源数据 (模型,贴图,脚本,数据表等等),通过合理安排布局来提高资源访问的效率,进而改善游戏体验的过程。在布局方面的一些实践,譬如“如何区分对待不同的资源类型,如何做到更新友好”等等,这里就不详细讨论了。今天主要谈一下在大量资源已合理布局的情况下,如何有效地处置它们相互之间巨量的依赖和引用关系的问题。

简单地说,如果 A 引用了 B,那么应该如何简洁有效地表达这种引用呢?

有经验的开发者知道,这个问题并不像看上去这么简单。随着资源量的剧增,以及牵扯到的工作流程的细碎化,如果处置不善,资源引用问题会成为影响整个架构的根本性问题。

传统实践

方式 I - 基于偏移 (指针) 的引用

文件偏移 (file-offset) 应该是最基本最原始的引用方式了。在一个运行着的 C/C++ 程序中,通常我们通过在对象 A 中存储指针来引用对象 B。如果在序列化时,把这种指针引用以文件偏移的形式直接写入文件,就是最原始的资源引用管理。

这种最原始的依赖管理,细分一下还有两种形式:

1.每个对象的地址 (&object) 被一并存下来用作该对象的 ID (顺便保证了全局唯一),将引用者写入文件时,如果出现被引用者的指针,就直接写入其地址。这么做的好处是简单直接,速度快,与运行时地址空间一一对应,有时候甚至非常有利于调试。但缺点和限制是每个对象需要额外的4个字节 (64位就是8个字节),而且必须保证在序列化的过程中不发生相关内存的释放和重新分配 (因为可能导致同一地址被不同的对象“复用”了)。

2.每个对象在被写入文件时,使用当时的文件偏移作为该对象的 ID (通过每个偏移在文件中的唯一性来保证全局唯一),将引用者写入文件时,如果出现被引用者的指针,就写入其文件偏移。这么做省去了指针的存储开销,但由于文件写入是有先后次序的,先写入的对象如果引用了后写入的对象,此时还不知道文件偏移,就只有在第一遍写完所有对象之后,再写第二遍填上引用的空缺(或者是预先在内存中把偏移算好)。

为什么说这种方案很原始呢,因为一个地址所能携带的信息太少了。在载入时,我们必须在整个过程中都非常清楚自己在操作什么类型的数据,这样就需要大量额外的代码来在不同的情况下创建不同类型的对象,这是非常繁琐和易错的。究其原因,就是引用的信息量不够,做不到某种程度的自描述。

关于打包的单独讨论

由于这种方案足够的快,在一些游戏引擎的二进制数据文件中有非常普遍的应用。为了保证读取效率,游戏引擎通常会把逻辑上相关的资源打包在一起,避免反复读取零散的文件。由于在包内的文件仍保持着与文件系统相一致的树状存储结构,所以“物理包文件 + 虚拟的内部文件结构”,本质上跟典型的OS树状文件系统并无不同。提供这种打包机制的引擎通常会把这一层给抽象掉,大多数情况下,游戏代码仍像访问普通文件一样去访问内部的一个资源。这也就是在说,理想情况下,一个考虑周详的打包机制,应做到保留 OS 文件系统的基本语意,将其自身透明化,不破坏和干扰已有的文件访问方式。

出于简化讨论的目的 (不影响讨论的内容和结果),我们将只讨论基于传统的 OS 文件系统下的资源相互引用问题,而把“是否应该打包,如何打包”等问题正交地拆分出去,视作另一个维度的考虑。

方式 II - 基于路径的引用

(形如 '/foo/bar/miracle.png')

  1. texture = "/foo/bar/miracle.png";

复制代码

正如标题里的例子那样,按照路径来索引资源,应该是最自然和直观的引用方式了。事实上,互联网上的资源和服务,大部分都是通过 URL,以路径方式来提供的。

使用路径来索引资源时,如有可能,应当尽量使用相同格式的归一化的平台无关的路径。混用 '\\' 和 '/',使用 "/../" 或 "/./",等等,都会造成无法直接比较两个引用是否指向同一份资源,而且对同一资源的引用字符串 hash 的结果会不一致。

当需要移动或重命名资源的时候,路径就失效了。这时候,简单的做法是,总是在编辑器提供的资源管理工具中进行 move/rename 的操作,这样可以自动更新所有对该资源的引用。涉及到全库范围的扫描和修改,当资源量大时可能会非常慢。

一个常见的实践是使用所谓的 "Redirector",当 move/rename 发生时,在原来资源的位置放置一个跳转,指向新的位置,这样所有的相关资源都可以保持对原资源的引用,无需被动更新。在全库范围内,可以定期地运行自动化工具来清理这些跳转,手机号买号平台更新引用以直接指向真正的资源。除了把操作的影响局部化以外,这种做法还有一个好处是,如果团队内一个人在 move/rename 时,另一个人创建了对老资源的引用,这个机制可以确保两个人的工作被合并时能够正常工作,而上面的“扫描并更新”的实践则会导致后者的引用失效。

方式 III - 基于 GUID 的引用

(形如 '{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}')

  1. texture = "{77BA2B2B-3EA5-4C49-A3D2-0DA6A03D2B44}";

复制代码

使用 GUID 的优点非常明显——由于不依赖在磁盘上的具体位置,不管路径和命名怎么变,只要 GUID 保持不变,就能保证总是索引到对应的资源。

但问题也非常明显:

1.首先是可读性问题,给定任意一个 GUID 必须依赖工具查找才知道对应的资源是什么,对工作效率的影响是很大的。考虑到有时会无意中删除或者忘了提交某个资源,仅凭一个 GUID 没有任何可能的途径来知道缺失了什么,而如果是路径的话我们至少有机会知道是哪个文件的问题。(是的我们可以通过版本管理软件来 blame 可是如果该文件被多人修改过就很被动了)

2.其次是额外信息的存储和同步的问题,由于很多文件格式本身是找不到位置存 GUID 的,这就需要单独建一个同名的 .metadata 文件并与原文件一同管理,这进一步增大了负担,降低了工作效率。更重的实践使用一个中央数据库来把所有资源的 GUID 收拢到一处统一管理,这就需要提供各种工具去处理更新,合并,与版本管理软件协作等问题。

确定性的 GUID 生成

由于工作关系,我曾在一个商业引擎的资源管理相关代码上工作过一段时间。不幸的是,该引擎使用了 GUID 来管理资源的标识和引用。更为不幸的是,该引擎通过“在打包时动态地为资源生成 GUID ”来成功地把打包问题和资源管理问题深深地耦合在了一起。由于在开发过程中,代码和资源会持续地迭代变化,打包的环境总是处于或微小或剧烈的干扰之中,所有这些带来的直接后果就是,打出的资源包内大部分资源的 GUID 几乎总是随着版本在持续地变化,而前后两次打包出的资源也无法兼容和重用。可以想见,对于一个需要联网并时常热更新的游戏来说,这是一个多么不幸的设计。

为了解决这个问题,经过我跟另一位同事的先后努力,这个引擎中,涉及资源管理方面的所有的 GUID 生成都被我们改为了确定性的 (deterministic guid generation)。也就是尽量保证,在任何一个给定的上下文中,生成的 GUID 总是确定一致,并与该上下文基本对应。这个确定性的 GUID 生成实践,本质上是一个通过使用互不干扰的多个随机序列 (std::mt19937 & std::uniform_int_distribution ) ,抓取并嵌入上下文相关的信息,来把 GUID 的生成尽可能局部化的过程。关于此问题的更详细的记录信息可参阅此文档 (PDF),这里就不再细说了。

经过这次折腾,俺对 GUID 用于折腾所能产生的巨大能量有了充分而深刻的认识。此事的一个后遗症是,从那以后听到用 GUID 管理引用和依赖的方案,俺就不由自主想呵呵了。

方式 IV - Unique Name 全局唯一命名

(形如 'v1_ui_mainframe_miracle_png_hd')

  1. texture = "v1_ui_mainframe_miracle_png_hd";

复制代码

简单来说,Unique Name 本质上是一个改良版的 (具有一定可读性的) GUID。它兼具了路径引用和 GUID 引用的优点 (可读性好,可随意修改物理路径) 但除了改良的可读性这一点之外,上面所有的 GUID 相关讨论也同样适用于 Unique Name。

当资源量大到一定的体量并仍在持续增长时,(为了避免冲突) Unique Name 将变得越来越臃肿。过长的描述不仅容易造成额外的管理和沟通负担,也会加大运行时的内存开销,实践中在需要时可以 hash 一下。

改进的实践 - 路径 + 摘要 (" Path + Digest ")

(形如 '/foo/bar/miracle.png: (digest-string)')

呼~~终于说到这一次的实践了。

  1. texture = "/foo/bar/miracle.png:bd37de66ffdcfd5bf544502a1fae1e99";

复制代码

还好一句话就能说清楚:在路径后面加一个该资源的内容摘要 (算法随意不影响,目前使用 MD5) 就是我目前采取的方案。

关键点那么与上面的方案相比,这个方案有何不同呢?

1.资源重命名或移动时,能够做到自动检测和修改。

  • 一般情况下,如果仅仅是重命名或移动,根据内容算出来的摘要是不变的,当通过路径找不到资源时,通过比较摘要,就可以提示用户 (或自动重定向到) 重命名或移动后的资源。
  • 检测和修改是可惰性的,可延迟至对应的资源打开时再转换,不必立即一次性扫描和更新所有引用。
  • 重命名和更新可以在 OS 的文件系统内完成,无需在特定工具内。

2.资源更新时自动识别和更新摘要。

  • 当资源发生变化时 (通常是美术/策划保存了一个新版本) 编辑器会在加载此资源的引用者时为其生成新的摘要。
  • 这个也是可惰性的,也就是加载了哪个资源,哪个资源才需要重新生成。

3.不像 GUID 那样需要单独存储,无需额外的 metadata 文件管理负担。

  • 由于摘要没有产生资源以外的额外信息,随时可以根据资源本身生成,所以无需额外的 metadata 文件。

4.简化全库范围的操作。

  • 方便检查重复资源 (全库比较摘要即可);
  • 全库范围自动修复所有的重命名和移动 (完全应用 1.);
  • 全库范围自动重算 (完全应用 2.)。

实现逻辑

有同学可能会问:“如果移动,重命名,更新等各种操作混杂在一起,我怎么知道什么时候该自动重定向,什么时候该更新摘要呢?”

嗯,这就是路径 (Path) 和摘要结合 (Digest) 的精髓所在了。我们根据引用去查找资源时,是按照下面伪码的逻辑进行的:

  1. Resource* getResource(const std::string& refString)
  2. {
  3. // 分解为路径和摘要两部分
  4. std::string path = GetPathPart(refString);
  5. std::string digest = GetDigestPart(refString);
  6. // 尝试访问位于此路径的文件
  7. Resource* res = GetActualFile(path);
  8. if (res)
  9. {
  10. // 文件存在的情况,检查摘要是否一致
  11. if (digest == GetActualDigest(path))
  12. {
  13. return GetActualFile(path);
  14. }
  15. else
  16. {
  17. // 文件存在,摘要不一致,则认为是资源更新,重算摘要
  18. RefreshDigest(path);
  19. }
  20. }
  21. else
  22. {
  23. // 文件如果不存在,符合重命名/移动的条件,提示用户资源未找到,是否进行全库范围搜索
  24. if (/*在另一个地点找到了摘要符合的资源*/)
  25. {
  26. // 提示用户 (或自动) 更新引用路径
  27. RefreshPath(newPath);
  28. }
  29. else
  30. {
  31. // 提示资源缺失 (in-editor) 或使用 err-placeholder (in-runtime)
  32. ...
  33. }
  34. }
  35. }

复制代码

也就是说,路径的判定优先级高于摘要。在认定属于何种情况时,路径为主导,摘要为辅助。如果路径吻合但摘要不符,则认为属于资源更新的情况;如果路径失效,则使用摘要去全库匹配。两种行为分别针对两种不同情况的处理,泾渭分明,各司其职。

批量处置

上面的代码是单个资源获取的流程,实际上在编辑器中打开一张地图 (或一个 UI 界面) 时,如果一个资源一个资源地单独汇报和处置,效率就太低了,可以在全部加载完毕后,统一批量地进行一次全库范围的匹配,然后弹出一个汇报和处置的对话框。在这个处置对话框中,重命名/移动/更新都是黄色叹号,而无法识别/找不到资源则是红色叹号,通常如果都是黄色叹号的话直接全部更新就可以了。

代码中的引用

在代码中为了简便,可以仅使用路径即可。在运行游戏的过程中,会自动生成一个 digest_cache.txt 文件,每一行是一个资源的完整引用,可以把这个文件提交到版本管理的库中。这样,很容易通过程序手段在资源发生重命名,移动和更新等事件时,检测并更新这个文件,必要时,可提示用户代码内的路径需要更新。

小结

总得来说,这个方案具有以下的特征:

  • 良好的可读性;
  • 无需额外的 metadata 文件存储;
  • 对资源的重命名/移动无需在编辑器等专有工具内完成,没有潜在的破坏其他资源引用的心理负担;
  • 唯一需要保证的是,重命名和移动资源的时候,不要同时更新其内容即可。

好了,关于这个资源引用管理的实践,到这里就讲完了。在资源管理方面,你有什么心得呢?欢迎跟我一起讨论。

利用文件摘要简化游戏资源的引用管理相关推荐

  1. cocos2dx游戏资源加密之XXTEA

    在手机游戏当中,游戏的资源加密保护是一件很重要的事情. 我花了两天的时间整理了自己在游戏当中的资源加密问题,实现了跨平台的资源流加密,这个都是巨人的肩膀之上的. 大概的思路是这样的,游戏资源通过XXT ...

  2. 【Android游戏开发二十七】讲解游戏开发与项目下的hdpi 、mdpi与ldpi资源文件夹以及游戏高清版本的设置...

    今天一个开发者问到我为什么游戏开发要删除项目下的hdpi.mdpi和ldpi文件夹:下面详细给大家解答一下: 首先童鞋们如果看过我写的<[Android游戏开发二十一]Android os设备谎 ...

  3. 如何优雅的管理游戏资源

    在游戏的开发过程中,前期的规划 往往比 后期的"优化"更为重要!比如多分辨率适配,如果前期没有规划好,可能导致的情况是,画面只在当前测试开发机或者一部分机型正常显示.做了多套资源适 ...

  4. MSSQL · 最佳实践 · 利用文件组实现冷热数据隔离备份方案

    摘要: 摘要 在SQL Server备份专题分享中,前四期我们分享了:三种常见的数据库备份.备份策略的制定.如何查找备份链以及数据库的三种恢复模式与备份之间的关系.本次月报我们分享SQL Server ...

  5. ASP.NET -- WebForm -- Cookie的使用 应用程序权限设计 权限设计文章汇总 asp.net后台管理系统-登陆模块-是否自动登陆 C# 读写文件摘要...

    ASP.NET -- WebForm -- Cookie的使用 ASP.NET -- WebForm --  Cookie的使用 Cookie是存在浏览器内存或磁盘上. 1. Test3.aspx文件 ...

  6. android h5游戏图片不缓存,H5小游戏资源缓存方法与流程

    本发明涉及H5资源缓存领域,尤其涉及H5小游戏资源缓存方法. 背景技术: 随着移动互联网的发展和手机硬件性能的不断提升,H5小游戏这种不需要下载安装即可使用的全新游戏应用得到了爆发式发展.这种用完即走 ...

  7. unity 转微信小游戏 资源优化

    资源优化 可通过转换工具配套提供的资源优化工具,将游戏内纹理资源针对webgl导出做优化. 工具入口 菜单栏-微信小游戏-资源优化工具 工具介绍 Texture 区域1: 选中目录,以设定规则扫描目录 ...

  8. dds提取工具_游戏资源提取器(game extractor)

    game extractor提供游戏资源获取功能,可以帮助用户在软件上快速将游戏中的资源提取,您可以将游戏文件添加到软件,随后选择需要提取的格式,可以选择对视频.音乐.素材.xml文件等内容提取,方便 ...

  9. 电脑怎么迁移游戏资源,数据迁移能把游戏数据迁移吗

    概述:玩家们在打游戏的过程中,会产生很多数据,尤其是那些大型游戏的玩家,都会珍惜游戏数据.电脑怎么迁移游戏资源?如果您刚刚购买了一台新电脑,并且正在寻找将游戏迁移到新电脑的方法,相信本文会对你有一定帮 ...

最新文章

  1. 一文让你明白Redis持久化
  2. 大话设计模式C++版——装饰模式
  3. 李宏毅深度学习——Why Deep?
  4. kernel: CPU9: Temperature above threshold
  5. 交叉编译器的命名规则及详细解释(arm/gnu/none/linux/eabi/eabihf/gcc/g++)
  6. stm32 usmart使用
  7. python编_python编
  8. 前端学习(1981)vue之电商管理系统电商系统之完成可选项的添加操作
  9. Python-02-基础知识
  10. android 编辑自定义可编辑表格,smart 框架 列表 可编辑表格
  11. php输入安全验证漏洞,PHP 输入验证错误漏洞
  12. 如何在SQL Server数据库中删除角色
  13. linux检查nfs服务,Linux-nfs服务
  14. 老显卡都涨价了,所以我把坏的显卡拿出来修
  15. 服务器虚拟软件哪个好,服务器虚拟化哪一款软件是最佳选择?
  16. 等保三级核心-物理安全
  17. idea退出debug模式_一文搞懂如何在Intellij IDEA中使用Debug,超级详细
  18. 【毕业设计】47-基于单片机的锅炉过热汽温控制系统设计(原理图工程+仿真工程+源代码+答辩论文)
  19. 怎么登录163vip邮箱?163vip邮箱登录方式有哪些?
  20. 2021年【机械员】通用基础及岗位技能-考试题库及答案(三)

热门文章

  1. python tutorial_Python Tutorial笔记
  2. Comet---“服务器推”技术实现
  3. Random Forest(sklearn参数详解)
  4. Linux日志 系统日志及分析
  5. linux作业(第四章练习题)
  6. python中对列表和循环使用的小练习
  7. PHPstorm相同变量标识
  8. ubuntu下载软件安装包
  9. ios开发 json数据文件的存取
  10. Log4j 2使用教程