Два порта Godot-игры за неделю: сборка под Linux и запуск на карманной консоли R36S через PortMaster

—

от автора

Я делаю «Балконный сад» — пиксельный симулятор растений в реальном времени на Godot 4.3. Игра задумана как фоновая: сад растёт, пока вы заняты своими делами. Сначала она вышла только под Windows, а игроки сразу попросили Linux. Потом я задумался, где ещё может спокойно жить такая игра, и ответ нашёлся быстро: в кармане, на карманной консоли.

В статье расскажу про оба порта. Сначала про Linux-сборку: что нужно настроить в Godot, почему я отдаю архив .tar.gz и какие баги нашёл первые же игроки. Потом про R36S и PortMaster: как игра понимает, что запущена на консоли, как кнопки превращаются в клавиши, как сделать интерфейс, рассчитанный на мышь, управляемым крестовиной, и как упаковать всё для PortMaster. Кода немного, но проблем хватает.

Часть 1. Порт на Linux

Экспорт

В Godot порт на Linux делается пресетом экспорта «Linux». Мне хватило нескольких настроек:

name="Linux"platform="Linux"export_path="build/linux/BalconyGarden.x86_64"exclude_filter="tools/*, _test/*, README.md, build/*"include_filter="plants_data.csv"binary_format/embed_pck=truebinary_format/architecture="x86_64"
  • embed_pck=true — игра вшита в один исполняемый файл. Игрок получает один файл вместо двух, ничего не потеряется и не надо думать о путях.

  • exclude_filter — тестовые сцены, скрипты генерации спрайтов и сборки не должны попадать в игру.

  • include_filter="plants_data.csv" — таблица с параметрами растений лежит в обычном csv, и Godot по умолчанию не кладёт такие файлы в сборку, пока их не добавишь вручную. Без этой строки игра на Linux просто не нашла бы растения.

Экспорт идёт с одного компьютера: для сборки под Linux не нужен Linux. Нужны только шаблоны экспорта Godot 4.3.

Архив: tar.gz, а не zip

Готовый файл BalconyGarden.x86_64 весит около 66 МБ, в архиве около 25 МБ. Я упаковываю его в .tar.gz, а не в zip. Причина простая: в tar сохраняется флаг «исполняемый». Из zip файл часто распаковывается без этого флага и игроку приходится вручную делать chmod +x.

Инструкция для игрока получилась из двух строк:

tar xzf BalconyGarden_linux.tar.gz./BalconyGarden.x86_64

Что нашлось при проверке

При проверке на Linux в режиме «Подоконник» (игра сворачивается в маленькое прозрачное окно поверх рабочего стола) нашлись глитчи с землёй в горшке. Причина была в самом спрайте: в нём остался зазор в 22 пикселя. Я закрасил его цветом земли и добавил видимую полоску почвы внутри обода, чтобы пустой горшок выглядел как горшок. Исправление вошло в версию 0.3, а в углу экрана теперь показывается номер версии, чтобы игроки сразу видели, свежая ли у них сборка.

Прозрачное окно и «клики сквозь пустые места» в Godot идут через DisplayServer.window_set_mouse_passthrough. На Linux это работает в зависимости от оконного менеджера и композитора, поэтому важно просить игроков с разных систем показать, как выглядит режим у них.

Часть 2. Порт на консоль R36S

Что такое R36S и PortMaster

R36S — недорогая карманная консоль на Linux (у меня на даркос) с экраном 640×480, крестовиной, четырьмя кнопками, плечевыми кнопками и двумя стиками. Обычно на ней запускают эмуляторы, но есть и проект PortMaster: коллекция портов игр под такие устройства. Он ставит игры, подтягивает нужные «рантаймы» и запускает их одним пунктом меню.

Для Godot у PortMaster есть готовый рантайм godot_4.3: сам движок лежит на устройстве, а игра приезжает отдельно в виде одного файла .pck. Это очень удобно: не нужно собирать Godot под каждую прошивку.

1. Как игра понимает, что она на консоли

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

static func enabled() -> bool:    return OS.get_environment("BG_HANDHELD") == "1" or OS.has_feature("handheld") \        or "--handheld" in OS.get_cmdline_user_args() or "--handheld" in OS.get_cmdline_args()

Переменную BG_HANDHELD=1 выставляет скрипт запуска из пакета PortMaster. Если она есть, в сцену добавляется отдельный узел Handheld. Он ничего не знает про логику игры, только перестраивает управление:

  • прячет всё, что нужно мыши (кнопку «Подоконник», верхнюю панель с кнопками);

  • делает поля заметок только для чтения, потому что клавиатуры на консоли нет;

  • прячет курсор мыши;

  • рисует подсказку по кнопкам вместо кнопок.

Такой подход хорош тем, что основной код игры почти не менялся: консольный режим живёт в одном файле и включается одной строкой.

2. Кнопки консоли → клавиши

У PortMaster есть утилита gptokeyb: она слушает геймпад и превращает кнопки в нажатия клавиш. Поведение описывается в маленьком конфиге .gptk:

a = enterb = escx = py = kstart = ml1 = qr1 = eup = updown = downleft = leftright = rightleft_analog_up = upleft_analog_down = downleft_analog_left = leftleft_analog_right = right

Левый стик я тоже сделал стрелками: так игрок может выбирать и стиком, и крестовиной.

Важное решение, о которое я споткнулся: в Godot я полностью убрал события геймпада из стандартных действий (ui_left, ui_accept и других) и оставил только клавиатуру:

func _setup_actions() -> void:    # кнопки работают только через клавиши от gptokeyb:    # так раскладка одинакова на всех прошивках    for a in ["ui_left", "ui_right", "ui_up", "ui_down", "ui_accept", "ui_cancel"]:        for ev in InputMap.action_get_events(a):            if ev is InputEventJoypadButton or ev is InputEventJoypadMotion:                InputMap.action_erase_event(a, ev)

Почему так. Если оставить и геймпад, и клавиши, одно нажатие может прийти дважды: один раз как кнопка геймпада, второй как клавиша от gptokeyb. А на разных прошивках кнопки геймпада нумеруются по-разному. Клавиши от gptokeyb одинаковы везде, поэтому надёжнее опираться только на них.

Раскладка получилась такая:

Кнопка

Действие

A

Полить / выбрать в окне

X

Аптека

Y

Книга садовника

Start

Меню

L1 / R1, крестовина

Предыдущий / следующий горшок

Крестовина

Двигаться по списку

B

Закрыть окно

Select + Start

Выход (хоткей PortMaster)

3. Интерфейс, рассчитанный на мышь, но без мыши

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

Идея простая: Godot уже умеет «фокус» у элементов Control. Нужно лишь включить его у кнопок и сделать фокус видимым:

func _make_focusable(b: Control) -> void:    b.focus_mode = Control.FOCUS_ALL    b.add_theme_stylebox_override("focus", _focus_box)   # жёлтая рамка

Рамка фокуса — жёлтая, 2 пикселя, чтобы её было видно на маленьком экране и на любом фоне.

Дальше пришлось разобраться с мелочами:

  • Фокус должен куда-то попадать. Когда окно открывается, первой выбирается первая кнопка. Но списки семян и склада пересобираются, поэтому фокус ставится не сразу, а через кадр (await get_tree().process_frame).

  • Прокрутка списка должна идти за фокусом. Для всех ScrollContainer включено follow_focus = true, иначе выбранная кнопка уходит за край экрана.

  • Фокус нельзя терять. Если после выбора семени окно закрылось, а фокус остался на невидимой кнопке, управление «умирает». После выбора фокус сбрасывается и возвращается к подоконнику.

  • Книга садовника устроена иначе. В ней нет кнопок, а страницы листаются крестовиной < > , поэтому фокус там не нужен.

  • У лекарств нет названий на кнопках, только иконки. Когда фокус попадает на иконку, название всплывает подсказкой над растением.

Обработка нажатий вынесена в unhandledinput: сюда попадает всё, что не забрала выбранная кнопка. Так «A на подоконнике» поливает растение, а «A в окне» выбирает пункт:

elif open.is_empty() and event.is_action("ui_accept"):    GameCore.apply_item(GameCore.WATER_ID)   # A на подоконнике — полить

4. Меню по Start

На ПК сверху есть ряд кнопок: «Семена», «Выкопать», «Склад», «Пауза», «Сохранить» и другие. На консоли для них не хватило бы кнопок, поэтому всё собрано в одно меню по Start:

const MENU := [    ["seeds", "Семена", "Seeds"], ["dig", "Выкопать", "Dig up"], ["storage", "Склад", "Storage"],    ["freeze", "Заморозить / разморозить", "Freeze / unfreeze"], ["pause", "Пауза", "Pause"],    ["save", "Сохранить", "Save"], ["music", "Музыка вкл/выкл", "Music on/off"],    ["lang", "Язык: English", "Language: Русский"], ["exit", "Выход", "Quit"]]

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

5. Упаковка для PortMaster

Пакет для PortMaster — это zip с понятной структурой:

balconygarden.zip├── Balcony Garden.sh       # скрипт запуска└── balconygarden/    ├── balconygarden.pck   # игра    ├── balconygarden.gptk  # раскладка кнопок    ├── cover.png, screenshot.png    └── licenses/

Рядом лежат port.json (название, описание, жанр, какие рантаймы нужны) и gameinfo.xml для списка игр.

Игру я экспортирую не как исполняемый файл, а как .pck:

godot --headless --export-pack "<имя пресета>" build/pm/balconygarden.pck

Главная часть — скрипт запуска. Он находит PortMaster, проверяет и скачивает рантаймы, монтирует их и запускает движок:

godot_runtime="godot_4.3"weston_runtime="weston_pkg_0.2"...$GPTOKEYB "$godot_executable" -c "$GAMEDIR/$gptk_filename" &$ESUDO env $weston_dir/westonwrap.sh headless noop kiosk crusty_x11egl \  XDG_DATA_HOME="$CONFDIR" BG_HANDHELD=1 \  "$godot_dir/$godot_executable" --resolution "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" -f \  --rendering-driver opengl3_es --audio-driver ALSA --main-pack "$GAMEDIR/$pck_filename"

Что здесь важно:

  • weston — это маленький оконный сервер из рантайма PortMaster: Godot 4.3 рисует своё окно внутри него в режиме киоска (так сделано во всех Godot-портах PortMaster).

  • --rendering-driver opengl3_es — Godot 4 на таких слабых GPU работает через OpenGL ES.

  • BG_HANDHELD=1 — та самая переменная, которая включает консольный режим.

  • XDG_DATA_HOME="$CONFDIR" — сохранения лежат рядом с игрой, а не где-то в системе. Так их проще найти и скопировать.

Шрифт и всё остальное внутри .pck, поэтому дополнительные файлы не нужны.

6. Тест на настоящей консоли

Автоматические тесты в редакторе проверяют, что фокус ходит по спискам, что окна открываются и закрываются, что полив и лекарства применяются. Но настоящее управление проверяется только на устройстве.

При первом запуске заметил что кнопки верх/вниз на правой кристовине перепутаны местами.

Одна проблема уже решена еще на версии для Windows: без bios батарейки часов время сбрасывается при выключении. Для игры, где растения растут в реальном времени, это неприятно: пока консоль выключена, часы не идут, и растения просто не растут. Один игровой час равен 10 минутам настоящего времени, а предел оффлайна — 400 суток:

func save_game() -> void:    cfg.set_value("game", "saved_at", Time.get_unix_time_from_system())    ...func load_game() -> bool:    ...    var elapsed := Time.get_unix_time_from_system() - float(cfg.get_value("game", "saved_at", 0.0))    if elapsed > 0.0:        _accum += elapsed        var hours := int(_accum / REAL_SECONDS_PER_GAME_HOUR)        _accum -= hours * REAL_SECONDS_PER_GAME_HOUR        for i in mini(hours, MAX_OFFLINE_HOURS):            tick_hour()

Если время ушло в прошлое, elapsed получается отрицательным, проверка elapsed > 0.0 не пропускает его, и сад просто не растёт за время, пока консоль была выключена.

Итог

Что получилось:

  • Linux-сборка: один файл, один архив, две строки в инструкции;

  • один проект работает и на ПК, и на консоли; консольный режим включается одной переменной;

  • всё управление свелось к 8 кнопкам и одному меню по Start;

  • основной код игры почти не пришлось менять, консольный режим живёт в одном файле Handheld.gd.

Что я бы посоветовал, если собираетесь делать то же:

  1. Решите заранее, что на консоли «лишнее» (мышь, клавиатура, окна), и уберите это в одном месте.

  2. Не смешивайте события геймпада и клавиши от gptokeyb.

  3. Следите за фокусом: большая часть багов управления сводится к «фокус потерялся».

  4. Тестируйте на живом устройстве как можно раньше.

Версии для Windows, Linux и для PortMaste лежат на itch.io, а также — в релизах на GitHub. Если у вас есть R36S или другая консоль с PortMaster, буду рад отзывам: как идёт, не ломается ли управление и какие баги имеются.

ссылка на оригинал статьи https://habr.com/ru/articles/1099436/