也许你对程序化关卡生成很熟悉;那么,在这篇文章中,我们将重点介绍程序化任务生成。我们将介绍使用经典机器学习和循环神经网络为 roguelike 游戏生成任务的总体情况。
大家好!我叫 Lev Kobelev,是 MY.GAMES 的游戏设计师。在本文中,我想分享我使用传统 ML 和简单神经网络的经验,同时解释我们如何以及为何选择程序化任务生成,我们还将深入探讨该过程在 Zombie State 中的实现。
免责声明:本文仅供参考/娱乐之用,在使用特定解决方案时,我们建议您仔细检查特定资源的使用条款并咨询法律人员!
☝🏻 首先,一些术语:“竞技场”,“级别”和“位置”在这种情况下是同义词,以及“区域”,“区域”和“生成区域”。
现在,让我们定义“任务”。任务是敌人按照某些规则出现在某个位置的预定顺序。如前所述,在 Zombie State 中,位置是生成的,因此我们不会创建“分阶段”体验。也就是说,我们不会将敌人放置在预定点 - 事实上,没有这样的点。在我们的例子中,敌人出现在玩家或特定墙壁附近的某个地方。此外,游戏中的所有竞技场都是矩形的,因此可以在其中任何一个竞技场上进行任何任务。
让我们介绍一下“生成”这个术语。生成是指在指定区域的点上根据预定参数出现多个相同类型的敌人。 一个点 - 一个敌人。 如果区域内没有足够的点,则根据特殊规则进行扩展。 了解只有在触发生成时才会确定区域也很重要。 区域由生成参数确定,我们将在下面考虑两个示例:一个在玩家附近生成,一个在墙壁附近生成。
第一种生成类型是在玩家附近。玩家附近的外观通过扇区指定,扇区由两个半径描述:外部和内部(R 和 r)、扇区的宽度(β)、相对于玩家的旋转角度(α)以及敌人外观的期望可见性(或不可见性)。扇区内是敌人所需的点数——这就是它们的来源!
第二种生成方式是靠近墙壁。生成关卡时,每一侧都会标上一个标签——一个基本方向。有出口的墙壁总是在北方。靠近墙壁的敌人的外观由标签、与墙壁的距离 (o)、区域的长度 (a)、宽度 (b) 以及敌人外观的期望可见性(或不可见性)指定。区域的中心是相对于玩家当前位置确定的。
僵尸以波的形式出现。波是指僵尸出现的方式,即僵尸之间的延迟——我们不想让所有敌人同时攻击玩家。根据一定的逻辑,波次会组合成任务,并一个接一个地发起。例如,第二波僵尸可以在第一波僵尸之后 20 秒发起(或者如果其中 90% 以上的僵尸被杀死)。因此,整个任务可以看作是一个大盒子,盒子里面有中等大小的盒子(波次),波次里面有更小的盒子(僵尸)。
因此,在开始正式执行任务之前,我们已经定义了一些规则:
有一段时间,我们准备了大约 100 个任务,但过了一段时间,我们需要更多任务。我和其他设计师不想再花很多时间和精力创建另外 100 个任务,所以我们开始寻找一种快速且便宜的任务生成方法。
所有生成器都按照一套规则运行,我们手动创建的任务也是按照某些建议完成的。因此,我们对任务中的模式提出了一个假设,这些模式将充当生成器的规则。
✍🏻 你会在文中发现一些术语:
聚类是将给定集合划分为不重叠的子集(簇)的任务,使得相似的对象属于同一个簇,而来自不同簇的对象则有显著差异。
分类特征是从有限集合中取值且没有数字表示的数据。例如,出生点墙标签:北、南等。
分类特征的编码是将分类特征按照预先指定的一些规则转换为数值表示的过程。例如,北→0,南→1,等等。
规范化是一种预处理数值特征的方法,目的是使它们达到某种共同的尺度,而不会丢失有关范围差异的信息。例如,它们可用于计算对象的相似性。如前所述,对象相似性在聚类问题中起着关键作用。
手动搜索所有这些模式将非常耗时,因此我们决定使用聚类。机器学习可以很好地处理这项任务,因此非常有用。
聚类在某个 N 维空间中起作用,而 ML 专门处理数字。因此,所有衍生品都将成为向量:
因此,例如,描述为“在凹口为 2 米、宽度为 10 米、长度为 5 米的区域的北墙上生成 10 个僵尸射手”的生成变成了向量 [0.5, 0.25, 0.2, 0.8,...,0.5](←这些数字是抽象的)。
此外,通过将特定敌人映射到抽象类型,敌人集的威力也降低了。首先,这种映射使得将新敌人分配到特定集群变得容易。这也使得减少最佳模式数量成为可能,从而提高生成准确性——但稍后会详细介绍。
聚类算法有很多种:K-Means、DBSCAN、谱聚类、分层聚类等等。它们都基于不同的思想,但目标相同:在数据中找到聚类。下面,您将看到根据所选算法对同一数据找到聚类的不同方法。
K-Means 算法在产卵的情况下表现最佳。
现在,对于那些不了解该算法的人来说,这里有一点题外话(本文不涉及严格的数学推理,因为本文是关于游戏开发的,而不是机器学习的基础知识)。K-Means 迭代地将数据划分为 K 个簇,方法是最小化每个特征与其指定簇的平均值之间的平方距离之和。平均值由簇内平方距离之和表示。
了解有关此方法的以下内容非常重要:
让我们更详细地看一下第二点。
肘形法通常用于选择最佳簇数。其思想非常简单:我们运行算法并尝试从 1 到 N 的所有 K 个簇,其中 N 是某个合理的数字。在我们的例子中,它是 10 — 不可能找到更多的簇。现在,让我们找到每个簇内平方距离的总和(称为 WSS 或 SS 的分数)。我们将所有这些显示在图表上,并选择一个点,在此点之后 y 轴上的值不再发生显著变化。
为了说明这一点,我们将使用一个众所周知的数据集,即
如果看不到肘部,那么可以使用 Silhouette 方法,但这超出了本文的范围。
上面和下面的所有计算都是用 Python 完成的,使用了 ML 和数据分析的标准库:pandas、numpy、seaborn 和 sklearn。我不会分享代码,因为本文的主要目的是说明功能,而不是深入讨论技术细节。
在获得最佳集群数量后,应该详细研究每个集群。我们需要查看其中包含哪些生成以及它们所采用的值。让我们为每个集群创建自己的设置以供进一步生成使用。参数包括:
让我们考虑一下集群设置,可以口头描述为“在玩家附近某处短距离内生成简单敌人,并且很可能是在可见点处。”
集群 1 表
敌人 | 类型 | r | R-delta | 回转 | 宽度 | 能见度 |
---|---|---|---|---|---|---|
zombie_common_3_5=4,zombie_heavy=1 | 玩家 | 10-12 | 1-2 | 0-30 | 30-45 | 可见=9,不可见=1 |
这里有两个有用的技巧:
每个集群都完成了这一操作,由于集群数量不到 10 个,因此花费的时间并不长。
我们只是稍微触及了这个主题,但仍然有很多有趣的东西值得研究。这里有一些文章可供参考;它们很好地描述了处理数据、聚类和分析结果的过程。
除了生成模式之外,我们还决定研究任务中敌人总体生命值与预计完成时间的依赖关系,以便在生成过程中使用该参数。
在创建手动任务的过程中,任务是为章节建立一个协调的节奏——一系列任务:短任务、长任务、短任务、再短任务等等。如果你知道玩家的预期 DPS 及其时间,你如何获得任务中敌人的总生命值?
💡 线性回归是一种使用线性依赖函数重建一个变量对另一个变量或多个其他变量的依赖关系的方法。以下示例将仅考虑一个变量的线性回归:f(x) = wx + b。
我们来介绍以下几个术语:
所以,HP = DPS * 行动时间 + 空闲时间。在创建手册章节时,我们记录了每个任务的预期时间;现在,我们需要找到行动时间。
如果您知道预期任务时间,则可以计算行动时间并将其从预期时间中减去以获得空闲时间:空闲时间 = 任务时间 - 行动时间 = 任务时间 - HP * DPS。然后可以将该数字除以任务中敌人的平均数量,即可得到每个敌人的空闲时间。因此,剩下的就是简单地从预期任务时间到每个敌人的空闲时间建立线性回归。
此外,我们将构建一个行动时间与任务时间份额的回归模型。
让我们看一个计算示例并了解为什么使用这些回归:
这里有个问题:为什么我们需要知道敌人的空闲时间?如前所述,刷新是按时间排列的。因此,第 i 个刷新的时间可以计算为第 (i-1) 个刷新的活动时间与其中的空闲时间之和。
那么又有一个问题:为什么行动时间和自由时间的份额不是恒定的呢?
在我们的游戏中,任务的难度与其持续时间有关。也就是说,短任务比较容易,长任务比较困难。难度参数之一是每个敌人的空闲时间。上图中有几条直线,它们具有相同的斜率系数 (w),但偏移量 (b) 不同。因此,要改变难度,只需改变偏移量即可:增加 b 使游戏更容易,减少 b 使游戏更困难,并且允许使用负数。这些选项可帮助您更改每个章节的难度。
我相信所有设计师都应该深入研究回归问题,因为它通常有助于解构其他项目:
因此,我们设法找到了生成器的规则,现在我们可以继续生成过程。
如果你抽象地思考,那么任何任务都可以表示为一个数字序列,其中每个数字反映一个特定的生成集群。例如,任务:1、2、1、1、2、3、3、2、1、3。这意味着生成新任务的任务归结为生成新的数字序列。生成后,你只需根据集群设置单独“扩展”每个数字即可。
如果我们考虑一种生成序列的简单方法,我们可以计算出特定产卵在任何其他产卵之后的统计概率。例如,我们得到下图:
图的顶部是它所通向的簇,即一个顶点,边权重是下一个簇的概率。
通过这样的图,我们可以生成一个序列。但是,这种方法有许多缺点。例如,它缺乏记忆(它只知道当前状态),并且如果它有很高的统计概率转变为自身,则有可能“卡在”某一状态。
✍🏻 如果我们将该图视为一个过程,我们会得到一个简单的马尔可夫链。
让我们来看看神经网络,也就是循环神经网络,因为它们没有基本方法的缺点。这些网络擅长在自然语言处理任务中对字符或单词等序列进行建模。简单地说,网络经过训练,可以根据前面的元素预测序列的下一个元素。
由于这是一个庞大的话题,因此本文不介绍这些网络的工作原理。我们先来看看训练需要什么:
一个简单的示例,其中 N=2、L=3、C=5。我们取序列 1、2、3、4、1,并在其中查找长度为 L+1 的子序列:[1、2、3、4]、[2、3、4、1]。我们将序列分为 L 个字符的输入和答案(目标)——第 (L+1) 个字符*。例如,[1、2、3、4] → [1、2、3] 和 [4]。我们将答案编码为独热向量,[4] → [0、0、0、0、1]。
接下来,您可以使用 tensorflow 或 pytorch 在 Python 中绘制一个简单的神经网络。您可以使用下面的链接查看如何完成此操作。剩下的就是开始对上述数据进行训练,等待,然后……您就可以投入生产了!
机器学习模型具有某些指标,例如准确度。准确度表示正确给出答案的比例。但是,必须谨慎看待它,因为数据中可能存在类别不平衡。如果没有(或几乎没有),那么我们可以说,如果模型预测答案比随机预测更好,即准确度 > 1/C,则该模型运行良好;如果接近 1,则效果很好。
在我们的案例中,模型表现出了良好的准确性。这些结果的原因之一是通过将敌人映射到其类型和平衡性而获得的集群数量较少。
对于那些感兴趣的人,这里有更多关于 RNN 的资料:
训练好的模型很容易
为了与模型交互,在 Unity 中创建了一个自定义窗口,游戏设计人员可以在其中设置所有必要的任务参数:
进入设置后,剩下的就是按下按钮并获取一个可以在必要时进行编辑的文件。是的,我想提前生成任务,而不是在游戏过程中生成任务,这样就可以对其进行调整。
让我们看一下生成过程:
所以,这是一个很好的工具,它帮助我们将任务创建速度提高了几倍。此外,它还帮助一些设计师克服了对“写作障碍”的恐惧,因为现在你可以在几秒钟内得到一个现成的解决方案。
在这篇文章中,我以任务生成为例,试图展示经典机器学习方法和神经网络如何帮助游戏开发。如今,生成式人工智能的趋势十分明显,但不要忘了机器学习的其他分支,因为它们也有很多功能。
感谢您花时间阅读这篇文章!我希望您能了解生成地点任务的方法以及任务的生成。不要害怕学习新事物,发展自己,制作出好游戏!
插图作者:shabbyrtist