游戏种子设计与实验
把一串种子发给朋友,对方就能走进同一片山林。这件事看起来很像压缩:几万个格子、矿石和敌人,最后只剩下几个数字。
但种子并没有把地图装进去。它给生成程序一个确定的起点,程序再按规则把世界算出来。只要换了规则,同一串字符也可能长成另一片山川。
这次做一个游戏种子实验室,把两个世界并排放在一起:左边保留基准,右边改变种子、取数方式或生成参数,直接看结果。
先做五个小实验
| 实验 | 改变什么 | 观察什么 |
|---|---|---|
| 换一个种子 | 只改种子最后一个数字 | 山林、矿产和遭遇怎样变化 |
| 多取一次随机数 | 在生成前,为装饰多取一个数并丢弃 | 共用序列时,无关调用能否影响游戏内容 |
| 把系统分开 | 保留额外调用,改用系统分流 | 装饰能否不再影响地图与宝箱 |
| 倒过来生成 | 按稳定坐标/事件编号取数,倒序处理 | 生成先后顺序能否不影响最终结果 |
| 只改宝箱概率 | 稀有概率从 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 | 原来: r0 → 地形,r1 → 地形,…… → 资源 → 敌人 → 宝箱 |
程序员只想多生成一个装饰效果,后面的系统却都往后挪了一位。地图形状可能变化,敌人和宝箱也可能变化。
点击 Demo 的“多取一次随机数”就能观察这个问题。对于这个固定示例,七个维度都会出现差异;一般情况下,取数变了不保证离散结果必然改变,例如两个不同的数仍可能同时落进“普通宝箱”的区间。
共用序列适合过程简单、调用顺序容易控制的小实验。项目变大以后,它会让“哪一段代码多调用了一次随机数”变成很隐蔽的兼容性问题。
机制二:按系统分流
给每个系统一条自己的序列:
1 | 主种子 + 规则版本 + "terrain" → 地形序列 |
增加装饰调用,只会推进装饰自己的状态。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 | { |
这是设计示意,不是本实验 JSON 的导入格式。实际接入时,先决定哪些结果要一起变化,哪些要隔离,再决定是否需要按区块、对象和事件定位随机值。
本实验把文本统一为 NFC,再按 UTF-8 字节哈希;空文本也有效,前后空格保留,最多接受 64 个 Unicode 码点。中文和 emoji 因而有固定的处理方式。Xorshift32 的全零状态会被替换为固定非零状态,避免一直输出零。v2 只用来演示改变派生盐值,界面仍保留 v1。
测试中固定了两种算法和文本哈希的输出向量,并检查同种子重放、系统隔离、倒序生成、参数改变、分享链接与洗牌袋约束。这样的记录能帮助以后修改代码时发现“旧配方已经变味”。
种子也不应替代地图质量检查。要保证出生点可走、资源够用、首领可达,仍需要内容规则;要分享中途进度,仍需要存档。可以接着在迷宫实验室观察连通性,在节点地图实验室观察事件约束,再回到种子实验室理解这些规则如何消费同一批随机数。