- PVSM.RU - https://www.pvsm.ru -

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

В этой части соберем все вместе и сделаем простую скроллерную стрелялку на космическую тему: корабль летит и лазерами отстреливает врагов
<<< предыдущая [1] следующая >>>
image [2]
Источник [3]

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

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

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

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

  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

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

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

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

Фон со звездами

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; // насколько уже переместился
};

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

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

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
Дропбокс [7]
Гитхаб [6]

А теперь надо переписать все еще раз. Обработка нулевого спрайта и управление прокруткой происходят в обработчике 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
Дропбокс [8]
Гитхаб [6]

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

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

Очень много я почерпнул из примеров кода для cc65, которые написал Shiru. Кое-что из этих примеров использовано в этом туториале. Он же автор Famitone2 и NES Screen Tool. Его сайт с играми и примерами:
https://shiru.untergrund.net/software.shtml [10]
http://shiru.untergrund.net/articles/programming_nes_games_in_c.htm [11]

Две его игры продаются на GreetingCarts (Retroscribe):
http://www.greetingcarts.com/ [12] (сайт мертв — прим. перев.)

Хочу поблагодарить THEFOX за его помощь, когда я только начинал осваивать cc65. И за примеры, которые раньше были на его сайте:
https://www.fauxgame.com/ [13]
Но поиграть в его игру, Streemerz, все равно можно.

Rainwarrior сделал демо Coltrane, и дал хороший пример работы со звуком:
http://www.rainwarrior.ca/music/coltrane_src.zip [14]
А еще у него есть игра Lizard Game:
http://lizardnes.com/ [15]

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

Автор: Вадим Марков

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/razrabotka/274949

Ссылки в тексте:

[1] <<< предыдущая: https://habrahabr.ru/post/349742/

[2] Image: https://habrahabr.ru/post/350426/

[3] Источник: http://www.dailymail.co.uk/sciencetech/article-4060104/The-birth-gaming-legend-Nintendo-releases-original-drawings-used-create-Zelda.html

[4] одной из статей: https://habrahabr.ru/post/349376/

[5] Дропбокс: http://dl.dropboxusercontent.com/s/vcnifnoooflgilq/spacy.zip

[6] Гитхаб: https://github.com/BubaVV/nesdoug

[7] Дропбокс: http://dl.dropboxusercontent.com/s/fczfdpahrdgb7rl/spacy2.zip

[8] Дропбокс: http://dl.dropboxusercontent.com/s/70f89x9viu4r8mw/Spacy4.zip

[9] forum.nesdev.com: http://forum.nesdev.com

[10] https://shiru.untergrund.net/software.shtml: https://shiru.untergrund.net/software.shtml

[11] http://shiru.untergrund.net/articles/programming_nes_games_in_c.htm: http://shiru.untergrund.net/articles/programming_nes_games_in_c.htm

[12] http://www.greetingcarts.com/: http://www.greetingcarts.com/

[13] https://www.fauxgame.com/: https://www.fauxgame.com/

[14] http://www.rainwarrior.ca/music/coltrane_src.zip: http://www.rainwarrior.ca/music/coltrane_src.zip

[15] http://lizardnes.com/: http://lizardnes.com/

[16] Источник: https://habrahabr.ru/post/350426/?utm_source=habrahabr&utm_medium=rss&utm_campaign=350426