Майские праздники не прошли даром: на пару дней я основательно засел за Python и написал собственный лаунчер. Зачем? Он нужен для того, чтобы не привязывать игру к конкретной игровой платформе. В моем случае, я использую GameJolt, но такой подход работает для любого сервиса. Лаунчер служит посредником между игрой и API площадки. Все, что нужно делать игре — это сообщать лаунчеру о событиях наподобие «игрок получил такую-то ачивку». Лаунчер, в свою очередь, передает эти данные игровому сервису. Если надо переехать на другой сервис, достаточно просто обновить лаунчер, а игру патчить не придется — это удобно и экономит массу времени.
Для создания интерфейса я использовал WebView. Можно спорить до хрипоты о недостатках веб-стека, но на сегодняшний день он остается самым простым и универсальным решением для создания GUI. Нативные тулкиты платформозависимы, требуют соблюдения особой архитектуры приложения и чаще всего выглядят скучновато, а браузерный движок позволяет буквально за несколько часов наваять красивые формы с анимациями и спецэффектами — сделал и забыл!
Долгое время я писал веб-интерфейсы на CEF (Chromium Embedded Framework) — «Electron для Python». Он предоставляет portable браузерный движок и в целом свою задачу решает, но имеет ряд проблем. CEF Python уже несколько лет как не развивается, и версия Chromium в нем заметно устарела, новейшие свойства CSS не поддерживаются. Кроме того, в собранном виде CEF-приложение весит около 200 Мб, что для небольшого приложения, мягко говоря, жирновато. Все эти проблемы я решил переходом на PyWebView! Единственное, чем придется пожертвовать — совместимостью с Windows 8, что в 2025 году совсем не критично. Собранный PyInstaller’ом, лаунчер стал весить менее 50 Мб (используя UPX, можно сжать еще сильнее). А код под PyWebView гораздо компактнее и проще, чем под CEF Python.
Основная задача моего лаунчера — служить посредником между игрой и внешними сервисами. Для этого используется механизм IPC: игра отправляет события лаунчеру через TCP-соединение по кастомному протоколу. Например, когда игрок получает достижение, игра отправляет команду типа "eV:award=AchievementName"
. Лаунчер, получив такую команду, передает данные на сервер.
Важная особенность этого механизма — асинхронность: блокировать игровой цикл, чтобы отправить что-то по сети — не комильфо, задержки бывают в секунду и даже дольше. Выручил taskPool
из std.parallelism
— все вызовы IPC оборачиваются в task
и заносятся в очередь пула задач:
taskPool.put(task!ipcSendSync(message));
Это не блокирует поток и обеспечивает плавный игровой процесс. Кроме того, очередь гарантирует, что вызовы будут обработаны последовательно — не будет состояния гонки за исходящий сокет, если отправить несколько сообщений подряд.
GameJolt имеет простой и удобный API, который легко интегрировать в проект. Однако, чтобы не компрометировать приватный ключ, я решил не внедрять его напрямую в игру. Вместо этого ключ хранится на моем сервере, который и отправляет запросы API. Лаунчер напрямую с API не взаимодействует — он общается только с сервером. Пользователь может по желанию авторизоваться в лаунчере, используя логин и токен GameJolt — токен был специально придуман, чтобы не сообщать играм пароль от аккаунта. В случае успешной авторизации достижения в игре будут синхронизироваться с аккаунтом.
Лаунчер также позволяет пользователю настроить графику игры перед запуском. Для этого он взаимодействует с конфигурационным файлом игры — settings.conf
. Лаунчер считывает текущие настройки, обновляет их в зависимости от выбранных пользователем параметров и сохраняет изменения обратно в файл. Также он мониторит изменения в конфиге — если игра обновила файл, интерфейс лаунчера подхватит изменения.
На будущее есть план реализовать систему автообновления (полезно для игроков, которые не используют игровые платформы и предпочитают устанавливать игры вручную). Кроме того, можно добавить установку модов.