Разработка игр для Game Boy Advance

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

1. Архитектура GBA и настройка окружения разработки DevkitPro

Архитектура GBA и настройка окружения разработки DevkitPro

Добро пожаловать на курс по разработке игр для Game Boy Advance (GBA). Выбор этой платформы для изучения геймдева — отличное решение. GBA — это идеальный баланс между простотой, позволяющей понять каждый бит системы, и мощностью, достаточной для создания красивых 2D-игр. Здесь нет операционной системы, драйверов или тяжелых игровых движков. Есть только вы, ваш код и «железо».

В этой вводной статье мы разберем, как устроен GBA изнутри, почему математика здесь работает иначе и как подготовить ваш компьютер к созданию первой игры.

Сердце консоли: Процессор и архитектура

Game Boy Advance, выпущенный в 2001 году, был настоящим технологическим прорывом. Чтобы писать эффективный код, нужно понимать, на чем он исполняется.

!Блок-схема архитектуры GBA, показывающая взаимодействие процессора, памяти и периферии.

Процессор ARM7TDMI

Центральный процессор GBA — это 32-битный RISC-процессор ARM7TDMI, работающий на частоте 16.78 МГц. Давайте расшифруем, что это значит для нас как разработчиков:

* RISC (Reduced Instruction Set Computer): Упрощенный набор команд. Каждая инструкция выполняется очень быстро, обычно за один такт. * 32-битный: Процессор оперирует 32-битными числами. Это «родной» размер данных.

Особенность этого процессора в том, что он поддерживает два набора инструкций:

  • ARM (32-бит): Мощные инструкции, дают максимальную производительность. Но код занимает больше места в памяти.
  • Thumb (16-бит): Сжатый набор инструкций. Код занимает меньше места, но может работать чуть медленнее в определенных ситуациях.
  • > В разработке под GBA мы часто используем Thumb-инструкции для основной логики, чтобы экономить драгоценное место на картридже, и переключаемся на ARM для критически важных вычислений, требующих скорости.

    Память и её карта

    У GBA нет жесткого диска. Вся память доступна процессору напрямую через единое адресное пространство. Однако память неоднородна и разделена на секции:

    | Тип памяти | Название | Объем | Назначение | | :--- | :--- | :--- | :--- | | EWRAM | External Work RAM | 256 КБ | Медленная память для больших данных и переменных. | | IWRAM | Internal Work RAM | 32 КБ | Быстрая память внутри чипа. Здесь хранят критичный код и стек. | | VRAM | Video RAM | 96 КБ | Видеопамять. Сюда мы пишем то, что хотим увидеть на экране. | | OAM | Object Attribute Memory | 1 КБ | Память атрибутов объектов (спрайтов). | | ROM | Game Pak | до 32 МБ | Память картриджа (только для чтения). Здесь живет ваша игра. |

    Дисплей

    Экран GBA имеет разрешение 240x160 пикселей. Это может показаться малым по современным меркам, но именно это ограничение заставляет художников и программистов творить чудеса пиксель-арта. Частота обновления экрана составляет чуть меньше 60 Гц (точнее, 59.73 Гц).

    Математика на GBA: Числа с фиксированной точкой

    Одной из главных особенностей программирования под старые консоли является отсутствие FPU (Floating Point Unit) — специального блока процессора для работы с дробными числами (float, double).

    Если вы попытаетесь использовать тип float в своем коде (например, float x = 1.5;), процессор будет эмулировать вычисления программно. Это катастрофически медленно. Обычное сложение двух float может занять в 50-100 раз больше времени, чем сложение целых чисел.

    Решение? Арифметика с фиксированной точкой (Fixed Point Arithmetic).

    Суть метода в том, что мы используем обычные целые числа (int), но договариваемся, что часть бит отводится под целую часть, а часть — под дробную. Например, в формате «8.8» (всего 16 бит) 8 бит — это целое, и 8 бит — дробь.

    Формула перевода из реального числа в число с фиксированной точкой выглядит так:

    Где: * — полученное целочисленное значение, которое мы будем хранить в памяти GBA. * — исходное дробное число (например, 1.5). * — количество бит, отведенных под дробную часть (точность). * — масштабный коэффициент (сдвиг влево).

    !Визуализация принципа Fixed Point: как дробное число превращается в целое путем сдвига битов.

    Пример: Если мы используем 8 бит для дробной части (), то . Чтобы представить число , мы умножаем его на 256:

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

    Настройка окружения: DevkitPro

    Чтобы писать код на C или C++ и превращать его в файл .gba, нам нужен кросс-компилятор. Стандартом индустрии хомбрю (homebrew — любительская разработка) является DevkitPro.

    DevkitPro — это набор инструментов, включающий в себя специальную версию GCC (компилятора) для архитектуры ARM и необходимые библиотеки, главной из которых является libtonc.

    Шаг 1: Установка

    Процесс установки зависит от вашей ОС, но он максимально автоматизирован.

  • Перейдите на официальный сайт DevkitPro.
  • Скачайте установщик (графический для Windows или скрипт pacman для Mac/Linux).
  • При выборе компонентов убедитесь, что отмечена группа GBA (Game Boy Advance).
  • Шаг 2: Выбор редактора кода

    Я настоятельно рекомендую использовать Visual Studio Code. Это легкий и мощный редактор. Для удобства установите расширения: C/C++* (от Microsoft) — для подсветки синтаксиса и навигации. Makefile Tools* — так как сборка проектов GBA обычно идет через Makefile.

    Шаг 3: Эмулятор

    Вам не обязательно каждый раз записывать игру на реальный картридж. Для разработки мы используем эмуляторы. Лучшим выбором для разработчика сейчас является mGBA. Он точен, быстр и имеет встроенные инструменты отладки (просмотр памяти, палитр, тайлов).

    Ваш первый проект: Hello World

    В мире GBA «Hello World» — это не вывод текста в консоль (консоли-то нет!), а закрашивание экрана в какой-либо цвет. Это доказывает, что вы получили контроль над видеопамятью.

    Создайте папку проекта и файл main.c. Вот минимальный код:

    Разбор магии

  • Указатели: Мы обращаемся к памяти напрямую через адреса. 0x06000000 — это начало VRAM. Записывая туда число, мы меняем цвет пикселя на экране.
  • Регистры ввода-вывода: REG_DISPCNT по адресу 0x04000000 — это «пульт управления» видеочипом. Записывая туда битовые флаги, мы говорим GBA: «Включи видеорежим 3».
  • Volatile: Ключевое слово, говорящее компилятору: «Не хитри, эта память может измениться или важна для железа, пиши туда всегда, даже если тебе кажется это лишним».
  • Для компиляции этого кода вам понадобится Makefile, который идет в комплекте с примерами DevkitPro. После команды make вы получите файл .gba, который можно открыть в mGBA и увидеть красную точку на черном фоне.

    Заключение

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

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

    2. Основы графики: видеорежимы, тайлы и создание фонов

    Основы графики: видеорежимы, тайлы и создание фонов

    В предыдущей статье мы научились настраивать окружение и вывели на экран одну красную точку. Мы использовали Режим 3 (Mode 3), который позволяет обращаться к каждому пикселю экрана напрямую. Это интуитивно понятно, но крайне неэффективно для создания полноценных игр.

    Представьте, что вы хотите сдвинуть фон игры (например, в платформере). В растровом режиме (как Mode 3) процессору пришлось бы переписывать цвет каждого из 38 400 пикселей (240x160) в каждом кадре. Процессор GBA, хоть и шустрый для своего времени, захлебнется от такой нагрузки, и игра будет тормозить.

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

    Видеоконтроллер GBA

    В отличие от современных ПК, где видеокарта — это отдельное мощное устройство, в GBA видеосистема (PPU — Picture Processing Unit) тесно интегрирована в чип. Она берет на себя всю тяжелую работу по отрисовке, оставляя процессору время на игровую логику.

    У GBA есть 6 видеорежимов, которые делятся на две группы:

  • Тайловые режимы (0, 1, 2): Экран собирается из маленьких кирпичиков (тайлов). Это основной способ создания 2D-игр (Mario, Pokémon, Metroid).
  • Растровые режимы (3, 4, 5): Позволяют рисовать произвольные пиксели. Используются для 3D-игр (как Doom) или показа статических картинок, но требуют много памяти и ресурсов CPU.
  • Мы сосредоточимся на Режиме 0, так как он самый универсальный и предоставляет доступ ко всем четырем слоям фона одновременно.

    Что такое тайл?

    Тайл (Tile) — это квадратное изображение размером 8x8 пикселей. Это базовый строительный блок графики GBA.

    Вместо того чтобы хранить огромную картинку уровня целиком, мы разбиваем её на уникальные кусочки. Если у вас есть длинная кирпичная стена, вам не нужно рисовать каждый кирпич. Вы рисуете один тайл «кирпич» и просите видеоконтроллер повторить его 100 раз.

    !Принцип тайловой графики: изображение собирается из набора уникальных блоков 8x8 пикселей.

    Хранение тайлов в памяти

    Тайлы хранятся в специальной области видеопамяти (VRAM), которая называется Character Block (блок символов). GBA поддерживает разную глубину цвета, но стандартом для тайлов является 4 бита на пиксель (4bpp). Это означает, что каждый пиксель может иметь один из 16 цветов.

    Давайте посчитаем размер одного тайла в байтах:

    Где: * — размер тайла в байтах. * — ширина тайла (8 пикселей). * — высота тайла (8 пикселей). * — глубина цвета (4 бита). * Деление на 8 переводит биты в байты.

    Подставим значения:

    Итак, один тайл занимает ровно 32 байта. Это очень удобно для процессора.

    Анатомия фона: Тайлы, Карты и Палитры

    Чтобы вывести изображение на экран в тайловом режиме, нам нужны три компонента:

  • Tileset (Набор тайлов): Сами картинки 8x8. Хранятся в Character Blocks.
  • Tilemap (Карта тайлов): Сетка, которая говорит консоли: «В координатах (0,0) нарисуй тайл №5, а в (1,0) — тайл №2». Хранится в Screen Blocks.
  • Palette (Палитра): Набор цветов. Так как тайлы хранят не сам цвет, а индекс цвета (от 0 до 15), палитра определяет, какой реальный цвет соответствует каждому индексу.
  • Карта тайлов (Screen Entry)

    Карта тайлов — это массив 16-битных чисел. Каждое число описывает один блок 8x8 на экране. Структура этого 16-битного числа (Screen Entry) выглядит так:

    | Биты | Назначение | | :--- | :--- | | 0-9 | ID тайла (номер картинки в наборе, от 0 до 1023) | | 10 | Горизонтальный разворот (Flip H) — зеркалит тайл | | 11 | Вертикальный разворот (Flip V) — переворачивает тайл | | 12-15 | ID палитры (какую из 16 палитр использовать) |

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

    Практика: Рисуем паттерн в Режиме 0

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

    Шаг 1: Подготовка данных

    В реальной разработке мы используем конвертеры (например, Grit или Usenti), чтобы превращать PNG-картинки в массивы C-кода. Но для понимания сути создадим один тайл вручную. Представим тайл как массив из 8 полосок по 4 байта (так как одна строка 8 пикселей по 4 бита = 32 бита = 4 байта).

    Шаг 2: Настройка регистров

    Нам понадобятся новые адреса памяти и регистры.

    * REG_DISPCNT (0x04000000): Управление дисплеем. * REG_BG0CNT (0x04000008): Управление фоном 0. * MEM_PALETTE (0x05000000): Память палитры фона. * MEM_VRAM (0x06000000): Видеопамять.

    В VRAM память разделена на блоки по 16 КБ (для тайлов) и по 2 КБ (для карт). Мы должны сказать контроллеру, где что лежит.

    Шаг 3: Код программы

    Примечание к коду: В реальном коде мы бы использовали memcpy для копирования массивов, но здесь использован цикл для наглядности.

    Слои и приоритеты

    В Режиме 0 мы можем включить до 4-х фонов одновременно (BG0, BG1, BG2, BG3). Зачем так много?

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

    Каждый фон имеет свой приоритет (от 0 до 3). Фон с приоритетом 0 рисуется поверх всех остальных. Если пиксель верхнего фона имеет индекс цвета 0 (прозрачный), сквозь него будет виден пиксель нижнего фона.

    Заключение

    Мы перешли от рисования точек к управлению аппаратными блоками. Использование тайлов и карт — это ключ к производительности GBA. Вместо того чтобы менять каждый пиксель, мы просто меняем одно число в карте, и весь блок 8x8 мгновенно меняется на экране.

    В этой статье мы научились:

  • Различать тайловые и растровые режимы.
  • Понимать структуру тайла (8x8 пикселей, 4bpp).
  • Связывать тайлы, карты и палитры через регистры управления.
  • В следующем уроке мы добавим в наш мир жизнь: разберем спрайты (объекты) — подвижные элементы игры, которые могут свободно перемещаться поверх фонов, не будучи привязанными к сетке.

    3. Работа со спрайтами: память OAM, анимация и движение

    Работа со спрайтами: память OAM, анимация и движение

    В предыдущей статье мы научились создавать красивые статические миры, используя тайловые фоны. Но мир игры мертв без движения. Герой должен бежать, враги — атаковать, а монетки — вращаться. Для всего этого в Game Boy Advance используется специальная аппаратная сущность — спрайты.

    Спрайты (или аппаратные объекты, OBJ) — это графические элементы, которые могут свободно перемещаться по экрану поверх фона, не будучи привязанными к сетке тайловой карты. В этой статье мы разберем, как управлять ими через память OAM, как заставить их двигаться и как реализовать покадровую анимацию.

    Что такое OAM?

    В отличие от современных движков, где вы создаете объект командой new Sprite(), в GBA количество спрайтов строго ограничено железом. У нас есть фиксированная таблица в памяти, называемая OAM (Object Attribute Memory).

    Эта память находится по адресу 0x07000000 и имеет размер 1 КБ. Она содержит ровно 128 слотов для описания спрайтов. Даже если спрайт не виден на экране, слот под него существует. Чтобы «удалить» спрайт, мы просто перемещаем его за пределы видимой области или отключаем его отрисовку.

    !Структура памяти OAM: 128 слотов, управляющих отображением объектов на экране.

    Структура атрибутов спрайта

    Каждый спрайт описывается тремя 16-битными числами (атрибутами). Давайте назовем их attr0, attr1 и attr2. Именно манипулируя битами в этих числах, мы говорим видеопроцессору, где и как рисовать персонажа.

    #### Attribute 0: Y-координата и форма

    Первый атрибут отвечает за вертикальную позицию и общие настройки режима.

    | Биты | Назначение | | :--- | :--- | | 0-7 | Y-координата. Позиция по вертикали (0-255). | | 8-9 | Режим отрисовки. (0 = Обычный, 1 = Полупрозрачный, 2 = Окно, 3 = Скрыт). | | 10-11 | Режим GFX. (Используется для альфа-блендинга и мозаики). | | 12 | Мозаика. Включить эффект пикселизации. | | 13 | Цветность. (0 = 4bpp/16 цветов, 1 = 8bpp/256 цветов). | | 14-15 | Форма. (0 = Квадрат, 1 = Горизонтальный, 2 = Вертикальный). |

    #### Attribute 1: X-координата и размер

    Второй атрибут контролирует горизонталь и размер спрайта.

    | Биты | Назначение | | :--- | :--- | | 0-8 | X-координата. Позиция по горизонтали (0-511). | | 9-11 | Не используются (в обычном режиме). | | 12 | Horizontal Flip. Отразить по горизонтали. | | 13 | Vertical Flip. Отразить по вертикали. | | 14-15 | Размер. Вместе с формой из Attr0 определяет размеры в пикселях. |

    #### Attribute 2: Тайлы и палитра

    Третий атрибут связывает спрайт с графическими данными.

    | Биты | Назначение | | :--- | :--- | | 0-9 | Индекс тайла. Номер начального тайла в видеопамяти. | | 10-11 | Приоритет. Слой отрисовки относительно фонов (0-3). | | 12-15 | Палитра. Номер палитры (0-15), если используется режим 4bpp. |

    Размеры спрайтов

    Спрайты в GBA не обязаны быть размером 8x8. Комбинируя биты Shape (из Attr0) и Size (из Attr1), мы можем получить следующие размеры:

    | Size / Shape | 0 (Square) | 1 (Horizontal) | 2 (Vertical) | | :--- | :--- | :--- | :--- | | 0 | 8x8 | 16x8 | 8x16 | | 1 | 16x16 | 32x8 | 8x32 | | 2 | 32x32 | 32x16 | 16x32 | | 3 | 64x64 | 64x32 | 32x64 |

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

    Программирование спрайтов

    Чтобы работать с OAM удобно, нам нужно создать структуру данных на языке C, которая будет точно соответствовать «железу». Поскольку мы не хотим вручную сдвигать биты каждый раз (val << 8), мы используем битовые поля или макросы. Для простоты и надежности в GBA-разработке чаще используют макросы и typedef.

    Загрузка графики

    Тайлы спрайтов хранятся в VRAM, но отдельно от тайлов фона. Обычно для спрайтов выделяются верхние блоки памяти (начиная с адреса 0x06010000 в видеорежимах 0-2). Это называется OBJ VRAM.

    Процесс вывода спрайта на экран выглядит так:

  • Скопировать тайлы (картинку) в OBJ VRAM.
  • Скопировать палитру в OBJ Palette RAM (0x05000200).
  • Настроить атрибуты в OAM.
  • Движение: Изменяем координаты

    Движение — это просто изменение координат и в каждом кадре. Однако есть нюанс: экран обновляется 60 раз в секунду. Если мы будем менять координаты в момент, когда луч экрана рисует изображение, мы получим артефакт, называемый «разрывом кадра» (screen tearing).

    Чтобы этого избежать, мы должны менять OAM только во время VBlank (Vertical Blank) — короткой паузы, когда луч возвращается из правого нижнего угла в левый верхний.

    Формула обновления позиции проста:

    Где: * — новая координата (X или Y). * — текущая координата. * — скорость (пикселей за кадр). * — шаг времени (в нашем случае всегда 1 кадр).

    Пример кода движения

    Анимация: Смена кадров

    Анимация спрайта — это иллюзия, создаваемая быстрой сменой картинок. В терминах GBA это означает изменение индекса тайла (Tile Index) в attr2.

    Представьте, что у вас есть анимация бега персонажа, состоящая из 4 кадров. Каждый кадр занимает, например, 4 тайла в памяти (размер спрайта 16x16). Тайлы лежат в памяти последовательно.

    Математически индекс тайла для текущего кадра вычисляется так:

    Где: * — индекс тайла, который мы запишем в attr2. * — индекс самого первого тайла анимации в VRAM. * — номер текущего кадра анимации (0, 1, 2...). * — «шаг» или размер одного кадра в тайлах (сколько тайлов занимает одна картинка).

    !Принцип покадровой анимации: смещение индекса тайла в зависимости от текущего кадра.

    Реализация таймера анимации

    Мы не можем менять кадр каждый цикл процессора (это будет слишком быстро). Нам нужно замедление. Обычно вводят переменную-счетчик.

    Особенности координат и "заворачивание"

    Интересный факт: координаты спрайтов в GBA работают по модулю. * Y-координата имеет 8 бит (0-255), хотя высота экрана 160. * X-координата имеет 9 бит (0-511), хотя ширина экрана 240.

    Если вы запишете в Y значение 250, спрайт не исчезнет далеко внизу. Поскольку экран имеет высоту 160, значения от 160 до 255 находятся в «невидимой зоне», но если спрайт большой, его нижняя часть может «вылезти» сверху экрана. Это поведение нужно учитывать при реализации камеры.

    Заключение

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

  • Атрибуты OAM — пульт управления спрайтом.
  • VBlank — правильное время для обновлений.
  • Манипуляция индексами — основа анимации.
  • В следующей статье мы оживим нашу игру по-настоящему: научимся обрабатывать нажатия кнопок (Input Handling) и напишем простую игровую логику, чтобы наш персонаж слушался игрока.

    4. Обработка ввода, игровая логика и коллизии

    Обработка ввода, игровая логика и коллизии

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

    Чтобы превратить проект в игру, нам нужно добавить интерактивность. Игрок должен управлять героем, а герой должен взаимодействовать с миром — собирать монеты, прыгать по платформам или врезаться в врагов. В этой статье мы разберем три кита игрового процесса: чтение ввода (кнопок), организацию игрового цикла и физику столкновений (коллизии).

    Чтение ввода: Как GBA видит кнопки

    Game Boy Advance имеет 10 кнопок: крестовина (Up, Down, Left, Right), основные кнопки (A, B), плечевые шифты (L, R) и служебные кнопки (Start, Select). В отличие от современных геймпадов с аналоговыми стиками, все кнопки GBA — цифровые. Они могут быть либо нажаты, либо отпущены.

    Регистр ввода KEYINPUT

    Состояние всех кнопок хранится в одном 16-битном регистре памяти по адресу 0x04000130. Мы будем называть его REG_KEYINPUT.

    Главная особенность, которая сбивает с толку новичков: инвертированная логика (Active LOW).

    * Если бит равен 1, кнопка ОТПУЩЕНА. * Если бит равен 0, кнопка НАЖАТА.

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

    Карта битов выглядит так:

    | Бит | Кнопка | Шестнадцатеричное значение | | :--- | :--- | :--- | | 0 | A | 0x0001 | | 1 | B | 0x0002 | | 2 | Select | 0x0004 | | 3 | Start | 0x0008 | | 4 | Right | 0x0010 | | 5 | Left | 0x0020 | | 6 | Up | 0x0040 | | 7 | Down | 0x0080 | | 8 | R | 0x0100 | | 9 | L | 0x0200 |

    Три состояния кнопки

    Просто знать, нажата ли кнопка прямо сейчас, недостаточно. В играх нам нужны разные типы реакций:

  • Held (Удержание): Персонаж бежит, пока мы держим стрелку вправо.
  • Down (Нажатие): Персонаж прыгает только один раз в момент касания кнопки A. Если мы держим A, он не должен улетать в космос.
  • Up (Отпускание): Зарядка выстрела происходит, пока держим B, и выстрел происходит, когда отпускаем.
  • Чтобы реализовать это, нам нужно хранить состояние кнопок в предыдущем кадре и сравнивать его с текущим.

    !Диаграмма, показывающая, как сравнение текущего и предыдущего состояния кнопок формирует события ввода.

    Давайте напишем простой менеджер ввода на C:

    Теперь в игровом цикле мы можем писать логичный код:

    Игровая логика и структура кадра

    Игры работают в бесконечном цикле. Один проход этого цикла называется кадром. На GBA мы стремимся к стабильным 60 кадрам в секунду (FPS). Чтобы игра работала предсказуемо, порядок действий внутри цикла должен быть строгим.

    Типичный игровой цикл (Game Loop) выглядит так:

  • VBlank (Вертикальная синхронизация): Ждем начала паузы отрисовки.
  • Input (Ввод): Опрашиваем кнопки (key_poll).
  • Logic (Логика): Обновляем мир игры. Двигаем персонажей, проверяем коллизии, считаем таймеры.
  • Render (Отрисовка): Подготавливаем данные для видеопамяти. Копируем новые координаты спрайтов в OAM.
  • > Важно разделять логику и отрисовку. Логика меняет переменные (x, y, hp), а отрисовка просто берет эти переменные и переносит их в видеопамять. Не пишите в OAM посреди проверки коллизий.

    Коллизии: AABB

    Самая сложная часть для новичка — научить объекты не проходить сквозь друг друга. В 3D-играх используется сложная математика с полигонами, но на GBA стандартом является AABB (Axis-Aligned Bounding Box) — выровненный по осям ограничивающий прямоугольник.

    Суть метода проста: мы представляем каждый объект (персонажа, врага, стену) как прямоугольник. "Выровненный по осям" означает, что этот прямоугольник не может вращаться; его стороны всегда параллельны краям экрана. Это значительно упрощает математику.

    Математика пересечения

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

    Два прямоугольника пересекаются тогда и только тогда, когда выполняются все четыре условия одновременно:

    Где: * — координаты верхнего левого угла первого объекта. * — ширина и высота первого объекта. * — координаты верхнего левого угла второго объекта. * — ширина и высота второго объекта. * — логическое "И".

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

    !Иллюстрация принципа AABB коллизий: условия пересечения двух прямоугольников.

    Реализация на C

    Создадим простую структуру для хитбокса (зоны поражения) и функцию проверки:

    Реакция на коллизию

    Обнаружить столкновение — это полдела. Нужно правильно на него отреагировать. Реакция зависит от типа объекта:

  • Сбор предмета (Монетка): Если пересечение есть -> Увеличить счет, пометить монетку как "неактивную" (скрыть спрайт).
  • Вредный объект (Шипы): Если пересечение есть -> Уменьшить здоровье, отбросить игрока назад.
  • Твердый объект (Стена): Это самое сложное. Нам нужно не просто зафиксировать факт, а вытолкнуть игрока из стены, чтобы он не застрял.
  • Для стен обычно используют проверку перед движением:

    Разделение движения по осям X и Y критически важно. Если двигать игрока сразу по диагонали и проверять коллизию, он может застрять углом в стене или провалиться сквозь пол.

    Оптимизация: Fixed Point и хитбоксы

    В первой статье мы говорили о числах с фиксированной точкой. В логике коллизий они тоже важны. Координаты спрайтов на экране — целые числа (пиксели). Но координаты в логике игры должны быть более точными (Fixed Point), чтобы обеспечить плавное движение.

    Однако для проверки AABB мы обычно конвертируем координаты обратно в целые числа (пиксели), так как коллизия с точностью до 1/256 пикселя избыточна и дорога для процессора.

    Также помните: Хитбокс != Спрайт. Спрайт персонажа может быть 32x32 пикселя, но вокруг него может быть много пустого места (прозрачные пиксели). Если использовать весь размер спрайта для коллизий, игрок будет умирать, даже не коснувшись шипов визуально. Хорошей практикой считается делать хитбокс чуть меньше видимого спрайта.

    Заключение

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

  • Вы умеете рисовать мир (тайлы).
  • Вы умеете рисовать героев (спрайты).
  • Вы умеете управлять героем (ввод).
  • Вы умеете взаимодействовать с миром (коллизии).
  • В следующей статье мы добавим в нашу игру звук и музыку, используя движок GBA Direct Sound и программируемые каналы PSG, чтобы мир стал по-настоящему живым.

    5. Звуковая подсистема, прерывания и прямой доступ к памяти (DMA)

    Звуковая подсистема, прерывания и прямой доступ к памяти (DMA)

    Мы уже создали графику, настроили движение персонажа и обработали коллизии. Наша игра выглядит живой, но она всё ещё «немая». Звук — это 50% атмосферы игры. Представьте Super Mario без звука прыжка или Doom без рычания монстров.

    Однако работа со звуком на Game Boy Advance (GBA) ставит перед нами серьезную техническую проблему. Воспроизведение качественного цифрового аудио требует передачи данных тысячи раз в секунду. Если мы заставим центральный процессор (CPU) заниматься этим вручную, у него не останется времени на обработку игровой логики, и игра превратится в слайд-шоу.

    В этой статье мы разберем три взаимосвязанные темы:

  • Звуковая архитектура GBA: Как устроены каналы PSG и Direct Sound.
  • Прерывания (Interrupts): Как заставить процессор реагировать на события мгновенно.
  • DMA (Direct Memory Access): Как передавать данные (например, аудиосэмплы) без участия процессора.
  • Анатомия звука GBA

    Звуковая подсистема GBA — это гибрид двух эпох. Она содержит в себе полное звуковое ядро от старого Game Boy (Game Boy Color) для обратной совместимости и новые цифровые каналы для воспроизведения сэмплов.

    !Блок-схема звуковой подсистемы, объединяющая старые синтезированные каналы и новые цифровые каналы.

    Всего нам доступно 6 каналов:

    1. PSG Каналы (Programmable Sound Generator)

    Это «чиптюн» каналы, генерирующие волны математически. Они почти не нагружают процессор и память.

    * Канал 1 (Square with Sweep): Прямоугольная волна с возможностью изменения частоты (sweep). Идеально для звуков прыжков, падений или усиления (power-up). * Канал 2 (Square): Прямоугольная волна без sweep. Используется для основной мелодии. * Канал 3 (Wave): Канал произвольной волны. Мы можем загрузить в него крошечный сэмпл (32 полубайта), чтобы получить более сложные тембры (например, бас). * Канал 4 (Noise): Генератор белого шума. Незаменим для звуков взрывов, ударов и перкуссии (барабанов).

    2. Direct Sound (Каналы A и B)

    Это главное нововведение GBA. Direct Sound позволяет воспроизводить 8-битные PCM-сэмплы (Pulse Code Modulation) — то есть реальные записи звука, как в формате .wav.

    Именно через эти каналы звучат голоса персонажей, реалистичная музыка и сложные звуковые эффекты. Однако, в отличие от PSG, Direct Sound не генерирует звук сам. Мы должны «скармливать» ему байты данных с определенной скоростью (частотой дискретизации).

    Проблема производительности

    Допустим, мы хотим воспроизвести звук с частотой 16 кГц (16 000 сэмплов в секунду). Это значит, что каждые 62.5 микросекунды мы должны брать новый байт из памяти звука и класть его в регистр аудио.

    Если делать это в основном цикле while(1), процессор будет занят только перекладыванием байтов. Игра остановится. Нам нужен механизм, который будет делать это автоматически.

    Здесь на сцену выходят Прерывания и DMA.

    Прерывания (Interrupts)

    В наших прошлых уроках мы использовали опрос (polling) — мы постоянно спрашивали в цикле: «Нажата ли кнопка? Наступил ли VBlank?». Это как если бы вы ждали доставку пиццы и каждые 5 секунд открывали дверь, чтобы проверить курьера.

    Прерывание — это дверной звонок. Вы занимаетесь своими делами, а когда курьер приходит, он звонит, вы прерываетесь, забираете пиццу и возвращаетесь к делам.

    Когда происходит аппаратное событие (нажатие кнопки, окончание отрисовки строки, срабатывание таймера), процессор может приостановить выполнение основной программы, прыгнуть в специальную функцию (ISR — Interrupt Service Routine), выполнить её и вернуться назад.

    Настройка прерываний

    Чтобы прерывания заработали, нужно включить три «рубильника»:

  • IME (Interrupt Master Enable): Главный рубильник процессора. Если он выключен (0), процессор игнорирует все звонки.
  • IE (Interrupt Enable): Регистр, где мы выбираем, какие именно события нас интересуют (например, только VBlank и Timer 0).
  • Флаги в периферии: В регистрах самого таймера или дисплея нужно разрешить отправку сигнала прерывания.
  • DMA: Прямой доступ к памяти

    Даже с прерываниями копирование данных — это рутинная работа. Процессор — это архитектор, ему не пристало таскать кирпичи. Для этого есть DMA (Direct Memory Access).

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

    У GBA есть 4 канала DMA (0, 1, 2, 3). Для звука обычно используются каналы 1 и 2.

    Как работает звук через DMA?

    Схема воспроизведения сэмпла выглядит так:

  • Мы загружаем звуковой файл (массив байтов) в память (ROM или RAM).
  • Мы настраиваем Таймер на нужную частоту воспроизведения (например, 16 кГц).
  • Мы настраиваем DMA так: «Копируй данные из массива в звуковой буфер (FIFO). Делай это каждый раз, когда тикает Таймер».
  • Процессор запускает этот механизм и забывает о нем. DMA сам подкидывает дрова в топку звукового чипа по сигналу таймера.
  • Математика звука: Настройка таймера

    Таймеры GBA работают на частоте системной шины — 16.78 МГц (точнее Гц). Таймер — это 16-битный счетчик, который увеличивается каждый такт (или каждые 64/256/1024 такта, если включен прескейлер). Когда счетчик переполняется (доходит до 65535 и сбрасывается в 0), он посылает сигнал DMA.

    Чтобы получить нужную частоту дискретизации (Sample Rate), нам нужно вычислить начальное значение таймера. Таймер будет считать от этого значения до переполнения.

    Формула расчета значения для регистра таймера:

    Где: * — значение, которое мы записываем в регистр данных таймера (REG_TMxD). * — максимальное значение 16-битного счетчика (). * — частота процессора GBA, равная Гц. * — желаемая частота воспроизведения звука (например, Гц, Гц, Гц).

    Пример: Мы хотим воспроизвести звук с частотой 16 кГц ( Гц).

    В шестнадцатеричной системе это 0xFBE7. Если мы запишем это число в таймер, он будет переполняться ровно 16 000 раз в секунду, каждый раз заставляя DMA отправить новый байт звука в динамики.

    Реализация на C

    Давайте посмотрим, как это выглядит в коде. Мы будем использовать Direct Sound Channel A.

    Разбор магии FIFO

    Вы могли заметить странность: мы копируем 32 бита (DMA_32), хотя сэмплы 8-битные. И мы копируем в один и тот же адрес REG_FIFO_A.

    Звуковой буфер GBA (FIFO — First In, First Out) вмещает 32 байта. DMA работает очень быстро и «закидывает» туда сразу пачку данных (4 байта за раз). Звуковой чип затем медленно «достает» оттуда по одному байту с той скоростью, которую задает таймер. Если буфер опустеет, звук прервется. DMA следит за тем, чтобы буфер всегда был полон.

    Двойная буферизация (Double Buffering)

    Приведенный выше код имеет недостаток: он может проиграть только один короткий звук, который целиком помещается в память. А что делать с фоновой музыкой, которая длится 3 минуты?

    Мы не можем загрузить 30 МБ музыки в 256 КБ оперативной памяти. Мы используем кольцевой буфер или двойную буферизацию.

  • Выделяем небольшой буфер в памяти.
  • Запускаем воспроизведение из начала буфера.
  • Пока проигрывается первая половина, мы (с помощью прерываний!) быстро загружаем новую порцию музыки во вторую половину.
  • Когда проигрывание доходит до второй половины, мы обновляем первую.
  • Это создает иллюзию бесконечного потока музыки, используя минимум памяти.

    Заключение

    Сегодня мы коснулись самой сложной части низкоуровневой разработки — синхронизации железа. Мы узнали:

    * Как GBA объединяет старые PSG каналы и новые Direct Sound каналы. * Зачем нужны прерывания (чтобы не опрашивать железо постоянно). * Как DMA позволяет перекладывать данные без участия процессора. * Как использовать математику для настройки точной частоты воспроизведения.

    Теперь ваша игра может звучать! В следующей статье мы разберем работу с файловой системой картриджа и сохранение прогресса (SRAM), чтобы игрокам не приходилось начинать игру заново каждый раз.