Разработка → Внутренняя структура игры Contra

перевод
PatientZero 6 июня в 10:51 35,5k
Оригинал: Allan Blomquist
image

Как и многие из тех, кто программирует видеоигры, в детстве я часто играл в игры для NES. Меня всегда поражало, как разработчики смогли добиться столь многого минимальными усилиями, поэтому я потратил кучу времени на анализ внутренней работы некоторых игр. Сегодня я начинаю серию постов, в котором буду документировать то, чему научился, с точки зрения программиста игр. Я постараюсь сосредоточиться на работе систем игры на уровне движка, а не на аппаратном уровне (то есть буду говорить о том, как игра решает, что нужно отрисовать в текущем кадре, а не о том, как работают спрайты на NES). Также я постараюсь добавить любые крупицы информации об играх, которые мне покажутся интересными, например, неочевидное с точки зрения игрока поведение или примеры багов в логике игры.

Введение в Contra


Я начну описание с игры Contra для NES. В этом разделе я сделаю краткий обзор объектов и данных, существующих в игре, а в следующих разделах подробно рассмотрю каждую систему игры, используемую для управления моделью мира. Упрощённая модель игры состоит из:

  • Персонажей игроков
  • Пуль игроков
  • Врагов и других объектов
  • Данных уровня

Персонажи игроков — это самые «тяжёлые» объекты игры. Как и можно ожидать, в ней огромное количество кода, выполняющего исключительно обработку персонажей. Пули персонажей обрабатываются по-разному для каждого типа объектов в игре по причинам, которые, возможно, связаны с производительностью. Позже мы рассмотрим, как отделение пуль игрока от других игровых объектов делает более эффективными различные аспекты игры. Последний класс объектов — это враги, управляемые простой системой объектов. В неё включены сами враги, их пули и взрывы, а также пара «дружественных» игроку объектов, таких как летающие капсулы с бонусами и сами лежащие на земле бонусы. Я буду называть все эти сущности «врагами», чтобы не использовать более общий термин «объекты». Самое важное — то, что в игре есть код, обрабатывающий их абстрактно, в отличие от персонажей и пуль игрока, которые всегда управляются специализированным кодом.

Последняя часть игровой симуляции — основанные на тайлах данные уровней. В Contra есть стандартные горизонтальные уровни, вертикальные уровни и уровни в псевдо-3D. По уровням можно перемещаться только вперёд. Игра хранит двойной буфер тайлов: в первом буфере хранятся данные коллизий и графики текущего экрана, во втором создаётся следующий экран. Ниже мы увидим, как организованы данные уровней, как они обновляются при перемещении игрока и как на их основе создаются враги.

Тайловая карта


Каждый уровень в игре составлен из серии независимых экранов. На рисунке ниже показан общий вид первого уровня игры, разделённый на 13 экранов, из которых состоит этап.

image

Каждый экран строится из набора непересекающихся друг с другом «супертайлов», каждый из которых занимает 32×32 пикселя экранного пространства. Экран всегда состоит из 8×7 супертайлов. В результате окончательный размер экрана составляет 256×224 пикселя. Нативное разрешение NES — 256×240 пикселей, поэтому Contra просто оставляет пустое экранное пространство, а не пытается разделить супертайлы пополам, чтобы полностью заполнить экран. Это довольно стандартное поведение для консольных игр того времени. Их не очень беспокоило то, что отображается сверху и снизу экрана, потому что эти части не всегда были видимы на ЭЛТ-телевизорах.

На рисунке ниже показан седьмой экран первого уровня, разделённый на 56 составляющих фон супертайлов.

image

Каждый супертайл, в свою очередь, составлен из обычных тайлов размером 8×8 пикселей, расположенных в сетке 4×4. Именно эти обычные тайлы оборудование NES использует для отображения карт фонов, поэтому все игры должны на каком-то этапе разбивать карты на тайлы размером 8×8 пикселей.

Данные коллизий


Информация о коллизиях для каждого экрана получается из окончательной схемы расположения обычных тайлов, а не создаётся как отдельная карта. Получившаяся карта коллизций составлена из блоков экрана размером 16×16 пикселей. Это значит, что каждый блок данных коллизий состоит из сетки обычных тайлов 2×2, но при получении типа коллизии для каждого блока рассматривается только верхний левый обычный тайл.

Каждый уровень определяет четыре диапазона индексов тайлов, и каждый из тайлов в диапазоне принадлежит одному из четырёх типов коллизий. Например, на первом уровне, тайлы с 0x01 по 0x05 принадлежат типу «земля». На них можно приземляться при падении, но проходить насквозь при движении влево, вверх и вправо. Тайлы с 0x06 по 0xF8 принадлежат типу коллизии «пустое пространство». Тайлы с 0xF9 по 0xFE имеют тип «вода», а тайл 0xFF принадлежит к «совершенно непроницаемым» тайлам.

На рисунке ниже показана часть первого уровня игры с наложенной на неё получившейся картой коллизий. Блоки с типом коллизии «вода» помечены синим, а блоки «земли» — зелёным. Блоки коллизии типа «пустое пространство» помечены чёрным, а непроницаемых блоков в этой части уровня нет.

image

Враги


На каждом экране есть список врагов, которых нужно создать в мире, когда игрок прокручивает его. Список отсортирован в соответствии с расстоянием, на которое нужно прокрутить экран для создания врага. Например, первый враг на первом уровне имеет значение прокрутки (скроллинга) 16, поэтому он будет создан, когда игрок начнёт игру и продвинет экран вперёд на 16 пикселей. Поскольку список отсортирован по расстоянию прокрутки, а экран можно прокручивать только в одном направлении, всегда существует только один новый «кандидат» на создание.

Вторая часть данных в каждом элементе списка — тип создаваемого врага, а также количество создаваемых врагов этого типа. Затем для каждого из этих количеств врагов есть общее значение контекста создания и значение позиции для создания врага на экране. Контекст создания не имеет значения для самой системы создания врагов, но позже интерпретируется собственной логикой обновления врага. Он используется, например, для настройки того, какой бонус упадёт из летающей капсулы, и для управления определёнными аспектами поведения бегущих солдат. Значение позиции создания врага даёт только половину информации, необходимой для размещения нового врага на экране. Для уровней с горизонтальным скроллингом он даёт положение врага по оси Y, а положение по оси X всегда будет в крайней правой стороне экрана. Если уровень скроллится вертикально, то значение положения создания является положением по оси X, а положение по Y находится всегда в верхней части экрана. Враги, которые, как кажется, создаются за уходящей назад/вниз частью экрана, на самом деле имеют встроенную логику, первым действием перемещающую их на противоположную сторону экрана.

Список врагов переменной длины завершается байтом с магическим значением, после чего игра ждёт, пока игрок выполнит скроллинг до следующего экрана, чтобы начать поиск новых создаваемых врагов из списка врагов этого экрана. Уничтожением врагов после скроллинга и исчезновения их с экрана занимаются сами враги, нет никакой системы, делающей это автоматически.

Случайное создание врагов


Кроме врагов, создаваемых в определённых местах уровня, существуют также враги, появляющиеся немного случайно. Эти враги — бегущие солдаты, часто создаваемые по краям экрана в процессе игры. Система, управляющая этими случайно создаваемыми врагами, на удивление сложна, несмотря на свою кажущуюся простой задачу. На рисунке ниже показаны те самые враги. Все бегущие по экрану солдаты были созданы случайно, пока игрок стоял неподвижно.

image

Сердце системы случайного создания врагов — это таймер, снова и снова отсчитывающий от некоторого начального значения до нуля. Каждый раз при достижении нуля существует вероятность создания врага. У каждого уровня есть интервал, используемый таймером, а на некоторых уровнях случайное создание врагов отключается присваиванием таймеру значения нуля (например, на уровне с базой и на последнем уровне). Поверх интервала по умолчанию вносится ещё две модификации для достижения необходимого конечного интервала, определяемого уровнем. Во-первых, интервал уменьшается примерно на 20% после каждого прохождения игры. Это значит, что после того, как игра отправит вас после прохождения снова на первый уровень, случайные враги создаются чаще, и так далее, пока вы будете проходить игру снова и снова. Другой фактор, снижающий интервал таймера — используемое игроком оружие. Оружие классифицируется с соответствии с тем, как их воспринимает игра: P считается наихудшим, F — чуть лучшим, M и L считаются одинаково хорошими, а S (конечно) — самое лучшее. Интервал таймера случайного создания врагов уменьшается примерно на 3% за каждое очко (0-3) текущего оружия игрока. На рисунке выше показано четвёртое прохождение игры, у игрока в руках Spread gun (S), но скриншот не даёт представления о количестве противников, постоянно выбегающих на экран.

Скорость уменьшения счётчика таймера тоже является переменной и зависит от того, скроллится ли экран в текущий момент. Если считать, что скорость уменьшения в неподвижном состоянии равна 1,0, то при беге вперёд она равна 0,75. Это приводит к тому, что при движении генерируется меньше случайных врагов. Думаю, разработчики хотели таким образом компенсировать то, что игроку при скроллинге придётся столкнуться и с неслучайно создаваемыми врагами.

Когда таймер достигает нуля, возникает вероятность создания врага. Сторона, с которой появится враг, обычно совершенно случайна. За одним специфическим исключением: когда вы находитесь на первом уровне, проходите игру впервые и на текущем экране было создано менее 30 случайных врагов, то враги будут всегда создаваться справа. Это позволяет новичкам в игре освоиться в ней. Положение по оси Y создания нового врага выбирается одним из трёх разных способов, в зависимости от того, сколько прошло кадров после запуска игры. Одну четверть времени игра ищет сверху экрана вниз платформу для создания врага. Одну четверть времени она начинает снизу экрана и выполняет поиск вверх. Оставшуюся половину времени игра пытается использовать случайное текущее положение игрока по оси Y как начальную координату Y для поиска вверх. Однако в этой логике есть «баг», который приводит к тому, что поиск начинается с самого верха экрана половину времени, только если в игре жив только один игрок. Этот «баг» на самом деле ничему не мешает, единственный его эффект — враги могут создаваться в нижней части экрана несколько чаще, чем должны.

После того, как нужное положение для создания врага выбрано, выполняется ещё несколько проверок для определения того, нужно ли выполнять создание. Если выбранное положение слишком близко к верху или низу экрана, то создание отменяется (исключение — уровень с водопадом, где создание врагов вверху экрана разрешено). Если вы находитесь на самом первом экране любого уровня, создание врагов отменяется. Ещё одна жёстко заданная проверка — если игрок находится на нескольких последних экранах уровня Snowfield, то создание противников в левой части экрана всегда отменяется. Эта проверка приводится в действие, когда игрок добирается до самой последней заснеженной платформы уровня, когда внизу видны деревья, в игрока бросают бомбы и один враг стреляет в него из стационарной пушки. Для чего нужна такая специфическая проверка, неизвестно никому. Возможно, один из разработчиков посчитал, что будет слишком сложно справляться с таким количеством опасностей одновременно (однако практически такая же ситуация возникает в самом начале этого уровня, но создание врагов слева там разрешено).

Ещё одно жёстко заданное правило добавлено, чтобы немного упростить первое прохождение игры. Если на текущем экране было меньше тридцати случайно созданных врагов, игрок проходит игру впервые и он стоит слишком близко к той стороне экрана, с которой должен возникнуть враг, то такое создание врага отменяется. Это защищает от глупых смертей при столкновении с врагами, возникшими прямо перед игроком.

Если все эти проверки пройдены, то остаётся ещё одна проверка перед созданием врага. У каждого экрана игры есть уникальное значение, управляющее различными аспектами случайно создаваемых на этом экране противников. Каждый отдельный экран может управлять дополнительной вероятностью отмены создания врага. Экраны могут либо всегда разрешать создавать врагов, либо постоянно запрещать, случайно запрещать создание 50% времени или 75% времени. И когда эта последняя проверка пройдена, настаёт время создания врага.

Существует два типа процесса создания врагов. Если игрок находится не на уровне с водопадом и экран в текущий момент скроллится, то имеется 25-процентная вероятность создания группы из трёх бегущих солдатов. Эти солдаты настроены так, что никогда не стреляют. Возможно, это ещё один «баг» или сделано специально, но для настройки поведения каждого из солдатов игра использует неинициализированную память. Поведение определяет, как они поступят при достижении конца платформы, по которой бегут (должны ли они спрыгивать или могут повернуть назад). Если это и был «баг», а не просто попытка создания выглядящего случайным поведения, то довольно несущественный.

Другой тип процесса создания, получаемый, если условия для первого типа не удовлетворяются — создание единственного бегущего солдата, поведение которого настраивается немного по-другому. В этом случае для случайной настройки возможности прыжков солдата используется обычный генератор случайных чисел. Для таких солдатов также на основании ещё одного значения экрана выбирается возможность стрельбы. Идея этого процесса в том, что каждый экран выбирает одну из небольшого набора выбранных заранее групп поведений. В каждой из групп имеется различное сочетание отсутствия стрельбы, стрельбы стоя и стрельбы лёжа. Затем для каждого создаваемого солдата случайным образом из выбранной уровнем группы выбирается соответствующая возможность стрельбы. Однако в игре есть ещё одни «баг»: на одном экране уровня Hangar определяется восьмая группа поведений, хотя существует всего семь групп. Это приводит к тому, что игра назначает солдатам на этом экране «мусорное» значение поведения (действительное значение получается из части указателей на списки экранных врагов, о которых мы говорили выше). По этой причине возникают разные несмертельные побочные эффекты. В основном солдаты мгновенно меняют направление и убегают с экрана.

Враги внутри баз


Статичные враги на базах


Враги на псевдотрёхмерных уровнях «баз» в Contra создаются по немного другой системе. Кроме того, уровни-базы разделены на экраны, каждый из которых тоже получает собственный список создаваемых врагов, но информация в списке отличается.

image

Экраны на уровнях баз не скроллятся, поэтому каждый враг в списке врагов экрана создаётся в момент перехода игрока к экрану. Каждый список врагов начинается с количества целей, которые нужно уничтожить для отключения электрического барьера и прохождения экрана. На самом деле в игре есть часть неиспользуемой логики, сразу убирающей барьер при отсутствии целей, несмотря на то, что в игре нет экранов с такой конфигурацией. Оставшаяся часть списка состоит из набора записей для каждого создаваемого врага. Первая часть данных в каждой записи сообщает координаты X и Y места создания врага на экране. Вторая часть данных задаёт тип создаваемого врага, а третья, последняя, часть — это непонятное контекстное значение создания, так же, как и в списках врагов для других типов уровней.

В момент попадания на новый экран создаются все враги из списка врагов экрана, и чтение этого списка больше не выполняется. Эти враги в основном являются статичными объектами на дальней части экрана, такими как цели и стреляющие в игрока пушки. Все другие враги, вбегающие на экран во время игры, не создаются по списку врагов экрана. Ими управляет другая система.

Циклические враги на базах


Один тип объекта из списка врагов каждого экрана на уровнях-базах на самом деле является не видимым врагом, а сущностью, управляющей всеми врагами, забегающими на экран с двух сторон. Этот объект-«родитель» обрабатывает свой собственный отдельный список врагов для каждого экрана, говорящий, когда нужно создать врагов и какого типа. Такие списки врагов тоже содержат наборы записей, по одной на каждого создаваемого врага. Первая часть данных в каждой записи определяет тип создаваемого врага и задаёт контекстное значение создания для этого врага. Вторая часть данных содержит время задержки перед созданием следующего врага. Также с каждой записью связан флаг, сообщающий, является ли текущая запись последней для экрана. После обработки последней записи объект-«родитель» возвращается к первой записи списка и начинает создавать врагов по тому же шаблону снова и снова. После того, как на одном экране этот шаблон был повторён семь раз, «родитель» уничтожает себя, и создание врагов прекращается. Это событие запускает стрельбу целей на стене по игроку.

Враги с бонусами


На некоторых экранах уровней-баз встречаются красные прыгающие враги, дающие при убийстве бонус. Появление таких врагов в основном определяется тоже по вторичному списку объекта-«родителя», но с дополнительными правилами: на каждом из экранов может быть создан только один красный враг с бонусом и создание этого врага запрещается при первом проходе вторичного списка. Данные контекста создания прыгающего врага в списке врагов «родителя» определяют, есть ли в нём бонус, и если есть, то указывается тип бонуса, падающего к игроку после убийства врага. Правило одного врага на экране применяется к созданию врага, но не убийству, поэтому если игрок пропустит красного врага, то больше на экране он не появится.

Распознавание коллизий в Contra


Распознавание коллизий


В игре Contra могут возникнут два типа коллизий: коллизии «объект-объект» и «объект-уровень». Каждый из них обрабатывается совершенно разным кодом и распознаётся по совершенно разным данным, поэтому, в сущности, они являются двумя отдельными системами.

Коллизии «объект-объект»


В начале этой статьи я упоминал, что игра отслеживает три типа объектов: игроков, пули игроков и врагов. Распознавание коллизий — это одна из тех областей, в которых используется такое разделение. Вместо проверок коллизий между всеми объектами, игра проверяет только коллизии между группами, реагирующими друг на друга. Игроки никогда не взаимодействуют с другими игроками или их пулями, а враги никогда не взаимодействуют с другими врагами, поэтому единственными важными остаются коллизии между игроками и врагами, а также между пулями игроков и врагами.

С учётом того, что в важных коллизиях «объект-объект» всегда участвуют враги, распознавание коллизий реализовано как часть обновления состояния всех врагов. В каждом кадре игра обходит циклом каждого активного врага и выполняет его логику обновления. После обновления всех врагов в дело вступает система коллизий. Она проверяет коллизии врагов с игроками, а потом коллизии с пулями игроков. Каждый враг имеет пару флагов, которые можно использовать для передачи информации о том, какие типы коллизий задействованы для этого врага. Именно поэтому некоторых врагов, таких как башни, можно подстрелить (коллизии с пулями игроков разрешены), но они не способны убить игрока при контакте (коллизии с игроками запрещены). Кроме этих флагов не используется никакой специальной логики для сужения списка потенциальных коллизий врага. Каждый враг просто проверяется на столкновение с каждым игроком и каждой пулей.

Проверка коллизии между конкретным врагом и конкретным игроком основана на проверке «точка-прямоугольник». Точка, представляющая текущее положение игрока, проверяется вместе с прямоугольником, представляющим текущий хитбокс (hit box) врага. Если на момент выполнения проверки точка находится внутри прямоугольника, то возникла коллизия. На первый взгляд, это выглядит немного странно. Нужно, чтобы игрок умирал от выстрела и в голову, и в ногу, но текущее положение игрока представлено всего одной точкой (которая обычно довольно близко к центру спрайта игрока), которая и проверяется на коллизию с вражеским хитбоксом. Чтобы система заработала, хитбоксы врагов становятся не просто границами вокруг спрайтов врагов, а скорее представлением того, где бы произошла коллизия, если бы мы проводили стандартную проверку «прямоугольник-прямоугольник». Иными словами, хитбоксы врагов представляют собой пространство, в котором должно находиться положение игрока, чтобы спрайт игрока накладывался на спрайт врага.

image

Этот процесс в действии показан на рисунке выше. На каждом из трёх скриншотов положение игрока представлено единственным зелёным пикселем в центре зелёного круга. Розовый прямоугольник показывает хитбокс, используемый белой пулей врага для проверки коллизий с положением игрока. Заметьте, что пуля врага использует разные хитбоксы в зависимости от действий игрока. Именно так игра изменяет уязвимые места игрока вместо обработки его хитбокса. У каждого врага есть своя версия хитбокса для игрока в воде, прыгающего игрока, игрока на земле, стоящего/бегущего игрока и пули игрока. Тип пули игрока не берётся в расчёт при подборе хитбокса, поэтому с точки зрения коллизий пуль нет никакой разницы между разным оружием.

Коллизии «объект-уровень»


Коллизии между объектами и самим уровнем обрабатываются системой иначе. Коллизии с уровнем проверяются только тогда, когда они необходимы. Если игроку нужно узнать, упал ли он с края платформы, то код, обновляющий бегущего игрока каждый кадр запрашивает коллизии с ногами игрока, пока не обнаружит, что под ними ничего нет. Большинству врагов вообще не нужно знать о коллизиях с уровнем, а те, кому они нужны, тоже проверяют их при необходимости в нужное время. Поддерживается единственный тип запроса — проверка точки и карты коллизий уровня, о которой мы говорили выше. Запрос коллизий возвращает код коллизии для тайла под проверяемой точкой (пустой, непроницаемый, вода или платформа), а затем вызывающий объект на основании полученных результатов выполняет необходимые действия.

3D-коллизии


Коллизии на псевдотрёхмерных уровнях-базах работают примерно так же, как и на 2D-уровнях, но с определёнными ограничениями, чтобы сохранить эффект поддельного 3D. Враги не могут сталкиваться с игроком, если их положение Y в пространстве экрана слишком мало (например, слишком близко к верхней части экрана, то есть они находятся «в глубине» экрана). Это работает, потому что известно, что игроки на этих уровнях всегда находятся внизу экрана. У пуль игроков есть таймер и они могут сталкиваться с большинством врагов, только если были на экране достаточно долго, чтобы достичь «дальней» части экрана. Для врагов, которых можно уничтожить в любом положении по оси Z в комнате (например, для катящихся гранат, которые нужно успеть взорвать, пока они не подкатятся и не убьют игрока) таймер пуль не используется. Вместо него проверяется флаг, обозначающий положение игрока «стоя/лёжа». Если пуля была выпущена, когда он лежал, то выполняется обычное двухмерное распознавание коллизий. Это срабатывает только потому, что распознавание выполняется между двумя объектами, оба из которых находятся на земле, где положение по оси Y напрямую накладывается на положение по оси Z (например, если похоже, что два объекта сталкиваются в 2D, то мы понимаем, что они сталкиваются и в 3D, но только вдоль поверхности земли). Благодаря этим ограничениями для распознавания коллизий между объектами в псевдотрёхмерном окружении можно использовать обычное, плоское 2D-распознавание.

Управление игроков


Физика игроков


Код, управлающий низкоуровневым движением игроков в Contra, очень прост по сравнению с тем, что возможно в современных физических движках. В то же время, мне всегда нравилось сверхотзывчивое управление в старых играх по сравнению с играми, где есть более физически реалистичное управление. Данные, связанные с низкоуровневым движением в Contra — это, фактически, только положение в 2D и скорость. Оба значения представлены в формате 8.8 с фиксированной запятой. Единицами измерения в них являются пиксели и пиксели/кадр, соответственно. В каждом кадре игра определяет, какая скорость должна быть у игрока и просто добавляет эту скорость к положению игрока. Горизонтальная скорость в начале кадра всегда сбрасывается до нуля, поэтому в направлении оси X нет никакого постоянного ускорения (поэтому физику движения на льду в Contra реализовать невозможно). У персонажей-врагов в игре нет даже собственной выделенной памяти для отслеживания скорости, потому что многие из них не двигаются. Те враги, которые обычно двигаются, каждый кадр «вручную» добавляют к своему текущему положению постоянную величину. В тех редких случаях, когда врагу нужно более сложное движение, он реализует собственную версию того, что им нужно делать, и не используют общий с персонажами игроков код физики.

В целом, в игре нет никакой постоянно работающей физической системы, управляющей движением игроков. Вместо этого каждый кадр, в зависимости от действий игрока, выполняются определённые вычисления, связанные с движением игрока. Например, когда игрок бежит по земле, код управления устанавливает горизонтальную скорость игрока и проверяет коллизии на уровне ног персонажа, чтобы проверить, не надо ли ему начать падать. Однако вертикальная скорость игрока не обновляется и даже не добавляется к положению игрока по оси Y. Также коллизии не проверяются над головой игрока, потому что известно, что при беге персонаж вверх не движется. Такой подход отличается от более общего подхода к симуляции физики, когда гравитация постоянно тянет игрока к земле, а реакция постоянно возвращает игрока обратно для разрешения коллизии. Подобным же образом коллизии при прыжках, пока игрок движется вверх, проверяются только над его головой. Когда достигнут пик прыжка и игрок начинает двигаться вниз, код переключается на проверку коллизий под игроком. Также во время прыжка вертикальное ускорение применяется ручным прибавлением в каждом кадре фиксированного значения к скорости игрока по оси Y. Нет никакой общей переменной ускорения, всегда добавляемой к скорости игрока, ускорение имеет ненулевое значение только при прыжке. Для вычисления движения игрока не выполняется никаких лишних проверок коллизий и математических расчётов. Каждый кадр выполняются только самые необходимые вычисления, и благодаря этому возникает ощущение очень отзывчивого управления без странного поведения.

Состояния игрока


На следующем, более высоком уровне управления поверх низкого уровня физики обычно определяется набор возможных состояний игрока, а затем каждый кадр выполняется обновление игрока в соответствии с текущим состоянием. В Contra на самом деле реализуется такая система, хотя и для текущего состояния игрока не используется единое значение. Вместо него у игроков есть несколько групп флагов, обозначающих текущее состояние игрока. Их можно разделить на группы флагов прыжков, флагов падения и флагов нахождения в воде. Когда игрок находится на земле, все эти флаги сброшены. Если он нажимает A для прыжка, устанавливается флаг прыжка, а несколько других флагов, относящихся к прыжку, обновляются в соответствии с направлением движения и направлением персонажа во время прыжка. Похожий набор флагов существует для падения. Это состояние, в которое игрок переходит при падении с края платформы или прыжке вниз сквозь землю. Флаги нахождения в воде используются только на первом уровне, когда игрок попадает в воду и начинает плыть. Эти флаги никогда не используются одновременно (например, флаги прыжка и падения никогда установлены одновременно), поэтому я не понимаю, почему вместо них не использовали единственную переменную состояния. Обычно функции управления начинают с тестирования различных флагов, а затем в зависимости от установленных флагов переходят к нужным частям кода.

Подробно об управлении


Бег в Contra включается/отключается мгновенно, в отличие от некоторых других игр, таких как Super Mario Bros., добавляющих персонажу игрока момент движения. Как я упомянул ранее, в результате горизонтальная скорость игрока в начале каждого кадра обнуляется, а затем обновляется до нужного значения в зависимости от действий игрока. Если персонаж бежит по земле, а игрок перестаёт жать кнопку «влево» или «вправо», то горизонтальная скорость остаётся нулевой и персонаж мгновенно останавливается.

Поведение при прыжках и падениях немного отличается: начав двигаться вперёд или назад, вы продолжите двигаться в том же направлении, пока либо не столкнётесь с чем-нибудь, либо не нажмёте кнопку обратного направления для движения в другую сторону. Начав двигаться горизонтально в воздухе, невозможно перестать двигаться горизонтально, пока вы не приземлитесь. Это похоже на прыжок во вращении из Metroid и отличается от поведения в таких играх, как Mega Man, где можно прекратить двигаться в воздухе, если перестать нажимать кнопку направления. Поведение в прыжке реализовано через пару флагов, являющихся частью описанных выше флагов прыжка. При нажатии влево или вправо во время прыжка включается соответствующий флаг прыжка влево/вправо, но при отпускании кнопок флаги не отключаются. Горизонтальная скорость в прыжке определяется по текущему состоянию флагов, а не напрямую по вводимому нажатию кнопок.

Кроме того, при нажатии «вниз-прыжок» Contra позволяет прыгать вниз сквозь землю на нижние платформы. Такая же механика используется в таких играх, как Chip ‘n Dale Rescue Rangers на NES, но отсутствует в других играх с похожими «односторонними» платформами (которые можно пролететь насквозь прыжком вверх, но остановиться на них при падении вниз), в таких как Super Mario Bros. 2. В Contra, когда игрок нажимает A, одновременно удерживая «вниз», вместо флага прыжка устанавливается флаг падения персонажа. Чтобы позволить пройти сквозь землю, игра записывает место на экране, которое на 20 пикселей (немного больше полного тайла коллизии) ниже текущего положения игрока по оси Y. Потом, когда игрок падает, проверка коллизий, которая обычно выполняется у ног игрока в состоянии падения, пропускается, пока текущее положение игрока по оси Y находится выше этого записанного значения.

Другие темы


Чтобы завершить с обсуждением Contra, я рассмотрю другие темы, не стоящие подробного анализа. Некоторые из этих тем относятся к подробностям программирования игры, другие — просто интересные факты о самой игре, которых я не знал раньше.

Случайные числа


В качестве источника случайных чисел всей игры используется единственное глобальное восьмибитное значение. Способ обновления в каждом кадре случайного значения довольно интересен тем, что в нём не используется какой-то конкретный алгоритм, который можно вызывать каждый кадр и получать следующее значение последовательности. Вместо этого следующее случайное значение получается проходом по небольшому циклу, пока игра находится в режиме ожидания начала следующего кадра (ожидая прерывания vblank). В это время игра постоянно прибавляет значение счётчика текущего кадра к случайному числу, снова и снова, пока видеооборудование NES не даст игре сигнал, что настало время обрабатывать следующий отображаемый кадр. Одно из последствий такого подхода: получаемая последовательность случайных чисел сильно зависит от точного выполнения уровня циклов ЦП и взаимодействия между ЦП и видеооборудованием. Это значит, что даже если два эмулятора идеально реализуют логику инструкций ЦП, они всё равно будут генерировать разные случайные последовательности, если их тайминги не точны. Явное свидетельство этого можно увидеть в демо-режиме Contra, в котором используется записанный поток нажатых кнопок. Демо должно быть детерминированным и каждый раз проигрываться одинаково. Ниже показано сравнение одного кадра демо-режима, запущенного на двух разных эмуляторах. Заметьте, что для бегущего солдата, созданного случайным образом в правом нижнем углу экрана, на Nintendulator выбрано одиночное создание, а на NESten — тройное. Так получилось потому, что последовательность случайных чисел, генерируемая двумя экземплярами игры, неодинакова. К счастью, влияние случайных чисел достаточно мало и не может полностью нарушить воспроизведение в демо-режиме.

image

Структура массивов


Contra хранит все данные о врагах и пулях в формате структуры массивов, а не в более объектно-ориентированном формате массива структур. Причина этого не имеет ничего общего с использованием кэша ЦП или инструкциями SIMD, как можно ожидать. Это просто следствие того, что у NES 16-битная адресация памяти, но регистры ЦП 8-битные. Если нужно получить доступ к данным косвенно, через указатель, то нельзя хранить адрес объекта в указателе и жёстко задать смещение к нужному элементу в команде загрузки, потому что адрес объекта может не поместиться в регистр ЦП. Вместо этого необходимо всё перевернуть и жёстко задать 16-битный адрес массива элементов для нескольких объектов в команде загрузки. После этого надо использовать регистр для хранения смещения внутри массива, чтобы получить элемент, относящийся к нужному объекту.

Пространство экрана


В Contra все действия выполняются в пространстве экрана. Положения игроков, врагов и пуль хранятся и обрабатываются в системе координат экрана (например, положение 0 всегда означает точку в пространстве, находящейся в левой части экрана, вне зависимости от того, в какое место уровня проскроллен экран). Сначала эта оптимизация кажется контринтуитивной, потому что хранение всех положений в пространстве экрана намного усложнит такие действия, как скроллинг. В этом случае для симуляции скроллинга вместо изменения единственной переменной положения скроллинга нужно каждый кадр вручную перемещать положение каждого врага, игрока и пули. Однако в конечном итоге такое решение оказывается хорошим, потому что позволяет сэкономить за один кадр гораздо больше, чем потратить. Скроллинг реализовать сложнее, но на самом деле всё сводится к прибавлению всего одного числа к положению объекта при его покадровом обновлении. И часто это просто добавляет одну лишнюю команду ЦП для каждого объекта. Большой выигрыш заключается в том, что теперь все вычисления, связанные с положениями объектов, можно выполнять через восьмибитные значения (экран NES имеет разрешение 256×240, поэтому можно определить любое положение на экране с точностью до пикселя всего одним байтом для X и Y). Поскольку NES нативно поддерживает только восьмибитную арифметику, в результате эти расчёты получаются гораздо быстрее, чем эмуляция 16-битной арифметики для управления 16-битными положениями в мире. Разумеется, это работает только для таких игр, как Contra, в которых соотношение между пространством мира и пространством экрана является простой трансляцией без масштабирования и вращения (т.е. трансформация из пространства мира в пространство экрана оказывается коммутативной с другими трансляциями, даже несмотря на то, что в целом применение трансформаций не является коммутативной операцией). Если камера могла бы отдаляться, то объекты в игре начали бы двигаться слишком быстро, а если бы камера поворачивалась, то объекты двигались бы в неправильном направлении.

Скрытое поведение


Некоторые параметры Contra изменяет в зависимости от того, сколько раз игрок прошёл игру за один раз, и от того, какое оружие у него в руках. Ниже приведён полный список различий, где FINISHED во время первого прохождения будет равно 0, во время второго — 1, и т.д. GUN равно 0 для оружия по умолчанию, 1 — для огнемёта (Fire, F), 2 — для пулемёта или лазера (Machine gun, M и Laser, L) и 3 — для Spread (S):

  • Здоровье (HP) промежуточного босса и последнего босса уровня 8 = 55 + (16 * FINISHED) + (16 * GUN)
  • HP снарядов промежуточного босса уровня 8 = 2 + FINISHED
  • HP стреляющих пастей в стенах уровня 8 = 4 + (2 * FINISHED) + GUN
  • HP скорпионов уровня 8 = 2 + FINISHED + GUN
  • HP коконов вокруг последнего босса уровня 8 = 24 + (2 * FINISHED) + (2 * GUN)
  • HP босса уровня 6 = 64 + (8 * GUN)
  • У появляющихся из-под земли красных пушек пауза перед началом стрельбы становится меньше при увеличении FINISHED
  • Случайное создание врагов происходит чаще при бОльших значениях FINISHED и GUN, а когда FINISHED равно 0, игра не создаёт врагов прямо перед игроком, а на первом уровне создаёт их только в правой части экрана
  • Серые башни в стенах поворачиваются за игроком быстрее и делают меньшие паузы перед стрельбой после стрельбы пулями из оружия с бОльшим значением GUN
  • Чем больше GUN, тем больше пуль выпускают стреляющие солдаты
  • Чем больше GUN, тем меньше времени перед стрельбой ждут пушки на экранах с боссами уровней 2 и 4, а также сами боссы
  • Если GUN равно 3, то промежуточный босс уровня 8 выстреливает три снаряда, и два во всех других случаях
  • Скорость падения на игрока скорпионов с потолка в бою с последним боссом увеличивается пропорционально значению GUN

Заключение


Contra — это одна из моих любимых игр на NES, и как программист игр я получил большое удовольствие от изучения подробностей её работы. Меня всегда удивляло, как разработчики игр того времени могли извлечь так много из смехотворно слабого и ограниченного по сравнению с современными консолями «железа». Если вам есть, что сказать об этой статье, свяжитесь со мной, чтобы я решил, стоит ли продолжать, и если да, то какие игры рассматривать дальше. Можно найти меня в Твиттере: @allan_blomquist.
Проголосовать:
+111
Сохранить: