随机化

👍

译者注

本章由 Evilcome 进行翻译,相关博客 GameplayKit - SWIFT.HOW
如您对游戏开发,设计感兴趣,请加入QQ群:453564812,或扫描二维码┏ (゜ω゜)=☞

游戏充满了各种各样的未知和可能性:棋牌类游戏利用掷骰子控制玩家前进的步数;卡牌类游戏每次都会对卡库进行洗牌;街机游戏里的怪物会不时地随机出现;角色扮演的游戏里每一个动作都会导致成功或者失败;开放世界(open-world)游戏每一个背景人物都可以自由活动,等等等等。游戏中出人意料的惊喜可以带给玩家更多乐趣,每次通过后的不同行为也能增加游戏的可玩性,模仿自然界纷繁效果的元素也能使得游戏世界更加逼真。

编写这类依赖元素不断改变的游戏往往需要利用随机数字生成器,或者随机资源。然而,并非所有的随机资源都是一样的,一个随机资源应用不当同样会带来不好的效果。为了打造游戏强大的随机行为,你通常需要以下的部分或全部特征:

  • 随机值(Randomness) 一个随机数生成器应该产生无法预测的行为(或者仅仅是显示出)。然而,如何量化这个随机性呢?电脑产生的伪随机数事实上是基于那些看起来没有顺序和结构的有限序列。最终,序列会出现重复,到底有多少随机数在随机源产生之前发生重复取决于这个随机源采用的算法。另外,当产生的数字以二进制形式查看,这个数字中的一些数位是可以根据随机源的算法或多或少的预测的。
  • 性能 复杂的算法可以在消耗较多运算时间的基础上产生更好的随机效果。如果你的游戏的每一帧使用了许多随机数,并且需要保证游戏画面每秒达到60帧,一个复杂的随机源就显得太慢了。选择一个合适的随机源需要在随机性和游戏性能间做出妥协。
  • 确定性 软件质量要求软件测试,但是真正的随机性使得难以重建一组特定的条件下的测试。随机源应该对玩家保持不可预测性,但是允许在需要的时候产生出固定结果。确定性的随机源也是网络游戏所必备的——你需要确保每一位玩家的随机机制是相同的。
  • 独立性 因为随机源是基于数字序列的,下一个数字的产生在一定程度上取决于它以前的数字。然而,比赛中往往含有多个随机元素,他们的随机行为也应该不同。比方说,设想一个游戏机随机化了游戏元素和特效元素,如一个竞技游戏,每个玩家每回合的行动都是随机选择的,并且玩家玩家角色将会随机的播放画外音对话来为游戏添油加醋。如果这个对话系统和玩家的可用操作都使用相同的随机数发生器,一个精明的玩家就能在对手对话的基础上预测对手的下一个行动。
  • 已知分布方式 在许多使用随机化的游戏中,有些时候随机数需要符合均匀分布,也就是说,在一个范围内,随机源生成的每个具体随机值得概率相同。另外一些时候,随机数需要符合更具体的分布方式——比如说,许多自然过程需要一个正态分布,其中一个随机抽样经常产生平均值附近的结果,其他与平均值更远的结果将不太可能产生。

GameplayKit 提供了一套随机类来满足这些目标。

在你的游戏中使用随机化

在 GameplayKit 中所有的随机类或者随机发生器(randomizers)都符合GKRandom协议,这个协议描述了生成随机数字的最基本接口。你首先需要选择适合你的任务的随机发生器:

  • 大多数情况下,你需要在一个特定区间内均匀生成随机数,这时你应该使用GKRandomDistribution类。

  • 要自定义随机行为,但仍然保持均匀分布,你可以选择不同的GKRandomSource子类的GKRandomDistribution对象提供基本的随机值。

  • 要自定义随机数的分布,使用GKGaussianDistributionGKShuffledDistribution类。

  • 如果不需要随机数在一个特定区间或者特定分布中,你可以直接使用GKRandomSource的子类。

    你也可以直接使用GKRandomSource类来随机排列一个数组中的元素。比方说,要实现洗牌,首先选择你所需要的随机源对象,之后将数组传递给随机源的arrayByShufflingObjectsInArray:方法,这个方法将返回原始数组的一个副本,但打乱了数组元素的顺序。

下面的章节详细介绍了随机源和随机分布类的差异。

随机源:随机化的基石

GKRandomSource类和它的子类提供了生成随机数字的算法。在构建随机行为之前,选择一个具体的GKRandomSource子类:

  • GKARC4RandomSource类使用 ARC4 算法,这适用于大多数游戏。(这个算法很像 C 语言中的 arc4random 接口,但是GKARC4RandomSource类并不依赖那些方法。)
  • GKLinearCongruentialRandomSource类使用的算法更快,但相较于GKARC4RandomSource类不那么随机。(具体说就是生成出的低位数比高位数要更多。)当性能比强大的不可预测性更重要时,你需要使用这个类。
  • GKMersenneTwisterRandomSource类使用的算法更慢,但是相较于GKARC4RandomSource类更加随机。当你需要生成不常重复的随机数并不太关注性能是,可以使用这个类。

当你创建了任何一个以上类型的实例时,返回的结果是一个独立的随机源——也就是说,一个实例生成出来的一组随机数和另一个生成器生成出来的一组随机数之间毫无关系。

以上所有的类型都实现了确定性的随机数生成器。每一个类使用一个种子值来确保生成过程。当你使用init方法初始化一个随机源后,GameplayKit 使用了一个非确定性种子值。如果你想重新创建一个具体随机源实例的行为,读取它的seed属性值,然后用initWithSeed:方法初始化一个新源来重新生成之前随机源的一系列随机数。

对一个随机源进行归档是另一个保持其状态的方法。比方说,如果你用NSKeyedArchiver对象对一个GKRandomSource(或其子类)实例进行编码,任何两个从同一个档案解码出的随机源将产生相同的随机数序列。

随机分布产生专门的随机行为

一个随机源提供了一个基础的随机数生成器的算法。构建一个随机机制游戏通常需要进一步专业化,如出现在大量随机采样中一定范围内的随机数字或者特定模式(或分布)。请使用GKRandomDistribution类或者它的子类来实现:

  • GKRandomDistribution类自身将产生一个特定范围内的均匀分布——也就是说,任何一个在既定的最大值和最小值之间的值将有同等概率出现。任何时候当你需要生成一个具体区间的随机值时,你可以使用这个类。掷骰子就是一个特定范围内的产生均匀分布的常见例子,如图 2-1 所示。

    图 2-1 随机分布模拟单个骰子

    比方说,一个正常的六面骰掷出,每个面朝上的几率是一样的。你可以方便的使用d6d20这两个初始化方法模拟常用的这些骰子,或者使用distributionForDieWithSideCount:方法来初始化自定义面数的骰子。

  • GKGaussianDistribution类将提供了一个高斯分布(又称正态分布)模型,如图 2-2 所示。在一个正态分布中,处于中间值(均值)的结果出现的概率最高,而更大值或更小值的结果出现的概率更低。这种分布是对称的:无论是高于或低于平均值,平均值距离相同的结果的发生概率都是一样的。

    图 2-2 高斯分布相当于多个骰子投掷的结果

    高斯分布出现在许多你可能模拟在游戏中的真实世界的现象。例如:

    • 多个骰子投掷。 如果一个角色扮演游戏利用投掷三个六面骰子(通常简称3D6)来计算一击的伤害,你可以在游戏中击中最常造成 9 到 13 点伤害的前提下安全的平衡其他变量。
    • 玩家投掷飞镖。 每个飞镖击中 X 或者 Y 坐标是均匀分布的,这导致了飞镖将在飞镖盘的中心区域聚集。
    • 随机生成个性角色。 在身高,体重,以及其他非玩家角色的物理特性上使用正态分布,将使你的游戏人口更加真实,其中大多数人是平均身高,很高或者很矮的人则并不多见。
  • GKShuffledDistribution本身是均匀分布的,但随着时间推移也阻止了重复值在序列中发生,如图 2-3 所示。

    图 2-3 洗牌类随机分布避免了重复出现相同值