The Jerusalem PostLikud braces for turnout battle, divided over Netanyahu's chances on eve of Election Day - analysisESPNAre the Lions, Seahawks, Bengals and Giants on the right track?PunchEkiti education agency graduates 76, seeks more adult learnersESPN DeportesF1 ¿Qué necesita Kimi Antonelli para ser campeón en México?Bollywood HungamaCBFC faces reported data security scare as confidential documents linked to over 500 films are allegedly exposedDaily MaverickCYBER SAFETY: SA Human Rights Commission urges stronger laws to protect children onlineStraits Times SportScotland name squad for Nations ChampionshipZDF heuteAktuelle Pressemitteilungen des ZDFIl Fatto QuotidianoSinner ancora in vetta, ma ora Zverez può superarlo. Musetti non guadagna posizioni dopo la vittoria a Lione | La nuova classifica AtpGolem.deKurioses Gefährt: Polizei stoppt Sofa auf Lime-RollernRai NewsElezioni in Israele, la sfida è tutta su Netanyahu: proteste e sondaggi indicano uno stalloDeadline‘The Lunchbox’ Director Ritesh Batra Sets Saif Ali Khan, Vidya Balan & Mélanie Laurent For ‘Long Years Ago’; Filming Begins In Mumbai
The Daily Newsstand · Free, Always
Monday, October 26, 2026

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

Translate

Я делаю «Балконный сад» — пиксельный симулятор растений в реальном времени на 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=true
binary_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 = enter
b = esc
x = p
y = k
start = m
l1 = q
r1 = e
up = up
down = down
left = left
right = right
left_analog_up = up
left_analog_down = down
left_analog_left = left
left_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, буду рад отзывам: как идёт, не ломается ли управление и какие баги имеются.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.