欢迎光临散文网 会员登陆 & 注册

《超级马里奥64》的AI技术分析

2022-04-21 17:33 作者:皮皮关做游戏  | 我要投稿

原文链接:https://www.gamedeveloper.com/blogs/analysing-the-ai-of-super-mario-64

作者:Tommy Thompson

译者:网易·毛毛

校对/编辑/润色:皮皮关


《超级马里奥64》是有史以来最重要、最受欢迎的电子游戏之一。在行业的关键阶段,它确立了3D游戏的标准。它给N64以及其他竞争平台上的游戏带来了深远影响。

但这是如何做到的?游荡在Bob-Omb mountain、Whomp’s Fortress以及Tick Tock Clock(注:都是《超级马里奥64》中的关卡)中的栗宝宝慢慢龟,它们脑子里都在想些啥有趣的事情呢?让我们来了解一下。


打开引擎盖

《超级马里奥64》发售已经超过25年了,它的AI可能很简单,但我们需要意识到这款游戏的意义。它是游戏开发历史上的一个关键点,是N64平台最畅销的游戏,标志着游戏开发从此进入 3D 时代。它对 3D 游戏的设计产生了巨大影响。虽然它不是第一款 3D 平台跳跃游戏,也不是任天堂制作的第一款 3D 游戏,但《超级马里奥 64》的遗产不容小觑。Tim Schaeffer和Michael John等设计师直言不讳地表示,它直接影响了他们的游戏,例如《脑航员》《小龙斯派罗》。虽然镜头控制等领域还有必要进一步迭代,但视觉设计、角色动画、玩家移动、关卡和任务构建等方面为许多其他游戏直接建立了模板。

尽管当时的AI无疑还处于初级阶段,但任天堂构建和设计《超级马里奥64》中行为的方法,仍然非常值得分析。为此,我下载了 GitHub 上的 Mario 64 反编译。该项目首次发布于2019年,是业余反编译者社区的众多作品之一,他们将ROM的原始二进制文件逆向工程成可解析、易读的C语言代码。所以我所讲述的并不是基于任天堂本身的原始源代码,而是公开的、尽可能接近原始代码的逆向工程代码。


这种反编译并不是为了窃取《马里奥》游戏的资产素材或者移植到新的平台。取而代之的是,它是一种帮速通爱好者们找到新漏洞的机制。对涉及这个项目的开发者来说,能够拼凑出这个谜题的答案,哪怕一次只拼一个功能,都是很棒的回报。像这样反编译源代码从来都不是一件容易的任务,而我们还必须弄清楚他们在1996年使用了哪些开发工具,这就让事情变得更加困难。要反编译这样的游戏,需要了解是哪个版本的Silicon Graphics IDO编译器生成的原始ROM文件。而测试它们需要模拟原始Silicon Graphics开发单元(SGI Visual Workstation)的系统行为,因为这就是开发者们为N64开发游戏所用到的机器。这是一项真正的工程壮举。如果您想了解有关这项工作的更多信息,包括反编译《塞尔达传说:时之笛》等作品的努力,可以看看这篇arstechnica 文章。

正确配置后,经过反编译的源代码可以为游戏最终版本编译 每个区域(日本、美国和欧洲)的ROM文件。但为了创建一个完全可玩的游戏,您仍然需要原始游戏 ROM 的副本,因为该项目仅包含社区编写的源代码。它不包含任何游戏内资产。意味着你不能只编译它:哟,游戏的免费版get。

对象和行为

有关《马里奥64》的代码是如何被拆解的,首先需要意识到:游戏世界中的几乎每个元素,除了关卡几何空间之外,都被视为一个对象。大多数游戏对象也可以附加一个行为。  

行为支持各种常见功能,例如在某个位置生成一个对象(例如金币),让某个对象因交互而下落,为游戏中对象之间的碰撞定制碰撞盒,当提示出现时自动把脸转向镜头,当对象被破坏时禁用渲染器等等。行为脚本并非 AI 角色独有:移动平台、门、金币、火球、冲击波,甚至一些树木和蝴蝶都被认为是具有行为的,因为它们都以某种形式对游戏世界做出反应。

不过,所有带有行为脚本的对象都有两个主要函数,begin()函数和update()函数。它们做了两件非常具体的事情,begin() 负责配置游戏中刚生成的这个对象,而 update() 则处理游戏每帧或每个时隙的对象行为。《马里奥 64》通常以每秒 30 帧运行,在某些最激烈的部分以大约 20 帧运行。因此,begin() 仅在对象存在的第一帧被调用,而update() 在之后的每一帧都会被调用。如果你自己是一名游戏开发者,那么这一切对你来说都非常熟悉,因为 Unity 和 Unreal 之类的引擎具有类似的结构。    

update()函数处理每个对象类型的常见行为命令执行,沿 x 和 z 轴以及 y 轴(补偿重力)的单位移动。此外,它还会留意管理动作计时器的所有逻辑,对象是否从一个动作切换到另一个动作,所有这些都是粗略实现的有限状态机,主要来自代码中的条件分支。但最重要的是检查马里奥相对于它的位置——这实际上是每个对象所做的第一个计算。  


每个带行为脚本的对象所进行的前两次计算都是计算出与马里奥之间的距离,以及转向和面朝马里奥所需的角度。我们很快就能看到这些会被用于各种AI功能。但它也被广泛用于游戏内部对象的渲染。《超级马里奥64》中的每个行为驱动对象都是基于其与马里奥的接近程度而决定是否渲染。每个对象都有自己的绘制距离参数,每一帧它都会检查马里奥现在是否比绘制距离更远,答案如果是肯定的,那这时它就会告诉该对象的渲染器禁用自己。有趣的是,我找不到证据表明,当马里奥离得太远时行为本身不会被执行。

唯一的例外是在房间里。《马里奥》的关卡要么是开放空间,如Bob-Omb Battlefield,要么由多个房间组成。这方面的三个典型例子是Peach’s Castle,Big Boo’s haunted和Hazy Maze Cave。在这些情况下,只有当马里奥在同一个房间时,该对象才会渲染。事实上,许多行为脚本也只有在马里奥也处在同一个房间时才会被激活。

除了主要行为函数之外,一个对象还会携带一个特殊的指针,该指针会指向单独的行为命令脚本。这些脚本是为游戏更定制化的元素而编写的,并能执行大部分的游戏交互和行为逻辑。游戏中有超过 500 个像这样的机制,包括特定的NPC行为,触发收集红色硬币,激活陷阱,甚至收集能量星结束关卡。

但除此之外,还有一些碰撞盒决定了马里奥如何与物体之间互动。如果你对碰撞盒的概念不太熟悉的话这里介绍一下:碰撞盒会告诉游戏逻辑“有两个物体正在交互”,进而可以让游戏根据具体情景让不同的事情发生。就像马里奥杀死敌人,或者敌人伤害玩家一样。有趣的是,《马里奥 64》 中几乎所有的敌人都拥有两个碰撞盒,而且它们都是圆柱形的。一个是用于游戏内碰撞和逻辑的标准对象碰撞盒,另一个则是伤害碰撞盒 ,仅适用于马里奥会受到伤害的情况。大多数NPC都会使用伤害碰撞盒,它的高度和半径都与标准碰撞盒不同。然而也有一些例外,特别是Koopa the Quick(跑得很快的那个慢慢龟),他没有用上伤害碰撞盒,因为他只是把你从身边推开,而不是在碰撞时触发伤害。

NPC设计

与主要行为脚本一样,NPC 用到的单独行为动作脚本也有有自己的begin()和update()函数。这里我会介绍我在当中发现的一些有趣的东西。    

如前所述,所有游戏对象在渲染时都会计算它们与马里奥的距离和角度,但许多NPC也会将此作为其核心逻辑。玩过游戏的玩家可能会注意到这一点,因为栗宝宝和慢慢龟会对马里奥的出现做出反应。

但除此之外,很多角色还记录了他们的“家”。这是游戏世界中的某个位置,相当于它们存储了一个关于它们出生的准确位置(或附近)的引用。这是为了让 NPC 大致知道当关卡加载时它在游戏世界中的大致位置。

值得一提的是,《马里奥64》中的NPC无法通过导航网格计算出在游戏世界中的移动方式。因为呢,我们现在普遍认为游戏引擎中的导航网格直到 1999 年《雷神之锤3:竞技场》才开始存在。相反地,像栗宝宝和慢慢龟这样的NPC会四处游荡,在改变之前会走多远有固定的规则,并且通常会以固定的角度旋转方向。你会注意到它们大部分时间都在地表运动,另外《马里奥64》的关卡中并未充斥着杂乱无章的物件,这意味着它们很难被障碍卡住。只有少数敌人能够在平台间移动,但这主要是由于跳跃时的撞大运,又或者它们本来就会到处飞,而不是代码级别所发生的事情。


但当四处走动时,它们经常注意两件事。首先是:自己是否会与墙壁发生碰撞,如果答案是肯定的,则它们会转身远离。但怎么知道所面对的是一面墙呢?有一些函数可以让它们根据当前的朝向解决碰撞问题,而且通常能保留一些回旋的空间。这同样适用于它们看向马里奥的时候。不是基于它们的确切朝向(因为那太精确了),而是“在他们朝向的某个范围内物体是否在面前”。某些情况下(尤其是栗宝宝)会简单地对马里奥在他们一定距离内做出反应,而不管方向如何。

它们关注的第二个关键事实是:离家有多远。每个 NPC 对允许离家多远有不同的规则,无论是离家的总距离(这是栗宝宝和慢慢龟用到的逻辑)还是到特定轴上的累计距离(炸弹兵用的这个)。又或者像羞羞鬼这样,判断是否落在其原点的某个半径内(用 X 和 Z轴的 移动来计算)。无论哪种情况,通用规则是他们会转身,面向家,然后开始朝那个方向游荡。但在某些情况下,特别是炸弹兵和Bully,他们只有在觉得安全的情况下才会回家,这取决于马里奥目前是否在他们家附近。

就是这样的逻辑封装了大多数角色,不过与此同时它们也都有自己独特的预警情况。如果总距离太远,栗宝宝就会急转远离墙壁并冲回家。而如果马里奥离得太近,无论角度如何,它们都会开始追赶,阈值大概是500个距离单位,且追逐总是优于跳跃。供参考:距离单位几乎直接映射到现实世界中的厘米。马里奥在游戏中的身高为 161 个单位,根据马里奥wiki -他的设定身高也为 155 厘米。因此当我们说栗宝宝在 500 单位以内时,差不多就是不到5米。


与此同时,慢慢龟也有类似的逻辑,尽管他们会在 300 距离单位内就会逃离马里奥。慢慢龟逻辑中有趣的部分是,一旦你跳到它们背上,它们会失去外壳。通常他们会试图跑回外壳中,但马里奥此时往往离得很近,因此经常会迫使他们逃跑。但是,如果壳足够近,尽管此时马里奥也很近,它还是会冒险尝试潜入壳中。

就像前面提到的,炸弹兵会根据他们离家的某个轴上移动的距离进行寻路。虽然坦率讲他们很难在之后全身而退(炸弹兵经常会自爆)。但与其他敌人(例如栗宝宝)不同的是,当一个炸弹兵死亡时,它会触发代码中的重生函数,这将在马里奥安全离开后,于上一个重生地创建一个全新的炸弹兵。

与此同时,诸如“砰砰”(石板状的敌人)这样的其他敌人,会检查它们离家的距离,并将其与预定义的巡航距离进行比较,然后基于具体关卡进行配置。绝大部分关卡中砰砰的巡航距离都相同,但Bowser in the Sky关卡是例外,在这一关中巡航距离要短得多。

出现在Tick Tock Clock关卡和Wet Dry World关卡中的 Heave-Ho(一种发条机器人外形的敌人)也用到了家的概念 以及马里奥的位置,但方式略有不同:Heave Ho 在一开始就会记录它的家的位置。一旦马里奥进入家的一定范围内,它就会开始瞄准马里奥了。

现在我不能不提《马里奥64》里的鱼了!这种鱼有两种颜色:蓝色和青色。对应生成的鱼群有两种变体:20条鱼的大鱼群,和5条鱼的小鱼群。鱼以不同的初始速度开始生成(添加了一点随机噪声),另外它们在水中的高度也会有所变化,这样确保它们不会聚集在一起。

鱼本身有一套非常简单的行为,它们会下潜和上浮,转动固定角度(加一些随机噪声),然后确保能返回。它们的高度通常被限制,以让它们不能靠近水面,如果不慎靠得太近了则会被施加一个很强的负竖直速度。一个例外是Secret Aquarium关,因为该关卡不存在水面。

为确保它们不会离开原点太远,鱼只能在大约 700 个距离单位的固定范围内移动。但鱼群通常会在距马里奥1500 到 2000 个距离单位的位置生成。因此,700 个单位意味着鱼只在 7 米左右的范围内真正游动。鱼生成的位置距离马里奥约 15-20 米。另外,所有的鱼都会对在水中的马里奥做出反应。当距离马里奥不到 150 单位时,它们会转而指向远离马里奥的方向,并且会开始加速。这一切都为游戏行业设立了未来许多年都会遵循的一种标准。

要了解更多有趣的事实,一定要瞅瞅视频中的“speed round”部分。

结语

考虑到这是油管系列中的第64集,所以安排《超级马里奥64》是合理的。关于更多N64的分析,请参考我关于《007黄金眼》AI的文章。另外,如果你喜欢有关源代码的片段,一定要看看我在《命令与征服》、《半条命》和《极度恐慌》中的AI。

想要学习更多游戏开发的知识,欢迎大家了解了解皮皮关与网易联合开发的游戏开发线上课程《网易游戏开发就业班(Unity)》。第二期招生火热进行中,戳下面链接便可了解课程详情:https://ke.study.163.com/course/detail/100106669

无论是课程还是游戏开发的相关问题,快来咨询我们网易云课堂游戏开发专属助教老师——YOYO(快来拍一拍老师,一起学习吧!)


《超级马里奥64》的AI技术分析的评论 (共 条)

分享到微博请遵守国家法律