Super Mario 64 — одна из самых важных и любимых игр в истории. Она задала стандарт для 3D-игр на критическом этапе развития отрасли и повлияла не только на игры для Nintendo 64, но и на проекты многих конкурирующих платформ.
Но как она работала? Происходило ли что-то интересное в головах Goomba, Koopa и и других врагов, которых мы встречаем на уровнях Bob-Omb mountain, Whomp’s Fortress и Tick Tock Clock? Давайте выясним это.
Открываем капот
Super Mario 64 уже исполнилось 25 лет, и хотя её искусственный интеллект может показаться довольно простым, нам важно понимать, что было в этой игре. Она является поворотным моментом в истории разработки игр. Это бестселлер платформы Nintendo 64, отражающий дух времени, когда разработчики игр переходили к 3D. Она сильнейшим образом повлияла на дизайн трёхмерных игр. Хоть она и не была первым 3D-платформером, да и первым 3D-проектом Nintendo, её наследие нельзя недооценивать. Такие дизайнеры, как Тим Шеффер и Майкл Джон подтверждают огромное влияние, оказанное игрой на их собственные проекты: Psychonauts и Spyro the Dragon. Некоторые аспекты, например, управление камерой, потребовали дальнейшей доработки, однако визуальный дизайн, анимация персонажей, движение игрока, структура уровней и миссий создали успешный шаблон для многих последующих игр.
Хотя ИИ проекта, без сомнений, очень прост, стоит проанализировать подход, использовавшийся Nintendo при структурировании и разработке поведений. Чтобы заняться этим, я скачал декомпилированный код Mario 64, выложенный на GitHub. Этот проект был опубликован в 2019 году, он стал одним из множества работ сообщества любителей декомпиляции, которые взяли на себя задачу реверс-инжиниринга сырого двоичного кода ROM-файла в узнаваемый, читаемый код на языке программирования C. Поэтому мой анализ строится не на исходном коде самой Nintendo, а на полученном реверс-инжинирингом коде, публично доступном и максимально приближенном к оригиналу.
Работа над декомпиляцией была вызвана не стремлением украсть ресурсы игры или создать новые порты. Это был механизм, помогающий спидраннерам находить новые хитрости. Для множества участвовавших в проекте разработчиков возможность функция за функцией собрать эту головоломку стала настоящей наградой. Декомпиляция подобного исходного кода даже в лучших случаях не была простой задачей. Ещё больше усложняла её необходимость в определении того, какие инструменты разработки использовались при создании игры в 1996 году. Чтобы декомпилировать подобную игру, нужно иметь представление о том, какакя версия компилятора Silicon Graphics IDO применялась для генерирования оригинального ROM-файла. Для тестирования всего этого необходимо эмулировать поведение комплектов разработки Silicon Graphics: SGI Visual Workstation, ведь именно их использовали разработчики, собиравшие игры для Nintendo 64. Это настоящий инженерный подвиг. Если вы хотите узнать больше об этой работе, в том числе и о трудах по декомпилированию таких игр, как The Legend of Zelda: Ocarina of Time, то прочитайте статью Arstechnica.
При правильной настройке декомпилированный исходный код позволяет скомпилировать ROM-файл для каждой региональной версии готовой игры (для рынков Японии, США и Европы). Однако для создания полностью играбельной игры вам всё равно нужна копия исходного ROM игры, ведь в этом проекте содержится только написанный сообществом исходный код. В нём отсутствуют внутриигровые ресурсы. Это значит, что нельзя просто скомпилировать его и получить бесплатную копию игры.
Объекты и поведения
В первую очередь о декомпиляции кода Mario 64 нужно знать то, что практически каждый элемент мира, за исключением геометрии уровня, считается объектом. И у большинства игровых объектов может существовать поведение.
Поведения позволяют использовать множество общих функций, например, создание объекта в нужной точке в виде монеты, падение объекта в результате взаимодействия, настройка хитбокса для коллизий между внутриигровыми объектами, поворот лицом к камере, отключение рендерера после уничтожения объекта, и так далее. Скрипты поведений используются не только для персонажей с ИИ: движущиеся платформы, двери, монеты, огненные шары, ударные волны, даже некоторые деревья и бабочки имеют поведения, поскольку в той или иной форме реагируют на игровой мир в результате взаимодействия или с течением времени.
Все объекты, имеющие скрипты поведений (Behaviour Scripts) имеют две основные функции, begin() и update(). Эти функции выполняют очень конкретные задачи: begin() отвечает за конфигурирование объекта для игры при его создании, а update() обрабатывает поведение объекта в каждом такте или кадре игры. Обычно Mario 64 выполняет эту функцию с частотой примерно 30 кадров в секунду, а самые вычислительно сложные части игры выполняются примерно раз в 20 кадров. То есть begin() вызывается только в первом кадре существования объекта, а update() — в каждом последующем кадре. Если вы сами разрабатываете игры, то это может показаться вам очень знакомым, потому что в движках наподобие Unity и Unreal используется похожая структура.
Функция update() обрабатывает исполнение стандартных команд поведения для каждого типа объектов, движение по осям x, y и z, учёт гравитации. Плюс она уделяет внимание всей логике управления таймерами для действий, переключению с одного действия на другое; всё это грубо реализовано в виде конечного автомата, в основном состоящего из условного ветвления в коде. Но самая важная задача функции — проверка местоположения Марио относительно объекта (эти вычисления выполняются первыми).
Первые два вычисления каждого объекта со скриптом поведений используются для расчёта расстояния до Марио и угла, требуемого для поворота в его сторону. Эти расчёты используются во многих задачах ИИ, и вскоре мы это увидим. Но особенно важно, что это используется для рендеринга внутриигровых объектов. Каждый управляемый поведениями объект в Super Mario 64 на основании близости к Марио принимает решение, должен ли он рендериться. Все объекты имеют собственный параметр дальности отрисовки, и в каждом кадре они проверяют, находится ли Марио дальше расстояния отрисовки. Если это так, то объект сообщает рендереру, что нужно его отключить. Любопытно, что я не нашёл свидетельств того, что эти поведения не исполняются, если Марио очень далеко.
Единственное исключение это уровни-помещения. Уровни Mario представляют собой или открытые пространства, например, Bob-Omb Battlefield, или состоят из множества комнат. Примерами этого могут быть Peach’s Castle, Big Boo’s Haunt и Hazy Maze Cave. На этих уровнях объект рендерится, только если Марио находится в одной комнате с ним. И многие скрипты поведений тоже активируются, только если Марио находится в той же комнате.
За пределами основных функций поведений объект может также хранить указатель на отдельный скрипт команд поведений. Эти скрипты написаны для более специализированных элементов игры и позволяют исполнять основную часть логики взаимодействий и поведений. В игре их более пятисот, это может быть специфическое поведение NPC, сбор красных монет, активация ловушек и даже завершение уровня подбором звезды.
Но поверх всего этого существуют хитбоксы, определяющие способ взаимодействия Марио с объектами. Хитбокс сообщает игровой логике, что два игровых объекта взаимодействуют и при помощи различной логики мы можем сделать так, чтобы в зависимости от контекста происходили различные действия. Например, чтобы Марио убивал врага или враг наносил урон Марио. Любопытно, что почти все враги в Mario 64 имеют два хитбокса и оба они цилиндрические. Стандартный хитбокс объекта используется для многих внутриигровых коллизий и логики, однако есть и hurtbox, используемый исключительно в ситуациях, когда наносится урон Марио. У большинства NPC есть hurtbox, и часто он отличается от обычного высотой и радиусом. Однако есть и исключения, например, Koopa the Quick, который не использует hurtbox, потому что просто отталкивает игрока, а не вызывает урон при коллизии.
Дизайн NPC
Как и в основном скрипте поведений, в используемых NPC отдельных действиях поведений есть собственные функции begin() и update(). Я расскажу о самом интересном, что мне удалось там найти.
Как уже говорилось, все игровые объекты вычисляют своё расстояние и угол относительно Марио для рендеринга, однако многие NPC также используют эти данные для своей базовой логики. Игравшие в игру должны были это заметить, учитывая, как враги наподобие Koopa и Goomba реагируют на присутствие Марио.
Но кроме этого многие из них записывают местоположение своего «дома». Они хранят ссылку на то место в игровом мире, где они были созданы, или на близлежащее место. Эти данные используются, чтобы NPC приблизительно знали, в каком месте игры они появились при загрузке уровня.
Стоит упомянуть, что NPC в Mario 64 не могут вычислять что-то вроде сетки навигации, чтобы понимать, куда можно перемещаться в мире. Потому что на самом деле её нет: то, что мы теперь называем navmesh в игровых движках, появилось только в Quake III Arena в 1999 году. NPC наподобие Goomba и Koopa Troopa просто блуждают по окрестностям, пользуясь фиксированными правилами о том, насколько далеко они могут зайти, прежде чем изменить расстояние, и часто поворачиваются на фиксированные углы. Вы заметите, что чаще всего они остаются на поверхности, на которой были созданы, к тому же многие уровни Mario 64 не сильно заполнены объектами, поэтому им сложнее запутаться в препятствиях. Есть лишь несколько врагов, которые двигаются по платформам, но они не сваливаются с них только благодаря чистой удаче, или летают, когда что-то произойдёт на уровне кода.
Но когда они движутся, то часто уделяют внимание двум аспектам. Во-первых, они смотрят, не столкнутся ли со стенами, и если это так, то они отворачиваются от них. Но как объект знает, что он смотрит на стену? Есть функции, позволяющие ему разрешать коллизии на основании текущего направления, и в них часто есть возможности для поворотов. То же самое применимо, когда они смотрят на Марио, только это определяется не по точному направлению, а в секторе круга. В некоторых случаях, например, в случае Goomba, персонаж реагирует на Марио просто когда тот находится на определённом расстоянии до него, вне зависимости от направления взгляда.
Второй важный аспект для NPC — это текущее расстояние от «дома». У каждого NPC есть собственные правила относительно того, как далеко можно уходить от «дома», будь то общее расстояние (такая логика используется для Goomba и Koopa), или расстояние по определённой оси (логика Bob-Omb). В случае Boo это расстояние рассчитывается как радиус от исходной точки (вычисляемый движением по X и Z). Как бы то ни было, общее правило заключается в том, что они поворачиваются, берут направление к «дому» и начинают брести обратно в его сторону. Однако в некоторых случаях, в частности Bob-Omb и Bully, они возвращаются домой, если это считается безопасным, что определяется по близости Марио к «дому».
Эта логика описывает большинство персонажей, но у всех у них есть собственные уникальные особенности. Goomba делают резкие повороты от стен и дают задний ход к «дому», если общее расстояние слишком велико. Они преследуют Марио, если он слишком близко, вне зависимости от угла, в радиусе примерно 500 единиц. При этом перед погоней они всегда подпрыгивают. Для справки: единицы расстояния почти полностью совпадают с реальными сантиметрами. Марио имеет в игре рост 161 единицу, а согласно Mario wiki его рост составляет 155 см. Поэтому расстояние в 500 единиц до Goomba — это чуть меньше 5 метров.
У Koopa Troopa используется схожая логика, однако они убегают от Марио в пределах 300 единиц. В логике Koopa интересно то, что когда игрок прыгает на них, они могут потерять свой панцирь. Обычно они пытаются подбежать к панцирю, но довольно часто Марио слишком близко и это заставляет их отбегать. Однако если панцирь достаточно близко, несмотря на близость Марио, NPC рискует и залезает обратно в свой панцирь.
Как уже говорилось, Bob-Omb патрулируют окрестности в зависимости от того, как далеко они удалились по любой оси от своего «дома». Хотя, честно говоря, им сложно уйти на достаточно далёкое расстояние, не взорвав себя. Но в отличие врагов наподобие Goombas, когда Bob-Omb погибает, она запускает в коде респаун, создающий новую Bob-Omb там, где появилась предыдущая, после того, как Марио покинул уровень.
Другие враги, например Whomp, проверяют расстояние до «дома» и сравнивают с заданным расстоянием патрулирования. Оно устанавливается отдельно для каждого уровня. Whomp имеет одинаковое расстояние патрулирования на всех уровнях, за исключением Bowser in the Sky, где оно гораздо короче.
Heave-Ho, появляющиеся на уровнях Tick Tock Clock и Wet Dry World, тоже используют местоположения «дома» и Марио, но немного иначе: Heave Ho записывает местоположение «дома» в start. Но когда Марио приближается на определённое расстояние к «дому», персонаж начинает двигаться в его сторону.
Нельзя говорить о Mario 64, не упомянув рыб! Рыбы создаются в двух вариантах, синем и сиреневом. Кроме того, создаваемые стаи рыб тоже имеют два варианта: большая стая из 20 рыб и мелкая стая из 5 рыб. При создании в функции start рыбам придаётся разная начальная скорость (добавляется немного случайного шума), плюс их высота в воде тоже немного варьируется, чтобы они не были слишком скученны в одном месте.
Сами рыбы имеют довольно простой набор поведений, они поднимаются и опускаются, поворачиваются на фиксированный угол плюс случайный шум, а затем возвращаются обратно. Их высота обычно ограничена, чтобы они не поднимались слишком близко к поверхности воды, и если они оказываются слишком близко, прикладывается большая отрицательная вертикальная скорость. Единственным исключением является уровень Secret Aquarium, потому что на нём нет поверхности воды.
Чтобы рыба не слишком удалялась от исходной точки, она может двигаться в фиксированном диапазоне, примерно равном 700 единицам. Кроме того, стая обычно создаётся в 1500-2000 единицах от Марио на карте. 700 единиц означают, что на самом деле рыбы плавают в диапазоне всего менее 7 метров. А стая создаётся примерно в 15-20 метрах от Марио. Плюс рыбы реагируют на нахождение Марио в воде. Когда Марио находится менее чем в 150 единицах, они поворачиваются так, чтобы отвернуться от Марио и все ускоряются. Этому стандарту игровая индустрия продолжала следовать ещё много лет.