Разработка игр под NES на C. Главы 17-21. Своя игра +8


В этой части соберем все вместе и сделаем простую скроллерную стрелялку на космическую тему: корабль летит и лазерами отстреливает врагов


<<< предыдущая следующая >>>


image
Источник


Планирование


Нам нужно реализовать такие режимы работы игры:


  • Заставка
  • Игровой режим
  • Режим паузы
  • Экран проигрыша
  • Битва с боссом
  • Экран победы

Код нужно организовать примерно так:


  1. Инициализация
  2. Отрисовка заставки
  3. Оживление заставки:
    • Ожидание кнопки Старт
    • Фоновая музыка
  4. Отрисовка игрового экрана
  5. Игровой цикл:
    • Получение события джойстика
    • Движение корабля
    • Появление врагов
    • Движение врагов и снарядов
    • Обработка коллизий
    • Фоновая музыка
  6. Если закончились жизни, то показать экран проигрыша
    • Опять музыка
    • Возврат к заставке
  7. Если игра пройдена, то перейти к битве с боссом
  8. При победе:
    • Показать экран победы
    • И опять включить соответствующую музыку

И еще надо нарисовать всю нужную графику и написать музыку.


Начнем с экрана заставки, потом сделаем игровой экран. Для показа жизней и очков используем Спрайт 0. Прокрутка будет вертикальная. Вот макет:


image


Кое-что из текстов тоже будет реализовано спрайтами, например "Пауза" и "Конец игры". Это упростит разработку.


Пишем код


Первый логически связный кусок игры — заставка, игровой экран и экран паузы. Переход между ними будет по кнопке Старт.


Заставку проще всего нарисовать в Фотошопе. Arial Black хорошо смотрится в названии игры, особенно после добавления небольшой перспективы и пары фильтров. Сжимаем до 128 пикселей в ширину, 4 цвета, и переносим в YY-CHR.


image


Корабль и звезды для фона можно делать сразу в YY-CHR. Звезды набросаем рандомно, а NES Screen Tool отлично упакует их в RLE .h файл. Текст сохраняем в таблицы имен.


image


Сразу же можно сделать и экран победы, временно повесив его на Селект. Музыку тоже импортирую на этом этапе, эффекты можно переключать стрелками. Потом все это удалю. Вообще, более правильно добавлять музыку в самом конце — один и тот же трек во время отладки очень быстро начинает раздражать.


Теперь можно реализовать корабль и логику его движения стрелками. Он продолжает двигаться с небольшой инерцией несколько кадров после отпускания кнопки — легкий намек на законы движения в космосе. В принципе, это можно будет отключить по итогам тестов.


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


Фон со звездами
Vert_scroll2 = ((Vert_scroll & 0xF8) << 2);
Sprite_Zero(); // ждем коллизии нулевого спрайта
PPU_ADDRESS = 0;
SCROLL = Vert_scroll;
SCROLL = 0;
PPU_ADDRESS = Vert_scroll2;

Тут возникли затруднения: через несколько секунд все разъезжалось. В отладчике FCEUX можно поставить брейкпоинты на запись в регистры, поставил их на регистры управления прокруткой — $2000, $2005, $2006. Значения записывались правильные, но установка верхней координаты экрана должна быть в V-blank, а в реальности получалась на 40-й строке. Это получилось из-за музыки, процедуры которой не поместились в V-blank. Переставил их в самый конец очереди, все стало работать нормально.


Теперь можно заняться разными режимами игры.


main()
void main (void){
  while (1) { // бесконечный цикл
    while (GameMode == TITLE_MODE){ 
    // Заставка
    }
    while (GameMode == RUN_GAME_MODE){ 
    // Игра
    }
    while (GameMode == PAUSE_MODE){ 
    // Пауза
    }
    while (GameMode == GAME_OVER_MODE){ 
    // Конец игры
    }
    while (GameMode == VICTORY_MODE){ 
    // Победа
    }
  }
}

Спрайтовые объекты лучше реализовать структурами:


struct ENEMY
struct ENEMY {
unsigned char anime; // номер спрайта
unsigned char dir; // направление - если 0, то отзеркаливаем влево
unsigned char Y; // верх
unsigned char X; // левый край
unsigned char delay; // задержка начала движения
unsigned char type; // тип объекта
unsigned char move; // куда его двигать
unsigned char count; // насколько уже переместился
};

Враги будут набегать волнами, так что его тип будет устанавливаться перед ее началом.
Дропбокс
Гитхаб


Отдельная структура для снарядов:


struct BULLET
struct BULLET {
unsigned char Y; // y = 0 - объект за экраном, и не отображается
unsigned char Y_sub;
unsigned char tile;
unsigned char attrib;
unsigned char X;
unsigned char X_sub;
unsigned char Y_speed; // в старшем полубайте скорость, в младшем - ускорение
unsigned char X_speed;
};

Спрайты надо реализовать чуть по другому — динамически отрисовывать их каждый кадр. Спрайты лежат в буфере OAM, адреса в памяти $200-$2FF, и сначала находятся за экраном — вертикальная координата больше 0xF0. Затем отрисовываю нулевой спрайт, а после этого уже каждый активный спрайт помещаю в буфер. Порядок размещения спрайтов в буфере меняется каждый кадр, так что спрайты мерцают.


Метаспрайты бОльшего размера надо подготовить в NES Screen Tool. Код из примера Shiru мне не понравился, пришлось переписать. В частности, когда метаспрайт выходит за границу экрана, он не появляется с противоположной стороны, а просто теряется из виду. Это не вяжется с логикой игры, хоть и работает быстрее. Кроме того, возникли затруднения с отражениями спрайтов. Пришлось писать скрипт на Пайтоне, который конвертирует готовые метаспрайты в формат, удобный для импорта в код. Это слегка ускорило процесс.


На этом этапе кнопка Вниз добавляет снаряды, Селект — добавляет врагов. Для некоторых из них работает отзеркаливание. Обработка коллизий переписана на Ассемблере и позволяет увеличить количество объектов.


image


Дропбокс
Гитхаб


А теперь надо переписать все еще раз. Обработка нулевого спрайта и управление прокруткой происходят в обработчике NMI. Каждый кадр происходят примерно такие действия:


  1. Получение событий джойстика
  2. Спрайты из буфера выносятся за экран, нулевой спрайт кладется на место
  3. Если Master_Delay обнуляется в этом кадре, то запускается волна врагов. Для копирования из ROM в RAM используется memcpy.
  4. Все ли враги убиты? При смерти координата Y обнуляется
  5. Есть ли попадания по нашему кораблю? Если да, то уменьшается счетчик жизней, и по соответствующему таймеру на несколько кадров рисуется взрыв вокруг корабля.
  6. Если были нажаты стрелки, двигаем корабль
  7. Есть ли попадания по врагам? Если да, то рисуем взрыв и обнуляем ему Y
  8. Рендерим все спрайты — пишем их данные в OAM
  9. Играем музыку. Этот этап можно перенести в NMI
  10. Обновляем счетчик очков
  11. Если надо, переходим в режим Паузы по кнопке Старт
  12. Если счетчик жизней ушел в минус, переходим в Конец игры

Нужно придумать способ, как красиво разместить вражеские корабли в начале каждой волны. Появляются они тоже не одновременно. Каждый враг активен и двигается, пока по нему не попадут, или пока он не уйдет за экран. Когда все враги из волны исчезнут тем или иным способом, активируется таймер Master_Delay, отсчитывающий кадры до следующей волны. Когда волны закончатся, начинается режим Босса.


У Босса есть список возможных ходов и хитпоинты, снимающиеся при попаданиях. Когда они обнулятся, включаются нехитрые звуковые эффекты и тряска экрана. После этого игра переходит в режим Победы. Из него можно начать заново, с сохранением жизней и очков.


Остальное предлагаю смотреть в коде игры. Используйте его как есть, или как основу для своего проекта. Спасибо за внимание!


image
Дропбокс
Гитхаб


Благодарности


Хочу поблагодарить всех, кто помогал мне изучить программирование для NES, особенно участников форума forum.nesdev.com.


Очень много я почерпнул из примеров кода для cc65, которые написал Shiru. Кое-что из этих примеров использовано в этом туториале. Он же автор Famitone2 и NES Screen Tool. Его сайт с играми и примерами: раз и два.


Две его игры продаются на GreetingCarts (Retroscribe) (сайт мертв — прим. перев.)


Хочу поблагодарить THEFOX за его помощь, когда я только начинал осваивать cc65. И за примеры, которые раньше были на его сайте.


Но поиграть в его игру, Streemerz, все равно можно.


Rainwarrior сделал демо Coltrane, и дал хороший пример работы со звуком


А еще у него есть игра Lizard Game.


Всем спасибо!


А мне теперь надо сделать то же самое, но для приставки SNES...




К сожалению, не доступен сервер mySQL