全部文章
教程 · 6 min read

为什么程序化散布必须是确定性的——种子如何让随机性可重复、把散布烘焙成原生实例为何让一个散布关卡变得可 diff 与可 merge,以及如何防止一支团队的植被在源代码控制下悄悄改变。

Coloured prayer flags strung across a mossy, fern-covered forest floor

简而言之: 程序化散布只有可复现时才值得信任——同样的输入必须在每一台机器、每一次构建 里都产生同样的森林。这意味着理解种子(如何让随机性可重复)、为什么一个存活的程序化系统在 源代码控制下是危险的,以及为什么把散布烘焙成原生引擎实例,能把一个不可合并的黑盒变成一个 可 diff、可评审、可版本化的资产。本文覆盖确定性、种子,以及如何防止一支团队的植被在所有 人脚下悄悄改变。

蕨类在森林地面上以重复但富有变化的丛丛铺开 看起来随机,但对一支团队而言这必须是可重复的随机——在你的机器、队友的机器和构建服务器 上都一致。确定性才是让一份漂亮的散布变成你真的能出货并维护之物的关键。

## 为什么确定性重要

如果散布在每次打开或构建时都以不同方式重新生成,你的关卡就会悄悄改变——碰撞会 挪动、隐藏物件会冒出来、玩测停止可复现。确定性的散布,永远从同样的输入产出完全相同的结果。

不可重复的随机性在关卡里就是一间产 bug 工厂。如果在另一台机器上打开这场景、或者下周重构它 时,会把每一块石头、每一丛灌木落到哪儿重新洗牌——那么一处曾经清空的位置现在被堵住了,你调 过的视线断了,玩测的结果也不能信,因为被测的关卡不是你出货的那一份。确定性——同样输入、同 样输出、始终如此——才是让程序化散布安全的东西。它是"电脑放了一些植物"与"关卡有一个我可 以依赖的、被定义好的、稳定的状态"之间的差别。

## 种子:让随机性可重复

种子是一段随机序列的起始数字。同样的种子 → 每次都得到同样的"随机"摆放。把种子 暴露出来并保存,你的散布就是可复现的;把它交给真正的随机,就是每次运行都是不同的关卡。

电脑并不做真正的随机——它跑的是看起来随机的确定性序列,每一条序列由一个叫种子的起始 数字定义。喂进同样的种子,你就得到同样的序列,因此得到同样的摆放,每一次都是。这正是可复现 散布的关键:工具暴露一个种子,你把它随场景保存下来,森林就成了(种子 + 参数 + 表面)的 纯函数。改一下种子去探索你喜欢的另一种排布,然后锁住它。任何反过来从一个真正未被播种的 随机源取值的做法——时钟、每次打开新一次投掷——都会产生一个每次加载都不同的关卡,这在生产 里根本没法用。

## 版本控制的问题

以"一个系统 + 参数"形式存下的存活程序化散布,对于源代码控制而言就是一个黑盒 ——你没法 diff 它、没法评审它,两个人同时编辑就会产生无法合并的冲突。团队需要散布以一种 Git 或 Perforce 真的能追踪的形态存在。

这里就是散布撞上团队现实之处。如果你的森林只作为"一个用了这些设置的散布系统"存在,源代码 控制看不到它产生了什么——只能看到某些参数变了。你没法在一个 pull request 里评审一次摆放 上的改动、也说不清什么在挪动;如果两个美术碰同一份散布,你就会得到一个没有工具能有意义地 解决的合并冲突。一个只活在存活生成器里的散布关卡,对团队为了安全协作而依赖的那些系统,正是 不可见的——这也是为什么"一切都是程序化的"会悄悄变成"没人能安全地编辑这片森林"。

森林地面上散布的小蘑菇 这里的每一个都是一个带位置、旋转和缩放的实例。当散布被烘焙成原生实例时,它们就成了真实 的、可被检查的数据——森林不再是一个公式,而变成你可以逐行评审的东西。

## 烘焙让它变得可 diff

把散布烘焙成原生引擎实例——带具体 transform 的真实摆放对象。现在它就是普通的 场景数据:可 diff、可评审、可合并,并且在每台机器上都一致,因为它是被存下的,而不是被 重新生成的。

解决确定性和版本控制两件事的,是同一招:烘焙。你不是去出货一个存活的生成器,而是把散 布烘焙成简单的原生引擎实例——真正摆放好的 mesh,具体的位置、旋转、缩放存在场景里。这一步 一次性把每一个问题都塌陷掉。它按定义就是确定性的(这是被存下的数据,不是又一次投掷)。它 可 diff(源代码控制看得到变化了的具体 transform)。它可评审(一位 lead 能在 pull request 里精确地看到什么挪了)。而且它在运行时对引擎不会带来任何异常成本,因为不过就是实例化的 mesh。一个先用规则涂密度、再烘焙成原生实例的工作流——Numivo 就正是这么 工作的——把程序化散布的作者速度手放置内容的稳定性一起给你,而没有黑盒的脆弱性。

## 保持一支团队的植被稳定

锁住种子、提交前先烘焙,并把烘焙后的结果当作事实之源。有意地去重新散布(调 种子、看 diff),永远不是意外——这样森林只有当有人有意去动它时才会变。

一个团队实用的纪律很短。锁住种子,让散布可复现。提交前先烘焙,这样进入源代码控制的 是所有人共享的具体结果,而不是一份在每台机器上重新投掷的配方。把烘焙后的实例视作事实 之源——你评审并出货的那件东西。而当你确实想改森林时,有意地去做:调参数或种子、重新 烘焙、像对待任何其他修改一样评审 diff。目标是一片森林,只有当人决定它应该变时它才变,而 且它的每一次变化都是可见且可评审的——就是你会对关卡任何其他部分持有的同一标准。为速度用 程序化、为稳定烘焙下去:这就是全部技巧。

## 值得偷的现场数字

  • 确定性法则:同输入 → 同输出,始终如此——否则你的关卡就在漂移
  • 一个种子让随机性可重复——暴露它、保存它、锁住它
  • 存活的程序化散布对 Git/Perforce 而言是黑盒——不可 diff、不可合并
  • 烘焙成原生实例 → 可 diff、可评审、可合并、处处一致
  • 团队习惯:锁种子 → 烘焙 → 提交;有意地重新散布,绝不出于意外

## 迷你问答

难道我不能就让散布保持程序化、永远不烘焙吗?独自的快项目也许可以。对一个团队或者你打 算维护的东西——不行,你会失去 diff、评审和安全合并,还冒着悄悄漂移的风险。烘焙才是让程序 化散布在生产上安全的东西。

那我究竟该把什么放进源代码控制——设置还是结果?烘焙后的结果就是事实之源;设置/种子也 一起留着,好让你能有意地重新生成。只提交设置意味着源代码控制看不到关卡里到底变了什么。

两个美术编辑了同一片森林——我们怎么合?用烘焙后的实例,它就是普通场景数据,你正常的 合并工具就能用。用一个存活的生成器,往往就是解决不了的——这就是"提交前先烘焙"和"协调好谁 拥有某片散布"的核心论点。

烘焙会不会让我失去以后调整的能力?不会——种子和参数都留着,重新烘焙就是你要变排布时有 意为之的一步。你既能迭代能稳定,只要重新散布是一次有意的、被评审的动作,而不是自己 发生的事。

程序化散布只有当它可复现时才配得上它的位置。锁住你的种子、烘焙成原生实例,并把烘焙后的 结果当作关卡的真相——你的森林就成了一整支团队都能在其上建造的东西:作者快、出货稳、只有 当有人有意去动它时才会安全地变。