什么是适用于游戏的良好随机数生成器?

59

什么是在C++游戏中使用的好的随机数生成器?

我的考虑因素如下:

  1. 需要大量的随机数,速度很重要。
  2. 玩家总会抱怨随机数,但我希望能够向他们指出一篇参考文章,解释我真的做到了我的工作。
  3. 由于这是一个商业项目,我没有太多时间,所以如果算法a)相对容易实现或b)有一个良好的非GPL实现,则非常好。
  4. 我已经在很多地方使用了rand(),因此任何其他生成器都必须足够好,才能证明所有需要的改变是值得的。

我对这个主题不太了解,所以我唯一想到的替代方案就是Mersenne Twister;它是否满足所有这些要求?还有更好的选择吗?


Mersenne Twister似乎是共识选择。但关于第4点呢?它真的比rand()好那么多吗?

让我在第2点上更加明确:玩家无法通过知道随机数来作弊。我希望它足够随机,以至于人们(至少是那些了解随机性的人)无法抱怨它,但我不担心预测。

这就是为什么我将速度列为首要考虑因素。


4
Stobor,你可以通过均匀随机数很好地生成其他分布。 - Joey
3
是的,我知道你可以将任何类型转换为任何其他类型,但如果你知道你主要在离散二元决策(真/假,左/右等)中使用随机变量,那么你可以通过使用二进制生成器而不是整数生成器并计算(x > RAND_MAX / 2)来提高速度。 - Stobor
这并不与之前的答案相矛盾,但确实提供了一些洞见,解释了为什么Mersenne Twister在C++中备受推崇:向标准库添加可扩展随机数设施的提案。第H节大致概述了比你可能遇到的更多算法的优缺点,并且整篇论文从C++程序员的角度对它们进行了讨论。 - Kylotan
如果您正在使用vim编辑器,您可以使用命令 :%s/rand/newrand/g ,它会将每个'rand'实例替换为您新函数的名称。更不用说大多数好的编辑器都内置了搜索/替换的方法。上次我检查过,甚至Windows记事本也有这个功能。如果该函数参数有所不同,您始终可以编写一个包装器来最小化您需要执行的重复编辑量。 - Braden Best
此外,如果有必要的话,可以重新定义常量RAND_MAX。在#undef RAND_MAX之后加上#define RAND_MAX [new max]即可。 - Braden Best
显示剩余10条评论
25个回答

80

其他线程提到了Marsaglia的Xorshf生成器,但没有人发布代码。

static unsigned long x=123456789, y=362436069, z=521288629;

unsigned long xorshf96(void) {          //period 2^96-1
unsigned long t;
    x ^= x << 16;
    x ^= x >> 5;
    x ^= x << 1;

   t = x;
   x = y;
   y = z;
   z = t ^ x ^ y;

  return z;
}

我在许多地方都使用过这个生成器。唯一失败的地方是当我尝试生成随机二进制矩阵时。对于大约95x95的矩阵,它开始生成太少或太多的奇异矩阵(我忘记了哪一个)。已经证明这个生成器等效于一个线性移位反馈寄存器。但除非你正在进行密码学或严肃的蒙特卡罗工作,否则这个生成器非常好用。


1
《数值计算方法》(我知道,它在这些年里加入了很多无意义的内容)建议不要单独使用 XOR-shift,而仅在组合生成器中使用。 - Joey
2
应该是太少的奇异矩阵,因为在所有矩阵的空间中,奇异矩阵是“奇异”的。 - a06e
2
64位版本在不调用此函数两次的情况下可以是什么?将其替换为uint64_t并将第一个移位从16更改为32是否足够? - Serge Rogatch
2
我尝试了一下,在我的硬件/编译器上,它比维基百科页面上的xorshift128稍微慢了一些。 - Paul Brannan
5
虽然这个回答获得了很多赞同,但我还发现了维基百科页面上的xorshift128在Skylake架构上运行速度快了2.5倍。在我的系统上,xorshf96需要大约4.4纳秒,而xorshift128只需要大约1.4纳秒。 - Ody
显示剩余2条评论

44

有时候游戏开发者不想要真正的随机性,而是更适合使用洗牌袋

如果你确实需要随机性,Mersenne Twister可以满足你的需求。它速度快、统计随机、周期长,而且有很多实现。

编辑:rand()通常实现为线性同余发生器。最好根据你的目的做出明智的选择。


5
非常准确。如果没有这个,玩家们会不断抱怨游戏太随机了。 - toholio
我已经阅读了那个问题(实际上,它引发了这个问题的讨论),但我不认为那是解决方案。这里的“命中”已经从玩家身上抽象出来了,所以玩家们可能甚至都没有注意到。 - Michael Myers
空的 for 循环是撒旦的后代,但现在先不要理它。+1 笑话和好答案。 (具有讽刺意味的是,我曾经在 Perl 中经常使用它们...) - Matthew Scharley

38

现在有比Mersenne Twister更好的选择。这里有一个名为WELL512的RNG,由Mersenne的设计者设计,10年后开发,是游戏的更好选择。该代码由Dr. Chris Lomont放入公共领域。他声称这个实现比Mersenne快40%,不会出现状态包含许多0位时的扩散和陷阱问题,并且代码明显更简单。它具有2 ^ 512的周期; PC需要超过10 ^ 100年才能循环遍历状态,因此足够大。

这里有一篇概述PRNG的论文,我在其中找到了WELL512的实现。 https://www.lomont.org/papers/2008/Lomont_PRNG_2008.pdf

所以-更快,更简单,由同样的设计师10年后创建,并且产生比Mersenne更好的数字。你怎么会错呢? :)

更新(11-18-14):修正错误(根据上面链接的论文将0xDA442D20UL更改为0xDA442D24UL)。

/* initialize state to random bits */
static unsigned long state[16];
/* init should also reset this to 0 */
static unsigned int index = 0;
/* return 32 bit random number */
unsigned long WELLRNG512(void)
   {
   unsigned long a, b, c, d;
   a = state[index];
   c = state[(index+13)&15];
   b = a^c^(a<<16)^(c<<15);
   c = state[(index+9)&15];
   c ^= (c>>11);
   a = state[index] = b^c;
   d = a^((a<<5)&0xDA442D24UL);
   index = (index + 15)&15;
   a = state[index];
   state[index] = a^b^d^(a<<2)^(b<<18)^(c<<28);
   return state[index];
   }

4
我浪费了一个晚上的时间来理解为什么我的代码不能正常运行:在 64 位机器上,这段代码会产生 64 位数!请使用“sizeof(unsigned long) * 8”。 - Ruggero Turra
1
@wiso,请使用sizeof(unsigned long) * 8代替哪个部分? - Shahbaz
3
常数应为(根据原论文):0xDA442D24UL。 - Mitch Wheat
1
@MitchWheat的评论是正确的:0xDA442D20UL应该是0xDA442D24UL。(我尝试在一年前编辑过,但由于某种原因被拒绝了。)这个有问题的版本出现在上面链接的Lomont_PRNG_2008.pdf文件中,后来得到了更正(请参见文件末尾的历史部分)。但是这里的代码仍然显示错误的版本。为了比较,WELL创建者的原始实现在这里:http://www.iro.umontreal.ca/~panneton/well/WELL512a.c (请注意,这个更改很小,但这些东西对随机生成器算法可能会产生巨大影响。) - Tyler Streeter
@Shahbaz(和其他可能阅读此内容的人):我认为他的意思是,这段代码的用户应该检查其机器/编译器上sizeof(unsigned long)* 8的结果...如果结果为64(即unsigned longs宽度为64位),那么这段代码将产生意外的结果,因为它假设unsigned longs宽度为32位。请参考原始实现,该实现使用unsigned ints而不是unsigned longs:http://www.iro.umontreal.ca/~panneton/well/WELL512a.c - Tyler Streeter
显示剩余3条评论

33

从英特尔网站上获取的两个好的替代方案:

1) fastrand - 它比std rand()快2.01倍。 该例程返回一个整数,输出值范围与C lib相似。

inline int fastrand() { 
  g_seed = (214013*g_seed+2531011); 
  return (g_seed>>16)&0x7FFF; 
} 

2)一个SSE版本(参见下面的链接)比标准rand()快约5.5倍,但它每次生成4个随机值,需要使用支持SSE的处理器(几乎所有处理器都支持),并且更加复杂。

http://software.intel.com/en-us/articles/fast-random-number-generator-on-the-intel-pentiumr-4-processor/


很好,使用这个函数代替rand()在Tegra 3上加速了大约2.5倍的程序。 - Learn OpenGL ES
太棒了!我需要生成几百万个随机数,这让速度提升了不少。 - Armin Meisterhirn
2
g_seed是什么?是一个long类型还是一个long long类型? - JHBonarius
1
"g_seed" 是一个静态的无符号整数。 - darksinge
SSE版本并不比pcg32_fast更快,而pcg32_fast既简单又出色:https://en.wikipedia.org/wiki/Permuted_congruential_generator#Example_code - Andriy Makukha

29

乔治·马萨利亚已经开发出目前可用的一些最好、最快速的随机数生成器,倍增进位随机数生成器是其中一个值得注意的均匀分布算法。

=== 2018-09-12 更新 ===

为了我的工作需要,我现在正在使用Xoshiro256**,它是Marsaglia XorShift的一种演化/更新版本。

=== 2021-02-23 更新 ===

.NET 6(目前处于预览版阶段)中 System.Random 的实现已更改为使用 xoshiro256**,但仅适用于无参数构造函数。接受种子的构造函数将继续使用旧的 PRNG 以保持向后兼容性。有关更多信息,请参见Improve Random (performance, APIs, ...)


1
是在维基百科关于Mersenne Twister的文章中看到Marsaglia的批评,这才促使我提出这个问题。不过,有人真正使用它们吗? - Michael Myers
不确定这些的使用有多普遍,但在我的个人项目中我使用了Marsaglia的异或位移随机数生成器,并将该随机数生成器发布为:http://www.codeproject.com/KB/cs/fastrandom.aspx随后,该代码被用于一个随机数生成器代码库:http://www.codeproject.com/KB/recipes/Random.aspx我还被要求修改其许可证以便在Mono上运行的Novell某个项目中使用。 - redcalx
我接受了这个建议,因为在我的简单测试中,Marsaglia提出的XOR-shift生成器比竞争对手快得多。我也会考虑在某些时候切换到WELL512;我已将所有对'rand'的调用封装到一个类中,所以更改实现非常容易。 - Michael Myers
谢谢,我选择它是因为它速度快,同时也通过了Marsaglia的RNG测试套件。另一个好处是种子可以快速重置,而其他RNG需要在设置种子时运行初始化程序。如果您需要重新生成完全相同的数字序列(从相同的种子开始)并且需要频繁更改种子,则这是一个有用的功能。 - redcalx

12

请参阅随机数生成专家George Marsaglia提供的这些生成器。它们实现为C宏,速度非常快,每个生成的数字只需要几个操作。


11

购买一台便宜的网络摄像头和一款电离烟雾探测器。拆开它们两个,烟雾探测器含有少量放射性物质——γ波源——这将导致向您的网络摄像头发射光子。那就是你的真正随机源 :)


1
再次强调,我并不在意真正的随机性。我只需要它足够接近和足够快速即可。 - Michael Myers
2
这个想法会在随机的时间生成事件,但不能按需生成随机位。此外,烟雾探测器网络摄像头硬件将被客户群体视为一种防拷锁。 - Jeff Sharp
3
我会购买这个游戏,仅因为它带有一个贴着放射性警告标签的“拷贝保护”加密狗! - Grant Peters

10

梅森旋转发生器在工业中很常见,尤其是因为它适合于SIMD并且可以非常快速地生成。 Knuth 也很受欢迎(谢谢David)。

在大多数游戏应用中,速度真的是至关重要的因素,因为玩家更容易抱怨低帧率,而不是抱怨生成3时,在前面依次出现7、2和9时存在轻微偏差。

当然,唯一的例外是赌博,但是你的相关许可机构将明确规定你可以使用哪些算法。


3
他们可能会抱怨很多(声音相对较大),说3从来没有出现在他们的机会中。这取决于用途。真正的随机适用于伤害等事物,但对于物品掉落之类的事情,使用像@David Johnstone发布的洗牌算法是有意义的。 - Matthew Scharley
足够正确;根据“公平性”或真正的随机性哪个更重要,你会需要不同的算法。 - Crashworks
他们实际上看不到随机数,但他们会注意到有时候在不期望的情况下输了。(我相信反过来也会发生,只是他们几乎从不注意到。)也许对于战斗部分使用正态分布会更好一些。 - Michael Myers
此外,大多数编程语言都有一个成熟的现成机器翻译实现,并且很可能有一个许可证允许您将其纳入商业软件中。 - ConcernedOfTunbridgeWells

8
Ivy Bridge架构开始,英特尔添加了 RdRand CPU指令,AMD在2015年6月后也添加了该指令。因此,如果您的目标处理器足够新,并且不介意使用(内联)汇编语言,那么调用RdRand CPU指令以获取16位、32位或64位随机数应该是生成随机数最快的方法,如 这里所述。请滚动到页面中部查看代码示例。在该链接中,还有一个检查当前CPU是否支持RdRand指令的代码示例,并且还可以参考维基百科中的说明来执行此操作。

相关问题: 如何使用 sandy bridge 硬件真随机数生成器?(尽管根据维基百科,RdRand 指令首次出现在 Ivy Bridge 中,而不是 Sandy Bridge 架构,正如那个问题所说的那样)

基于_rdrand64_step()的C++示例代码:

#include <immintrin.h>

uint64_t randVal;
if(!_rdrand64_step(&randVal)) {
  // Report an error here: random number generation has failed!
}
// If no error occured, randVal contains a random 64-bit number

3
rdrand 实际上很慢。请参见 https://software.intel.com/en-us/articles/intel-digital-random-number-generator-drng-software-implementation-guide 的3.4.1节和https://dev59.com/qGkv5IYBdhLWcg3wlR71。在一个快速的基准测试中,`xorshift128` 的时钟速度大约比 rdrand32 快13倍。英特尔声称每个线程持续70-200 MB/s。 - ZachB

6
Mersenne Twister非常好,而且速度也很快。我在游戏中使用它,实现和使用都不难。 WELL随机算法是作为对Mersenne Twister的改进而设计的。如果您可以借用或拥有Game Gems 7,那么上面有更多信息。
在我给您提供的WELL页面上,该数字是算法的周期。也就是说,在需要重新生成种子之前,您可以获得2^N-1个数字,其中N为512、1024、19937或44497。Mersenne Twister的周期为N=19937,即2^19937-1。您会发现这是一个非常大的数字 :)
我能指出的唯一另一件事是,boost 有一个 随机库,你应该会发现它很有用。
回应你的编辑,是的,Twister或WELL比rand()好得多。而且,旧的模数技巧会损害数字的分布。更多使用boost的理由 :)

1
仅仅为了生成随机数,是否值得使用Boost库?目前我们根本没有使用它。 - Michael Myers
Boost非常值得使用。它提供了干净的C++语法(只需查看页面),以及所有其他boost所提供的东西,如智能指针、boost::function、lambda表达式、线程等等......我基本上将Boost视为STL。一旦设置好了(这并不太困难),您就可以忘记它的存在,并且在需要时会感到高兴。 - GManNickG
2
请记住,Boost 不是二选一的问题。您只需包含所需的单个头文件即可,这就是您获得的全部内容。大多数 Boost 库甚至都是仅限头文件库,因此无需链接到任何内容,所有内容都在头文件中。 - Bjarke Freund-Hansen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接