Портирование браузерной игры на электронную книгу

Игра 2048 появилась в 2014 году — Gabriele Cirulli написал её за выходные на JavaScript и выложил в открытый доступ. Механика простая: плитки с числами скользят по полю 4×4, одинаковые складываются, цель — получить плитку 2048. Игра мгновенно стала популярной и обросла портами на все мыслимые платформы.
У меня есть электронная книга PocketBook, и я решил добавить её в этот список.
PocketBook SDK и InkView
Приложения для PocketBook пишутся на C с использованием проприетарного API — InkView. SDK поставляется с кросс-компилятором для ARM Linux и эмулятором для Windows. Сама разработка выглядит довольно старомодно: никаких фреймворков, никакого UI toolkit — только функции вроде DrawTextRect, FillArea, SetFont. Зато всё под контролем, что для e-ink вполне разумно.
Для того чтобы разрабатывать и тестировать приложения не перекидывая каждый раз файл на устройство, я сделал симулятор на базе Emscripten и WebAssembly — C-код компилируется в WASM, а InkView API реализован поверх Canvas 2D в браузере.
Реализация
Вся игра написана в одном файле game2048.c на чистом C. Никаких внешних зависимостей, никаких изображений — только вызовы InkView API.
Плитки отрисовываются прямоугольниками с заливкой и узором из линий. Поскольку экран e-ink — оттенки серого, цвета задаются через макрос:
1 |
Двенадцать уровней плиток — от светло-серых до чёрных, с горизонтальной штриховкой, вертикальной или сеткой. Плитка 2048 — чёрная сплошная.
Анимация работает через SetHardTimer — это однократный таймер, который нужно перезапускать каждый кадр. Два этапа: скольжение плиток (110 мс) и «хлопок» слияния (180 мс). Время измеряется через emscripten_get_now() в симуляторе, и через gettimeofday() на устройстве — всё это скрыто за #ifdef __EMSCRIPTEN__.
Проблемы
C89
SDK поставляется со старым компилятором ARM GCC, который по умолчанию работает в режиме C89. Это означает: никаких объявлений переменных внутри for, никаких инициализаторов составных типов, никакого bool из стандартной библиотеки. Пришлось переписать весь код:
1 | /* C99 — не работает */ |
Вместо (MoveRec){r, c, dr, dc} — явное присваивание полей. Вместо #include <stdbool.h> — переносимый вариант:
1 |
|
Определение ориентации при запуске
Приложение получает событие EVT_ORIENTATION только при смене ориентации, но не при старте. Из-за этого переменная is_landscape оставалась false независимо от реального положения устройства, и в режиме ландшафта всё рисовалось неправильно.
Решение простое — в EVT_INIT явно спросить у системы:
1 | is_landscape = (GetOrientation() == 1 || GetOrientation() == 2); |
Мигание экрана
После каждого хода вся игра перерисовывалась через FullUpdate() — стандартный полный рефреш e-ink. На экране это выглядит как заметное мигание: экран сначала уходит в чёрный, потом полностью отрисовывается заново.
Для первого показа FullUpdate() нужен — он даёт максимальное качество. Но после каждого хода достаточно SoftUpdate(), который обновляет экран без полного цикла перезаписи:
1 | static void draw_game_full(void) { draw_game_buf(); FullUpdate(); } /* при запуске */ |
Со временем на e-ink появляются артефакты от мягких обновлений — «призраки» старых плиток. Но для игры это некритично, и пользователь всегда может сделать полное обновление вручную.
Размер на экране
На устройстве игра занимала четверть экрана — оказалось, что у PocketBook 631 экран 1264×1680, а весь layout был захардкожен под 600×800.
Исправил масштабированием: вычисляю коэффициент при старте приложения и применяю его ко всем координатам через макрос S():
1 | static float g_scale = 1.0f; |
После этого все вызовы вроде FillArea(S(375), S(15), S(100), S(58), ...) масштабируются автоматически. Размеры шрифтов тоже умножаются на g_scale. При смене ориентации шрифты закрываются и открываются заново с новым размером.
Итог
Игра работает на устройстве: свайп двигает плитки, анимация плавная насколько позволяет e-ink, счёт сохраняется между сессиями. Код — на GitHub, в репозитории есть готовые бинарники для PB631 и для Windows-эмулятора.