介绍

在做项目的游戏持续集成时,往往有一些通用的指导原则来帮助更好地规划,减少持续集成产生的问题。

网上大部分持续集成的文章都是讲如何实施持续集成,很少介绍指导思想、原则之类的内容。而做实施持续集成时会遇到一些问题难以理清楚,不知道该用何种方案处理,这里给出一些较为通用的思想帮助解决问题。

原则

明确需求

明确需求是最重要的事情,这决定了将会达成什么目录、需要做多少工作、何时完成。

完全自动化

从开始到结束,必须做到完全自动化。只有人不去处理才是合理的,中间有人工介入说明设计不合理。

最终的目标肯定是完全自动化,如果很难一次性实现,那么可以将其划分阶段,按照二八原则优先做成本低收益大的部分。最后渐进式地将所有流程自动化,达成最终目标。

尽可能将要填的参数固化,不用人来填写,从设计层面去规避。例如资源版本号完全可以做到打包时自增,另外要求打包失败时资源可以正常工作。

系统化思维

从整体的角度去思考如何解决问题,因为有些问题只能在出现问题的地方修复,而不能在别的地方绕过。

站在更高的角度考虑问题也会简化实现,甚至可能发现去除一些不必要的任务。

例如游戏可能会有如下模块:

  • 资源加载
  • 配置管理
  • 数据表管理
  • UI 管理
  • 声音播放
  • 战斗管理

例如资源管理在设计时,最好不要只管理 Unity 的 AssetBundle,应该所有其他东西都纳入管理,例如数据表、声音等等。这样可以将与 Unity 无关的资源从构建 AssetBundle 流程中解脱出来,加快打包时间。

例如声音播放一般要求声音文件同步流式加载,否则会因为异步加载的延时导致音画不同步。

迭代反馈速度

如果要改完一个东西看效果需要一小时,那么这个东西本身可能会非常不完善,会遗漏大量的边角情况未处理。

如果这个反馈过程缩短到几秒钟甚至是实时反馈,那就会完全不一样,人们会轻易地发现并修改很多以前无法发现的问题。

确定性与非确定性

如果项目本身是确定性的,那完全可以不用保存所有生成的构建产物,每次使用的时候重新生成。

但是很多时候是非确定性的,项目使用了你无法控制的非确定性的黑盒,例如 Unity。

造成非确定性的原因有很多,例如随机数、时间戳、用户输入或其他任何可能变化的输入。

应对方法:

  • 非确定性的内容要单独管理,例如 Artifacts AssetBundles Data
  • 非确定性的内容要与确定性的内容分离,Artifacts 构建产物要与仓库分离到不同目录,防止被还原。
  • 确定性的内容要保证在确定的状态,例如仓库本身在构建前要更新且恢复到干净的状态。

幂等性

简单说就是指使用相同参数构建时,会得到相同的结果。

在实践中必然有一部分不是幂等的,例如想要在构建产物中增加时间戳等变化的量。

对待这个问题的建议是将非幂等的步骤与幂等的步骤分离并且放在第一步,这样后续如果需要重新构建,可以跳过非幂等的步骤,使用非幂等步骤产生的结果作为幂等步骤的输入,然后从幂等的步骤开始执行。

正交性

任务划分要合理,该在哪里处理的任务就在哪里处理。

比如 Unity 应该只负责输出工程,Gradle 和 Xcode 负责构建工程,那么修改输出工程参数的事情就应该放到脚本中单独处理,而不是写在 Unity 脚本中。

修改工程参数放在单独脚本中可以与 Unity 之间解耦,从而简化调试流程。例如输出完工程后将整个工程添加到版本控制中,然后调试修改工程参数的脚本,有问题可以直接回退到之前刚导出的工程状态并重新运行。

内聚性

应该尽可能少地依赖其他东西。构建过程的参数不要依赖于 Jenkins 构建序号、Job 名字之类的变量,而是完全自行管理。

例如将配置写入到文件或 Pipeline 脚本中,随每次构建变化的值放到单独文件中在构建时修改。

比如使用 Jenkins 时就不要使用任何插件,默认的 Jenkins 就可以,这样可以使你的构建脚本负责所有事情,而不会出现外部依赖出现问题时导致整个流程卡住。

版本锁定

构建过程中用到的所有依赖必须锁定版本,只有这样才能保证不会出现兼容性问题。

版本锁定包括操作系统、第三方库、Unity 版本、中间件版本、打包脚本依赖库等等。

例如:Unity 5.3 无法在 macOS Mojave 的 APFS 文件系统上运行。

实施

日志

需要保留所有的日志,方便后续查找问题。

要解决两个问题:

  • 将日志正确输出到标准输出,这样可以结合构建工具收集日志到 Web 面板上(例如 Unity 在 Windows 下只能输出日志到文件中,需要使用另外的方法修改)。
  • 输出正确的日志,编写代码时需要在正确的地方输出关键日志,尽量控制数量做到不多也不少。

配置文件格式

下面是常用的配置文件格式:

  • ini
  • csv
  • toml
  • yaml
  • json

必须支持注释,毕竟只通过配置项无法得知更多的信息,所以 json 直接被排除掉。

需要有库的支持,易于使用。例如项目使用 C# 与 Python 编写打包脚本,那么就同时需要这两种语言的库,要求简单易用。 如果库没有对对象的支持,需要自行手动封装对象供外部使用。之前项目中 toml 的使用是不合适的。

使用具有语义的配置文件,可以正确地读取与修改配置文件,极大降低了出错的可能性。

而且还可以减少粗暴的正则表达式文本替换,正则表达式如果要非常完整,可能会变得很复杂,而这本身是没有必要的。 完全可以使用正确的配置语言去除这无用的复杂度。

跨平台

一般手游会支持 Android 与 iOS,Android 可以使用 Windows 或 macOS,而 iOS 只能使用 macOS 打包,因此打包脚本必须支持双平台。

  • Windows
  • macOS

这决定了可以使用的合理的构建胶水语言只剩 Python。

路径

Windows 与 Linux/macOS 的路径分隔符不同,由于脚本中有大量操作路径的地方,处处修改实在是太麻烦且无必要,必竟大部分库都支持两种分隔符。 那么最重要的事情在于需要在输入输出的边界修正分隔符,例如读取、写入到文件、输出中需要根据目标状态修改分隔符。 例如

  • 输出当前路径时需要根据当前所在平台决定换行符
  • 输出远程 FTP 路径时需要使用 Unix 分隔符
  • 输出到自定义格式的文件中可以根据需要决定输出当前平台分隔符还是为了不同平台下文件一致输出指定分隔符

文件名与内容

文件名与文件内容是完全分离的,利用这一特点可以将内容校验值放到文件名中。

如果文件在传输到其他地方后不能保持原有目录结构,那么目录名字中携带的信息就丢失了,这时需要文件名包含足够多的信息。

构建制品

又名构建产物,英文名 Artifacts。构建产生的结果需要放在这里。

如果要同时构建多个包,需要明确所有的配置从哪里来、在哪里应用配置、是否存在共用配置等等问题。

例如游戏中最常见的多渠道出包。

部署

最终的构建结果需要交付,这个过程就是部署。

部署需要做到幂等性,即重复操作结果是一样的。因为在部署的过程中可能会出现网络中断、磁盘空间占满等等非可控因素,在重新部署时需要保证与之前的部署结果相同。

注意在部署过程中不要使用可变的变量,如构建序号、构建时间等等;一定要使用的话也应该将这些信息放在部署前的步骤中生成好,部署时只管读取这些信息。

推荐阅读

这里推荐一下自己写的其他持续集成实践相关的文章:

参考资料