Тетрис на Nintendo одна из моих любимых версий тетриса. Моя единственная жалоба заключается в том, что ему не хватает возможности Hard Drop мгновенного падения текущей фигуры и её фиксации на месте. Давайте её добавим
В этом посте описывается модификация, которую я внёс в тетрис, нажатие кнопки вверх приводит к мгновенному падению текущей фигуры и отображению призрачной фигуры точечный контур текущей фигуры, показывающий, где она приземлится.
Текущая фигура перемещается на одну клетку вниз за каждый тик игры. Реализации тетриса обычно предоставляют два способа ускорить падение ускоренное и мгновенное падение.
В случае ускоренного падения нажатие кнопки мгновенно переместит текущую фигуру вниз на одну позицию, а удерживание кнопки заставит её упасть быстрее.
Hard drop мгновенно опускает текущую фигуру и фиксирует её на месте. Поскольку игроку может быть сложно визуально определить, выровнена ли фигура с тем местом, куда она должна приземлиться, реализации тетриса с мгновенным падением обычно отображают контур этой фигуры, показывающий, где она в конечном счёте окажется.
До моих изменений NES Тетрис поддерживал только ускоренное падение.
Я сделал программу на rust, которая считывает файл NES ROM в формате INES. Если на входе был NES Tetris (обычно файл назван что-то вроде Tetris(U)[!].nes), на выходе она создаст новый файл ROM NES, который представляет собой NES Tetris, с добавлением быстрого падения фигуры.
Входной файл должен иметь хэш sha1 a99f922e9da20b2a27e4398348505d2e9d15271b.
$ cargo install nes-tetris-hard-drop-patcher # install my tool$ nes-tetris-hard-drop-patcher < 'Tetris (U) [!].nes' > tetris-hd.nes # patch a NES Tetris ROM$ fceux tetris-hd.nes # run the result in an emulator
Этот инструмент полагается на то, что пользователь получит ROM-файл NES Tetris. В нём нет встроенного тетриса. Полученный файл ROM совместим со всеми эмуляторами NES он неспецифичен для fceux.
После публикации этого поста некоторые отметили, что существует стандартный формат для патча ROM (IPS), который широко поддерживается эмуляторами. Вы можете скачать мой патч здесь.
Пару лет назад я сделал эмулятор NES. Оказывается, это полезный инструмент для реверс-инжиниринга, поскольку эмулятор легко настроить для проведения экспериментов с запущенной им программой. В частности, очень пригодилась возможность логировать каждую инструкцию, перемежающуюся интересными событиями, такими как обновления видеопамяти. Также он может отображать гифки, и я использовал его для создания всех анимаций в этом посте.
Чтобы протестировать свой эмулятор, я создал библиотеку для написания ассемблерных программ на языке Rust. Вот пример, в котором значение в регистре аккумулятора умножается на 12:
b.inst(Clc, ()); // clear carry flagb.inst(Rol(Accumulator), ()); // rotate accumulator 1 bit to the left (x2)b.inst(Rol(Accumulator), ()); // rotate accumulator 1 bit to the left (x4)b.inst(Sta(ZeroPage), 0x20); // store current accumulator value at address 0x0020b.inst(Rol(Accumulator), ()); // rotate accumulator 1 bit to the left (x8)b.inst(Adc(ZeroPage), 0x20); // add the accumulator with the value at 0x0020 (x12)
Это позволяет мне использовать rust как язык высокого уровня для ассемблерных программ NES. Гибкость Rust важна при добавлении пользовательского кода к существующей программе, написанной в 1980-х годах.
Во время отладки эмулятора я написал простой дизассемблер, который может отображать сборку программ NES для каждой функции.
Наконец, я использовал сторонний эмулятор NES под названием Mesen, который может похвастаться богатым набором инструментов отладки. Он был полезен для понимания содержимого памяти и состояния графического чипа в конкретный момент.
В NES есть два разных типа графики:
фон представляет собой сетку из плиток 8x8 пикселов;
спрайты это плитки, которые можно рисовать в произвольных местах на экране.
В большинстве игр используется комбинация фонов и спрайтов, и Тетрис не исключение.
Спрайты в тетрисе используются, чтобы отрисовать текущую и следующую фигуры, а также фоновую графику для всего остального. На изображениях ниже демонстрируются два типа графики, с фоном вверху и спрайтами внизу.
В игре явно уже есть логика для рисования текущей фигуры с использованием спрайтов, поэтому самый простой способ визуализации конечного положения фигуры, похоже, заключается в повторном использовании этой логики, но с использованием контурной плитки, а не обычной.
Говоря о контурных плитках, я добавил в игру новую плитку, чтобы использовать её для призрачной фигуры:
Моя цель здесь выследить ту часть кода, которая рендерит текущую фигуры, чтобы повторно использовать этот код для рендеринга конечного положения фигуры.
Для рендеринга спрайтов на NES вы заполняете область основной памяти метаданными спрайта (положение, плитка и т. д.), а затем записываете адрес начала этой области памяти в регистр OAMDMA. (Прямой доступ к памяти атрибутов объекта OAM это специальная память для хранения метаданных спрайта, а DMA это общий термин для устройств, непосредственно считывающих и записывающих основную память.) Запись адреса в OAMDMA заставляет графическое оборудование NES копировать метаданные спрайта указанной области основной памяти и в специализированную память атрибутов объекта, которая будет использоваться во время рендеринга для рисования спрайтов.
Регистр OAMDMA отображается в адресное пространство ЦП по адресу 0x4014. Поиск в дизассемблированной программе по этому адресу показывает:
0xAB63 Lda(Immediate) 0x02 # load accumulator with 20xAB65 Sta(Absolute) 0x4014 # write accumulator to 0x4014
При этом значение 2 записывается в OAMDMA, в результате чего память от 0x0200 до 0x02FF копируется в OAM. И одна функция определённо передаёт управление подпрограмме как ответственная за заполнение буфера. Она находится в 0x8A0A и может многое рассказать о том, как работает Тетрис.
Она начинается с чтения значений из адресов 0x0040 и 0x0041, умножения каждого на 8 и добавления их к некоторым смещениям. На NES каждая плитка имеет размер 8x8 пикселей, так что это, по-видимому, переводится из координаты плитки в координату пиксела, где смещения являются компонентами координаты пиксела верхнего левого угла доски. Несколько минут копания в мезене подтверждают это: 0x40 это координата x, а 0x41 координата Y текущего фрагмента.
Затем функция считывает данные из 0x42. Это место всегда содержит значение от 0 до 12, которое, по-видимому, кодирует форму текущей фигуры, а также её вращение. Для фигур с вращательной симметрией (например, фигура S) несколько одинаковых вращений получают одно значение в 0x42. Я буду называть это значение индексом формы.
Каждая фигура в Тетрисе состоит из 4 плиток, и для каждой плитки рендерится один спрайт. Координаты в 0x40 и 0x41 это позиция фигуры, но для рендеринга спрайтов мы должны узнать положение каждой плитки. С этой целью эта функция обращается к таблице в ПЗУ по адресу 0x8A9C, которую я буду называть таблицей форм. Каждая из 13 частей (включая уникальные вращения) имеет 12-байтовую запись в таблице форм. Запись таблицы форм для фрагмента хранит по 3 байта для каждой из 4 плиток:
смещение плитки по оси y (относительно 0x41);
индекс спрайта, используемый при рендеринге плитки;
смещение плитки по оси x (относительно 0x40).
Эта функция вычисляет местоположение и индекс спрайта каждой плитки текущей фигуры и заполняет буфер OAM DMA этой информацией. Чтобы визуализировать призрачную фигуру, мне нужна аналогичная функция, за исключением того, что она отображает каждую плитку с контуром, а не плиткой из таблицы форм, и визуализирует фигуру с вертикальным смещением, так что фигура появляется в том месте, где она должна приземлиться после hard drop. Было бы нетривиально изменить эту функцию на месте, чтобы она была общей для призрачной и обычной фигуры, поэтому вместо этого я скопировал/вставил код и изменил его, чтобы сделать то, что мне нужно.
Сначала я стал использовать программу для просмотра памяти mesen, чтобы найти, казалось бы, неиспользуемую область ПЗУ. Я не знаю, что здесь делают строки с 0x00 и 0xFF! Также я не знаю, как изменить шрифт в mesen на Monospace!
Я выделил 512 байт памяти, начиная с адреса 0xD6D0. Первым кодом, который я добавил в эту область, была функция, которая просто вызывает существующую функцию обновления буфера DMA OAM:
b.label("oam-dma-buffer-update");// Call original functionb.inst(Jsr(Absolute), 0x8A0A);// Returnb.inst(Rts, ());
Мой инструмент для патча заменяет все вызовы исходной функции (0x8A0A) вызовами новой функции.
Затем я взял дизассемблированный код из исходной функции обновления буфера DMA OAM и вручную перевел его на язык rust для сборки NES.
Этот код:
0x8A0A Lda(ZeroPage) 0x400x8A0C Asl(Accumulator)0x8A0D Asl(Accumulator)0x8A0E Asl(Accumulator)0x8A0F Adc(Immediate) 0x600x8A11 Sta(ZeroPage) 0xAA...
превратился в:
b.label("render-ghost-piece"); // function label so it can be called by name laterb.inst(Lda(ZeroPage), 0x40);b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Asl(Accumulator), ());b.inst(Adc(Immediate), 0x60);b.inst(Sta(ZeroPage), 0xAA);...
Я изменил свою копию обновления буфера DMA OAM, чтобы использовать контурную плитку вместо плитки, считанной из буфера формы. Чтобы проверить это изменение, я обновил oam-dma-buffer-update, чтобы вызвать мою функцию вместо оригинала:
b.label("oam-dma-buffer-update");// Call new functionb.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());
Затем я заставил мою функцию рендеринга призрачной фигуры принимать аргумент, определяющий вертикальное расстояние, на котором должна оказаться призрачная фигура. В конце концов, это будет вычисляться на основе того, сколько раз фигура может сдвинуться вниз до столкновения, но сначала я попытался вызвать функцию с константой, равной 6.
b.label("oam-dma-buffer-update"); // Call original function first b.inst(Jsr(Absolute), 0x8A0A); // Render the ghost piece, passing the vertical offset argument in address 0x0028. b.inst(Lda(Immediate), 6); b.inst(Sta(ZeroPage), 0x28); b.inst(Jsr(Absolute), "render-ghost-piece"); // Return b.inst(Rts, ());b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Lda(Immediate), 6);b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");// Returnb.inst(Rts, ());
Теперь вычислим истинное вертикальное смещение от текущей фигуры до места, где она приземлится после падения. Наблюдая за памятью с помощью mesen, я заметил, что ничего, похоже, не работает с памятью от 0x0020 до 0x0028. Первые 256 байтов памяти называются нулевой страницей и обеспечивают более быстрый доступ, чем остальная часть памяти. Мне нужно 8 байтов нулевой страницы для хранения координат X, Y каждой плитки текущей фигуры и обнаружения столкновений, а также один дополнительный байт для хранения временных значений во время вычислений.
Начните с инициализации значений от 0x20 до 0x27 координатами X, Y каждой плитки текущей фигуры:
b.label("compute-hard-drop-distance"); // function label so it can be called by name laterconst SHAPE_TABLE: Address = 0x8A9C;const ZP_PIECE_COORD_X: u8 = 0x40;const ZP_PIECE_COORD_Y: u8 = 0x41;const ZP_PIECE_SHAPE: u8 = 0x42;// Multiply the shape by 12 to make an offset into the shape table,// storing the result in IndexRegisterX.b.inst(Lda(ZeroPage), ZP_PIECE_SHAPE); // read shape index into accumulatorb.inst(Clc, ()); // clear carry flag to prepare for arithmeticb.inst(Rol(Accumulator), ()); // rotate left: index * 2b.inst(Rol(Accumulator), ()); // rotate left: index * 4b.inst(Sta(ZeroPage), 0x20); // store index * 4 at 0x0020b.inst(Rol(Accumulator), ()); // rotate left: index * 8b.inst(Adc(ZeroPage), 0x20); // add to 0x0020: index * 12b.inst(Tax, ()); // transfer accumulator to IndexRegisterX// Store absolute X,Y coords of each tile by reading relative coordinates from shape table// and adding the piece offset, storing the result in zero page 0x20..=0x27.for i in 0..4 { // this is a rust loop - the assembly generated inside will be generated 4 times b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read Y offset from shape table b.inst(Clc, ()); // clear carry flag to prepare for addition b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y); // add to Y coordinate of piece b.inst(Sta(ZeroPage), 0x21 + (i 2)); // store the result in zero page b.inst(Inx, ()); // increment IndexRegisterX to sprite index b.inst(Inx, ()); // increment IndexRegisterX to X offset b.inst(Lda(AbsoluteXIndexed), Addr(SHAPE_TABLE)); // read X offset from shape table b.inst(Clc, ()); // clear carry flag to prepare for addition b.inst(Adc(ZeroPage), ZP_PIECE_COORD_X); // add to X coordinate of piece b.inst(Sta(ZeroPage), 0x20 + (i 2)); // store the result in zero page b.inst(Inx, ()); // increment IndexRegisterX to next tile}
Теперь о фактическом обнаружении столкновений! Неоднократно увеличивайте компонент Y каждой координаты плитки в адресах от 0x20 до 0x27, пока одна из плиток не столкнётся с зафиксированной плиткой или не выйдет за нижнюю часть поля. Изучая память с помощью mesen, я узнал, что состояние поля хранится в виде строкового массива индексов спрайтов, начинающихся с 0x0400, и что 0xEF это индекс плитки пустого пространства. Стратегия будет заключаться в использовании координаты каждой плитки для построения индекса в этом массиве и остановке, если будет найдено что-либо кроме 0xEF.
Возможная путаница в приведённом ниже коде заключается в том, что он реализует цикл в сборке, но есть также цикл for в rust, который генерирует сборку. Эти два цикла не связаны между собой. Сборка в цикле rust проходит 4 раза, и результат составляет тело цикла сборки.
const BOARD_TILES: Address = 0x0400;const EMPTY_TILE: u8 = 0xEF;const BOARD_HEIGHT: u8 = 20;b.inst(Ldx(Immediate), 0); // Load 0 into IndexRegisterX - this will be our loop counterb.label("start-ghost-depth-loop"); // This is a label - a target for branch instructionsfor i in 0..4 { // the assembly in this rust loop will be emitted 4 times // Increment the Y component of the coordinate b.inst(Inc(ZeroPage), 0x21 + (i * 2)); // Break out of the loop if the tile is off the bottom of the board b.inst(Lda(ZeroPage), 0x21 + (i * 2)); b.inst(Cmp(Immediate), BOARD_HEIGHT); b.inst(Bpl, LabelRelativeOffset("end-ghost-depth-loop")); // Multiply the Y component of the coordinate by 10 (the number of columns) b.inst(Asl(Accumulator), ()); b.inst(Sta(ZeroPage), 0x28); // store Y * 2 b.inst(Asl(Accumulator), ()); b.inst(Asl(Accumulator), ()); // accumulator now contains Y * 8 b.inst(Clc, ()); b.inst(Adc(ZeroPage), 0x28); // accumulator now contains Y * 10 // Now add the X component to get the row-major index of the cell b.inst(Adc(ZeroPage), 0x20 + (i * 2)); // Load the tile at that coordinate b.inst(Tay, ()); b.inst(Lda(AbsoluteYIndexed), BOARD_TILES); // Test whether the tile is empty, breaking out of the loop if it is not b.inst(Cmp(Immediate), EMPTY_TILE); b.inst(Bne, LabelRelativeOffset("end-ghost-depth-loop"));}// Increment counter and loopb.inst(Inx, ());b.inst(Jmp(Absolute), "start-ghost-depth-loop");b.label("end-ghost-depth-loop");
Это приводит к тому, что IndexRegisterX содержит количество повторений цикла, которое также является вертикальным расстоянием от текущей фигуры до того места, где она окажется после падения. Для удобства эта функция вернёт результат через регистр аккумулятора:
// Return depth via accumulatorb.inst(Txa, ()); // transfer IndexRegisterX to accumulatorb.inst(Rts, ()); // return
Вот полный код замещающей функции обновления буфера DMA OAM:
b.label("oam-dma-buffer-update");// Call original function firstb.inst(Jsr(Absolute), 0x8A0A);// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Check if the distance is 0, and skip rendering the ghost piece in this caseb.inst(Beq, LabelRelativeOffset("after-render-ghost-piece"));// Render the ghost piece, passing the vertical offset argument in address 0x0028.b.inst(Sta(ZeroPage), 0x28);b.inst(Jsr(Absolute), "render-ghost-piece");b.label("after-render-ghost-piece");// Returnb.inst(Rts, ());
Результат:
Теперь, когда выполняется рендеринг призрачной фигуры, следующий шаг сделать так, чтобы при нажатии кнопки вверх на контроллере происходило мгновенное падение. Кнопка вверх не используется в Тетрисе, поэтому нам не нужно беспокоиться о потере некоторых функций, чтобы получить hard drop.
Как и в случае с добавлением контуров фигуры, я решил найти функцию, которую я мог бы заменить новой функцией, которая вызывает оригинал перед выполнением дополнительных действий, в этом случае проверяется, была ли нажата кнопка вверх, и выполняется мгновенное падение фигуры, если да.
Моя первая попытка состояла в том, чтобы найти код, который считывает регистр состояния контроллера 0x4016, но, похоже, существует довольно много косвенных связей между чтением этого регистра и обновлением состояния игры на основе того, какие кнопки нажимаются.
Моя вторая идея заключалась в том, чтобы настроить мой эмулятор для логирования каждой выполненной инструкции. Я загрузил Тетрис и прошёлся по меню, чтобы начать новую игру, а затем сохранил файл состояния. У моего эмулятора есть возможность запускать определённое количество кадров. Я настроил его на работу в 20 кадров, загрузил файл состояния и записал каждую инструкцию, не нажимая никаких элементов управления. Затем я повторил этот процесс, но на этот раз я нажимал левую кнопку на протяжении 20 кадров. Теперь у меня было два лога потока инструкций один без нажатых элементов управления, а второй с их нажатием. Само собой разумеется, что в первую очередь эти потоки различаются, когда программа в первый раз разветвляется по состоянию левой кнопки.
Конечно же:
@@ -116912,9 +116912,175 @@ 0x89B8 Lda(ZeroPage) 0xB5 0x89BA And(Immediate) 0x03 0x89BC Bne(Relative) 0x15-0x89BE Lda(ZeroPage) 0xB6-0x89C0 And(Immediate) 0x03-0x89C2 Beq(Relative) 0x45+0x89D3 Lda(Immediate) 0x00+0x89D5 Sta(ZeroPage) 0x46+0x89D7 Lda(ZeroPage) 0xB6+0x89D9 And(Immediate) 0x01+0x89DB Beq(Relative) 0x0F...
Перекрёстная ссылка с дизассемблированным ПЗУ, эта функция начинается с:
0x89AE Lda(ZeroPage) 0x400x89B0 Sta(ZeroPage) 0xAE0x89B2 Lda(ZeroPage) 0xB60x89B4 And(Immediate) 0x040x89B6 Bne(Relative) 0x51 (relative: 0x51, absolute: 0x8A09)0x89B8 Lda(ZeroPage) 0xB50x89BA And(Immediate) 0x030x89BC Bne(Relative) 0x15 (relative: 0x15, absolute: 0x89D3)0x89BE Lda(ZeroPage) 0xB60x89C0 And(Immediate) 0x030x89C2 Beq(Relative) 0x45 (relative: 0x45, absolute: 0x8A09)...
Это ветвление на основе содержимого адресов 0x00B5 и 0x00B6. Во время наблюдения за этими адресами в mesen во время затирания элементов управления у меня создаётся впечатление, что 0xB5 хранит различия между кадрами в состоянии контроллера, а 0xB6 хранит текущее состояние контроллера. Несмотря на то что тетрис не использует её, состояние кнопки вверх отражается в этих значениях.
Я запустил эту функцию так же, как и мою замену для обновления буфера DMA OAM. Всё, что он сделал, это вызвал исходную функцию и вернул:
b.label("handle-controls");// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Returnb.inst(Rts, ());
Теперь добавим проверку, нажата ли кнопка вверх. А пока просто телепортируем текущую фигуры на фиксированную высоту при нажатии кнопки:
b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Set the current piece's Y coordinate to 7b.inst(Lda(Immediate), 7);b.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());
Вот код в действии, когда я несколько раз нажимаю вверх:
Затем замените тестовую константу 7 на фактическое положение, в котором деталь окажется после резкого падения. Используйте функцию compute-hard-drop-distance, которую мы написали для рендеринга призрачной части, а затем просто добавьте текущую позицию фигуры, чтобы получить абсолютную координату Y, в которой он окажется после падения:
b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);b.label("controller-end");// Returnb.inst(Rts, ());
Выглядит неплохо!
Однако имеется небольшая проблема со скоростью. Похоже, что игра ожидает окончания текущего тика, прежде чем создать следующую фигуру. После hard dropa текущий тик должен немедленно закончиться и следующая фигура должна появиться без задержки.
Глядя на память с помощью mesen, можно увидеть, что есть счётчик по адресу 0x0045, который ведёт отсчёт до некоторого числа, а затем сбрасывается на следующем тике (когда текущая фигура перемещается вниз сама по себе). Чтобы узнать больше, я заставил свой эмулятор записывать все инструкции и запускать игру в течение 13 тиков. Я выбрал 13, потому что казалось маловероятным, что они генерируются случайно.
Во время этого прогона таймер истёк бы 13 раз. Где-то в логах инструкций есть связанная инструкция, которая была выполнена ровно 13 раз. Давайте найдём!
Логи инструкций находится в файле с именем /tmp/log.txt:
cat /tmp/log.txt | sort | uniq --count | sort --numeric-sort
Мы сортируем инструкции по частоте. Просматривая те, которые были выполнены 13 раз, я заметил:
13 0x8958 Lda(Immediate) 0x0013 0x895A Sta(ZeroPage) 0x45
Это кажется актуальным, потому что он взаимодействует с таймером по адресу 0x0045!
Обращение к дизассемблированному коду этой инструкции:
0x8980 Lda(ZeroPage) 0x45 # load the timer value0x8982 Cmp(ZeroPage) 0xAF # compare with the value at 0x00AF0x8984 Bpl(Relative) 0xD2 (relative: D2, absolute: 8958) # branch if it was higher0x8986 Jmp(Absolute) 0x89720x8972 Rts(Implied)0x8958 Lda(Immediate) 0x00 # load 0 into the accumulator0x895A Sta(ZeroPage) 0x45 # store the accumulator (0) in the timer
Две последние инструкции устанавливают значение таймера в 0, и они выполняются ровно 13 раз. Единственный способ получить эти инструкции через ветвь (0x8984), что означает, что условие ветвления выполняется только 13 раз вероятно, один раз за такт. Таким образом, вероятное повествование состоит в том, что таймер увеличивается на единицу каждый кадр, а кадр, в котором он становится больше значения в 0xAF, отмечает конец текущего тика, в этот момент таймер сбрасывается, и текущая фигура перемещается вниз.
Наблюдаем за 0x00AF в mesen, и это, кажется, максимальное значение, которого достигает таймер в 0x0045. Кроме того, когда вы завершаете уровень, значение 0x00AF уменьшается, что ускоряет игру! Поэтому после hard drop просто установите значение таймера на значение 0x00AF:
b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);b.label("controller-end");// Returnb.inst(Rts, ());
Выглядит лучше, но всё равно есть большая задержка, если вы очень быстро опустите первую фигуру во время первого тика. Оказывается, первый тик занимает больше времени, чем все остальные тики. Глядя на память в mesen, я заметил, что значение 0x004E увеличивается во время первого тика. Для всех остальных тиков он установлен на 0. Установка его на 0 после появления hard dropa решает проблему с синхронизацией.
b.label("handle-controls");const CONTROLLER_STATE: u8 = 0xB6;const CONTROLLER_BIT_UP: u8 = 0x08;const TIMER: u8 = 0x45;const TIMER_MAX: u8 = 0xAF;const TIMER_FIRST_TICK: u8 = 0x4E;// Call the original functionb.inst(Jsr(Absolute), 0x89AE);// Skip to the end if the UP bit of the controller state is not setb.inst(Lda(ZeroPage), CONTROLLER_STATE);b.inst(And(Immediate), CONTROLLER_BIT_UP);b.inst(Beq, LabelRelativeOffset("controller-end"));// Compute distance from current piece to drop destination, placing result in accumulatorb.inst(Jsr(Absolute), "compute-hard-drop-distance");// Add the current piece's Y coordinateb.inst(Clc, ());b.inst(Adc(ZeroPage), ZP_PIECE_COORD_Y);// Update the current piece's Y coordinate with the resultb.inst(Sta(ZeroPage), ZP_PIECE_COORD_Y);// Set the timer to its maximum valueb.inst(Lda(ZeroPage), TIMER);b.inst(Sta(ZeroPage), TIMER_MAX);// Clear the first tick timerb.inst(Lda(Immediate), 0x00);b.inst(Sta(ZeroPage), TIMER_FIRST_TICK);b.label("controller-end");// Returnb.inst(Rts, ());
Кажется, это работает!
Исходный код инструмента исправления доступен на
github. Загрузите патч IPS, который применяет изменения,
описанные в этом посте,
здесь. Второй патч, который добавляет hard drop, но не
визуализирует конечное положение фигуры, доступен
здесь.
А если хотите создать свой игровой бестселлер, который, как и
детище Алексея Пажитнова войдёт в историю приходите к нам на курс
Разработчик игр на Unity, на котором мы рассказываем про все
тонкости разработки игр.
Узнайте, как прокачаться в других специальностях или освоить их с нуля:
Другие профессии и курсыПРОФЕССИИ
КУРС
Игра |
Объем, МБ |
Super Mario Odyssey |
5747 |
New Super Mario Bros. U Deluxe |
2572 |
Paper Mario: The Origami King |
6662 |
Mario Kart 8 Deluxe |
7059 |
Mario Tennis Aces |
2491 |
Luigi's Mansion 3 |
7554 |
Mario + Rabbids |
2980 |
Super Mario Party |
2885 |
Super Mario Maker 2 |
3135 |
VS. SUPER MARIO BROS. |
98 |
Марио и Соник на Олимпийских играх 2020 в Токио |
6574 |
Super Mario 3D All-Stars |
5013 |
Итого |
52770 |
Геймеры смирились с этим ещё в девяностые. Платформеры, шутеры от первого лица, и любой из жанров игр, основанных на фильме будет ужасным. Но вышла GoldenEye 007 и всё изменила.
Игру выпустили через два года после проката фильма в кинотеатрах, всего за несколько месяцев до выхода следующей части бондианы Завтра не умрёт никогда. И даже команда, ответственная за проект, особого успеха не ожидала. Затем картридж разошёлся ошеломляющим тиражом в 8 миллионов копий, попав в тройку самых продаваемых игр на 64-битной консоли Nintendo. В дальнейшем, GoldenEye 007 неизменно присутствовала в списках Лучших игр.
Примечательно, что для большинства разработчиков, GoldenEye стал первым серьёзным проектом. Тим и Крис Стамперы, руководители Rare, постоянно напоминали команде, что это не университетский проект и затягивать с разработкой не стоит (на игру ушло более трёх лет). Однако, поскольку большая часть коллектива были новичками в этом бизнесе, означал, что они не будут ограничены представлениями о том, что возможно, а что нет. Придумав хорошую идею они пытаются её реализовать.
Этот подход привёл к революционным результатам. В игре впервые реализована реакция на попадание по телу противника, оружие не привязано к игровой камере, есть продвинутое освещение (посмотрите внимательно, и увидите отражение окружения на блестящих поверхностях). Была даже снайперская винтовка и использование оружия двумя руками. Более того, GoldenEye доказала, что в сюжетный шутер от первого лица можно играть на консолях, и что режим Deathmatch в мультиплеере никогда не устареет.
Ключевое оружие в игре
Разработкой занималась команда Rare из девяти человек. После, некоторые остались в компании и работали над духовным продолжением игрой Perfect Dark. Другие ушли в Free Radical Design (позже Crytek UK) разработчик серии TimeSplitters. Третьи и вовсе покинули геймдев. Однако, все остаются согласны по двум вещам: это был опыт, который они никогда не забудут, а играть за Oddjob (громила из Голдфингера) в многопользовательской игре всегда жульничество.
Костяк проекта
Для Марка Эдмондса, работа началась с пробного задания в особняке Штамперов, где его оставили работать в одиночестве в одной из комнат. Я не знал, над чем работаю, объясняет Марк. Меня попросили исследовать создание скругленных соединений (filleted joints) для анимированной трёхмерной системы персонажей. По сути, это гладкая кожа над суставами такая как локоть, а не просто твёрдый блок (solid block) для верхней и нижней части руки. Я и понятия не имел, что это связано с Джеймсом Бондом, но тест прошёл и попал в команду. Было здорово начать работу над своей первой игрой!
Программист Марк присоединился к руководителю проекта Мартину Холлису и художнику Карлу Хилтону, и трио приступило к работе над ранними сборками. На игру сильно повлияла Virtua Cop от Sega, где игрок следовал определённому маршруту по уровням (ходил по рельсам). Мы использовали это удивительное новое изобретение, называемое аналоговым стиком, для наведения прицела, шутит Марк. Но потом подумали: разве не будет круто сыграть в такую игру, как Doom с настоящей 3D-графикой? Это будет новый опыт! И мы знали, что Nintendo 64 способна отображать трёхмерную графику в любой ориентации и направлении.
Команда решила снять Бонда с рельсов, хотя точного представления, на что способна N64, у них не было. В разработке использовались высокопроизводительные машины Silicon Graphics, с которыми приятно работать, хотя они и подвержены перегреву. Однако, спецификации будущей консоли Nintendo ещё не были определены. Я помню, как был разочарован, увидев технические демонстрации, запущенные на первых консолях для разработки, признаётся Марк. Но как только наши художники подключились к проекту им таки удалось добиться хорошей графики!
Марк увлёкся работой над движком GoldenEye, применяя такие пакеты, как Alias и GameGen срендерив их в игре. Он помог разработать систему, которая обрабатывает анимацию захвата движения новаторскую для того времени. Также она работает с искусственным интеллектом противника, чтобы враги хоть как-то противостояли игроку, прежде чем рухнуть в агонии, когда им прострелят пах. Тем не менее, несмотря на все эти новаторские приёмы, у команды не была уверености в том, что игроков это впечатлит.
Было потрясающе побывать на выставке E3 в 1997 году, но я не помню особой реакции на игру, говорит Марк. Вероятно, это была неподходящая среда для людей, чтобы окунуться в игру. И только потом, когда начали поступать отзывы, мы поняли, что людям действительно понравилось.
E3 1997 года
Дэвид Доак гордо улыбается, когда мы спрашиваем о подходе к дизайну миссий и искусственному интеллекту противника. Мне больше всего запомнился момент встречи с парнями из Valve на выставке ECTS в Великобритании в 1998 году. Тогда они шутили, что GoldenEye заставила их переделать кучу вещей в Half-Life.
Когда Дэвид присоединился к команде в конце 1995 года, базовый геймплей был готов. Управление было отзывчивым, основная механика встретить врага, застрелить и перейти к следующему работала эффективно, а звуковые и визуальные эффекты давали игроку обратную связь. Проблема заключалась в очень простых уровнях, объясняет он. Их создали для тестирования игрового процесса, хотя, даже на этом этапе, в них были инновационные функции, которые подчеркивали амбиции игры, например, система сигнализации на локации Severnaya Bunker. Её может активировать охранник, который побежал нажать большую красную кнопку, или камера, заметившая Бонда. Лучшим шутером тогда считали Doom, в котором игрок расстреливает монстров и собирает ключи, чтобы открыть двери, но мы хотели отойти от этого.
Хотя Дэвид и признает, что, по сути, они просто поменяли концепцию ключей на схожую: с декодерами, модемами и всевозможными гаджетами. Однако, он был полон решимости разнообразить темп и сложность уровней. Уровень Severnaya Bunker 1 очень простое и маленькое пространство, но на более высокой сложности, здесь начинаются проблемы с выполнением задачам, сигнализацией и врагами. Было приятно устроить побег из камеры в локации Bunker 2 стелс хорошо разнообразил игровой процесс и это была новая механика (Metal Gear Solid и Thief вышли позже GoldenEye).
Дэвид есть в игре в роли учёного-секретного агента, как и большая часть команды: их лица на безымянных охранниках или на экранах компьютерных мониторов. Дэвид удовлетворен разнообразием миссий и интересным дизайном уровней, но признаёт: не всё, что они пробовали, сработало. Цель некоторых уровней защищать объект от повреждений. Для более открытых уровней, таких как Runway и Depot, было сложно создать интересный игровой процесс, и результаты получились неоднозначными. И миссии по сопровождению казались хорошей идеей в то время. То есть, что может быть веселее, чем наблюдать, как ваш соратник Наталья бросается под пули или бежит к месту подрыва?
Герои и злодеи GoldenEye
Несмотря на эти осечки, сюжетный режим был новаторским, предлагая игрокам выбор. Пройти уровень расстреливая всё что движется или действовать аккуратно? Мимо охранников можно проскользнуть, но как было здорово убить противника выстрелом в голову. То, как враг реагировали на действия, и неожиданные, иногда раздражающие, вмешательства NPC, создавали ощущение живого мира.
Работа над ИИ велась в сотрудничестве с Марком Эдмондсом, рассказыает Дэвид. Например, я придумал как сделать сцену более интересной. Марк покачал головой, объяснил, почему это невозможно затем вернулся к своему столу и проделал какое-то чудо кодирования, чтобы это произошло. Что тут сказать он легенда.
В титрах Би Джонс указана дизайнером костюмов. Возможно, это ироничная отсылка к миру кино, но она оказалась на удивление уместной, поскольку никто не умеет создавать пиксельные смокинги лучше неё. Изначально в игре было ещё три Бонда, объясняет она. Мне пришлось сделать разные текстуры смокинга в текстуре 64x32 пикселей. Роджер Мур получил белый с гвоздикой, Тимоти Далтон двубортный, а Шон Коннери надел классический смокинг шестидесятых. И все они очень похожи на свои версии из кино.
Поскольку в середине 90-х не было удобных интернет-библиотек, из которых можно было взять материал, Би использовала собственную коллекцию книг и памятных вещей о Бонде. Фотографии персонажей были отсканированы, инструкции по эксплуатации гаджетов тщательно изучены, а обеденные перерывы посвятили просмотру фильмов о Бонде на видеокассетах. У нас был перерыв всего на полчаса, поэтому мы могли просмотреть только 20 минут видео, вспоминает Би.
У команды был ограниченный доступ к съёмочной площадке, и они принесли новую цифровую камеру на студию Leavesden Studios в Хартфордшире (Великобритания), чтобы заснять как можно больше. Камера была огромной, очень тяжёлой и стоила около двух тысяч долларов, негодует Джонс. Но, именно так, мы собрали большую часть справочных материалов. Я использовала ту же камеру, чтобы сфотографировать лица, которые мы применяли в игре. Делала снимки спереди, сбоку, сзади и сшивала их вместе. Это время до Photoshop, поэтому у меня была только программа для рисования пикселей NinGen и всего лишь 38x32 пикселей для текстур. Попробуйте нарисовать там правдоподобное человеческое лицо!
Сейчас довольно легко сделать анимацию, чтобы персонаж поднял бровь в духе Роджера Мура, но в то время, это была новаторская работа, как и использование технологии захвата движения. Первоначально, установка представляла собой магнитную систему под названием стая птиц (flock of birds), в которой на теле были закреплены огромные маркеры. Надо быть осторожным с движениями, иначе они отлетят от стены и ударят исполнителя, вспоминает Би Джонс.
Наиболее эффективно, новая технология применялась, чтобы заставить врагов выглядеть так, будто в них действительно попали. Выстрелите им в плечо и они отшатнутся, а в результате попадания в голову рухнут на пол. Мы хотели, чтобы анимация выглядела так, будто жертва этого не ожидала, поэтому попросили члена команды Дункана Ботвуда закрыть глаза, а я внезапно хлопнула его по плечу, не сказав об этом. Мы не хотели, чтобы противники были приклеены к земле, поэтому обвязывали верёвкой его талию, а потом сбивали с ног. Было много мягких матов, но не думаю, что сейчас такое сойдёт с рук: со всей этой заботой о здоровье и безопасности сотрудников.
Именно внимание к деталям во всех аспектах визуального оформления помогло сделать игру такой захватывающей. После ухода из Rare Би Джонс работала в кино и на телевидении, в том числе на фильмах Доктор Кто и Стражи Галактики, но она поддерживает связь с товарищами. Мы задали ей вопрос: мешал ли тебе факт того, что ты была единственной женщиной в команде? Нет и нет, и очевидно, что вы просто со мной близко не знакомы, прямо отвечает она.
Карл Хилтон до сих пор помнит, когда Мартин Холлис задал этот вопрос: Он начал со слов: тебе нравится Джеймс Бонд? смеётся Карл, который тогда только присоединился в команду к Мартину главным художником. Я большой фанат Бонда, и этот сарказм на тему плохих игр по фильмам звучал очень смешно. Меня беспокоила репутация игр по фильмами. Я только попал в Rare и знал, что они не выпускают плохие игры. Но помню, как смотрел на Blast Corps (другой проект Rare), который делали по соседству с нами, и думал, что он будет намного популярнее, чем наше творение.
Карл усмотрел потенциал использования старых фильмов о Бонде, на которых вырос, особенно тех, где снимался его любимый Бонд Роджер Мур. Первоначально, он хотел включить базу подводных лодок из части Шпион, который меня любил, но понимая, что это будет слишком сложно выбрал базу шаттлов из фильма Лунный гонщик. Множество пасхалок из бондианы и приём врезания камеры в затылок агента 007 в начале каждого уровня позволили игрокам почувствовать себя Джеймсом Бондом.
Мы хотели подчеркнуть, что вы играете за Бонда, но в шутере от первого лица редко появляется шанс увидеть себя со стороны, говорит Карл. И эти приёмы отлично работали. Кому только не доставалась роль Бонда в кино, и теперь эта возможность появилась у игрока.
Тонкие штрихи, такие как кинематографическая завеса крови стекающая по экрану, когда вы умираете, и манжеты смокинга чётко видимые, когда вы проверяете свои часы на предмет важной информации о миссии, всё это добавило атмосферы бондианы. У часов были и другие цели, объясняет Карл. Мы все согласились с тем, что сведение к минимуму иконок на экране даст максимальное ощущение погружения, а часы помогут почувствовать себя агентом 007, а не персонажем обычного шутера. Правда, часто шутили над тем, насколько глупо выглядел бы Бонд, если так пристально станет разглядывать свои часы в фильмах.
Это подводит нас к ключевому вопросу: насколько важна была лицензия по фильму? Игра осталась бы хорошо продуманным шутером со множеством инновационных функций, но имела бы она такой коммерческий успех без Бонда?
То, что могло быть истолковано, как жестокий шутер от первого лица стало открыто для более широкой семейной аудитории, потому что в культурном отношении Джеймсу Бонду разрешено убивать людей, и его не считают плохим, утверждает Карл. Это означало, что дети могли попросить родителей об игре! Я думаю, что, в любом случае, получилась бы хорошая игра, но сомневаюсь, что она бы так проникла в поп-культуру. И Perfect Dark это подтверждает. Она почти во всех отношениях превосходила GoldenEye, поскольку мы уже многому научились к тому времени, но при этом было продано в два раза меньше копий. Возможность сыграть за Джеймса Бонда отличный аргумент для покупки игры.
Это перевод статьи из 178 номера журнала Retro Gamer.
Сегодня в это трудно поверить, но в 80-е и 90-е двухмерные битемапы безраздельно властвовали, особенно в аркадных залах. С тех пор, как Ёсихиса Кишимото усовершенствовал формулу с помощью Renegade и Double Dragon, все разработчики стремились повторить успех, и многим это даже удалось.
Хотя к этому времени компания Sega уже представила несколько популярных проектов вроде Altered Beast, Golden Axe и Alien Storm, именно Capcom оседлала этот жанр спасибо таким хитам, как Dynasty Wars, Captain Commando и Final Fight. Подвиг Хаггара, Коди и Гая, расчищавших улицы Метро Сити от всякой шпаны, обернулся для Capcom большим успехом, и вскоре Final Fight была лицензирована для множества домашних компьютеров: от ZX Spectrum до 16-битной Амиги. Компания Nintendo быстро осознала популярность творения Capcom и сделала его своим консольным эксклюзивом для SNES.
Вырезанные кооператив и целый уровень (промышленная зона, если вам интересно) не помешали игре стать успешной, причём настолько, что были выпущены два эксклюзивных сиквела только для консоли Nintendo. Тем временем Sega была вынуждена просто наблюдать, как один из самых популярных аркадных проектов бьёт рекорды продаж на приставке её конкурента. Как показывает история, лучший порт Final Fight вышел именно на Sega Mega-CD, но до этого оставались ещё годы, а Sega не умела предсказывать будущее. Компании нужен был хит, который побьёт популярный эксклюзив Nintendo, и она нуждалась в нём прямо сейчас.
Поэтому перед Sega встала задача создать свою собственную игру, которая будет не хуже Final Fight и даже превзойдёт её. Этим проектом стала Streets of Rage, которая дебютировала на консоли Mega Drive в августе 1991 года, через девять месяцев после успешного выпуска Final Fight для SNES.
Мы взяли интервью у Ацуши Сеймии, расспросив его о работе над культовым битемапом. Во время создания Streets of Rage (или Bare Knuckle, как игру называли в Японии) он работал художником. Первый вопрос заключался в следующем: всегда ли Streets of Rage планировалась как ответ Sega на популярный эксклюзив для SNES?
Не могу этого отрицать, честно отвечает Ацуши. На самом деле мы купили аркадный автомат с оригинальной игрой и долго изучали её всей командой.
Внимательное изучение игры от Capcom окупилось. Streets of Rage предлагает множество преимуществ по сравнению с Final Fight на SNES. В первую очередь, можно играть с другом, а это куда веселее. Streets of Rage позволяет выбирать из трёх бойцов, у которых есть счёты с мистером Икс, контролирующим город и полицию. Как и в Final Fight, каждый герой уникален, у каждого есть свои сильные и слабые стороны. Адам Хантер основан на Хаггаре он более медленный, но при этом сильный герой, умеющий справляться с группами противников. Блейз Филдинг больше похожа на Гая она куда быстрее, но не способна выдерживать сильные удары. Последний герой Аксель Блейз, он обладает всеми способностями Коди, но не такой прыгучий, как его товарищи. Все трое являются бывшими полицейскими и поклялись передать мистера Икс в руки правосудия.
Нам было интересно узнать, за что отвечал Сеймия во время создания игры. Как художник, я работал над широким спектром задач: от проработки персонажей до ландшафта, объясняет он. Я впервые занимался главными героями, поэтому пришлось многократно переделывать свою работу. Возможно, для молодого художника это было боевым крещением, но переделки в итоге окупились. Многие уровни очень похожи на проект от Capcom например, городские улицы и промышленные районы. Но хватает и разнообразия за счёт прекрасных пляжных участков, увлекательной поездки на лифте и заключительного тяжёлого прохода по коридору, ведущему к пентхаусу мистера Икс, перед битвой с которым игроку предстоит сразиться с боссами из всех предыдущих уровней. Неудивительно, что внешне Streets of Rage был крайне не оригинальным проектом: Сеймия признал, что Sega просто давала игрокам то, чего они хотели: В Final Fight, Double Dragon и других аркадных играх того времени использовался реалистичный визуальный стиль, и я думаю, что это было просто в тренде, признаёт Сеймия.
Возможно, игра и выглядит очень похожей на некоторые из популярных битемапов того времени, но ей удаётся выделяться благодаря разнообразию локаций, интересному дизайну уровней, а также куче интересных врагов и боссов самых разных форм и размеров. В дополнение к простым уличным головорезам имеются ниндзя (как в Teenage Mutant Ninja Turtles), госпожи с хлыстами, сумасшедшие, жонглирующие топорами и факелами, а также мастера боевых искусств.
Боссы тоже очень разнообразны: от маньяков с когтями до борцов, похожих на Последнего воина (американский рестлер, известный под псевдонимом The Ultimate Warrior). Другие запоминающиеся главари это здоровый огнедышащий толстяк, а также Мона и Лиза две девушки, которых фактически сделали из Блейз путём смены палитры. В итоге Сеймия поработал над всеми персонажами в игре: Команда дизайнеров поручала мне создавать модели на основе историй персонажей, их размера в пикселях и других справочных материалов, вспоминает он. Я так и поступал, хотя и было довольно сложно анимировать персонажа с ограниченным количеством пикселей.
Когда мы обсуждали боссов в игре, всплыла интересная информация. Хоть Сеймия и не сообщил, кого считает своим любимым персонажем, но рассказал, что на пятом уровне игры вместо Моны и Лизы планировался другой босс. Может, однажды мы узнаем, как выглядел этот тип, но теперь понятно, почему вместо оригинального злодея мы получили две копии Блейз.
Несмотря на силы и размеры главарей в Streets of Rage, главные герои способны с ними справиться. Все трое имеют доступ к сериям ударов ногами и кулаками, а также умеют наносить удар в прыжке и применять захваты. В этом нет ничего революционного, но управление очень отзывчиво, поэтому игроки чувствуют полный контроль над своими героями в бою. Противники тоже не согласны работать грушами для битья: они могут схватить игрока и бросить его на землю. Но вы можете приземлиться на ноги своевременным нажатием кнопки прыжка и даже атаковать, когда вас берут в захват. Можно даже объединиться со своим товарищем, чтобы выполнять совместные приёмы, а при нажатии на кнопку A приезжает полицейская машина, и бывший коллега героев уничтожает всех врагов с помощью ракетницы. К сожалению, Сеймия не отвечал за боевую механику, но вот что думает по этому поводу: Мне очень нравится, как в игре реализовали совместные приёмы для двух игроков.
Еще одним аспектом игры, фанатом которого является Ацуши, стал прекрасный саундтрек. Музыка очень важна, поэтому мы позвали Юдзо Косиро, вспоминает он. До этого Косиро написал отличную музыку к игре The Revenge of Shinobi. А поскольку глава разработки Streets of Rage, Нориёси Охба, также руководил серией Shinobi, договориться о совместной работе с композитором было нетрудно.
Сеймия считает, что именно режиссёрский стиль Охбы привёл к необычному для того времени множеству концовок в игре. Например, одна из них даёт игроку возможность объединиться с мистером Икс. Как мне кажется, это черта его стиля: добавить неожиданный поворот в сценарий, утверждает Ацуши. К слову, разработка Streets of Rage была завершена лишь с точки зрения игрового процесса. Релизная версия это то, что решила выпустить Sega, а сами авторы хотели добавить в игру куда больше контента. Много чего не попало в финальную версию, но геймплейные механики мы успели реализовать все до единой, говорит Сеймия.
Streets of Rage получила признание критиков и показала отличные продажи. Её успех на консоли Mega Drive привёл к тому, что в 1992 году появился урезанный порт для Game Gear, в котором нет Адама. А в 1993 году для PAL-региона вышла версия для Master System без режима кооперативной игры. Мы спросили, причастен ли Сеймия к этим портам низкого качества, и он ответил: Нет, я не работал над этими проектами.
Хотя оригинальная Streets of Rage снискала огромный успех, именно продолжение, выпущенное в 1992 году, заставило геймеров обратить внимание на серию. В сиквеле всё лучше: от музыки (написанной тем же Юдзо Косиро) до более детальных спрайтов и большего количества боевых приемов. Многие игроки до сих пор считают Streets of Rage 2 вершиной жанра. Сеймия работал над сиквелом в качестве художника фонов. Он также участвовал в создании Streets of Rage 3 1994 года, печально известной своим боксёрским кенгуру и большим количеством различий между западной и восточной версиями игры. Есть даже серия комиксов, но Сеймия тут уже точно ни при чём.
Несмотря на спорный триквел, серия Streets of Rage остаётся примером отличных битемапов и важным релизом в 16-битной битве между Sega и Nintendo. Недавно выпущенная четвёртая часть понравилась фанатам серии и поклонникам жанра: да, там нет запоминающегося саундтрека, но в остальном у Lizardcube и Guard Crush Games получилось создать достойное продолжение.
Это перевод статьи из 133-го номера журнала Retro Gamer. Ранее выкладывал материал на ресурсе Идеальный Пиксель сайт понравится всем фанатам ретро-игр и старых компьютеров.
Ревизия |
Nintendo Switch 2017 |
Nintendo Switch 2019 |
SoC |
NVIDIA Tegra X1, 20 нм, 256 ядер GPU, NVIDIA Maxwell |
NVIDIA Tegra X1, 16 нм, 256 ядер GPU, NVIDIA Maxwell |
RAM |
4 ГБ, Samsung LPDDR4, 3200 Мбит/с, 1,12 В |
4 ГБ, Samsung LPDDR4X, 4266 Мбит/с, 0,65 В |
Встроенная память |
32 ГБ |
|
Дисплей |
IPS, 6,2'', 1280720 |
IPS IGZO, 6,2'', 1280720 |
Аккумулятор |
4310 мАч |
Консоль |
Nintendo Switch 2017 года |
Nintendo Switch 2019 года |
Время автономной работы, 50% яркости дисплея |
3 часа 5 минут |
5 часов 2 минуты |
Время автономной работы, 100% яркости дисплея |
2 часа 25 минут |
4 часа 18,5 минуты |
Максимальная температура задней крышки |
46 C |
46 C |
Максимальная температура на радиаторе |
48 C |
46 C |
Максимальная температура на радиаторе в доке |
54 C |
50 C |
Игра |
Объем, ГБ |
Super Mario Odyssey |
5,7 |
Mario Kart 8 Deluxe |
7 |
New Super Mario Bros. U Deluxe |
2,5 |
Paper Mario: The Origami King |
6,6 |
Xenoblade Chronicles: Definitive Edition |
14 |
Animal Crossing: New Horizons |
7 |
Super Smash Bros. |
16,4 |
DRAGON QUEST XI S: Echoes of an Elusive Age Definitive Edition |
14,3 |
The Legend of Zelda: Links Awakening |
6 |
The Legend of Zelda: Breath of the Wild |
14,8 |
Bayonetta |
8,5 |
Bayonetta 2 |
12,5 |
ASTRAL CHAIN |
10 |
Witcher 3: Wild Hunt |
28,7 |
Doom |
22,5 |
Wolfenstein II: The New Colossus |
22,5 |
The Elder Scrolls V: Skyrim |
14,9 |
L.A. Noire |
28,1 |
Assassins Creed: Мятежники. Коллекция (Assassins Creed IV: Черный флаг + Assassins Creed Изгой) |
12,2 |