
Я делаю «Балконный сад» — пиксельный симулятор растений в реальном времени на 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.
Что я бы посоветовал, если собираетесь делать то же:
-
Решите заранее, что на консоли «лишнее» (мышь, клавиатура, окна), и уберите это в одном месте.
-
Не смешивайте события геймпада и клавиши от
gptokeyb. -
Следите за фокусом: большая часть багов управления сводится к «фокус потерялся».
-
Тестируйте на живом устройстве как можно раньше.
Версии для Windows, Linux и для PortMaste лежат на itch.io, а также — в релизах на GitHub. Если у вас есть R36S или другая консоль с PortMaster, буду рад отзывам: как идёт, не ломается ли управление и какие баги имеются.
ссылка на оригинал статьи https://habr.com/ru/articles/1099436/