以三款经典游戏为例 分析“伪随机”设计原理

你希望自己的游戏含有多少随机要素?这些随机要素是什么性质的?它在哪些层面可能出错?我建议所有开发者在制作中仔细考虑这个问题。

有些人认为游戏应该尽量不出现随机要素,或者只起极小的作用,把所有决定性因素都交给玩家操作。如果一个攻击会生效,那他产生的反馈同样应该是确定性的,而非随机的反映。比如在射击游戏中,如果你用枪指着一个人物并扣下扳机,那么目标应该被击倒,子弹不应该随机漂移或受瞄准系统干扰。

还有一些开发者担心玩家对随机系统有什么误解。比如攻击命中率是90%,但他们觉得这其实是虚假的数字。在实际的攻击和“掷骰子”过程中,他们期望有98%的攻击可以命中。

抽卡手游所谓的“随机”多数时候是招人恨的

很少有计算机程序能实现真正的“随机性”。在我们广阔的“牛顿世界”中,“完全随机”的现象其实是相当罕见的。我们以为掷骰子时哪面朝上是随机的,实际上结果在固体离开我们手掌的时候就已注定了。用“随机”解释这些现象,其实是因为我们无知——我们不知道骰子在空中运动的矢量模型,也不知道它撞击地面的反应,会怎样翻滚和停止——这对有人垄断结果收益其实是有利的。

你可以在量子物理层面实现真正的“随机”,但在日常生活中大多数时候无此技术条件和必要,我们都使用其他替代手段来实现相似的效果。在电子游戏等娱乐软件中,一般使用的手段是“伪随机数生成器”。游戏中的随机性,在各个社区已经引起了开发商和玩家注意。本文我以三款经典游戏为案例,看看它们的伪随机机制是的本质是什么、如何运作,以及在某些情况下,是如何被利用的。

《最终幻想》:从预先生成的数字列表中依次读取

初代《最终幻想》的主程序是传奇8位机程序员Nasir Gebelli。他在Apple II电脑平台为自己赢得崇高的声望。Famicom(下简称“FC”)主机与Apple II使用的都是6502芯片,所以Gebelli转移到FC上可谓如鱼得水,将他的精湛技艺和宝贵经验发挥得淋漓尽致。

《最终幻想》第一个伪随机运用场景是遇敌。游戏的ROM中存储着一份含有256个数字的列表。每次游戏系统重启时,该列表的数值读取进度都会被初始化。玩家每走一步,系统就读取一个数值,将其与后台设置的遇敌阈值做比较,如果小于阈值,触发遇敌;大于阈值则无事发生。换言之,这是一个数列重复排列组合使用的过程。

当然,这不是初代《最终幻想》使用的唯一一套随机系统。游戏还有另一套遇敌机制,使用与上面原理相同的数字列表系统——不同的是,该列表与上面使用的列表不是同一份,而是保存在卡带靠电池驱动的RAM中,与玩家的存档放在一起,也不会随着重启刷新。这就意味着,如果玩家在特定区域存盘,然后在某个位置遇到了敌人,你读档后永远能在相同的位置遇到同一组敌人。有趣的是,这份列表虽然和存档一同放在RAM上,但通过导出我们发现,两份文件是互相独立的,存档文件本身不包含任何乱数列表。

《最终幻想》

那么,老遇见相同的怪,岂不是很无聊?

别急,在战斗中还有随机性发挥的空间。引擎程序员做了一个相当聪明的设计,防止乱数序列被玩家破解、利用,从而创造优势。

在玩家进入战斗后,游戏每刷新2帧就会读取一次乱数,而战斗中却不会进行遇敌判定。如此高频率的乱数消耗,使战斗结束后,玩家无法在底层运算不可见的情况下判断乱数究竟读到了哪里。所以,虽然读档后第一次遇敌结果都一样,但除非你能精确到帧地复制每次游玩的操作,后续游戏体验很快就会朝未知的方向发散。

对游戏体验来说,这种“发散”很重要。我们可以说,《最终幻想》的乱数运行机制无关乱数本身的价值,只是不断使数字滚动——换言之,将系统计算结果与玩家难以精准还原的操作关联起来了。

电脑程序在无其他因素干扰的情况下,只会按照一种既定方式运行,导出每次都相同的结果。因此具体到RPG设计中,我们必需小心玩家利用伪随机机制创造有利状态,比如避免遇敌,或者为战斗挑选最合适的敌人。解决这个问题的办法就是在系统之外寻找更多变量,改变程序的运行状态。在FC和SFC上,很多游戏采用在部分帧或所有帧上不断读取乱数的做法。因为玩家的操作很难在相同1帧内重现,随着时间的推移,程序导出的结果自然也会越来越发散,因为玩家给程序提供了变量。

如果是有时钟的操作系统,根据时间读取或重置乱数也是可行的办法——但时钟的时间通常是玩家自己设置的,这可能也会导致乱数被利用。然而如果时钟的单位刻度足够精准,比如达到毫秒级,那么根据时钟设置乱数是非常好用的,因为在毫秒单位内玩家很难做出准确的操作。

回到《最终幻想》。严格来说,这是一种过于随意地“利用”玩家的做法——即便玩家本身也可以做一些操作来改变列表状态。但是如果某场高难度战斗,我们希望玩家尽量避免或者强制触发,要怎么办呢?

在游戏中有一个隐藏BOSS“死亡机甲”(WarMECH),出现在一个遇敌率非常高的场景里。每个场景能遇到的怪物都是有分组的,设计师将“死亡机甲”单独分进“第8组”,并且设定遇敌乱数只有撞上固定的三个数才会碰到“第8组”,因此理论上玩家遭遇“死亡机甲”的概率只有3/256。

与“死亡机甲”对战

《超级马力欧64》:线性同余法

现在基于线性同余法编写随机数生成器是软件开发采用的主流解决方案,游戏圈也一样——当然前提同样是不被破解和利用。《超级马力欧64》是使用线性同余法生成随机数的代表游戏之一,任天堂使用这种方法在地图中大量随机生成金币。

线性同余法就是使用线性同余方程(编者按:我不是数学帝,这个方程我也不懂,有兴趣的可以百度或请教程序员)无限计算,得出一个数列。根据该方程的特性,这个数列在不懂公式的人眼中看起来近似完全随机,但在N次计算后必定会开始重复之前的结果,形成循环。

《超级马力欧64》

在游戏中,玩家行为(比如走几步路)会生成一个“种子”数值带入方程算出一个解,这个解本身对应游戏中的某种结果反馈给玩家,同时本身会作为第二个“种子”数值再次带入方程运算,如此无限重复,形成一个庞大的数列。

《超级马力欧64》已经被破解,游戏使用的底层公式也被曝光了。根据这个公式,任意数值在带入方程运算65,114次后就会形成循环,开始重复之前的结果。如果是在科研领域,规律如此明显的伪随机数肯定是不能做样本使用的。然而对游戏来说,这个方程的运算结果“外行”看起来已经相当随机,而且在进入循环之前给出的数值样本足够大,基于数学公式进行运算对程序员来说既方便学习又不吃太多性能,是一种不错的解决方案。

《精灵宝可梦》:复杂的多重算法结合

《最终幻想》的“古典”伪随机数生成法可以决定玩家遇到哪些敌人、何时遇敌,但是耗费的工作量是巨大的;《超级马力欧64》的线性同余法可以用于大量在游戏中刷道具或者定义敌人行为,然而从游戏技术上说,也谈不上多大进步。在游戏业,玩“伪随机”的集大成者当属任天堂的另一招牌IP《精灵宝可梦》。

影响玩家遇到一只宝可梦的乱数可能产生在很早以前,并且在不知不觉中,已经影响你的游戏进程很长时间了。这些数据有些隐藏在玩家身上,有些是保密的,操纵他们很可能从根本上改变游戏体验——而且因为宝可梦是可交换的,受影响的可能不止是你一人。

当年刷神兽个体值可是个苦差事

因为随机性是游戏核心玩法的一部分,所以宝可梦系列使用的伪随机算法特别复杂,比如“马特赛特旋转演算法”(Mersenne Twister)等等。由于该系列在全球范围内旺盛的人气,有无数玩家乐此不疲地尝试破解它的算法,还由此诞生了专门的极客站Smogon University。

破解算法的一个目的是获取“闪光”宝可梦——它没有对战优势,但极其罕见。在“宝石”系列(第三世代)中,平时游戏每1帧就会读取一次随机数,而在战斗中读取频率还要翻倍,至于其他随机元素的复杂程度就更不必说了。Smogon University有一个单独页面是专门计算“绿宝石”的随机数的,因为“绿宝石”在设计上有个缺陷:游戏重启后,系统不会根据时钟设置伪随机数的种子数值,而是总将其重置为0,在“红宝石/蓝宝石”中则不存在这一问题。

知道哪些时段、哪个区域、哪几帧里能遇到“闪光”并不能保证你一定可以得到它。时间精确与否是个问题,而且你在游戏中的各种行为都可能触发另一次乱数判定,“吃掉”你期望撞上的那个伪随机数,但至少玩家还是因此有了个看得见摸得着的目标,能朝这个方向努力。而且恰恰由于绿宝石每次开机会重置种子数值的特性,“刷闪光”还是比普通玩家忍受的1/8192的概率高多了,操作上也比蓝宝石/红宝石更便捷。

决定《精灵宝可梦》乱数的因素非常多

需要注意的是,《精灵宝可梦》中用于生成伪随机数的重要数值“训练师ID”及“隐藏ID”是与存档绑定的,在游戏流程中不可更改。

破解随机数的另一个目的是为了刷个体值,抓“高V”宝可梦——这相关的算法在“绿宝石”时代几乎已经被完全破解了,计算结果非常可靠。然而进入“第六世代”以后,官方大幅降低了“高V”宝可梦的获取难度,如今已经没有费半天劲算伪随机数的必要了。

本文来自灰机GAME,本文观点不代表GameLook立场,转载请联系原作者。

关注微信