Как устроен мультиплатформенный движок для Nokia 3310 Jam

В сентябре 2026 года разработчик Hoani принял участие в восьмом конкурсе Nokia 3310 Jam. Его игра Trick Shot II заняла первое место среди 51 работы. Главной задачей конкурса было создать проект в духе оригинального телефона Nokia 3310, сохранив ограничения его аппаратной платформы.
Участники должны были ориентироваться на монохромный экран размером 84×48 пикселей, звук пьезодинамика и единственный аудиоканал. Управление ограничивалось кнопками телефона: мышь разрешалось использовать только при работе с виртуальной клавиатурой. Чтобы одна и та же игра запускалась в браузере и на реальном устройстве, автор разработал собственный мультиплатформенный движок на Go.
От макета на проводах до платы для Nokia 3310
Изначально у разработчика не было самого телефона. Для прототипирования он соединил любительский ЖК-дисплей PCD8544, пьезодинамик и кнопочный блок. Распиновка этой временной схемы совпадала с той, что позднее использовалась в собственной плате для Nokia 3310.
Получив телефон, автор разобрал его, чтобы изучить конструкцию корпуса и электронику. Контур печатной платы, контактные площадки, расположение светодиодов и кнопочной панели пришлось определять по реальному корпусу с помощью обычной линейки. Первая версия платы была изготовлена всего за три дня до начала джема и спаяна вручную.
В первоначальном варианте оказалось несколько ошибок: нижние отступы были измерены неправильно, а толщина платы составила 1,6 мм вместо требуемого 1 мм. Из-за этого пришлось обрабатывать края платы дремелем, а корпус телефона закрывался неидеально. Поздние ревизии устранили эти недостатки. В конструкции также появились дополнительные возможности, включая RGB-подсветку и измерение заряда батареи.
Архитектура игрового движка
За основу API автор взял подход Ebitengine — движка для двумерных игр на Go. Проект разделён на две части. Папка engine содержит интерфейсы и библиотеки, используемые самой игрой, а platform отвечает за работу с конкретной средой: ввод, экран, звук и игровой цикл.
Игра должна реализовать три основных метода: настройку окружения, обновление состояния и отрисовку. На этапе настройки приложение получает доступ к клавиатуре, звуковому проигрывателю и дисплею. Метод обновления отвечает за пошаговую игровую логику, а метод отрисовки выводит изображение на холст размером 84×48 пикселей.
Графический API поддерживает текст, геометрические примитивы, спрайты и инструменты дизеринга. Для текста используется библиотека TinyFont из TinyGo: шрифты преобразуются в двоичные данные, которые можно разместить непосредственно в памяти прошивки.
Графика и экономия памяти
Чтобы не расходовать оперативную память микроконтроллера, движок не распаковывает изображения из форматов PNG, JPEG или GIF во время работы. Спрайты хранятся в ROM в формате PBM. Утилита png2pbm позволяет преобразовать исходные изображения в оттенки серого или монохромный вид, а также подготовить ленты кадров для анимации.
Серые изображения выводятся с помощью дизеринга — чередования доступных цветов, которое создаёт иллюзию промежуточных оттенков. Яркость пикселя сравнивается с пороговой картой 8×8, основанной на матрице M8. Значения яркости находятся в диапазоне от 0 до 64: ноль означает прозрачность, а максимальное значение — полностью закрашенный пиксель. В Trick Shot II такой подход использован, например, на титульном экране, тогда как уровни собраны из монохромных спрайтов.
Набор примитивов включает треугольные веера и полосы, заимствованные из подходов GameMaker. Эти инструменты позволяют создавать произвольные формы даже при жёстком ограничении экрана и палитры.
Звук и управление
Аудиосистема движка рассчитана не на воспроизведение готовых файлов, а на последовательности нот. Нота описывается частотой, индексом, амплитудой и длительностью, а объект звука умеет переходить к следующей ноте и сообщать об окончании последовательности. Для преобразования MIDI-файлов предусмотрен инструмент midi2go, но входной файл должен содержать только одну дорожку: аппаратный ШИМ-драйвер поддерживает один канал.
Движок включает ноты от C0 до B8, однако на практике ноты ниже C3 на простом пьезодинамике звучат преимущественно как щелчки. На микроконтроллере период ШИМ настраивается под частоту ноты, а амплитуда изменяет коэффициент заполнения сигнала в диапазоне от нуля до 50%. При этом громкость меняется нелинейно.
Вместо аудиофайлов десктопная версия синтезирует звук из последовательности нот. Она формирует простой пилообразный сигнал и пропускает его через полосовой фильтр, имитируя звучание настоящего зуммера Nokia 3310.
Для ввода используется универсальный интерфейс команд. Для каждой команды можно зарегистрировать одну или несколько функций проверки. Благодаря этому движок различает нажатие, отпускание и удержание кнопки, а также умеет проверять события сразу для любой кнопки. На компьютере раскладка задаётся отдельно, что позволяет приблизить управление к телефонной клавиатуре.
Платформы и аппаратная абстракция
Встраиваемая версия собирается с помощью TinyGo и выбирается условной компиляцией. Игровой цикл последовательно обновляет клавиатуру, вызывает логику игры, обслуживает аудиодрайвер, рисует кадр на холсте, передаёт изображение дисплею PCD8544 и переводит микроконтроллер в режим ожидания до следующего кадра.
Клавиатурный драйвер работает с матрицей строк и столбцов, рассчитанной на 16 кнопок Nokia 3310. Он поочерёдно подаёт высокий уровень на каждую строку, оставляя остальные строки в низком состоянии, а затем считывает столбцы. Высокий уровень на пересечении строки и столбца означает нажатую кнопку.
Отдельный уровень абстракции позволяет использовать разные аппаратные платы. Интерфейс конфигурации описывает инициализацию, дисплей PCD8544, зуммер и клавиатуру, после чего движок может работать с выбранной реализацией платы. Это оставляет возможность заменить Raspberry Pi Pico на другой микроконтроллер без переписывания игровой логики.
Десктопная версия использует Ebitengine. В её игровом цикле также обновляются ввод и состояние игры, но изображение выводится на экран компьютера. Два шейдера имитируют особенности ЖК-дисплея: время отклика и послесвечение, а также структуру пикселей при увеличении изображения 84×48. Дополнительное расширение накладывает рамку корпуса телефона, выводит отладочную информацию и выполняет другие задачи за пределами основного цикла.
В результате получился не только движок для одной конкурсной игры, но и переносимая основа для проектов, которые могут запускаться на компьютере, в браузере и на реальном устройстве с ограниченными ресурсами. Такой подход позволяет сохранять общую игровую логику, заменяя лишь платформенные драйверы, способы вывода и конкретную аппаратную конфигурацию.


