游戏种子设计与实验

把一串种子发给朋友,对方就能走进同一片山林。这件事看起来很像压缩:几万个格子、矿石和敌人,最后只剩下几个数字。

但种子并没有把地图装进去。它给生成程序一个确定的起点,程序再按规则把世界算出来。只要换了规则,同一串字符也可能长成另一片山川。

这次做一个游戏种子实验室,把两个世界并排放在一起:左边保留基准,右边改变种子、取数方式或生成参数,直接看结果。

先做五个小实验

打开完整实验室 →

实验 改变什么 观察什么
换一个种子 只改种子最后一个数字 山林、矿产和遭遇怎样变化
多取一次随机数 在生成前,为装饰多取一个数并丢弃 共用序列时,无关调用能否影响游戏内容
把系统分开 保留额外调用,改用系统分流 装饰能否不再影响地图与宝箱
倒过来生成 按稳定坐标/事件编号取数,倒序处理 生成先后顺序能否不影响最终结果
只改宝箱概率 稀有概率从 20% 调到 65% 相同数字经过不同规则,会得到什么结果

想自由比较,可以把 B 设为基准 A,再改 B 的设置。点击“用 A 的设置重做 B”,七个维度应该全部一致。点击分享会生成保存两边参数的短链接:预设用名称,自定义只记录差异;日常操作保持地址简洁,无参数刷新会恢复默认对照。导出的 JSON 还保存本次结果。

本页是生成机制演示:地图为 24 × 16 网格,宝箱、天气、事件各展示 8 项,命中展示 12 项。这些序列用于观察取样,并没有接上实际战斗、移动或游戏进度。

种子、随机数与规则

可以把整个过程分成四步:

1
种子文本 → 固定转换与派生 → 随机数 → 地形、掉落和事件规则 → 游戏内容

种子负责给出起点。输入可以是数字,也可以是“山间小路🌱”这样的文字。文字需要按固定方式转换,才能用于初始化随机数状态。

**伪随机数生成器(PRNG)**负责从状态算出下一个数,并推进状态。它是确定的程序:相同初始状态、相同算法和相同调用过程,会得到相同的序列。这里的“随机”,主要描述结果的统计表现和不容易凭直觉预测的顺序。

生成规则负责解释数字。例如得到一个小数 u = 0.18:

  • 作为高度,它可能对应低地;
  • 与 0.20 比较,可以判定一次稀有掉落;
  • 用来选四种事件之一,可能得到商人;
  • 用来选择洗牌交换位置,就会改变后续发牌顺序。

种子本身没有“资源丰富”或“难度很高”的属性。 这些是种子经过特定规则之后的结果。换一种解释方式,原先的好种子可能就不再占优。

Demo 提供两个便于读源码的算法:LCG32 和 Xorshift32。LCG32 按 state = (1664525 × state + 1013904223) mod 2³² 更新;Xorshift32 使用固定的移位与异或。它们是教学选择,切换后无需期待同样的输出,也不能凭一张地图判断随机质量。

更完整的随机数库还会提供显式的序列选择、状态管理和有界整数抽样。例如 PCG 的最小 C 实现区分初始状态与序列选择参数,并提供避免取模偏差的有界整数接口。PCG 官方文档

种子影响哪些游戏内容

前提是游戏把该系统接到了种子控制的随机过程。不是每款游戏都用世界种子决定所有事情。

维度 种子可能决定的内容 对玩法的影响 Demo 中的对应内容
地形与空间 山脉、洞穴、房间、路线 探索成本、绕路、卡口和防守位置 高度场与五类地形
资源与经济 矿点、品质、商店货物 开局路线、扩张节奏、稀缺资源争夺 普通与稀有矿点
敌人与遭遇 敌人位置、编组、精英出现 难度曲线、风险分布、备战需求 普通与精英敌人落点
奖励与构筑 宝箱、词条、卡牌候选 角色构筑与临场调整 普通、精良、稀有宝箱
环境与节奏 天气、事件、刷新时机 能见度、路线选择、休整窗口 天气与四类事件序列
战斗反馈 命中、暴击、伤害浮动 风险预判、连续失误与戏剧性 固定 70% 阈值的命中序列
表现 草木摆动、粒子、装饰变体 视觉差异和环境丰富度 模拟额外取数,不绘制粒子
复现与协作 同局挑战、错误复现、回放 分享、调试与比赛可比性 双世界链接与结果 JSON

这些维度还会互相作用。Demo 中,资源与敌人的随机数即使没变,水位升高后,部分位置变成水域,也会失去放置资格。因此,隔离随机序列不等于隔离所有游戏规则之间的依赖。

机制一:共用随机序列

这是最容易理解的做法:大家轮流拿下一个数。

1
2
原来:  r0 → 地形,r1 → 地形,…… → 资源 → 敌人 → 宝箱
改动后:r0 → 装饰,r1 → 地形,…… → 资源 → 敌人 → 宝箱

程序员只想多生成一个装饰效果,后面的系统却都往后挪了一位。地图形状可能变化,敌人和宝箱也可能变化。

点击 Demo 的“多取一次随机数”就能观察这个问题。对于这个固定示例,七个维度都会出现差异;一般情况下,取数变了不保证离散结果必然改变,例如两个不同的数仍可能同时落进“普通宝箱”的区间。

共用序列适合过程简单、调用顺序容易控制的小实验。项目变大以后,它会让“哪一段代码多调用了一次随机数”变成很隐蔽的兼容性问题。

机制二:按系统分流

给每个系统一条自己的序列:

1
2
3
主种子 + 规则版本 + "terrain"   → 地形序列
主种子 + 规则版本 + "loot" → 宝箱序列
主种子 + 规则版本 + "cosmetic" → 装饰序列

增加装饰调用,只会推进装饰自己的状态。Demo 的“把系统分开”保持了同样的额外调用,但游戏内容会与基准一致。

这里的分流是工程上的取数隔离。简单地哈希几个名字,不等于已经证明这些序列在统计意义上相互独立;对大规模模拟,应选择有明确分流设计和质量说明的随机数方案。

分流还有边界:同一个系统内仍然按顺序取数。先生成第一个宝箱,再生成第二个宝箱,与先生成第二个再生成第一个,可能把同一批数字分配给不同对象。

机制三:按坐标与编号取值

如果地图按区块加载,不同玩家可能先探索不同方向。可以把稳定地址也纳入派生:

1
randomValue = f(主种子, 规则版本, 系统名, 区块坐标, 对象编号, 抽取用途)

例如 loot / chest-42 / rarity 表示 42 号宝箱的品质抽取。它不需要等待 1 到 41 号宝箱先生成。对应的 Demo 使用网格坐标和事件编号;“倒过来生成”会改变执行顺序,但结果应完全相同。

这仍然需要设计:

  • 对象编号要稳定,不能用随加载顺序变化的数组下标冒充永久 ID。
  • 同一个对象需要多次抽取时,要区分品质、数量、词条等用途,不能反复使用同一个地址。
  • 改地形、做碰撞修正或安排事件时,后续规则仍可能依赖邻居或先前结果。
  • 世界区块的地形函数需要共享边界采样,才能避免接缝;仅仅“每块一个种子”还不够。

Demo 的地址用 JSON 数组组合,避免直接拼接字符串时把不同字段拼成相同文本。它仍使用 32 位哈希,存在碰撞可能,不能把它当作全球唯一标识。

种子的配套机制

下面有些是随机数的使用方式,有些是内容约束或产品规则。它们决定了玩家怎样感受到随机性。

机制 怎么做 适合的游戏问题 需要注意
独立抽样 每次按相同概率重新抽取 暴击、简单掉落、装饰变体 小样本可能连续成功或失败
权重抽样 把区间按权重切开,数字落在哪段就选哪项 战利品表、商店、事件池 权重不是一局内的严格配额
Fisher–Yates 洗牌 从末尾向前,与剩余范围内随机位置交换 洗牌、关卡候选顺序 不能用随机比较函数排序来代替正确洗牌
洗牌袋 先把固定集合洗牌,取空后再装一袋 控制方块、卡牌或事件的长期缺席 袋内无重复,跨袋仍可能重复
保底/失败补偿 连续失败后提高概率,或到上限强制成功 减少极端等待 改变条件概率和长期平均,不能仍当作独立抽样
冷却与去重 暂时移除刚出现的事件,或禁止连续同类 防止连续商店、重复任务 候选集变空时必须有明确回退
噪声场与多尺度叠加 按空间位置采样平滑变化的数,再组合尺度 连续山脉、湿度、群落 种子改变布局,频率和阈值决定风格
约束生成与修复 先随机生成,再检查连通、出生点与必需资源 保证地图可玩 修复也要确定;重试需要上限和回退
每日挑战 固定某天的种子、规则与挑战条件 同题竞技、社群分享 同种子只是起点,还需固定版本与计分条件
状态保存与确定性回放 保存随机状态,或记录起点与完整输入过程 存档、调试、重放 种子本身无法恢复中途进度

Demo 下方把独立抽样与七块洗牌袋放在一起。每袋含 I J L O S T Z 各一次,顺序由种子控制。对这个规则,一种符号的两次出现之间最多可以隔着 12 个其他符号:它可能在前一袋最早出现、后一袋最晚出现。这个上界来自袋的结构,不是某个幸运种子。

洗牌使用 Fisher–Yates,并通过拒绝超出整倍数范围的整数来避免直接取模带来的区间偏差。它依赖底层随机源的质量;这不会把教学用生成器变成密码学随机数。Fisher–Yates 的原地交换过程可对照 Perl 官方文档的洗牌示例。

地形部分采用稀疏高度点与平滑插值,属于简单的值噪声式演示,没有复刻 Perlin 或 Simplex 噪声。若继续扩展地形,可以把高度、湿度等场结合起来,再按规则划分群落。Red Blob Games 的噪声地图教程

三个游戏案例

Minecraft:生成算法也是种子配方的一部分。 Java 版 21w41a 快照的官方说明明确提到替换世界生成使用的随机数生成器,并提醒世界布局会因此改变。把旧种子放进新版本,不能只凭字符一致就断言内容一致。Minecraft 官方快照说明

Factorio:共享一张地图,需要共享设置。 官方地图生成接口除了 seed,还有水域、悬崖、资源自动放置等设置。这体现了种子与参数的分工;地图交换字符串把生成设置一并打包,比只告诉朋友一个数字更完整。Factorio 地图生成 API、地图交换字符串格式

Spelunky:让大家挑战同一场冒险。 官方每日挑战介绍把“所有人经历同一场冒险”和“每天一次机会”作为核心规则。由此可以设计固定题目的竞技;但不能据此推断一款游戏内部所有随机调用都使用同一条流。Spelunky 官方每日挑战介绍

这些案例是机制参照。本实验不接受三款游戏的种子格式,也不复现它们的生成结果。

同一种子,不同结局

种子固定的是被它控制的随机起点。玩家选择不同路线、用不同顺序开箱、发动不同技能,都可能改变程序之后处理哪些事件。

如果要复现一段完整过程,通常还需要:初始游戏状态、规则与内容版本、玩家输入顺序、时间步进方式,以及随机数的调用状态或稳定地址。

例如存档发生在已经抽过第 100 个数之后,重新使用初始种子会从头开始。需要保存当前状态,或者从完整记录重新执行到对应位置。Unity 提供 Random.state 来保存和恢复随机数状态;它与保存初始种子解决的是不同阶段的问题。Unity 官方文档

联机回放还可能受浮点计算、物理模拟与执行顺序影响。固定随机数只是其中一环,不是跨平台确定性的充分条件。Glenn Fiedler 的确定性同步说明

种子系统设计步骤

1
2
3
4
5
6
7
{
"generatorVersion": "world-v1",
"seed": "山间小路🌱",
"randomAlgorithm": "固定版本的生成器",
"streams": ["terrain", "loot", "combat", "cosmetic"],
"settings": { "waterLevel": 0.4, "rareChance": 0.2 }
}

这是设计示意,不是本实验 JSON 的导入格式。实际接入时,先决定哪些结果要一起变化,哪些要隔离,再决定是否需要按区块、对象和事件定位随机值。

本实验把文本统一为 NFC,再按 UTF-8 字节哈希;空文本也有效,前后空格保留,最多接受 64 个 Unicode 码点。中文和 emoji 因而有固定的处理方式。Xorshift32 的全零状态会被替换为固定非零状态,避免一直输出零。v2 只用来演示改变派生盐值,界面仍保留 v1。

测试中固定了两种算法和文本哈希的输出向量,并检查同种子重放、系统隔离、倒序生成、参数改变、分享链接与洗牌袋约束。这样的记录能帮助以后修改代码时发现“旧配方已经变味”。

种子也不应替代地图质量检查。要保证出生点可走、资源够用、首领可达,仍需要内容规则;要分享中途进度,仍需要存档。可以接着在迷宫实验室观察连通性,在节点地图实验室观察事件约束,再回到种子实验室理解这些规则如何消费同一批随机数。