Меню Рубрики

Как написать игру для денди

Разработка игр под NES на C. Главы 1-3. От введения до Hello World

Впервые я задумался о том, как разрабатывают игры под приставки где-то через 20 минут после того, как в самый первый раз увидел Turbo Pascal. На глаза иногда попадался Subor с клавиатурой, и появилась мысль: «Наверное можно набрать какую-то программу, а потом в нее поиграть». Но интерес быстро затух, потому что абсолютно никакой информации по этой теме тогда не было доступно. Следующий раз эта же идея всплыла, когда увидел вполне играбельные эмуляторы старых консолей. Тогда стало ясно, что вбивать листинг в саму консоль и необязательно. Где-то очень потом появился Хабр с благожелательной аудиторией для таких вещей. В какой-то момент даже начал собирать разрозненную инфу чтобы написать мануал самому, и вот сегодня наткнулся на готовый учебник, который явно надо перевести.

Разработка под старые консоли документирована вдоль и поперек, но именно по NES 99% информации относятся к разработке на Ассемблере. Меня почему-то зарубило, что надо освоить именно работу с С.

следующая >>>

Всем привет.

Меня зовут Дуг. Я пишу игры для NES вот уже год, и решил начать этот блог. Я намерен написать туториал по разработке игр под NES, чтобы вдохновить других людей делать собственные игры.

Особенностью блога будет использование чистого С, так что другие программисты смогут начать писать быстро и не особо вникая в ассемблер для процессора 6502. Насколько мне известно, других туториалов такого формата по компилятору cc65 пока нет, кроме нескольких примеров игр на сайте Shiru.

Также помните, что я не профессионал ни в разработке, ни в ведении блога. Если возникнут вопросы по NES, скорее всего ответы найдутся в Вики.

Я постараюсь максимально упростить обучение, и использовать самые простые примеры. Также рекомендую начать с простейшей идеи игры. Читателю явно захочется сделать новую Зелду, но это не получится. Простейшая игра потребует 2-3 месяца на разработку, Зелда — 2-3 года. Такой проект скорее всего будет заброшен. Ориентируйтесь на Пакман, хотя бы первое время.

Память консоли

Поговорим о структуре памяти. У NES два независимых адресных пространства — память процессора с диапазоном $0-$FFFF и память PPU — видеочипа.

Начнем с памяти процессора.

  • Первые $800 это RAM.
  • Диапазон $6000-$7FFF некоторые игры используют для работы с SRAM (сохранение на картрирдж с батарейкой), или как дополнительный Work RAM.
  • На пространство $8000-$FFFF отображается ROM. Некоторые мапперы (дополнительный процессор в картридже) могут использовать более 32k ROM, но они все равно обычно работают через $8000-$FFFF.
  • Адрес $FFFC-$FFFD это вектор reset, который указывает на начало программы.

Здесь более подробная информация.

У PPU свое, независимое адресное пространство. Оно имеет размер $3FFF, но местами зеркалируется. Доступ к нему идет через регистры в памяти процессора. Видеопамяти хватает на 4 экранных буфера, но в подавляющем большинстве игр используется только 2 — для реализации прокрутки.

  • $0-$1FFF = здесь хранятся спрайты
  • $2000-$23FF = Таблица имен 0
  • $2400-$27FF = Таблица имен 1
  • $2800-$2BFF = Таблица имен 2
  • $2C00-$2FFF = Таблица имен 3
    При этом таблицы 2 и 3 это зеркало таблиц 0 и 1
  • $3F00-$3F1F = палитра

Таблица имен, nametable, связывает тайлы фона и их позицию на экране.

Зеркалирование позволяет управлять горизонтальной или вертикальной прокруткой, но всему свое время.

Еще в PPU есть отдельная область памяти OAM, Object Attribute Memory, размером 256 байт. Доступ к ней реализован через регистры в адресном пространстве процессора, и она позволяет управлять отображением спрайтов.

Еще один момент. Есть два типа картриджей. В некоторых два ROM чипа — PRG-ROM с исполняемым кодом и CHR-ROM с графикой. В таком случае графика автоматически отображается в адреса $0-1FFF PPU. Это позволяет очень просто сделать отрисовку — просто записать номер тайла в таблицу. Мы будем использовать этот формат.

Другой тип картриджа использует CHR-RAM вместо CHR-ROM. Это позволяет подгрузить часть графики в эту дополнительную оперативную память. Это сложная техника, и в этом туториале не рассматривается.

Теперь можно посмотреть на софт, используемый для разработки.

  • Компилятор
  • Редактор тайлов
  • Графический редактор
  • Notepad++
  • Хороший эмулятор
  • Упаковщик тайлов

В этом туториале рассматривается только cc65. Это один из лучших компиляторов для 6502, процессора NES.

Я использую версию 2.15 (для проверки введите ‘cc65 —version’ в консоли). Файлы из разных версий несовместимы, поэтому при необходимости используйте nes.lib из комплекта вашего компилятора.

Во-вторых, надо создать графику. Я использую YY-CHR

Для предобработки графики нужен любой графический редактор: Photoshop или GIMP, по вкусу.

Код удобно писать в Notepad++. У него есть подсветка сишного синтаксиса и нумерация строк — это облегчает отладку.

А теперь эмулятор. Я использую FCEUX 90% времени, потому что в нем есть крутой дебаггер и инструменты для работы с памятью, просмотрщики спрайтов и все такое. Но он не самый точный в эмуляции. Игры надо будет тестировать где-то еще. Судя по отзывам, самые точные эмуляторы это Nintendulator, Nestopia, и puNES. Еще желательно подгрузить более точную палитру — лежит здесь.

Есть две версии FCEUX — SDL и Win32. Первая работает почти везде, вторая только в Windows. Так вот, отладчик есть только во второй. Так что в случае альтернативной ОС придется воспользоваться виртуалкой или Wine.

И наконец расстановщик тайлов. Мы можем сделать игру без него, но он точно поможет. Я рекомендую NES Screen Tool. Он отлично показывает ограничения консоли по цветам и отлично подходит для одноэкранных игр. Для игр с прокруткой лучше подойдет Tiled map editor.

Как же всем этим пользоваться?

Надо сжать изображение до адекватного размера, например 128 пикселей в ширину. Потом преобразовать в 4 цвета и подправить при необходимости огрехи. Теперь можно копипастить в YY-CHR.

В YY-CHR надо проверить, чтобы цвет был двухбитный.

Палитра сейчас не имеет значения, потому что она все равно задается в другом месте.

Как работает сс65

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

Дл упрощения работы будем использовать .bat-скрипты и Makefile. Это позволит автоматизировать процесс и собирать образ картриджа в одно касание.

Процесс примерно такой. cc65 компилирует файл с кодом на С в ассемблерный код. ca65 собирает объектный файл. ld65 линкует его в образ картриджа .nes, который можно запустить в эмуляторе. Настройки хранятся в .cfg файле.

В приставке используется 8-битный процессор MOS 6502. Он не умеет просто обращаться к переменным больше 8 бит. Адресация 16-битная, из математики есть только сложение, вычитание и битовые сдвиги. Так что код придется писать с учетом этих факторов.

  • Большая часть переменных должна быть типа unsigned char — 8 бит, значения 0-255
  • Лучше не передавать значения в функции, или делать это через директиву fastcall, которая передает аргументы через 3 регистра — A,X,Y
  • Массивы не должны быть длинее 256 байт
  • printf отсутствует
  • ++g заметно быстрее, чем g++
  • cc65 не может ни передавать структуры по значению, ни возвращать их из функции
  • Глобальные переменные намного быстрее локальных, даже структуры

Испольуйте опцию -O для оптимизации. Есть еще опции i,r,s, которые иногда комбинируют в -Oirs, но они, например, могут удалить чтение из регистра процессора, значение которого не используется. А это фатально.

Оптимизация жизненно необходима, потому что у 8-битного процессора очень мало ресурсов, и в некоторых случаях надо следить за временем выполнения кода. А обычный код на С этим требованиям не соответствует.

Поддерживается импорт переменных из других файлов. cc65 умеет импортировать переменные и массивы из ассемблерных модулей командой

а если это символ из нулевой страницы памяти, то добавьте директиву

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

а потом импортировать в С как

Знак _ здесь критичен, потому что при компиляции в ассемблерный код cc65 добавляет _ перед каждым именем переменной. Нам надо этому соответствовать.

Можно вызывать функции, написанные на ассемблере, через __fastcall__. В этом случае аргументы передадутся в функцию через регистры, а не стек — экономит время. В некоторых случаях без ассемблерного кода не обойтись, например при инициализации приставки. В любом случае, чем меньше аргументов передается в функцию, тем лучше. Сравним две функции, причем переменные test и A глобальные:

Еще можно вставлять ассемблерный код прямо в сишный. Я так почти никогда не делаю, но наверное иногда это необходимо. Выглядит примерно так:

Кроме того, я заменил громоздкий код инициализации crt0.s на компактный reset.s, и подправил конфигурацию для всего этого. Эти файлы иногда будут меняться. nes.lib используется стандартный, из состава компилятора. Проект собирается с опцией –add-source, которая не удаляет промежуточные ассемблерные файлы — можно порассматривать сгенерированный код.

Удобней определить переменные в сишном коде, а потом импортировать в ассемблерный через

Но это вопрос вкуса, на мой взгляд, такой код наглядней.

Hello World

Эта программа будет просто печатать текст на экране. Надо помнить, что приставка вообще не знает про кодировку ASCII и работу с текстом в любом виде. Но зато есть возможность вывести картинки размером 8х8 поверх фона.

Так что делаем массив спрайтов-букв, чтобы адреса букв в нем соответсвовали их ASCII-кодам. Потом их можно будет дернуть из кода на С.

Код инициализации приставки пока берем как есть, после его выполнения происходит переход на main().

Нам надо сделать такие операции:

  • Выключить экран
  • Настроить палитру
  • Вывести заветные слова
  • Отключить прокрутку
  • Включить экран
  • Повторить

Выключение экрана нужно, потому что работа с видеопамятью вызывает мусор на экране. Надо или выключить экран, или ждать кадровый гасящий импульс (V-Blank). Детально этот вопрос мы рассмотрим в следующий раз.

Код инициализации заполняет память нулями, так что весь экран будет залит нулевым тайлом — в нашем случае, он пустой. А вся палитра заполнена серым цветом.

Для вывода на экран надо записать координаты начала заливки начиная со старшего байта по адресу $2006, а потом записывать номера тайлов в $2007. PPU будет выводить тайлы с соответствующими номерами один за другим, с переходом на новую строку. Можно перенастроить PPU на шаг вывода, равный 32 — тайлы будут выводиться один под другим. Нам же надо выставить шаг 1, через регистр $2000. Пересчитать координаты экрана в адрес можно через NES screen tool.

Нам также надо заполнить первые 4 цвета палитры — они отвечают за фон. Они записываются по адресу $3F00.

Запись в регистры PPU ломает положение прокрутки, так что ее тоже надо сбросить. Иначе картинка может уехать за экран. Мы делаем это через регистры $2006 и $2005.

Дропбокс
Гитхаб
На Гитхабе чуть исправил Makefile, чтобы корректно работал под Windows.

Строка
ONCE: load = PRG, type = ro, optional = yes;
внутри секции segments<> в файлах .cfg нужна для совместимости со свежей версией cc65.

Включение экрана через “PPUMASK = 0x1e” описано в Вики.

Все файлы здесь размером 0х4000. Это самый маленький возможный размер PRG ROM. 90% игр сюда не влезут, и будут отображаться на адреса $8000-$FFFF. У нас же игра загружается в адреса $C000-$FFFF и зеркалируется в $8000-$BFFF. Для разработки большей игры надо будет перенастроить адрес начала ROM на $8000, и выставить размер тоже $8000. А еще включить второй банк PRG ROM в секции header.

Редакторский дайджест

Присылаем лучшие статьи раз в месяц

Скоро на этот адрес придет письмо. Подтвердите подписку, если всё в силе.

Похожие публикации

Разработка игр под NES на C. Главы 11-13. Пишем и отлаживаем простой платформер

Разработка игр под NES на C. Главы 7-10. Работа с джойстиком. Коллизии спрайтов

Разработка игр под NES на C. Главы 4-6. Рисуем персонажа

Средняя зарплата в IT

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Комментарии 6

Шрифт весьма ущербный, на мой взгяд и вкус. Впрочем 8х8 сложно нормально нарисовать. Интересна была бы работа со шрифтом 8х16, литеры для которого можно выдрать из досовых консольных шрифтов, которых создано огромное множество, либо из *.psf, которые попадаются в Linux (один такой я для себя нарисовал). Или 6х8, чтобы в строку больше букв влезало.

Вторым уроком хочу видеть полноценную реализацию printf(). Третьим уроком — реальзацию примитивного текстового редактора.

Как и nesdoug, начинал с движка shiru и модифицировал его немного под себя.

Хорошо, что вы в своих примерах портируете код под новую версию cc65, буду следить за вашими переводами и добавлять замечания, может напишу от себя статью по темам, которые не раскрыты у nesdoug’а:

Отладка и профилирование под fceux с помощью lua, использование более продвинутого звукового движка от famitraker’а, использование различных блочных систем для экономии памяти и экспорт данных уровней из готовых игр с использованием редактора CadEditor, реальные примеры реализации спецэффектов из игр, реализация систем паролей/использование батарейки, написание своих компрессоров данных, возможно, создание меню для многоигровок.

Замечание и неточности в статье:
1. Флаг компилятору -Cl позволяет сделать все локальные переменные статическими, что позволит не терять в скорости. Однако переменные вообще лучше не заводить без особых на то причин.
2. Квалификатор типа volatile позволяет отмечать переменные, используемые для чтения/записи регистров процессора и внешних устройств, такие переменные не будут удалены как неиспользуемые при оптимизации.
3. Директива __fastcall__ задаёт передачу аргументов в функцию через регистры A,X и 2 специально отведённых компилятором байта из нулевой страницы памяти, регистр Y не используется. Если для передачи аргументов требуется больше 4х байт, то всё равно будет задействован стек.

На глаза иногда попадался Subor с клавиатурой, и появилась мысль: «Наверное можно набрать какую-то программу, а потом в нее поиграть»

У меня он был, и я, бывало, играл в то, что сам написал на G-BASIC. Но у меня не было никакого внешнего носителя, а потому программа каждый раз набиралась ручками с бумаги.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Источник статьи: http://habr.com/ru/post/348022/


0 0 голоса
Article Rating
Подписаться
Уведомить о
guest

0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии