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

Время на прочтение
8 мин

Количество просмотров 35K

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

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

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

Всем привет.
image

Меня зовут Дуг. Я пишу игры для 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, связывает тайлы фона и их позицию на экране.

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

image

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

Вот подробная информация по памяти PPU:
http://wiki.nesdev.com/w/index.php/PPU_memory_map

Еще один момент. Есть два типа картриджей. В некоторых два 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++. У него есть подсветка сишного синтаксиса и нумерация строк — это облегчает отладку.

image

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

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

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

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

image

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

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

image

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

Как работает сс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 умеет импортировать переменные и массивы из ассемблерных модулей командой

extern unsigned char foo;

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

#pragma zpsym (“foo”);

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

.export _foo
_foo:
.incbin "foo.bin"

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

extern unsigned char foo[];

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

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

void Test (char A) {
 test = A;
}
// функция с одним аргументом компилируется в 19 команд ассемблера

_Test

jsr pusha
 ldy #$00
 lda (sp),y
 sta _test  ; test = A;

 jmp incsp1

pusha: ldy sp 
 beq @L1 
 dec sp
 ldy #0 
 sta (sp),y 
 rts 

@L1: dec sp+1 
 dec sp 
 sta (sp),y 
 rts 

incsp1:

 inc sp
 bne @L1
 inc sp+1
@L1: rts

void Test (void) {
 test = A;
}
// аргумент не передается, компилируется в 3 команды

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


 asm ("Z: bit $2002") ;
 asm ("bpl Z") ;

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

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

.import _Foo

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

Hello World

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

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

image

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

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

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

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

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

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

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

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

lesson1.c


#define PPU_CTRL  *((unsigned char*)0x2000)
#define PPU_MASK  *((unsigned char*)0x2001)
#define PPU_STATUS  *((unsigned char*)0x2002)
#define SCROLL   *((unsigned char*)0x2005)
#define PPU_ADDRESS  *((unsigned char*)0x2006)
#define PPU_DATA  *((unsigned char*)0x2007)

unsigned char index;
const unsigned char TEXT[]={
"Hello World!"};

const unsigned char PALETTE[]={
0x1f, 0x00, 0x10, 0x20
}; //black, gray, lt gray, white

void main (void) {
 // turn off the screen
 PPU_CTRL = 0;
 PPU_MASK = 0;

 // load the palette
 PPU_ADDRESS = 0x3f; // set an address in the PPU of 0x3f00
 PPU_ADDRESS = 0x00;
 for(index = 0; index < sizeof(PALETTE); ++index){
  PPU_DATA = PALETTE[index];
  }

 // load the text
 PPU_ADDRESS = 0x21; // set an address in the PPU of 0x21ca
 PPU_ADDRESS = 0xca;  // about the middle of the screen
 for( index = 0; index < sizeof(TEXT); ++index ){
  PPU_DATA = TEXT[index];
  }

 // reset the scroll position 
 PPU_ADDRESS = 0;
 PPU_ADDRESS = 0;
 SCROLL = 0;
 SCROLL = 0;

 // turn on screen
 PPU_CTRL = 0x90; // NMI on
 PPU_MASK = 0x1e; // screen on

 // infinite loop
 while (1); 
}

image

Ссылка на код:

Дропбокс
Гитхаб
На Гитхабе чуть исправил 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.

В 1980-х годах, когда приставки только появлялись, вышла NES — Nintendo Entertainment System. В Россию она попала в виде китайского клона «Денди», «Кенги» и прочих, поэтому если у вас была восьмибитная приставка, то это была NES.

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

Та самая приставка, справа пока ещё две кнопки вместо четырёх.

Для разбора мы взяли видео из канала Morphcat Games — How we fit an NES game into 40 Kilobytes. Там разработчики повторяют опыт геймдизайнеров прошлого и пишут игру для старого железа. Как обычно, если знаете английский, то лучше посмотрите видео целиком, а если нет — держите наш текстовый вариант.

Почему именно 40 килобайт

В 1980-х объём памяти на цифровых устройствах измеряли в килобайтах, потому что ещё не было таких продвинутых её технологий. В большинстве картриджей для восьмибитных приставок было по 40 килобайт памяти. Для сравнения, это в сто тысяч раз меньше, чем на флешке в 4 гигабайта. Даже эта статья весит больше, чем 40 килобайт, так что по современным меркам этого действительно мало.

Два блока памяти в картриджах, 8 и 32 килобайта, в сумме — 40 килобайт.

Чтобы использовать больше памяти, нужно было идти на всякие ухищрения — ставить расширители памяти или отдельные блоки для работы с несколькими картриджами одновременно. Так как почти ни у кого из геймеров такой роскоши не было, то разработчики использовали только 40 доступных килобайт.

Когда у тебя мало памяти, у тебя мало возможностей: уровни однообразные, враги однообразные, геймплей одинаковый. Но иногда разработчики шли на безумные ухищрения, и в игру получалось запихнуть много «миров», секретов и вариантов геймплея.

Одна из игр, которая взорвала мозг всем в своё время, была та самая «Супер Марио»: в ней было огромное количество разнообразных уровней разной сложности, боссы, секретные уровни и непростой, очень насыщенный геймплей. Были уровни на земле, под землёй, под водой и даже на небе; у героя было несколько режимов — низкий, высокий, в белом комбинезоне. А как вам идея разрушаемого мира? А как вам атаки с воздуха? Короче, «Марио» была безумной, невероятной игрой для своего времени, а всё благодаря оптимизациям.

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

«Супер Марио» — игра, в которую играл каждый, у кого была приставка.

Логика игры

Чтобы в игру было интереснее играть дольше пяти минут, разработчики поставили такие требования:

  1. Это будет платформер — игра, где главному герою нужно бегать и прыгать по платформам, залезать наверх и скакать через препятствия.
  2. Герой сможет ловко двигаться и стрелять по врагам.
  3. Чтобы можно было играть компанией, делают мультиплеер на четырёх человек.

Так как у нас ограничения по памяти, всю игру пишут на Ассемблере — это язык, который работает напрямую с процессором. С одной стороны, код Ассемблера исполняется очень быстро; с другой — в нём работа идёт тупо с перекладыванием данных из одной ячейки процессора в другую. Это примерно как готовить суши, работая с индивидуальными рисинками.

Память распределили так:

  • 8 килобайт на графику,
  • 32 килобайта на сам код игры и хранение данных.

Персонажи

В игре есть два вида графики: статичный фон и движущиеся предметы — игроки, противники, боссы и выстрелы. Всё, что движется, называется спрайтами. Разработчики делят всю графическую память на две части — одну под спрайты, вторую под фон:

Каждая клеточка — это мини-квадратик 8 на 8 пикселей.
В каждом таком квадратике можно что-то нарисовать, но использовать при этом только три цвета.
Если объединить несколько квадратиков в один, получится метаспрайт. В нашем случае — персонаж.
Приставка может использовать одновременно только 4 вида палитры, поэтому у нас получается 4 цветных главных героя и нераскрашенный злодей.
Новое ограничение: на экране одновременно может быть только 8 спрайтов — на большее не хватает памяти. Поэтому для злодея места не остаётся. Можно пойти на хитрость и показывать их быстро-быстро по очереди, но тогда картинка будет мерцать и выглядеть хуже.
Разработчики радикально уменьшили размеры героев и злодея до одного спрайта. Теперь они выглядят более условно, зато помещаются на экран.
Меньше размер героя — больше свободного места для дизайна злодеев, боссов и спецэффектов. Сейчас в табличке собраны все варианты того, как может выглядеть персонаж в игре — и в прыжках, и на бегу.

Большой босс и оптимизация памяти

Если с персонажем всё стало проще, когда его уменьшили, то с боссом всё немного сложнее. Он большой, занимает много места и у него много анимации. Задача — сделать так, чтобы боссы занимали как можно меньше места в памяти.

Большой босс и все его варианты анимации.
Если мы распределим все спрайты по таблице один в один, то у нас быстро закончится место и один кусочек не поместится. Запомните эту картинку как пример неоптимизированной работы с памятью.
Для начала разработчики разбили босса горизонтально на три части, и каждая анимируется отдельно. Видно, что анимация причёски состоит из трёх картинок, каждая из которых немного отличается от остальных.
Если разбить картинки с причёской на отдельные квадратики, то мы заметим, что у них есть повторяющиеся части. Поэтому достаточно нарисовать одну деталь, а потом использовать её во всех трёх вариантах причёски.
Находим оставшиеся одинаковые части и тоже оставляем только одну из них.
А вот тут видно, что это один и тот же спрайт, только в зеркальном виде. Компьютеру несложно нарисовать его отражённым, поэтому тоже можно смело оставить только один из них. С последними треугольничками в каждой картинке — то же самое: это отзеркаленные первые спрайты.
В итоге вся верхняя часть босса вместе с анимацией поместилась в четырёх спрайтах. Это и есть оптимизация: было 16 спрайтов, стало 4.
То же самое делают для средней части. Сейчас она занимает 3 × 8 = 24 спрайта.
А сейчас — 7.
После полной оптимизации босс занимает всего 21 спрайт. Из этих кусочков собирается итоговый вид босса.
Сравните с первоначальным вариантом до оптимизации 🙂

Карта

Для карт у нас столько же памяти, сколько и на спрайты (то есть мало), поэтому разработчики будут действовать так же:

  • разбивать фон на отдельные ячейки;
  • смотреть, как можно оптимизировать эти ячейки для хранения в памяти;
  • смотреть, можно ли что-то использовать повторно, для экономии памяти.

Главная задача на этом этапе — максимальная экономия видеопамяти. Для этого каждый экран с уровнем игры разбивается не на метаплитки 2 × 2, как в примере выше, с персонажем, а на метаметаплитки или суперплитки — 4 × 4 ячейки. Вот для чего это нужно:

Если разбить просто на квадратики 8 × 8, как в памяти, то вся видимая на экране часть уровня займёт 960 байт. Это почти килобайт, и это очень много.
Разбивают уровень на метаплитки 16 × 16. Теперь на одну карту нужно 240 байт, чтобы пометить каждую такую метаплитку, но это всё равно много. Уменьшаем дальше.
Теперь уровень делится на супербольшие плитки по 16 ячеек в каждой. В итоге для того, чтобы пронумеровать каждую такую суперплитку, нужно всего 60 байт. Уже можно работать.
Вот так собираются метаплитки — из четырёх ячеек в памяти.
Теперь можно собирать такие метаплитки в виртуальные наборы и каждой присвоить какой-то код. Но и это ещё не всё.
Вот теперь получилась суперплитка. Это готовый блок для уровня, и чтобы собрать такое, нужно совсем немного памяти.
Коллекция виртуальных суперплиток. С ними можно сделать любые уровни и фоны.

Рисуем карты (и оптимизируем их)

Даже 60 байт на экран, которые у нас получились, — это всё равно очень много, ведь нужно сделать много разных карт, написать логику поведения персонажей и сделать меню, заставки и титры. Каждый байт на счету.

Первый вариант — уменьшить количество памяти для отрисовки карты: сделать их симметричными, что даст нам 30 байт вместо 60. Мы рисуем одну половинку карты, а потом просто отзеркаливаем её. Сравним с картой, которую мы бы хотели получить:

Вроде всё на месте, а выглядит плохо — сразу видна симметрия и доступ наверх закрыт блоками.

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

  1. Они дают для хранения одной суперплитки один байт.
  2. Считают по картинке, сколько получилось суперплиток в прошлом разделе — 96.
  3. Так как программисты начинают считать с нуля, то самое большое число, которое получится, — 95, а это 1011111 в двоичной системе счисления.
  4. В этом длинном числе всего 7 цифр, а в байте их 8, поэтому остаётся один лишний бит из каждого числа.
  5. 4 суперплитки дадут 4 бита.
  6. Эти 4 бита можно использовать, чтобы сдвинуть по кругу ряд с зеркальным отражением и получить как бы новый ряд, уже без видимой симметрии.

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

4 суперплитки дают 4 бита. Посмотрим, что можно с ними сделать.
Сначала делают симметричный уровень…
А затем сдвигают верхнюю полосу вправо по кругу. 1100 — это 12 в десятичной системе счисления, именно столько сдвигов вправо нужно сделать, чтобы получилось как на картинке.
То же самое делают с третьей строкой и получают уже приемлемое начало уровня.

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

Добавляем в игру сложный режим

Когда игрок прошёл все уровни, ему можно дать возможность поиграть на повышенной сложности: он уже знает всю игру и может пройти более сложные ловушки и боссов. Например, сложный режим может отличаться дизайном уровней и поведением противников.

Чтобы и этот режим поместился в оставшуюся память, снова используют трюки с памятью и графикой.

Чтобы игрок понял, что начались трудности, просто меняют палитру. Это почти столько же по памяти, но выглядит сложнее.
Уровень можно поменять так: берут исходную картинку, накладывают сверху новые детали и получают сложную локацию. В среднем на это уходит по 7 байт на каждый экран.

В чем оптимизация, брат

  1. Уменьшили персонажей: маленькие спрайты — меньше памяти.
  2. Оптимизировали графику: вместо больших повторяющихся картинок — много маленьких повторяющихся картинок.
  3. Оптимизировали архитектуру уровней: сделали их симметричными, но сдвинули ряды по кругу влево-вправо, чтобы добавить разнообразия.
  4. Для дополнительного разнообразия ввели новые цветовые палитры.
  5. Более сложные уровни не хранили в памяти целиком. Для них хранились лишь дополнительные ловушки и враги. А на фоне лежали те же старые уровни.
  6. И всё это на чистом Ассемблере.

image

2. Фундаментальные понятия

Содержание:

  • Работа с данными
  • Регистры процессора
  • Память
  • Как задаются данные
  • Как сделать данные человекочитаемыми
  • Соединяем всё вместе

Что такое компьютер?

Вопрос кажется простым, но он затрагивает самую суть того, что делаем мы как программисты. Пока скажем, что «компьютер» — это нечто, исполняющее программу. «Программа» — это просто последовательность команд, а под исполнением программы подразумевается, что команды выполняются с начала и одна за другой. (Если вы читаете программу и сами исполняете команды, то поздравляю! Вы — компьютер!)

У каждого компьютера есть конкретный набор команд, которые он умеет исполнять. Мы называем его набором команд компьютера (да, очень оригинальное название). Набор команд можно описать множеством способов, но пока давайте будем считать, что команды в наборе команд обозначены числами. То есть программа — это просто перечень чисел, каждое из которых задаёт определённое выполняемое действие. Вот пример гипотетического набора команд:

  • 1: двигаться вперёд
  • 2: повернуть влево
  • 3: повернуть вправо

По сути, это набор команд Logo — языка программирования «черепашьей графики», позволяющего перемещать робота по листу бумаги при помощи ручки, чтобы он создавал интересные рисунки. Изображение: Valiant Technology Ltd., CC-BY-SA 3.0.

Запущенная на компьютере с этим набором команд программа, которая должна переместиться вперёд три раза, повернуть вправо, дважды переместиться вперёд, повернуть влево и переместиться вперёд четыре раза, будет выглядеть так:

1 1 1 3 1 1 2 1 1 1 1

Работа с данными

Часто команды, которые должен исполнять компьютер, получают в каком-нибудь виде данные. Компьютеры часто складывают числа; гораздо проще иметь одну команду сложения, чем целое множество подобных команд:

  • 1: прибавить 1
  • 2: прибавить 2
  • 3: прибавить 3
  • 4: прибавить 4

Или же вместо одной команды «прибавить 1», вызываемой нужное количество раз, которую было бы столь же неудобно использовать. Программа, прибавляющая к числу 1000, занимала бы в 1000 раз больше места на накопителе (и выполнялась бы в 1000 раз дольше) по сравнению с программой, прибавляющей 1!

Сопровождающие команду данные должны находиться в какой-то части программы. В различных языках программирования эта задача решается по-разному. В некоторых языках «код» (команды) должен храниться совершенно отдельно от данных, в других они объединяются. Оба подхода имеют свои плюсы и минусы, но пока давайте рассмотрим объединённые команды и данные.

В нашем гипотетическом компьютере для «сложения каких-то чисел» набор команд мог бы выглядеть следующим образом:

  • 1: сохранить следующее число как «первое число»
  • 2: прибавить следующее число к первому числу, если оно сохранено

Программа, складывающая числа 2 и 7, выглядела бы так:

1 2 2 7

Пошагово двигаясь по программе по одному числу за раз, мы видим команду «1» («сохранить следующее число как первое число»). Следующее число — это «2», поэтому 2 сохраняется как первое число. Далее мы видим команду «2» («прибавить следующее число к первому числу»). Следующее число — это «7», поэтому наша программа прибавляет 7 к 2, получая результат 9. Здесь данные и команды перемешаны. Увидев «1 2 2 7», невозможно понять, какие из «2» — это команда «прибавить следующее число к первому числу», а какие — алгебраическое число «2», не посмотрев на начало и не пройдя пошагово всю программу.

Где находится результат (9)? Как дальше в программе нам сделать что-нибудь с этим результатом? И что же подразумевается под «сохранением» чего-либо?

Регистры процессора

Как мы только что увидели, программам часто требуется место для временного хранения данных. В большинстве компьютеров для этого используются регистры — небольшие участки внутри процессора, каждое из которых может хранить одно значение. [«Значения» — это просто числа; как мы уже делали с командами в наборе команд, можно взять значение любого типа и представить его в виде числа при условии, если у нас есть какое-то сопоставление между числами и обозначаемыми ими данными. Например, Unicode при помощи 32-битного числа описывает все возможные символы из каждой системы письма на Земле. (Подробнее о «битах» мы поговорим ниже.)]

Регистры могут быть обобщёнными или связанными с определёнными типами функциональности. Например, в процессоре NES есть регистр под названием накопитель (accumulator), часто сокращаемый до «A»; он занимается всеми математическими операциями. В наборе команд 6502 есть команды, работающие следующим образом:

  • сохранить следующее число в накопитель
  • прибавить следующее число к накопителю, результат записать в накопитель
  • поместить куда-нибудь число из накопителя

Это решает проблему сохранения чисел и доступа к ним. Но мы не ответили на ещё один вопрос: когда мы «помещаем куда-нибудь число из накопителя», где находится это «куда-нибудь»? Процессор 6502 консоли NES имеет всего три регистра, поэтому сложные программы не могут использовать для хранения результатов только регистры.

Память

Компьютеры предоставляют программам доступ к какому-то объёму (непостоянной) памяти для временного хранения, что позволяет компьютеру иметь небольшое количество (дорогих) регистров, в то же время обеспечивая возможность хранения за пределами самой программы приемлемого количества значений. Эта память выглядит как последовательность «ящиков» размером с регистр, каждый из которых содержит одно значение и ссылка к которому осуществляется по номеру. NES предоставляет разработчику программу с двумя килобайтами (2 КБ) пространства памяти, пронумерованную от нуля до 2047 — номер в пространстве памяти называется его адресом (как адрес дома). То есть рассмотренный нами ранее набор команд 6502 на самом деле ближе к такому:

  • сохранить следующее число в накопитель
  • прибавить следующее число к накопителю, результат записать в накопитель
  • поместить число из накопителя по адресу памяти следующего числа

Как задаются данные

И это приводит нас к последнему вопросу этой главы — как все эти числа представлены внутри компьютера?

Ранее мы использовали «стандартные» десятичные числа (по основанию 10). Это те числа, которые мы пользуемся повседневно, например, «2», или «7», или «2048». Однако компьютеры работают на электрических токах, которые могут быть или «включенными» или «выключенными», без каких-то промежуточных значений. Эти токи образуют основу всех данных внутри компьютера, поэтому компьютеры используют двоичные числа (по основанию 2).

Наименьшей единицей информации, которую может обработать компьютер, является «бит» (bit, сокращение от binary digit). Бит хранит в себе одно из двух значений — 0 или 1, «включено» или «выключено». Если мы объединим как одно число несколько битов, то сможем задавать больший диапазон значений. Например, двумя битами можно задать четыре разных значения:

00 01 10 11

Три бита позволяют задать восемь разных значений:

000 001 010 011 100 101 110 111

Каждый добавленный нами бит позволяет задавать в два раза больше значений, аналогично тому, как каждый десятичный разряд, добавляемый к десятичному числу, позволяет задавать в десять раз больше значений (1 → 10 → 100 → 1000). Объединённые вместе восемь бит, задающих одно значение, используются настолько часто, что имеют собственное название: байт (byte). В байте может храниться одно из 256 значений. Так как четыре бита являются половиной байта, их иногда называют полубайтом (nybble). В полубайте может храниться одно из 16 значений.

Часто говорят, что компьютеры (в том числе и видеоигровые консоли), имеют определённую битность. Современные десктопные компьютеры/ноутбуки обычно являются 64-битными, старые версии Windows, например, Windows XP, называют 32-битными операционными системами, а NES — это 8-битная система. Все эти числа характеризуют размер регистров компьютера — количество битов, которые может одновременно хранить один регистр. [Несколько усложняет понимание то, что адресная шина NES имеет ширину 16 бит, то есть NES может обрабатывать 65536 различных адресов памяти, а не 256. Однако каждый адрес памяти всё равно хранит только один байт.] Так как NES — это «8-битный» компьютер, каждый его регистр хранит 8-битное значение (один байт). Кроме того, каждый адрес памяти может хранить один байт.

Как же работать с числами больше 255? Игроки в Super Mario Bros. часто зарабатывают десятки тысяч очков, и одного байта явно недостаточно для хранения таких чисел. Когда нам нужно задать значение намного больше, чем может храниться в одном байте, мы используем несколько байтов. В двух байтах (16 бит) можно хранить одно из 65536 значений, а при увеличении количества байтов возможности задания чисел резко возрастают. В трёх байтах можно хранить число до 16777215, а в четырёх — до 4294967295. Когда мы используем таким образом больше одного байта, мы всё равно ограничены размером регистра компьютера. Чтобы работать с 16-битным числом на 8-битной системе, нам нужно получать или сохранять число в двух частях — «младший» (low) байт справа, «старший» (high) байт слева. [Именно из-за необходимости работы с такими значениями из нескольких регистров у процессоров есть порядок следования байтов (endianness) — т. е., у них определено, какой байт идёт первым при работе с большими числами. В процессорах с прямым порядком байтов (он называется little-endian), например, в 6502, сначала идёт младший байт, а затем старший. В процессорах с обратным порядком байтов (big-endian), например, в Motorola 68000, ситуация противоположная — ожидается, что сначала идёт старший байт, а за ним следует младший. Большинство современных процессоров является little-endian из-за очень популярной архитектуры x86 компании Intel, тоже являющейся little-endian.]

Так как управляющий консолью NES процессор 6502 одновременно работает с восемью битами данных, для задания чисел меньшего размера всё равно используется восемь бит. Это может быть неэффективно, поэтому при необходимости в одном байте часто хранят несколько значений меньшего размера. Один байт может содержать два четырёхбитных числа, или четыре двухбитных числа, или даже восемь отдельных значений «включено»/«выключено» (мы называем их флагами).

Например, байт 10110100 может задавать:

  • Одно 8-битное значение: 180
  • Два 4-битных значения: 11 (1011) и 4 (0100)
  • Четыре 2-битных значения: 2 (10), 3 (11), 1 (01) и 0 (00)
  • Восемь значений «включено»/«выключено» (или «истина»/«ложь»): вкл., выкл., вкл., вкл., выкл., вкл., выкл., выкл.
  • Любое другое сочетание битовых длин, в сумме составляющих восемь

Для удобства обсуждения таких ситуаций, когда в одном байте хранится несколько значений, обычно каждому биту в байте присваивается номер, почти так же, как мы давали название «младшему» и «старшему» байтам в 16-битном значении. Самый правый бит называется «бит 0» и счёт идёт до самого левого «бита 7». Вот пример:

байт:   1 0 1 1 0 1 0 0
№ бита: 7 6 5 4 3 2 1 0

Как сделать данные человекочитаемыми

Как мы видели, байты — это очень гибкий способ задания различных типов данных в компьютерной системе. Однако недостаток использования байтов заключается в том, что их сложно читать. Приходится прикладывать усилия, чтобы мысленно преобразовать «10110100» в десятичное число «180». Когда вся программа представлена в виде последовательности байтов, проблема сильно усугубляется.

Для решения этой проблемы основная часть кода представлена в виде шестнадцатеричных чисел. «Шестнадцатеричное» означает «по основанию 16»; одно шестнадцатеричное (hexadecimal, «hex») число может содержать одно из шестнадцати значений. Вот числа от нуля до пятнадцати, представленные в шестнадцатеричном виде:

0 1 2 3 4 5 6 7 8 9 a b c d e f

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

Работа с числами, которые могут быть десятичными, двоичными или шестнадцатеричными, может запутывать. Например, число «10» обозначает 10, если оно десятичное, 2, если оно двоичное или 16, если оно шестнадцатеричное. Чтобы понимать, какое значение мы имеем в виду, принято использовать префиксы. 10 — это десятичное число, %10 — двоичное, а $10 — шестнадцатеричное.

Соединяем всё вместе

Рассмотрев множество вопросов, связанных с работой компьютеров (и программ), давайте ещё раз взглянем на весь процесс, происходящий при выполнении программы.

Во-первых, сама программа представлена в виде последовательности байтов (так называемого машинного кода). Каждый байт — это или команда для процессора, или сопровождающие команду данные.

С самого начала программы процессор многократно выполняет трёхэтапный процесс. Сначала процессор получает следующий байт из программы. В процессоре есть специальный регистр под названием счётчиком программ (program counter), он отслеживает, каким будет следующий номер байта программы. Счётчик программ (program counter, PC) работает совместно с регистром под названием адресная шина (address bus), отвечающим за получение и сохранение байтов из программы или из памяти, для получения байтов.

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

3. Приступаем к разработке

Содержание:

  • Настройка среды разработки
  • Текстовый редактор
  • Ассемблер и компоновщик
  • Эмулятор
  • Графические инструменты
  • Инструменты для создания музыки
  • Соединяем всё вместе
  • Дальнейшие шаги

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

Настройка среды разработки

Здесь перечислены все инструменты, которые мы будем устанавливать. Некоторые из них мы будем использовать сразу (и постоянно), другие более специализированы и пригодятся позже. Для каждой категории я указал конкретное ПО, которое буду использовать в этой книге; однако есть множество других вариантов, так что освоившись с рекомендуемыми мной, вы сможете поэкспериментировать и с другими инструментами.

  • Текстовый редактор (на ваш выбор)
  • Ассемблер/компоновщик (ca65 и ld65)
  • Эмулятор (Nintaco)
  • Графический инструмент, способный считывать/сохранять изображения в формате NES (NES Lightbox)
  • Инструмент для создания музыки (FamiStudio)

Текстовый редактор

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

  • Sublime Text. Кроссплатформенная, популярная среди разработчиков, простая в освоении; когда вы освоитесь с основами, становится мощным инструментом.
  • Atom. По сути, ответ GitHub’а на Sublime Text. Кроссплатформенная, с гибкими настройками.
  • Visual Studio Code. Качественная платформа Microsoft для редактирования текста. Создавалась для веб-разработки, но имеет возможность расширения для любого вида программирования. Также кроссплатформенная, не только для Windows.
  • Vim, emacs, nano, и т. д. Текстовые редакторы с давней историей, работающие из командной строки. (Лично я пользуюсь Vim, но выбор за вами.)

Ассемблер и компоновщик

Ассемблер компилирует ассемблерный код (который мы будем писать в этой книге) в машинный код — сырой поток байтов, считываемый процессором. Компоновщик (linker) берёт набор файлов, который был пропущен через ассемблер, и превращает их в единый файл программы. Так как у каждого процессора свой машинный код, ассемблеры обычно предназначены только для одного типа процессора. Существует множество вариантов ассемблеров и компоновщиков для процессора 6502, но в этой книге мы будем использовать ca65 и ld65. Они имеют открытый исходный код и являются кроссплатформенными, а также обладают очень полезными функциями для разработки больших программ. ca65 и ld65 — это часть более масштабного комплекта программ «cc65», включающего в себя компилятор C и многое другое.

Mac

Для установки ca65 и ld65 на Mac нужно для начала установить менеджер пакетов Mac Homebrew. Скопируйте команду с главной страницы, вставьте её в терминал и нажмите Enter; выполните инструкции, после чего Homebrew будет готов к работе. Установив Homebrew, введите brew install cc65 и нажмите Enter.

Windows

В Windows необходимо скачать ca65 и ld65 в определённую папку на компьютере. Скачайте последний «Windows Snapshot» с страницы основного проекта cc65. Распакуйте содержимое в C:cc65. Также вам нужно будет дополнить системные пути, чтобы ca65 и ld65 были видны из любой папки. Этот процесс зависит от того, какой версией Windows вы пользуетесь. В самых новых версиях Windows можно нажать правой кнопкой мыши на «Мой компьютер», выбрать «Свойства», «Дополнительные параметры системы», а затем «Переменные среды». Вам нужно будет найти запись %PATH% и добавить в её конец C:cc65bin.

Linux

Вам нужно будет собрать cc65 из исходников. К счастью, это довольно простой процесс. Сначала убедитесь, что у вас есть git и базовая среда для сборки — например в Ubuntu для этого достаточно выполнить sudo apt-get install git build-essential. Затем перейдите в папку, куда вы хотите установить cc65, клонируйте репозиторий cc65 и соберите его:

git clone https://github.com/cc65/cc65.git
cd cc65
make

Затем сделайте программы cc65 доступными из любой папки, выполнив sudo make avail. Эта команда добавит символическую ссылку из вашей папки cc65 в /usr/local/bin.

Эмулятор

Эмулятор — это программа, запускающая программы, предназначенные для другой компьютерной системы. Мы будем использовать эмулятор NES, чтобы запускать создаваемые нами программы на том же компьютере, где их разрабатываем, вместо запуска на аппаратной NES. Существует множество эмуляторов NES (а когда вы наберётесь опыта в разработке для NES, будет интересно и написать собственный!), но для этой книги мы будем использовать Nintaco.

Nintaco.

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

Установка Nintaco на всех платформах происходит одинаково — достаточно скачать его с веб-сайта Nintaco и распаковать. Чтобы запустить Nintaco, нужно дважды нажать на Nintaco.jar. Для запуска Nintaco требуется Java; если на вашем компьютере не установлена Java, скачайте «Java Runtime Environment» с сайта java.com.

Графические инструменты

NES хранит графику в собственном уникальном формате, непохожем на традиционные типы изображений наподобие JPEG или PNG. Нам понадобится программа, способная работать с изображениями NES. Существуют плагины для больших графических пакетов типа Photoshop или GIMP, но мне нравится использовать для этого компактный специализированный инструмент. Для этой книги мы будем использовать NES Lightbox — кроссплатформенную производную от NES Screen Tool.

NES Lightbox.

Windows

Скачайте Windows-установщик (для 64-битных систем). Дважды нажмите на «NES Lightbox Setup 1.0.0.exe», чтобы установить программу.

Mac

Скачайте Mac DMG. Дважды нажмите на файл .dmg, чтобы открыть его, и перетащите приложение NES Lightbox в папку Applications. При первом запуске приложения нужно будет нажать на него правой клавишей мыши и выбрать «Open», потому что оно не «подтверждено» компанией Apple.

Linux

В системах с Ubuntu можно скачать файл Snap, который является автономным пакетом приложения. В случае других дистрибутивов Linux (или если вы предпочитаете AppImage) нужно скачать файл AppImage. Прежде чем запустить файл AppImage, его нужно пометить как исполняемый.

Инструменты для создания музыки

Как и в случае с графикой, звук на NES представлен в уникальном формате — это команды аудиопроцессора, а не что-то типа MP3. Самая популярная программа для создания звука для NES — FamiTracker, это мощный, но сложный инструмент, предназначенный только для Windows. Для этой книги мы будем использовать FamiStudio — кроссплатформенную программу с более дружественным интерфейсом, результаты работы в которой сохраняются в простой для интеграции формат.

FamiStudio.

Windows / Mac / Linux

Скачайте последнюю версию с веб-сайта FamiStudio.

Соединяем всё вместе

Установив все инструменты, нужно убедиться, что они работают. Мы создадим аналог «Hello World» для игр на NES: заполним весь экран одним цветом.

Откройте текстовый редактор и создайте новый файл helloworld.asm. Скопируйте и вставьте в файл следующий код:

Код в текстовом виде

.segment "HEADER"
.byte $4e, $45, $53, $1a, $02, $01, $00, $00

.segment "CODE"
.proc irq_handler
RTI
.endproc

.proc nmi_handler
RTI
.endproc

.proc reset_handler
SEI
CLD
LDX #$00
STX $2000
STX $2001
vblankwait:
BIT $2002
BPL vblankwait
JMP main
.endproc

.proc main
LDX $2002
LDX #$3f
STX $2006
LDX #$00
STX $2006
LDA #$29
STA $2007
LDA #%00011110
STA $2001
forever:
JMP forever
.endproc

.segment "VECTORS"
.addr nmi_handler, reset_handler, irq_handler

.segment "CHARS"
.res 8192
.segment "STARTUP"

Теперь нам нужно воспользоваться ассемблером. В папке, в которую вы сохранили helloworld.asm, выполните команду ca65 helloworld.asm. В результате появится новый файл helloworld.o. Это объектный файл — машинный код. Но он пока не находится в формате, готовом для запуска в эмуляторе. Чтобы преобразовать его, нужно запустить компоновщик. В той же папке выполните команду ld65 helloworld.o -t nes -o helloworld.nes. В результате этого должен появиться новый файл helloworld.nes — файл «ROM» для эмулятора.

Запустите Nintaco и выберите в меню «File» пункт «Open». Выберите только что созданный файл helloworld.nes и нажмите Open. В результате вы увидите зелёный экран.

[В оригинале главы зелёный экран — это настоящий работающий в браузере эмулятор NES! Я воспользовался потрясающим jsnes, созданным Беном Фиршманом. Каждый раз, когда мы будем компилировать файл .nes, я буду включать в текст подобное работающее демо. (Сейчас сложно это понять, но на самом деле эмулятор работает с частотой кадров 60fps.)]

Дальнейшие шаги

Если вы увидели в Nintaco зелёный экран, поздравляю! Ваша среда разработки готова к использованию. В следующей главе мы расскажем, что же делает скопированный нами код, и немного узнаем о том, как работает оборудование NES.

Создание игр для NES на ассемблере 6502: оборудование NES и знакомство с ассемблером

Изложено в формате ‘нет времени объяснять’. Подробное раскрытие каждого небольшого пункта потребует статьи большего объёма, чем этот обзор. Это будет сделано впоследствии, если у читателей обнаружатся конкретные интересы.

Хотите написать игру или демо для NES, Famicom, Денди?

Изучаем. Кладезь актуальной мудрости — NesDev Wiki. Вводные для начинающих — Nerdy Nights на английском, другая на русском. Помним, что старые описания неполны и неточны. Отдельно изучаем 6502. Книг и статей много, поиск начинаем отсюда.

Выбираем между ассемблером и C. Тысяча игр на ассемблере, десятки на C. Код на ассемблере в разы эффективнее, на C пишется в разы быстрее. С ассемлером проще получить помощь зала. Для отчаянных есть экзотика, от BASIC до минимальных Lisp и Python.

Выбираем ассемблер. Три популярных, десятки других. NESASM стар, имеет искусственные ограничения (пришёл с MS-DOS, перепилен с PCE/TG16) и мелкие глюки. Легко освоить, большинство старых уроков под него, выдержит средний проект. Современная альтернатива — ASM6, прост в освоении, но не так популярен. Выбор профессионала — CA65. Мощен и гибок, но сложен в освоении (линкер и конфигурация памяти).

Выбираем компилятор C — CC65. Для работы с железом есть библиотеки neslib и KNES, либо пишем свою. По скорости потолок чистого C — подобие Super Mario Bros. Можно писать частично на C, частично на ассемблере, переписывать фрагменты кода по ходу дела. Очень удобно для прототипирования.

Пишем код. Любимый текстовый редактор, bat или make файлы. Есть развитая IDE для CC65 — NESICIDE, но WIP и почти никем не используется.

Рисуем. Отдельные тайлы редактируем непосредственно в YY-CHR, NES Screen Tool, Tile Layer Pro и других, более сложные изображения импортируем из обычных форматов там же. Для сложных проектов может понадобиться свой велосипед. Без трюков NES не может отобразить полноценную картинку на весь экран, аналога Art Studio не ищите.

Музыка и звуки. Два основных трекера, но нужен проигрыватель, выбирается в зависимости от используемого ассемблера. Пишем в FamiTracker, играем FamiTone или Gradual Games Sound Engine. Пишем в Musetracker, играем MUSE. Штатного плеера FamiTracker хватит для простого демо, но не для игр. По желанию пишем свой плеер, оба трекера имеют текстовый экспорт. Для отчаянных есть древности и экзотика.

Делаем уровни. Готовых решений нет. Набираем вручную, пишем конвертер для универсальных редакторов, изобретаем свой редактор.

Для среднего или большого проекта наверняка понадобятся особые конвертеры и утилиты. Пишем на чём угодно, лишь бы работало. Обычно C++ или Python.

Отлаживаем в эмуляторах. Средства отладки развиты в FCEUX и NintendulatorDX. Высокая точность эмуляции в Nestopia и puNES, но отладчика в них нет. Даже самые точные эмуляторы не показывают всех глюков, проверяем во всех четырёх. Если пишем на C с готовой низкоуровневой библиотекой, на железе наверняка заработает. Выжимаем максимум на ассемблере — обязательно проверяем на железе при помощи Flash-картриджей (EverDrive N8, InviteNES, PowerPak — ищем в интернете) или самодельного картриджа и программатора ПЗУ.

Успехов!

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

     Этой статьей я хотел бы немного пролить свет на то, как создавались игры на Dendy. Причем речь пойдет не о том, как это можно делать сейчас, а как это происходило тогда — в 80-е и 90-е годы, и о том, с какими проблемами сталкивались разработчики в то время. Если вам наскучило читать очередные воспоминания менеджеров, дизайнеров, или программистов, переквалифицировавшихся в менеджеров, которые раскрывают техническую сторону дела чуть менее чем никак, то добро пожаловать под кат.

Тогда и сейчас

     В наше время разработка для игровых приставок все больше напоминает написание программ для обычных компьютеров, разница между тем что творилось тогда и тем, что имеет сегодняшний разработчик колоссальна. С одной стороны, сегодня, технологии и культура разработки шагнули далеко вперед, позволив вести разработку на языках высокого уровня, с другой — в 80-е фирмы выпускающие приставки еще не до конца понимали что следует дать сторонним разработчикам для создания игр. И поэтому если сейчас для очередной playstation можно получить документацию, пяток демо-дисков, а также мощную девелоперскую станцию, будущую по сути гибридом самой приставки и компьютера общего назначения, пригодного для непосредственной разработки, то например в 80-е многие разработчики довольствовались книжечкой со спецификацией консоли. А вот за аппаратной частью приходилось идти на радиорынок(или что там было в штатах вместо них?).

Немного истории

     Принято считать, что изначально была создана Nintendo Entertainment System(NES), с которой уже была спирачена наша Dendy и десятки других клонов по всему миру.

Как писались игры на Dendy

     После взгляда на картинку видно, что это, возможно, не совсем так. Изначально, в Японии в 1983 году, все-таки была выпущена консоль Famicom. В любом случае существовало 2 незначительно отличавшихся консоли — NES и Famicom. И разработка для них тоже велась разными инструментами, хотя программный код игр в картриджах для обеих был одинаков. Несколько различались сами картриджи. В случае NES на картридже присутствовало несколько дополнительных контактов, которые вели к чипу, который в теории должен был быть только на лицензионных картриджах…

     В далеком 1983 году Nintendo могла только мечтать о сторонних разработчиках для новоиспеченной консоли. Так что первые несколько игр были созданы самими авторами консоли. Некоторые из них были портами игр, уже существовавших на аркадных автоматах. Эти игры отличались примитивностью даже по меркам NES, не используя всех возможностей ее аппаратной начинки. Все игры того стартового периода умещались на один экран и не занимали более 32 килобайт данных на картридже. Наиболее известные игры того периода: Donkey Kong, Balloon Fighter, Mario Bros. Ирония заключалась в том, что NES имела специальную аппаратную поддержку для скроллинга уровней, но самые первые игры сайд-скроллерами не являлись, хотя и были выпущены компанией, который сам бог велел, зная все особенности их консоли, выпускать игры максимально подчеркивающие преимущество новой приставки.

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

     На картридж можно было засунуть все что угодно — любые чипы, которые душа пожелает: оперативную память, видеопамять, сопроцессор, энергонезависимую память для сохранений. Теоретически в картридж можно добавить такие вещи как модем или raspberry pi. Другое дело что для реализации этого потребуются титанические усилия, а также несколько свободных выходных.

Картриджи

     Спецификации NES картриджа до сих пор могут быть найдены в интернете, и, вообще, довольно подробно разобраны сообществом. Но сам процесс производства картриджей — это тайна покрытая мраком. Более менее достоверно известно что на территории Европы и США выпуском картриджей занималась исключительно Nintendo. В смысле самих коробочек с микросхемами.

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

     Каждый разработчик должен был явно указывать в заголовке скомпилированной игры(ROMа) перечень функций NES, которые будут использованы игрой. Некоторые функции поддерживались «обычными» картриджами — например маппер памяти, а вот за наличие сохранения или лишней оперативной памяти приходилось доплачивать Nintendo, чтобы та включила заветные чипы в ваш будущий картридж.

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

     Архитектура картриджей подразумевала что сама программа и графические спрайты находятся в разных чипах. Вот например фотография картриджа Super Mario Bros.

Как писались игры на Dendy

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

Оборудование для разработки

     Сведения об официальном SDK от Nintendo весьма скудны, настолько, что я уже склонен считать, что его просто не было. Т.е. в сети есть пара фотографий подобных юнитов, но нигде не сказано что они были выпущены Nintendo, более того — их вид говорит о том, что это были скорее всего поделки конечных разработчиков. Так что все, что было доступно — спецификации, а дальше каждый разработчик крутился как мог. Самодельное же оборудование для разработки делилось на 2 класса: видоизмененные картриджи с перезаписываемыми банками данных и отладочные станции.

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

     В случае RAM-картриджей разработка шла гораздо быстрее. Можно было редактировать память прямо в процессе игры, используя для этого компьютер на котором собственно и велась разработка. Чип EEPROM заменялся на банки оперативной памяти, которые с одной стороны виделись консолью как обычный картридж, а с другой стороны подключались к компьютеру разработчика и были для него обычной оперативной памятью… или диском… или устройством — все зависело от того какой именно драйвер программист писал для своего детища.

     Наиболее упорные или удачливые разработчики могли похвастаться отладочными станциями — модифицированными NES, которые вдобавок ко всем плюсам RAM-картриджей давали еще и возможность проводить глубокую отладку, просматривая содержимое видеопамяти, регистров процессора и т.п. Вот например одна такая станция.

Как писались игры на Dendy

     На каких компьютерах проходила разработка — неизвестно, но, учитывая тот уровень возни с радиодеталями, который требовался чтобы запустить отладочную версию игры на приставке, особого значения модель компьютера не имела. Точно известно, что японские разработчики применяли компьютеры MSX, которые практически являлись хорошо известными спектрумами. В США, с большой долей вероятности, можно сказать, что применялись компьютеры Apple 2, ввиду того, что они были довольно распространены во-первых, и имели процессор, аналогичный используемому в NES.

Программное обеспечение

     Основной и практически единственный язык программирования использовавшийся для разработки игр — ассемблер, некоторые счастливчики писали на С, если могли достать компилятор. Но даже в этом случае Некоторые фирмы не покупали у Nintendo ассемблер а писали свой собственный. Чем это было вызвано сказать тяжело, но многие источники говорят о том, что в первые годы существования NES Nintendo не делилась своими инструментами со сторонними разработчиками.

     Процессор, который использовался в NES, также имелся и в компьютерах вроде Apple 2 или Commodore 64, для которых были и свои ассемблеры и даже компиляторы С. Но архитектура NES все же имела некоторые отличия, а дикие системные ограничения и наличие банкинга(необходимости выгружать старые и загружать новые куски программного кода в область доступную процессору) не давали возможности писать на С игры, которые бы использовали максимум возможностей консоли.

     Ассемблер — далеко не единственная вещь, которую приходилось писать вручную: редакторы графики, программаторы, отладчики: все это было написано по несколько раз заботливыми руками программистов. Отдельной статьей можно считать редакторы карт — следует помнить, что компьютеры были слабы и малопамятны, так что, например, вся карта для Metroid была нарисована вручную на бумаге, а потом уже кусками кодировалась в игру.

Процесс разработки

     В разработчики часто набирали вчерашних студентов, не видевших NES в глаза. Для львиной доли мелких компаний создававших игры как нельзя лучше подходил термин «шарашкина контора». Типовая игра занимала 3-6 месяцев времени на разработку. Команды чаще всего были небольшими — 3-10 человек. Было много контор, которые выпускали 1 игру и потом исчезали без следа.

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

     Сперва дизайнер рисовал эскизы бэкграундов и персонажей, затем, после отбора подходящих рисунков, начиналась так называемая пикселизация — палитра NES поддерживала только 14 цветов на экране одновременно, так что приходилось перераскрашивать рисунки с учетом этого ограничения. Затем рисунок расчерчивался по сетке, и при необходимости растягивался или сжимался — NES аппаратно поддерживала спрайты размером 8×8 пикселей. Персонаж Марио рисовался в 4 прохода, после съедания гриба — в 8 проходов. Мелочь, но иногда из-за ее игнорирования мы видели в некоторых играх пропадающие спрайты, когда на экране становилось очень много врагов, так что программистам приходилось экономить и на спичках. Кстати видеопамяти было 2 вида — одна, в которой обычно хранились спрайты персонажей и прочая мелочевка, вторая, побольше в которой хранились карты уровней. Последняя находилась в отдельном адресном пространстве и поддерживала аппаратно ускоренные операции по скроллингу.

     Работа композитора плавно перетекала в работу программиста — написать музыку это только часть дела, закодировать ее в игру — тоже дело наживное, а вот затем начиналось самое интересное. Как было сказано выше, иногда игра подтормаживала, так что если в предрелизе такие тормоза достигали небывалых показателей, то композитору давали задачу «оптимизировать» музыку — убрать лишние опкоды, чтобы ускорить работу игры в целом. Тут-то и выяснялось истинное мастерство композитора. Лишнюю монетку в копилку позора композитора добавлял тот факт, что далеко не всегда разработчик игр по фильму получал лицензию на… использование музыки из фильма. Таким образом композиторам приходилось писать что-то оригинальное, и далеко не всегда хорошее.

     Среди игр для NES был распространен так называемый репэкедж — это когда на базе одной игры делают другую меняя спрайты и уровни, иногда немного меняя сам игровой процесс. Например Castlevania и Ninja Gaiden сделаны на одном движке, также как и Darkwing Duck был сделан на базе движка для серии игр Megaman(Rockman). Каковы были истинные масштабы репэкджа мы можем только догадываться, особенно учитывая тот факт, что огромный процент игр на денди — однотипные сайд-скроллеры.

Разработчики

     Как уже было сказано — многие из них были вчерашними студентами. В то время архитектур было немало, так что для принятия на работу часто достаточно было знания хоть какого-то ассемблера. Большой поток кадров перекочевал из когорты программистов аркадных игровых автоматов. Последних очень просто отличить по интервью — они всякий раз пытаются оправдаться системными ограничениями NES, с которыми они столкнулись после смены платформы. И правда — NES не была создана на острие технологий своего времени, она уступала тому железу, что ставилось в игровые автоматы. Зато стоила меньших денег покупателям и большей фрустрации программистам. Хорошее объяснение ограничений NES с точки зрения конечного программиста можно увидеть в этом видео: http://www.youtube.com/watch?v=Hvx4xXhZMrU.

     Меня конечно интересовало что же именно разработчики творили в те дни, но увы, именно этот вопрос так и остался без ответа. Программисты ни черта не помнят о том периоде. Т.е. они помнят какие-то организационные моменты, сложности возникавшие в команде, как они ели пиццу по выходным, как спешно искали работу программистом для NES, чтобы расплатиться с долгами, но не могут назвать марку компьютера за которым проводили бессонные ночи. И в целом можно сказать что их работа воспринималась как рутина, у них не было чувства, что они создают великие вещи, которые останутся в памяти многомиллионного поколения на всю жизнь.

     Только потом, годы спустя они стали понимать значимость того, что они писали. И тогда им стало по-настоящему стыдно. Их можно было понять — зоопарк консолей того времени, невнятность их будущего, микроменеджмент в разработке — все это создавало впечатление того, что ты пишешь нечто непонятное, непонятно для чего, и непонятно с какими перспективами.

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

     Удивительно, но остались и такие, что до сих пор продолжают разрабатывать игры, даже спустя 30 лет. Их можно найти в соцсетях, но после того как второй десяток попыток достучаться хоть до одного из них провалился, я бросил это бесполезное занятие. Впрочем список имен разработчиков, которые мне удалось выдрать из наиболее обширной коллекции ромов находится здесь: http://http://pastebin.kde.org/pyj9zj4b2.

Сегодня

     В наши дни немало людей пишет игры для NES, но это уже совсем не то — 99% программируют свои примеры исключительно для эмулятора, не задумываясь о создании картриджей. Даже нет, лучше так: сегодня у программистов есть эмуляторы со встроенным отладчиком, готовые макроассемблеры и компиляторы С, некоторое количество редакторов спрайтов и музыки, созданных коммьюнити. Сайт nesdev.com содержит все, чтобы самому заняться этим кропотливым делом. Или же хотя бы стоит просмотреть раздел Wiki на данном сайте, чтобы узнать как много вы не знали о Денди.

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

Заключение

     Судя по отрывочным сведениям которые удалось получить о разработке той эпохи, за почти 30 лет ничего не поменялось. И процесс создания игр на Денди в 1985 мало отличается от создания казуальных игрушек для смартофонов сегодня. Если кто-то обладает более подробной информацией, то я с удовольствием добавлю ее в статью. Еще вы можете погуглить и поопрашивать программистов из списка, может вам удача улыбнется больше. И если вы не дай бог клепаете сейчас игрушки для айфона, PSP, или разномастных планшетов — ведите дневник, лет через 20 он обязательно кому-то понадобится.

Автор: PerlPower

Источник

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

Создание игр для приставки денди требует от разработчика навыков написания программ на ассемблере и опыта в разработке других игр. Также необходимы навыки работы в графических редакторах.

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

Вам понадобится

  • — набор программ для редактирования изображений;
  • — программа для ассемблера;
  • — эмулятор.

Инструкция

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

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

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

Реализуйте графическую часть игры, здесь вам понадобятся навыки работы с соответствующими редакторами. Если вы не справляетесь с обеими частями разработки игры, привлеките сторонних дизайнеров или программистов. Старайтесь использовать профессиональные графические редакторы, поскольку в них вы сожжете наиболее детально продумать каждый момент интерфейса создаваемой вами игры на денди.

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

Запустите игру на эмуляторе. Если не будет выявлено багов, создание игры закончено. Не запускайте неисправленные версии игры и не выкладывайте их в интернет, лучше всего сразу доведите дело до конца.

Войти на сайт

или

Забыли пароль?
Еще не зарегистрированы?

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

У меня с детства была мечта написать игру для любимой приставки денди, шло время, мечта то появлялась то затихала снова. Она меня направляла в сторону магии программирование, и вот прошло больше 20-ти лет я программист, и снова в который раз пытаюсь постигнуть магию той самой денди что так будоражило моё воображение в тяжелом но счастливом детстве. Сегодня Я расскажу вам как постиг секреты этой магии, наконец то смог вывести спрайты на экран и научился рисовать фон.


Я php программист и по этому довольно далек от низкоуровневого программирования, по этому мне приходилось довольно долго изучать сам assembler но он оказался проще чем я думал, оказалось надо просто освободить голову от стереотипов современного программирования и смотреть на assembler как на инструмент передачи информации в некое api черный ящик. Так вот этим черным ящиком и является для нас Dendy (или NES, Famicom), так вот у этого ящика есть некие области памяти, одни предназначены только для чтения, другие для записи, а третьи и для чтения и для записи. Приставка воспроизводя игру использует несколько ключевых подсистем (процессоров):

1. CPU — центральный процессор
2. PPU — Графический процессор
3. APU — Аудио процессор

Сегодня, Я хотел бы затронуть тему работы с PPU ибо это наверное ключевое для многих людей графическое представление данных. Для начала разберем из чего состоит картридж, важными в картридже являются 2 чипа с памятью один это CHR (тут храниться битовая карта спрайтов и фона) имеет два банка памяти. Битовая карта представляет из себя набор 0-й и 1-к, размером 8х8, один спрайт состоит из 2-х таких областей. Пример ниже.

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0
0 1 1 1 1 1 0 0 + 1 1 1 1 1 1 1 1
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0
0 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0

При сложение этих двух банков они накладываются друг на друга и определяется цвет пикселя, таких цвета может быть 3-и для спрайта:

  • 0 + 0 = 0 — прозрачный цвет

  • 0 + 1 = 1 — первый цвет

  • 1 + 1 = 2 — второй цвет

  • 1 + 0 = 3 — третий цвет

Из этого примера и вытекают следующее умозаключение, что у background 4 цвета в палитре, а у спрайта всего 3-три. Потому что фон не имеет прозрачного цвета, 1-й цвет у него цвет фона.

Давайте перейдем к ассемблеру, у нес есть несколько аккумуляторов, их можно сравнить с переменными это A, X, Y. X, Y они имеют косвенное отношение к координатам. Чаще всего мы будем пользоваться аккумулятором A.

LDA {argument} - Загрузить в A значение argument (десятичное #20, hex #$20, двоичное %00010010)
STA {address} - Сохранить A в память (адрес ppu для примера)

Команды с аккумулятором X и Y

LDX {argument} - Загрузить аргумент в X
LDY {argument}
STX {address} - Сохранить X
STY {address} - Сохранить Y
INX - increment X
INY - increment Y
CPX {argument} - Сравнить x с аргументом
CPY {argument} - Сравнить y с аргументом

Команды ветвления:

JMP {address} - переход к адресу
BEQ {address} - перейти к адресу если CPX true
BNQ {address} - перейти если условие в CPX не равно 

С первоначальными командами разобрались, дальше надо разобраться с двумя основными вопросами:

  • Как отобразить фон?

  • Как нарисовать спрайт?

Отображение фона

Разрешение NES, В системе PAL 256×240, в системе NTSC 256×224. Фон состоит из спрайтов 8х8, соответственно 32х30 спрайтов в PAL, в NTSC верхний ряд спрайтов и нижний обрезаются. Для того что бы отобразить фон, необходимо 3 условия: палитра, nametable (таблица имен), таблица атрибутов.

  1. Палитра — палитра для фона содержит 16 цветов 4 вариации по 4 цвета. Как и палитра спрайтов собственно.

  2. Nametable — таблица 32х30 спрайтов, которая заполнена для отображения спрайтов на одном экране, таких таблиц может быть 2 без дополнительной оперативной памяти, если картридж содержит дополнительную память в размере 2КБайт то таких таблиц может быть 4-ре процессор поддерживает 4 таблицы. Но из за ограничения по памяти их две из коробки, в денди существует функционал зеркального отображения фона, который мы разберем в следующих статьях.

  3. Таблица атрибутов — в nes экран делиться на сетку с размером ячейки 32х32 или 2х2 спрайта, 8х8 ячеек, каждая ячейка содержит 1 байт битовой маски, %00000000. Что бы понять как она формируется давайте рассмотрим ячейку спрайтов 2 на 2 спрайта.
    ——————
    | 0 | 1 |
    ——————
    | 2 | 3 |
    ——————

    Я специально в разные ячейки поставил разные цифры, эти цифры означают порядковый номер политры, который будет применен к спрайту фона. Так вот каждые 2 бита маски обозначают какую палитру применить к спрайту в ячейке:

00 - 0-вая последовательность 4-х цветов
01 - 1-я
11 - 2-я
10 - 3-я

Немного отвлечемся и поговорим о принципе программирование на nes

Для того что бы отобразить к примеру спрайт, фон, или сделать скролинг экрана необходимо просто записать в соответствующий адрес данные, к примеру для прокрутки экрана по оси X нужно просто в адрес PPU $2005 записать сначала X координату потом Y, подчеркну последовательно.

LDA #25 ; десятичное значение X координаты
STA $2005 ; записываем #25 в адрес
LDA #00 ; 0 по оси Y
STA $2005 ; записываем #0 в адрес

Разберем другой пример рисование спрайта он довольно прост

LDA #100     ; координата Y спрайта
STA $2004   ; записываем в адрес OEM data PPU
LDA #00     ; номер спрайта в таблице тайлов (по сути таил и есть таблица спрайтов)
STA $2004   ; записываем в адрес
LDA #%00010111  ; маска которая определяет отражение спрайта по вертикали 
								; и горизонтали, а так же номер палитры
STA $2004   ; сохраняем в адрес
LDA #192    ; загружаем Y координату
STA $2004   ; и снова записываем

Такая последовательность команд выведет спрайт 8х8 в скриншоте таких спрайтов 6. Теперь перейдем к более сложному, допустим мы определили что у нас есть таблица имен адресное пространство таблицы от $2000 — $2400 видео памяти, вторая таблица $2400 — $2800 и так далее. Для начала наша задача записать в данную ячейку памяти данные nametable. Для этого нам надо просто

LDA #$20 ; старший байт
STA $2006 ; регистр 2байта тут мы указываем какой участок памяти мы бы хотели использовать
LDA #$00 ; младший байт
STA $2006

Тут мы снова видим принцип обычной последовательности записи, сначала X потом Y, либо адрес памяти в которую хотим произвести запись данных.

LDA $24 ; номер тайла или спрайта в банке CHR
STA $2007 ; регистр для записи данных
; далее возможно записывать каждый спрайт в таблицу имен отдельно

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

  LDX #00 ; загружаем 0 в x значение decimal так проще чем #$00 - #$FF к примеру
section: 
  ; какие то действия загрузка тех же спрайтов 
  LDA $24
  STA $2007
  CPX #255
  INX
  BNE section

Код секции выполниться 255 раз и заполнит 255 значений таблице имен, это как пример. Для заполнения всех значений необходимо использовать аккумулятор Y и пройтись 4 раза таким циклом. Далее используя ту же логику мы можем загрузить палитру, палитра в памяти храниться с адреса $3F00-$3F0F — 16 байт соответственно. Используя предыдущую логику загрузки nametable нам необходимо сделать следующее

LDA #$3F ; старший байт адреса $3F
STA $2006
LDA #$00 ; младший байт 
STA $2006

LDA $22 ; цвет палитры nes синий
STA $2007 ; записываем цвет в адресс $3F00

LDA $05 ; красный
STA $2007 ; записываем цвет в адресс $3F01

; и так далее еще 14 раз

Опять же палитру можно определить как последовательность байт и загрузить ее циклом который пробежится 16 раз или 16 байт

background_pallete:
  .byt $01, $02, $03, $04 
  .byt $05, $22, $23, $24 
  .byt $25, $26, $27, $28
  .byt $31, $32, $33, $34

   LDX #$00 ; x равен 0

   LDA #$3F ; старший байт адреса $3F
	 STA $2006
	 LDA #$00 ; младший байт 
	 STA $2006

loadPallete:
   LDA background_pallete, x ; background_pallete ссылается на адресс 
                             ; в котором храниться байт , x добавляет +1 к адресу
   STA $2007 ; записываем данные
   CPX #$0F ; сравниваем X с 16 
   INX ; X = X+1
   BNE loadPallete ; повторяем до тех пор пока X не будет 16 
                   ; Тогда будет загруженны все цвета палитры в видеопамять 

Осталось разобраться с тем как загрузить атрибуты фона, это тоже довольно просто атрибуты хранятся в адресном пространстве видеопамяти начиная с $23C0 — $23FF каждый ряд начинается с $23C0, $23C8 и так далее то есть 8 рядов по 8 столбцов.

Теперь нам известны область памяти для применения определенной палитры для спрайта фона, так что, что бы ее применить нам необходимо, снова, очень просто сказать в какую ячейку памяти мы бы хотели записать нашу маску, загрузить ее в аккумулятор А и записать в регистр $2007

LDA #$23
STA $2006
LDA #$C2
STA $2006 ; получаем адрес $23C2 
LDA %00001100 ; загружаем нашу маску атрибута, как ее вычислить я писал выше
STA $2007 ; записываем в регистр 

Таким образом отображение фона превращается в довольно тривиальную задачу, результатом 2-х недель изучения темы асемблеры вы видели на скриншоте в заголовке статьи. На самом деле assembler nes не такой сложный если сломать стереотипы программирования которыми мы сейчас пользуемся, и взглянуть на все это проще, когда наступает момент понимание того что, асемблером возможно обратиться в доступный участок памяти записать туда некоторую информацию, которая потом волшебным образом отобразиться на экране. Это маленькая победа над собой, маленький шаг к мечте. Спасибо всем за внимание и до скорых встреч.

Понравилась статья? Поделить с друзьями:
  • Как написать игру на source
  • Как написать игру на python для андроид
  • Как написать игру на python 2048
  • Как написать игру на pygame
  • Как написать игру на lazarus