Разработка игр под NES на C. Главы 17-21. Своя игра
И еще надо нарисовать всю нужную графику и написать музыку.
Начнем с экрана заставки, потом сделаем игровой экран. Для показа жизней и очков используем Спрайт 0. Прокрутка будет вертикальная. Вот макет:
Кое-что из текстов тоже будет реализовано спрайтами, например "Пауза" и "Конец игры". Это упростит разработку.
Пишем кодПервый логически связный кусок игры — заставка, игровой экран и экран паузы. Переход между ними будет по кнопке Старт.
Заставку проще всего нарисовать в Фотошопе. Arial Black хорошо смотрится в названии игры, особенно после добавления небольшой перспективы и пары фильтров. Сжимаем до 128 пикселей в ширину, 4 цвета, и переносим в YY-CHR.
Корабль и звезды для фона можно делать сразу в YY-CHR. Звезды набросаем рандомно, а NES Screen Tool отлично упакует их в RLE .h файл. Текст сохраняем в таблицы имен.
Сразу же можно сделать и экран победы, временно повесив его на Селект. Музыку тоже импортирую на этом этапе, эффекты можно переключать стрелками. Потом все это удалю. Вообще, более правильно добавлять музыку в самом конце — один и тот же трек во время отладки очень быстро начинает раздражать.
Теперь можно реализовать корабль и логику его движения стрелками. Он продолжает двигаться с небольшой инерцией несколько кадров после отпускания кнопки — легкий намек на законы движения в космосе. В принципе, это можно будет отключить по итогам тестов.
Дальше идет счетчик очков, обновляемый каждый кадр. Он сделан в другой таблице имен, их подмена в середине кадра реализована через нулевой спрайт. Эта техника подробно описана в одной из статей.
Тут возникли затруднения: через несколько секунд все разъезжалось. В отладчике FCEUX можно поставить брейкпоинты на запись в регистры, поставил их на регистры управления прокруткой — $2000, $2005, $2006. Значения записывались правильные, но установка верхней координаты экрана должна быть в V-blank, а в реальности получалась на 40-й строке. Это получилось из-за музыки, процедуры которой не поместились в V-blank. Переставил их в самый конец очереди, все стало работать нормально.
Теперь можно заняться разными режимами игры.
Спрайтовые объекты лучше реализовать структурами:
Враги будут набегать волнами, так что его тип будет устанавливаться перед ее началом. Дропбокс Гитхаб
Отдельная структура для снарядов:
Спрайты надо реализовать чуть по другому — динамически отрисовывать их каждый кадр. Спрайты лежат в буфере OAM, адреса в памяти $200-$2FF, и сначала находятся за экраном — вертикальная координата больше 0xF0. Затем отрисовываю нулевой спрайт, а после этого уже каждый активный спрайт помещаю в буфер. Порядок размещения спрайтов в буфере меняется каждый кадр, так что спрайты мерцают.
Метаспрайты бОльшего размера надо подготовить в NES Screen Tool. Код из примера Shiru мне не понравился, пришлось переписать. В частности, когда метаспрайт выходит за границу экрана, он не появляется с противоположной стороны, а просто теряется из виду. Это не вяжется с логикой игры, хоть и работает быстрее. Кроме того, возникли затруднения с отражениями спрайтов. Пришлось писать скрипт на Пайтоне, который конвертирует готовые метаспрайты в формат, удобный для импорта в код. Это слегка ускорило процесс.
На этом этапе кнопка Вниз добавляет снаряды, Селект — добавляет врагов. Для некоторых из них работает отзеркаливание. Обработка коллизий переписана на Ассемблере и позволяет увеличить количество объектов.
А теперь надо переписать все еще раз. Обработка нулевого спрайта и управление прокруткой происходят в обработчике NMI. Каждый кадр происходят примерно такие действия:
- Получение событий джойстика
- Спрайты из буфера выносятся за экран, нулевой спрайт кладется на место
- Если Master_Delay обнуляется в этом кадре, то запускается волна врагов. Для копирования из ROM в RAM используется memcpy.
- Все ли враги убиты? При смерти координата Y обнуляется
- Есть ли попадания по нашему кораблю? Если да, то уменьшается счетчик жизней, и по соответствующему таймеру на несколько кадров рисуется взрыв вокруг корабля.
- Если были нажаты стрелки, двигаем корабль
- Есть ли попадания по врагам? Если да, то рисуем взрыв и обнуляем ему Y
- Рендерим все спрайты — пишем их данные в OAM
- Играем музыку. Этот этап можно перенести в NMI
- Обновляем счетчик очков
- Если надо, переходим в режим Паузы по кнопке Старт
- Если счетчик жизней ушел в минус, переходим в Конец игры
Нужно придумать способ, как красиво разместить вражеские корабли в начале каждой волны. Появляются они тоже не одновременно. Каждый враг активен и двигается, пока по нему не попадут, или пока он не уйдет за экран. Когда все враги из волны исчезнут тем или иным способом, активируется таймер Master_Delay, отсчитывающий кадры до следующей волны. Когда волны закончатся, начинается режим Босса.
У Босса есть список возможных ходов и хитпоинты, снимающиеся при попаданиях. Когда они обнулятся, включаются нехитрые звуковые эффекты и тряска экрана. После этого игра переходит в режим Победы. Из него можно начать заново, с сохранением жизней и очков.
Остальное предлагаю смотреть в коде игры. Используйте его как есть, или как основу для своего проекта. Спасибо за внимание!
БлагодарностиХочу поблагодарить всех, кто помогал мне изучить программирование для NES, особенно участников форума forum.nesdev.com.
Очень много я почерпнул из примеров кода для cc65, которые написал Shiru. Кое-что из этих примеров использовано в этом туториале. Он же автор Famitone2 и NES Screen Tool. Его сайт с играми и примерами: раз и два.
Две его игры продаются на GreetingCarts (Retroscribe) (сайт мертв — прим. перев.)
Хочу поблагодарить THEFOX за его помощь, когда я только начинал осваивать cc65. И за примеры, которые раньше были на его сайте.