添加随机游戏元素

随机选择的项或值在许多游戏中都很重要。本部分将介绍如何使用 Unity 的内置随机函数来实现一些常见的游戏机制。

从数组中选择一个随机项

随机选取一个数组元素归结为选择零和数组最大索引值(等于数组的长度减去 1)之间的一个随机整数。使用内置的 Random.Range 函数可以轻松实现:

1
var element = myArray[Random.Range(0, myArray.Length)];

请注意,Random.Range 从包含第一个参数但不包含第二个参数的范围内返回一个值,因此在此处使用 myArray.Length 会得到正确的结果。

选择具有不同概率的项

有时需要随机选择项,但有些项比其他项被选中的几率更高。例如,NPC 在遇到玩家时可能会以几种不同的方式做出反应:

  • 友好问候的几率为 50%
  • 逃跑的几率为 25%
  • 立即攻击的几率为 20%
  • 提供金钱作为礼物的几率为 5%

可将这些不同的结果可视化为一张纸条,该纸条分成几个部分,每个部分占据纸条总长度的一个比例。占据的比例等于选择结果的概率。选择行为相当于沿着纸条的长度选择一个随机点(例如通过投掷飞镖),然后查看该点处于哪个部分。

img

在脚本中,纸条实际上是一个浮点数组,其中的浮点数按顺序包含项的不同概率。随机点是通过将 Random.value 乘以数组中所有浮点数的总和得到的(这些数值不需要加起来等于 1;重点是不同值的相对大小)。要找到该点“位于”哪个数组元素,首先要检查它是否小于第一个元素中的值。如果是,则第一个元素便是选中的元素。否则,从该点值中减去第一个元素的值,然后将其与第二个元素进行比较,依此类推,直到找到正确的元素。在代码中表示为以下所示的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
float Choose (float[] probs) {

float total = 0;

foreach (float elem in probs) {
total += elem;
}

float randomPoint = Random.value * total;

for (int i= 0; i < probs.Length; i++) {
if (randomPoint < probs[i]) {
return i;
}
else {
randomPoint -= probs[i];
}
}
return probs.Length - 1;
}

请注意,最后的 return 语句是必要的,因为 Random.value 可以返回 1 的结果。在这种情况下,搜索将无法在任何地方找到随机点。将以下行

1
if (randomPoint < probs[i])

…更改为“小于或等于”测试将避免额外的 return 语句,但也会允许偶尔选择某个项,即使其概率为零也是如此。

加权连续随机值

如果结果是不连续的,那么浮点数组方法会很有效,但在某些情况下希望产生更连续的结果;比如说,希望随机化一个宝箱中发现的金块数量,并希望能够出现 1 到 100 之间的任何数字,但让更小数字的概率更高。使用浮点数组方法来执行此算法将需要设置一个包含 100 个浮点数(即纸条上的部分)的数组,这是很不实用的方法;如果不局限于整数而是想要在该范围内的任何数字,则不可能使用这种方法。

一种适用于连续结果的更好方法是使用 AnimationCurve 将“原始”随机值转换为“加权”值;通过绘制不同的曲线形状,可产生不同的权重。代码编写起来也更简单:

1
2
3
float CurveWeightedRandom(AnimationCurve curve) {
return curve.Evaluate(Random.value);
}

此算法从 Random.value 读取值来选择 0 到 1 之间的“原始”随机值。然后,该值传递给 curve.Evaluate(),在此处将其视为水平坐标,并返回曲线在该水平位置处的相应垂直坐标。曲线较平缓的部分被选取的几率较高,而较陡峭的部分被选取的几率较低。

线性曲线根本不对值进行加权;曲线上每个点的水平坐标等于垂直坐标。线性曲线根本不对值进行加权;曲线上每个点的水平坐标等于垂直坐标。

这条曲线在开始时较平缓,然后在结束时变得越来越陡峭,因此较低值的几率较高,而较高值的几率较低。此处可以看到 x=0.5 时曲线的高度约为 0.25,这意味着有 50% 的几率得到 0 到 0.25 之间的值。这条曲线在开始时较平缓,然后在结束时变得越来越陡峭,因此较低值的几率较高,而较高值的几率较低。此处可以看到 x=0.5 时曲线的高度约为 0.25,这意味着有 50% 的几率得到 0 到 0.25 之间的值。

这条曲线在开始和结束时都很平缓,因此这些值接近极值的几率较高,而中间的陡峭部分表示得到这些值的几率较低。另外请注意,使用此曲线时,高度值已向上移动:曲线底部为 1,曲线顶部为 10,这意味着曲线产生的值将在 1-10 范围内,而不是像以前的曲线那样在 0-1 范围内。这条曲线在开始和结束时都很平缓,因此这些值接近极值的几率较高,而中间的陡峭部分表示得到这些值的几率较低。另外请注意,使用此曲线时,高度值已向上移动:曲线底部为 1,曲线顶部为 10,这意味着曲线产生的值将在 1–10 范围内,而不是像以前的曲线那样在 0–1 范围内。

请注意,这些曲线并非概率论指南中可能介绍的概率分布曲线,而更像是反向累积概率曲线。

通过在一个脚本上定义 AnimationCurve 公共变量,可使用 Inspector 窗口直观查看和编辑曲线,而无需计算值。

这种方法会产生浮点数。如果要计算整数结果(例如,需要 82 个金块,而不是 82.1214 个金块),可将计算值传递给 Mathf.RoundToInt() 之类的函数。

列表洗牌

一种常见的游戏机制是从一组已知的项中进行选择,但让这些项以随机顺序到达。例如,一副纸牌通常需要洗牌,因此不会以可预测的顺序绘制。为了对数组中的项进行随机洗牌,可访问每个元素,然后将其与数组中位于随机索引处的另一个元素进行交换:

1
2
3
4
5
6
7
8
void Shuffle (int[] deck) {
for (int i = 0; i < deck.Length; i++) {
int temp = deck[i];
int randomIndex = Random.Range(0, deck.Length);
deck[i] = deck[randomIndex];
deck[randomIndex] = temp;
}
}

从一组无重复的项中选择

一种常见的任务是从一组中随机选取一些项,但不可多次选取同一项。例如,可能希望在一些随机生成点生成多个 NPC,但要确保每个点只生成一个 NPC。为实现此目的,可按顺序遍历这些项,随机决定是否将每一项添加到所选集合中。当访问每一项时,该项被选取的概率等于仍然需要的项数除以仍然可供选择的项数。

例如,假设有 10 个可用的生成点,但只能选择其中 5 个。选择第一项的概率为 5/10,即 0.5。如果选择了该项,那么第二项的概率将是 4/9,即 0.44(即仍然需要 4 项,还剩下 9 项可供选择)。但是,如果未选择第一项,那么第二项的概率将是 5/9,即 0.56(即仍然需要 5 项,还剩下 9 项可供选择)。这一直持续到该集合包含所需的 5 项为止。可使用如下所示的代码实现此算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Transform[] spawnPoints;

Transform[] ChooseSet (int numRequired) {
Transform[] result = new Transform[numRequired];

int numToChoose = numRequired;

for (int numLeft = spawnPoints.Length; numLeft > 0; numLeft--) {

float prob = (float)numToChoose/(float)numLeft;

if (Random.value <= prob) {
numToChoose--;
result[numToChoose] = spawnPoints[numLeft - 1];

if (numToChoose == 0) {
break;
}
}
}
return result;
}

请注意,虽然选择是随机的,但所选集合中的项与原始数组中的项具有相同的顺序。如果要按顺序一次使用一项,那么这种排序可能使它们在一定程度上可预测,因此在使用之前可能需要对数组进行洗牌。

空间中的随机点

通过将 Vector3 的每个分量设置为 Random.value 返回的值可以选择立方体中的随机点:

1
var randVec = Vector3(Random.value, Random.value, Random.value);

这种算法可在边长为一个单位的立方体内部给出一个点。只需将矢量的 X、Y 和 Z 分量乘以期望的边长即可缩放该立方体。如果其中一个轴设置为零,则该点将始终位于单个平面内。例如,在“地面”上选取随机点通常需要随机设置 X 和 Z 分量并将 Y 分量设置为零。

当体积为球体时(即,希望从原点开始的给定半径内选取随机点时),可使用 Random.insideUnitSphere 乘以所需的半径:

1
var randWithinRadius = Random.insideUnitSphere * radius;

请注意,如果将结果矢量的某个分量设置为零,则不能在圆内获得正确的随机点。尽管该点确实是随机点并且位于正确的半径内,但是概率严重偏向于圆的边缘,因此点分布将非常不均匀。对于此任务,应改用 Random.insideUnitCircle:

1
var randWithinCircle = Random.insideUnitCircle * radius;