中EN
概念页

伪随机数生成

Pseudorandom Number Generation

游戏引擎到处要用随机数,但随机数生成器 (RNG) 并不真正随机:它只是给出一个复杂但完全确定、预先定义的序列,因此严格说应叫伪随机数生成器 (PRNG)。好坏取决于周期(多久重复)和能否通过各种公认的随机性测试。GEA 介绍了线性同余、梅森旋转、Marsaglia 的 Mother-of-All / Xorshift / KISS99,以及 PCG。

线性同余生成器 LCG(卷 I 5.7.1)

  • 非常快、非常简单;有些平台的 C 标准库 rand() 就用它,但不要假设 rand() 基于任何特定算法——想确定就自己实现。算法细节 GEA 引用《Numerical Recipes in C》,未展开。
  • 质量不高:同一种子总是产生完全相同的序列(这是所有 PRNG 的共性);不满足很多公认的理想标准,如长周期、低位和高位都有足够长的周期、生成值之间没有序列或空间相关性。

梅森旋转 Mersenne Twister(卷 I 5.7.2)

专门为改进 LCG 的问题而设计。GEA 引用 Wikipedia 列出的优点:

  1. 周期巨大:219937−12^{19937}-1(作者已证明;219937≈4.3×1060012^{19937}\approx4.3\times10^{6001}),实际应用很少需要更大的;
  2. 维度均匀分布阶数很高,连续输出之间的序列相关性可以忽略;
  3. 通过大量统计随机性测试,包括严格的 Diehard 测试;
  4. 快。
  • 网上有多种实现,其中 SFMT(SIMD-oriented Fast Mersenne Twister)用 SIMD 指令进一步提速(见 SIMD 向量化)。

Mother-of-All、Xorshift 与 KISS99(卷 I 5.7.3)

生成器作者/年代特点
Mother of AllGeorge Marsaglia,1994比梅森旋转更易实现、更快;32 位输出,Marsaglia 称周期 22502^{250};通过全部 Diehard 测试,GEA 称至今仍是高速应用最好的 PRNG 之一
XorshiftMarsaglia随机性介于梅森旋转与 Mother 之间,比 Mother 略快
KISS99Marsaglia(KISS = Keep It Simple Stupid 系列)周期大(21232^{123}),通过 TestU01 测试套件全部测试,是流行选择

Marsaglia 也是 Diehard 随机性测试集的作者。

PCG(卷 I 5.7.4)

另一个流行的高质量 PRNG 家族:用同余生成器 (congruential generator) 做状态转移(名字里的「CG」),再用置换函数 (permutation) 生成输出(「P」)。

取舍

需求GEA 给出的信息
最快、最简单、质量要求低LCG(但 rand() 实现不可依赖)
超长周期、统计质量高梅森旋转;SFMT 进一步提速
高速且质量好Mother-of-All、Xorshift、KISS99
现代高质量家族PCG
  • 所有这些都是确定性的:同一种子得到同一序列。GEA 把这一点作为 LCG 的缺点提出;书中未讨论把确定性用于回放或网络同步等用途。

Schreiber & Romero 的视角:设计师眼中的伪随机(第 21 章,p.509–523)

本页上面几节(Gregory)讲生成器的算法;Schreiber 与 Romero 从游戏设计角度讲随机为何不够随机,以及由此带来的设计取舍。

实体随机也不完美(p.509–511):"公平的 d6"是理想状态,就像化学中的理想气体或物理中无摩擦无质量的滑轮。点数凹刻的骰子 1 面最重,因而 6 朝上略多;圆角加工不一导致某些面更易继续滚动;多数 d20 略呈椭圆,玩家的"幸运 d20"可能确有其事。抛硬币略偏向起始朝上的一面,转硬币偏差更大。普通的鸽尾式洗牌 (riffle shuffle) 随机化很差:顶/底牌大多留在顶/底附近,单次洗牌保留两叠内部的相对顺序(不可能一次把整副牌倒序),某些洗法重复若干次会回到原序(魔术师借此假洗牌)。宾果球、彩票球的油漆重量不同,儿童桌游的转盘摩擦不均。赌场因而使用公差约 10 微米、油漆等量的昂贵骰子——被怀疑器材作弊可能丢掉牌照,而不公平器材又会被精明的玩家利用;手工洗牌已大多改为洗牌机,机械洗牌机可能比人手更不随机,电子洗牌机按算法逐张排序,同样受下述算法问题影响。对普通玩家,这些缺陷对所有人一视同仁,"足够接近"即可。

种子(p.512–513):计算机是确定性的,除非用测量混沌物理现象的专用硬件(如放射性衰变;脚注提到 random.org,但依赖第三方真随机数据在业界不实用)。RNG 由一个看似无规律的函数反复迭代得到,起点即种子,一般在游戏启动时设一次;同一种子得到完全相同的结果——测试复现很有用,但正式版若意外如此就糟了。种子的选取要让玩家难以复现:若只用系统时钟的毫秒小数部分(0–999),游戏实际只有 1000 种"洗法",重复游玩会看到雷同的局,PvP 中有心人能从前几个结果推断种子并预测之后的一切;即使用"午夜以来的毫秒数"也只有 8640 万个种子,玩家每天在同一时段游玩可能两次遇到同一个种子。

联网不同步(p.513):若双方客户端共用种子各自生成随机数,一处 bug 让一方多生成了一个数而没通知对方,游戏可以照常进行几个回合,直到某次掷骰一边成功一边失败,状态比对出错,双方都以为对方作弊。这类 bug 很难定位。脚注建议游戏逻辑放在服务器上,只保留单一游戏状态;无服务器的直连(如蓝牙)则指定一台设备为权威并广播状态。

存档与读档(p.513–516):单机游戏无法完全杜绝存读档利用,只能"选择毒药":

方案利用方式 / 缺点
随处存档大赌前存档、输了就读档(游戏内赌场全押);玩家没有作弊,用的是你设计的系统,但概率平衡被彻底抛开
随处存档 + 保存种子只是加了道小门槛:换做另一件随机的事(喝一瓶回复量随机的药、小额下注、换动作顺序)即可改变结果;而且读档后游戏完全确定,玩家能预知 Boss 的每个行动——这种预知比无限重掷更强
存档点早期游戏为节省存储而用;两次存档间需要大量游玩,减少了利用,但诚实玩家抱怨不能随时离开,被游戏"扣为人质";存档点放在难 Boss 门外则 Boss 可被利用,放在 15 分钟路程外则每次重来都极其挫败
受保护的存档读档即删除存档。《德鲁亚加的噩梦 (The Nightmare of Druaga)》甚至检测玩家用重置逃避损失,强制观看冗长的反作弊说教并按死亡惩罚——停电也被当作作弊
限次存档初代《古墓丽影 (Tomb Raider)》限定存档次数。不告诉玩家危险在哪,玩家觉得游戏要求自己会读心;告诉了又觉得太简单、失去未知的刺激;若存在最佳存档位置,不如直接放存档点

完美的存档系统是游戏设计中尚未解决的问题:玩家要么操纵过去(读档重掷),要么操纵未来(试一次再读档获得预知)。但它仍必须由设计师来设计——否则会落到某位程序员手里,按最容易写的方式实现。设计师能做的是了解可能的利用并让游戏其余部分把影响降到最低:例如允许随处存档又不想经济被轻易打破,就不要做能短时间翻倍财富的赌场,或严格限制赌场收益,或在赌场周围大范围禁止存档。桌游没有存读档,因此不存在这个问题。

洗牌算法(p.516–518):

  • 新手常用的"随机挑两张交换,重复几千次"既低效又不随机:任何一张牌总有非零概率从未被选中,因此出现在原位的概率永远略高于其他位置,交换次数越多这个偏差越小,但永远不为零。
  • 正确做法:从未选过的牌中随机挑一张放到下一个位置,即下面的循环(Fisher–Yates 洗牌,原书未用此名),每张牌一次随机数和一次交换:
for (int i=DECK_SIZE-1; i>0; i--) {
    int r = rand()%(i+1);
    tmp = Deck[i]; Deck[i] = Deck[r]; Deck[r] = tmp;
}
  • 支线 21.2 的变体把 rand()%(i+1) 改成 rand()%DECK_SIZE(每个位置可与整副牌中任意一张交换)。用 3 张牌验证:变体共有 32=93^2=9 条等概率的路径(原书循环 i 从 2 到 1,执行两次),要分配到 3!=63!=6 种排列上,9 不能被 6 整除,因此必然不均匀;正确算法的路径数为 3×2=63\times2=6,恰好每种排列一条。(本条为本 wiki 的推算,原书只给出提示。)
  • 种子空间不够大:64 位整数有 264≈1.8×10192^{64}\approx1.8\times10^{19} 个,而 52 张牌有 52!≈8.07×106752!\approx8.07\times10^{67} 种排列,绝大多数理论上可能的洗牌结果永远不会出现。log⁡252!≈225.6\log_2 52!\approx225.6,需要 226 位(原书说按 2 的幂即 256 位机器);log⁡260!≈272.1\log_2 60!\approx272.1,256 位也不够处理万智牌 60 张的套牌;100 张需要约 525 位(支线 21.3,本 wiki 计算)。若种子来自"午夜以来的毫秒数",种子空间只有约 8600 万,手边的电脑几秒内就能搜完;玩家只要猜出种子生成方式和 RNG 算法(如 C++ 标准库的 rand()),不需要几个结果就能定位序列并完全预测之后的结果。脚注警告:赌场有出色的实时统计,能迅速区分幸运儿与作弊者,后果可能是终身禁入乃至刑事或民事诉讼。涉及大笔金钱的赌博游戏或高水平竞技比赛,必须额外确保随机数无法被破解。

结论与练习:完全随机的"随机数"是理想情况,"足够接近"的标准取决于休闲游戏、高额竞技还是受严格监管的博彩业。支线 21.1 用电子表格 =RAND() 生成 1000 对数画散点图,作为快速粗略的 RNG 检验;21.4 自己写 200 次抛硬币程序,研究所用 RNG 和种子生成方式,把种子空间缩小到几百万以内后预先算出所有序列并搜索匹配,看需要几次抛掷才能唯一确定种子;21.5 分析一款游戏的存档系统(何时能存、种子是否保存、存档能否复制)并列出所有可行的利用及其实用程度。(本章讨论题在提取文本中缺失第 1、3、9 题及第 9 题前半,未能读到。)

两种观点对照

主题Gregory(本页上文)Schreiber & Romero
洗牌结果的可能数梅森旋转的周期为 219937−12^{19937}-1,内部状态远超一个机器字以"64 位机器的整数有 2642^{64} 个"推断种子数受机器字长限制,"需要 256 位的计算机"才能覆盖 52 张牌的所有洗牌结果

说明:可达到的洗牌结果数取决于 RNG 的内部状态位数和种子的熵,而不是 CPU 字长。梅森旋转 MT19937 有 19937 位状态(Matsumoto & Nishimura, ACM TOMACS 8(1), 1998),64 位机器也能使用。原书"需要 256 位的计算机"的说法不准确,正确的结论是需要至少约 226 位熵的种子和足够大的生成器状态。原书关于"用 32/64 位种子时绝大多数排列无法出现"的结论本身是正确的。

补充核对:鸽尾式洗牌"要洗好多次才够随机"——Bayer 与 Diaconis(Annals of Applied Probability 2(2), 1992)证明 52 张牌约需 7 次鸽尾式洗牌才接近均匀。硬币偏向起始朝上的一面:Bartoš 等人 2023 年的 35 万次实验测得约 50.8%(arXiv:2310.04153),与原书一致。

PBRT 的视角:PCG 与无偏有界整数(PBRT 4e B.2.12)

  • pbrt 使用 PCG(O'Neill 2014):通过严格的统计随机性测试,实现又很紧凑。用户可提供两个 64 位值:一个从 2632^{63} 条序列中选一条(每条长 2642^{64}),另一个是序列内的起始偏移。Advance() 可以把状态跳过任意步数,operator- 给出两个生成器之间相差的步数。
  • 有界整数:pbrt 前两版用 Uniform<int32_t>() % b,当 2322^{32} 不是 bb 的倍数时偏向较小的值。第 4 版先用 32 位运算求出 threshold=232 mod b\text{threshold}=2^{32}\bmod b,Uniform() 返回值小于 threshold 就丢弃重抽,剩下的取值个数恰为 bb 的整数倍,再取模就无偏了。
  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 5 章 5.7 节
  3. Game_Balance_-_Ian_Schreiber_Brenda_Romero.pdf
  4. Ian Schreiber & Brenda Romero《Game Balance》第 1 版(CRC Press, 2022, ISBN 978-1-4987-9957-7)第 21 章 Pseudorandom Numbers(印刷页 p.509–523)
  5. PBRT 4e source excerpt: chapter B (Utilities)
  6. Pharr, Jakob, Humphreys《Physically Based Rendering: From Theory to Implementation》第 4 版(MIT Press, 2023)附录 B B.2–B.7 节与延伸阅读,pbr-book.org/4ed/Utilities

相关页面

游戏数学数值表示与浮点数Numeric Representation & Floating Point游戏引擎里到处是数值,所以要清楚它们在机器中如何存储:二进制和十六进制、补码整数、定点数、IEEE-754 浮点数(量级与精度此消彼长、次正规数、机器 epsilon、ULP),以及多字节值的大小端问题和安全的类型双关 (type punning) 方法。书中一个直观的例子是:用 32 位浮点数记录以秒为单位的游戏时钟,大约 12.14 天后,加 1/30 秒就不再改变它的值。游戏开发SIMD 向量化SIMD VectorizationSIMD(单指令多数据)用一条机器指令并行处理多个数据。x86 上从 MMX 发展到 SSE(128 位,4 个 float)、AVX(256 位,8 个)和 AVX-512(512 位,16 个)。GEA 用 SSE 内建函数演示:数组相加、点积、向量×矩阵、矩阵×矩阵,强调两点——避免寄存器内的水平相加(先转置数据),以及把 SIMD 寄存器看成 N 条并行的「通道 (lane)」而不只是一个 4D 向量;条件分支则用向量谓词化 (vector predication / select) 代替。游戏数学点与向量运算Points & Vector Operations游戏程序员最常用的数学是三维向量与矩阵(三维线性代数)。本页按 GEA 第 5 章整理点与向量的基础:坐标系与手性、点与方向的区别、标量乘与 Hadamard 积、加减、模长与归一化、点积与投影、叉积、伪向量与外代数,以及线性插值 (lerp)。贯穿全章的两条经验是:能在二维里想清楚的问题先在二维里想,以及能不开方就不开方。图形学蒙特卡洛积分与重要性采样Monte Carlo Integration & Importance Sampling渲染方程里的半球积分没有闭式解,只能数值积分。蒙特卡洛积分在定义域里随机取样、按概率密度加权求平均,期望值正好等于积分真值,误差随样本数 N 按 1/\sqrt{N} 下降,而且与积分维数无关,所以成了光线追踪类渲染的数学基础。降低误差的主要手段是让采样分布接近被积函数的形状,也就是重要性采样(GEA4 卷 II 12.6.2、12.6.4)。游戏开发游戏音频功能与角色对话Game-Specific Audio Features & Character Dialog在 3D 音频渲染管线之上,游戏还要实现分屏、物理驱动音频、动态音乐、角色对话、声音合成和人群声音等专门功能。GEA 重点讲了分屏的多听者问题、顽皮狗《最后生还者》的角色对话系统(逻辑台词、随机不重复、优先级与打断、把对话作为一等实体、排他性、基于规则和事实字典的分支对话、按区域标签的情境台词、对话手势)以及音乐系统(播放列表、紧张度、插入乐、过渡)(GEA4 卷 II 15.6)。游戏策划概率与期望值Chance & Expected Value机会 (chance) 是 Schell 七类机制的最后一类(第 3 版第 12 章 Mechanic 7: Chance;第 1 版为第 10 章、六类中的第六类),涉及其他五类之间的交互:机会意味着不确定,不确定意味着惊喜,而惊喜是乐趣的秘密成分。设计师要掌握十条概率规则、会算期望值 (expected value),并同时考虑实际概率与玩家的感知概率,以及技能与运气的五种纠缠(第 10 章)。游戏策划Machinations 图示语言Machinations DiagramsMachinations 是 Joris Dormans 设计的可视化语言与在线模拟工具,用于创建、记录、模拟和测试游戏的内部经济。其理论前提是:玩法最终由有形、无形与抽象资源在游戏系统中的流动决定。图由存放资源的节点 (Nodes)、搬运资源的实线资源连接 (Resource Connections)、以及用当前状态去修改别处的虚线状态连接 (State Connections) 组成;语法足够严格,可被计算机按时间步执行,接近一种专门表示游戏机制的可视化编程语言。本页按第 5 章整理全部基本元素与《吃豆人》建模示例,并给出本 wiki 用 Mermaid 改画 Machinations 图游戏策划模拟试玩与人工玩家Simulated Playtests & Artificial Players简单的赌博游戏(21 点、轮盘)可以直接算胜率;复杂而带随机的游戏必须玩很多次才知道是否平衡。Machinations 工具提供人工玩家 (Artificial Player, AP):用简单脚本代替人点击节点,配合快速运行与批量运行收集上千局统计。Adams & Dormans 用它做了两个完整案例:给两人版《大富翁》加入房屋与房产税(动态摩擦)来遏制动态引擎;给 Will Wright 的假想 RTS SimWar 调参,使"乌龟流"与"速攻流"从 929:68 调到 500:500。核心纪律:人工玩家要做成始终坚持一种策略的漫画式角色;调机制,不调人工玩家;一次只改一处并先大幅改动。游戏策划经济建造游戏设计Designing Economy-Building Games大多数游戏的内部经济结构固定不变(如《大富翁》),而经济建造游戏 (Economy-Building Games) 让玩家亲手添加源、泄与连接,自己搭出经济结构——典型的是建造与管理模拟及策略游戏(《文明》《模拟城市》、程度较轻的《星际争霸》)。因为每局玩家接线不同,不可能画出"一张大图",Adams & Dormans 改为两个层次建模:主导经济结构 (Dominant Economic Structure)(抽象层,玩家的经济会趋向的模板)与构建块 (Building Blocks)(具体建筑的机制,带可多方式连接的输入输出)。第 9 章用《凯撒大帝 III》做分析,再从零设计桌面原型 L游戏策划独立随机与骰子概率Independent Randomness & Dice ProbabilitySchreiber 与 Romero 第 17 章(第二部分"平衡的数学"首章)讲独立随机:骰子记法与等价随机器、概率/赔率/机会三个词的区别、用计数求概率、乘法与加法法则、取反、期望值、排列、大数定律,并用 Kill the Elf、50d6 赛马、Pig 与 TCG 稀有度四个案例说明短期波动与"稀有"陷阱(p.431–454)。游戏策划相依随机与抽牌概率Dependent Randomness & Card DrawsSchreiber 与 Romero 第 18 章讲相依随机:以不放回抽牌为典型,已知一个结果会改变后续事件的概率。内容包括"揭示信息才改变概率"、两张牌成对、组合与排列公式、扑克牌型计数、《万智牌》洗牌计数,以及蒙提霍尔问题(含节目中的真实规则与一般化)和假阳性悖论(p.455–475)。游戏策划概率直觉与认知偏差Probability Intuition & Cognitive BiasesSchreiber 与 Romero 第 20 章讨论真实概率在人类心理中如何走样:期望值不等于效用(赔率、频率、金额门槛都影响意愿),一系列认知偏差与谬误让玩家把公平的随机感知为不公平;设计上的应对有三类——利用偏差(作者明确视为"黑暗面")、按偏差修改概率(并讨论其伦理)、以及诚实地公开随机结果与改进界面(p.491–508)。游戏策划电子表格中的随机模拟与矩阵求解Spreadsheet Simulation & Matrix Solving《Game Balance》第三部分里与"算概率、做模拟"有关的四章:第 31 章随机、统计与条件函数及"穷举还是蒙特卡洛",第 32 章洗牌与加权掉落表,第 35 章用迭代计算或逐行展开做逐回合模拟,第 37 章在表格里跑马尔可夫链、解收益矩阵和用规划求解。表格的布局、格式与组织规范见 电子表格平衡技法;本页对照原书逐段核对,并标出已过时或有误的说法。游戏数学随机变量变换与逆变换采样Random Variable Transforms & Inversion Sampling蒙特卡洛估计器要求能从指定的概率密度里抽样。最基本的工具是逆变换法 (Inversion Method):对 CDF 求逆,把 [0,1) 上的均匀随机数映射成目标分布;更一般地,随机变量经过一一映射后的密度由导数(多维时是雅可比行列式)决定;多维非独立分布则先采边缘密度、再采条件密度。本页按 PBRT 第 4 版 2.1.1、2.3、2.4 节整理,是 蒙特卡洛积分与重要性采样 的数学部分。游戏数学低差异序列Low-Discrepancy Sequences低差异序列 (low-discrepancy sequences) 是一类确定性的点序列,在 [0,1)^d 中分布得比随机点更均匀(星差异度 O((\log n)^d/n)),用于拟蒙特卡洛 (QMC) 积分和渲染中的采样。常见构造是基于根反演 (radical inverse) 的 van der Corput、Halton、Hammersley,以及基于二进制生成矩阵的 Sobol'。实际渲染中会对它们做随机加扰 (scrambling)(随机数字置换、Owen 加扰),既能估计方差、去掉高维投影中的规则图案,又能提高收敛率。本页以 PBRT 4e 8.5–8.7 节为主。游戏数学别名法与水塘采样Alias Method & Reservoir Sampling从 n 个离散结果中按概率 pi 采样,线性扫描 CDF 每次 O(n),二分查找 O(\log n);别名法 (alias method) 预处理 O(n) 后每次采样 O(1):均匀选一个桶,再以桶内概率 qi 决定取桶本身的结果还是它唯一的"别名"。水塘采样 (reservoir sampling) 则不需要预先知道结果个数、也不需要把所有候选存在内存里:候选逐个流过,每个以"自身权重 / 已见权重和"的概率替换水塘中的样本,最终保留的样本恰好服从全部候选的分布;两个水塘还可以无偏地合并。前者用于按功率选光源、环境贴图采样等,后者是 ReSTIR 等方法的基础。图形学着色器伪随机Shader Pseudorandom片元着色器里没有真正的「每次调用都不同」的随机:rand(x) 必须是确定性伪随机——同一输入永远同一输出。常用手法是 fract(sin(x)大数);二维则用 dot 把 vec2 压成标量再喂给同一套路。把空间 floor 成格子后,格子共享一个随机值,就能在秩序与混沌之间做生成设计。图形学噪声函数Noise FunctionsKen Perlin 为电影《电子世界争霸战》(Tron) 设计的噪声,解决「看起来随机却不像电视雪花」的程序纹理问题。入门路径是值噪声 (value noise):格子角点取伪随机,格内用平滑曲线插值;进阶有梯度噪声 (gradient noise)、五次插值改进版,以及用单纯形网格降复杂度的 Simplex Noise。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原