【游戏精粹】随机数

目录

    • 可预测随机数
    • 生成真正的随机数
    • 随机数生成

一、可预测随机数

前言:

            细节丰富的背景已成为成功游戏的重要因素,游戏中的各种动作正式在此前景之上发送的。不仅如此,背景还在玩家与游戏的交互中扮演着积极的角色。为了得到这种效果,传统的方法是将手工制作的关卡数据提取出来,然后保存在一个相对复杂、占用空间较大的关卡文件里,以备实时重放。但即便拥有强劲的资源配置,对于规模宏大而复杂的游戏而言,开发者也常常缺乏足够的空间进行调配。如果关卡数据不足,可能会使游戏的深度低于玩家的期望值。本文研究的是一种能用于提供给玩家应得的游戏深度同时无需过多空间的技术。        

        可预测随机数

            这一技术最重要的原则是:为了在一个游戏世界中给出无限空间的幻觉,我们需要满足两个分解条件:宏无限和微无限。宏无限涉及到问题的空间规模,或者说离散的实体数目。微无限则表明了每个对象所支持的细节级别。
            首先,每个游戏世界的对象都可用它的一个特征集来表示(比如方位坐标)。以此特征集,通过可预测随机数序列来生成随机数并作为该对象的特定属性。每当我们需要一个数字序列时,需要一个二级操作——给定发生器一个种子数来繁殖序列。当我们给出同一个种子数时总会生成同一个序列,这样序列就可以重复。因为它可以快速生成,我们就可以不必存储它(随机序列),这样就能得到一个近乎无限的游戏世界,同时极大减少静态存储的需求。
            以下为伪代码:一个给定有限规模的星系,假设栅格为100*100,我们就有10000个可能的空间存放恒星。随机数根据恒星位置(假设为二维空间,由x、y决定)产生,当随机数>70,则说明该位置存在恒星。srand()为发生器,当给定种子相同时能得到相同的随机数序列。

srand(1)
for galaxy_x = 1 to 100
    for galaxy_y 1to 100
        probability = rand() % 100
        if probability> 70 then
            universe(galaxy_x,galaxy_y) = star
        else
            universe(galaxy_x,galaxy_y) = no star


            上述方法有一个缺点,我们仍然需要存储每个恒星的位置。我们想得到一个接近无限的总体,最好是按需进行计算。代码如下:

int Start(int nGalaxy,int nX,int nY)
{
    int x,y.nReturn;
    srand(nGalaxy);
        for(y=0;y<=nY;y++)
        {
            for(x=0;x<nX;x++)
            {
                nReturn = rand() % MAXIMUM_VALUE;
            }
        }
        return nReturn;
}

        替换算法

           首先我们得到一个可重复的数字序列,它由一个给定的模式开始(一个乘法和一个加法),然后打破这一模式,由一个除法得到余数。然后再使用得到的结果生成一个具有同一基础方程式的新序列。这样,一个相当随机的序列就建成了。伪代码如下:

            限制:这种方法系统最大能生成数字是32位的无符号数,即4294967295,除此之外迭代4294967295次后序列会重复生成。如果需要更大的数字,必须改用不同的表示法。

       无限宇宙算法

            上文介绍了针对特定的游戏中的区域来确定某个给定的特征,这解决了一半的问题——宏无限分解。接下来将介绍使用伪随机数处理游戏对象的特征和细节——微无限分解。
            本质上,微无限分解对应于用户在某一点进行放大以查看那个地方有什么东西。以现在的例子为基础:每个恒星被0个或多个行星环绕运行,这些行星的轨道距这个恒星有一个确定的距离。为了确定这些项,我们可以为恒星类添加一个属性——行星数。下列代码片断展示了计算恒星类的一个属性——行星数目。

class star{
private:
    int x_pos,y_pos;
    int number_planets;
public:
    void SetNumberOfPlanets();

};
void star::SetNumberOfPlanets(){
    pseudorandom->seed(this->x_pos + (this->x_pos * this->y_pos));
    this->number_planets = pseudorandom->generate()%20;
}


            种子数(seed)源于恒星的唯一值——基于恒星对象的坐标属性x、y。这意味对于每个恒星,我们可以为具体对象播种生成值,同时也减少了循环中有2个同样的恒星对象的可能(还是有极低的可能性存在)。我们对生成的数字取模运算可以限制行星数,为了引入更多的真实性,这些代码也可以基于恒星的其他属性(如大小、强度)进行修改。
            接下来就是给定行星产生距离值,以下是结合恒星与此行星的位置计算得到的。

class planet{
private :
    int distance_from_star;
public:
    void SetDistanceFromStar(int planet_number,int star_x_pos,int star_y_pos);
}
void planet::SetDistanceFromStar(int planet_number,int star_x_pos,int star_y_pos){
    pseudorandom->seed(planet_number+(star_x_pos+(star_x_pos+star_y_pos)));
    this->distance_from_star = pseudorandom->generate() % 20;
}


            就确定位置而言,此时我们已经从宇宙放大到恒星,再到行星了。接下来我们可以使用更多的可预测随机数序列增加特性。
            下一阶段的关键是对于任何一个宇宙给定对象,我们要分离一组能描述该对象的属性。反过来看,每个属性本身可能有时一个对象(如行星对恒星系对宇宙),具有它自己类似能被设置的属性,借由一个唯一的基准来播种随机数。接下来我们假定每个行星对象有一个地图属性,地图是有简单的栅格表示,伪代码如下:

//Define the map size (side x side)
map->grid_side = pseudorandom->generate() % 100
//Place an object on the map for a given position x,y
pseudorandom->seed((map->grid_side * y) + x)
map->grid_square(x,y) = pseudorandom->generate() % 2


            (例如:模取2可能意为0是水,1是陆地)
            上述伪代码已经省略了播种,因为种子是在一个对象基于对象的基础上实例化对象生成的。
srand(x_position + (x_dimension * y_position ))
            这里生成器是基于目标容器的维数,根据惟一的基准点(x,y)播种,然而重复运行srand和rand表明这种方法产生的结果相当规律,下述代码能仅使用ANSI函数或与伪随机类自身一起使用,来为生成器使用正确播种。下一次调用rand会产生所需的数字,它可以被看作是后继序列的第一个。

srand(y_position)
x = x_position
while x>0
    rand()
    x = x - 1


        结论与展望:

           本文介绍了如何使用宏无限和微无限分解技术、通过伪随机数序列进行繁殖,以及在一个惟一对象的基准上进行播种。这些技术使我们能在资源有限的环境约束下创造近乎无限的游戏世界,并且在运行时生成。
            成功运用可预测随机数的关键就在于如何能正确地在对象属性与游戏状态上同时使用好种子数。

       个人观点:

            可预测随机数对于生成无限世界是非常有帮助的,开发者通过在游戏初始化时牺牲部分内存即可省下存储大量为满足游戏深度的关卡数据。当然,为无限世界的对象可预测随机创建诸如属性、方法等是比较繁琐且不易于后续维护,因此我们不必过于微无限分解,使用该技术做好宏无限以及必要的微无限分解即可。例如:一个关卡,地形、任务、角色等数据可以进行可预测随机生成,而任务的内容、报酬,角色的属性、外观等可以预先在数据库中或者预设体中设计好,在需要时,通过宏无限生成的索引进行访问即可。如果过于拘泥于微无限分解,反而是“过犹不及”,后续维护可能牵一发而动全身。
            学习技术主要是学习其核心的思考、逻辑过程,而不能按部就班,因为学习的技术就局限了自己的思维,望读者能勤思考、活学活用。            


二、生成真正的随机数

前言:

           计算机擅长生成伪随机数,不擅长生成真正的随机数。伪随机数看起来是随机的,但都是基于一个种子生成,如果种子相同,那么生成的随机序列就是可预测的。真正的随机数不但看起来是随机的,且是不可预测、不重复、不确定的。

        伪随机:

            伪随机数序列总会重复,只要种子相同,那么生成的随机序列就可准确再现。在许多编程语言中都有产生随机数的API,如Rand、Random等,它们大部分都是基于时间作为种子。所以经常会遇到在一条时钟周期轴上,相同的时间点生成的随机数序列都是一致的。随机程度是由种子决定,因此随机性的量级将小到难以接受的程度。

      真正随机:

            真正的随机数:随机的、分布不均匀、不可预测、不重复。
            生成随机数的理想方法是使用随机的物理源,如放射线衰变或热噪音。而大部分情况下,游戏是无法访问这样的设备。
            一种推荐技术:从大量不相关的源中获得随机输入,并使用一个强大的混合函数将其混合起来。通过从很多不相关的源那里获得输入(每个源有一定量级的随机性),并充分混合这些输入,即可获得一个混乱程度非常高的值——真正的随机数。

        随机输入源:

           比如日期、时间、用户名、栈中的内容、按键按下的时间间隔、硬件源(显卡、cpu、磁盘驱动器的寻道时间等)等。
            有些源是保持不变的,比如用户ID、硬件ID,之所以包含这些值,是由于它们随机器而异,这些源对于生成用于传输网络数据的密钥很有帮助。将诸如鼠标位置、键击等存储在循环队列,这样就可以将整个队列作为输入源。
            每种输入都提供一定程度的随机性,将其混合后,就可以获得非常高的随机性。从输入源获得的混乱程度越高,输出的随机性就越强。

      混合函数:

         定义:以非线性的方式将各项输入组合起来,从而生成输出。
            在输入的一个比特发生变化是,优秀的混合函数生成的输出中大约50%的比特也将变化。
            强大的混合函数:
                1.DES(以及其他对称加密程序)
                2.Diffie-Hellman(以及其他公共密钥加密程序)
                3.MD5、SHA-1(以及其他哈希加密程序)
            加密哈希函数(如Whirlpool或RIPEMD-160)是理想的混合函数:满足了优秀混合函数的基本要求、速度通常比对称、或非对称加密算法快、没有出口限制。公共密钥实现也被大量使用。
        局限性
            速度慢:为确保真正随机,必须对源进行取样,还得使用复杂的算法对输入进行混合;
            输入源少:不同的平台可选择的输入源不同,如游戏控制台的可选输入源就比较少,因此生成的结果随机程度比较低。
            随机程度完全取决于输入样本的混乱程度:输入样本越多,每个样本混乱程度越高,输出将越好。但这种算法被调用频率越高,输出的随机性越低:输入中的比特变化更加。这种技术不能替代伪随机数生成器,适用于可持续使用几小时或几天的随机数生成器种子值或者网络回话密钥。


三、随机数生成

前言:

            本节主要是介绍随机数生成器(Random Number Generators,RNGs),帮助读者选择合适的RNG。一个专业的开发者应该了解RNG的各种类型,并且知道什么时候用哪一种,就像要了解多种排序算法和多种数结构。        
      随机数应用
            1.AI算法,比如遗传算法和自动化的对手;
            2.随机游戏内容和关卡生成;
            3.模拟复杂现象,比如天气和火焰;
            4.数学方法,比如蒙特卡洛积分;
            5.素性证明之前一直在使用随机算法,直到最近才有所改变;
            6.加解密算法,比如RSA使用随机数来生成关键码;
            7.气象模拟和其他统计物理测试;
            8.最优化算法大量使用了随机——模拟退火、大空间搜索以及组合搜索。
        常见分布
            大多数随机数生成器返回一个从[0,m]之间均匀选择的整数。
            在游戏中最常见的分布是均匀分布。均匀分布需要从[a,b]间同等随机地挑出一个整数。一个常见的错误是使用如下代码:(rand()%(b-a+1))+a。这种错误会导致不是所有值都同概率出现——只有当(b-a+1)可以整除RAND_MAX+1时才安全,如果RAND_MAX是32767,试生成一个在[0,32766]之间的随机数,则会使0出现的概率是其他值的2倍(0和32767)。一种可行(但是更慢)的解决方案是将随机输出先缩至[0,1]区间,在放缩到[a,b]区间。
            其次比较常用的分布是高斯分布。高斯分布可由一个均匀分布生产。先由randf()返回[0,1]间的均匀分布的实数,再调用Box-Muller变换,它的极坐标形式将每次产生两个高斯值y1,y2。
        软件漂白
            许多随机源的bit位都有一些偏向性或者相关性。用于去除这些偏向性和相关性的方法被称为漂白算法。但是漂白过的数据流不经过进一步的处理还是不能视为安全的随机源。常见的选中如下几种:
            1.John von Neumann算法。每次取两位,当00和01时丢弃,01时输出1,10时输出0。去除了均匀偏向性,代价时需要更多位;
            2.每隔一位翻转,去除均匀偏向性;
            3.与另一个已知的好的随机位来源做异或操作,就像Blum Blum Shub算法一样;
            4.应用加密哈希算法(Whirlpool或RIPEMD-160),注意MD5已经不再安全。
        PS:接下来会简单介绍各类随机数生成算法,由于篇幅过长,为了避免照本宣科,有兴趣的话请读者翻阅原书《游戏编程精粹7》或者查询其他资料,原书有包含代码实现。

       不加密随机数生成算法

           不加密算法一般会比加密算法快,以下算法都是伪随机数生成器。
                平方取中法:取一个10位数作为种子,取平方,返回中间10位作为下一个数和种子。是一个有统计缺陷的差算法,已不再使用;
                线性同余生成器(LCG):优点在于是相对较快的算法,并且使用一个较小的状态。但是有多种缺陷,且模数捕食2的乘方,取模运算非常费时,已逐渐被一些新算法取代;
                截断线性同余生成器(TLCG):避免了LCG中低位的缺陷,但这个算法是不安全的;
                线性反馈移位寄存器法(LFSR):通过将一个内部状态一次一位地移出来产生随机位。新的数位将通过当前状态的线性函数获得并移入这个状态。优点:速度非常快,在硬件上容易实现,并能生成大范围的数列。不过这个方法还是不安全的;
                逆同余生成器:这个算法类似LCG但不是线性的。并且求逆操作非常耗时,并不常用;
                LFG:这个算法难以良好运行与初始化,周期取决于初始种子,而值空间分割成很难预测的循环。因为马特赛特旋转算法和新的生成器的出现,已经被抛弃了;
                细胞自动机
                线性递归生成器:LFSR的泛化,现在大多数在二进制有限域的快速伪随机数生成器都是由它衍生的;

            特殊的PRNG(最佳的通用RNG):

               马特赛特旋转法:包括MT11213和MT19937这2个版本。其中MT19937的周期应该能运算到远比整个宇宙的生命还要更长的事件。它使用624个长整型或者19968位作为内部状态,这大约符合超大周期的预期,并且比LCG更快,在高达623个维度是等分布的。它已经成为统计模拟中主要使用的RNG。它每次生成随机数时,只更新内部状态的一小部分,通过多次调用来遍历这个状态。马特赛特旋转算法是一个旋转广义反馈移位寄存器,它不是密码安全的,且有一些缺陷;
                组合LFSR Tausworthe生成器:LFSR113、LFSR258,它们是特别为32位和64位计算机设计的。它们非常快速、简单,只使用极小的内存——占用内存比马特赛特旋转法的MT19937要小。这个生成器也是良好等分布的,避免LCG的问题;
                WELL算法(良好等分布长周期线性):这类算法产生的随机数比MT19937拥有更好的等分布性,并且在“位混合”属性上做了改进。它们非常快速,有多种周期。最重要的是能产生更高质量的随机数。相对于MT19937,WELL的一个显著优点是能快速跳过大量0位的内部状态。唯一的缺点是比MT19937慢一点,不会慢太多,而优点是随机数质量更高,代码更简单。

加密RNG方法

                1.Blum Blum Shub;
                2.ISAAC、ISAAC+;
                3./dev/random;
                4.微软的CryptGenRandom;
                5.Yarrow;
                6.Fortuna。


学习资源

    • 《游戏编程精粹1》第二章第一节
    • 《游戏编程精粹2》第二章第十九节
    • 《游戏编程精粹7》第二章第一节

更详尽的内容和代码实现可在书中阅读。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值