Содержание урока · 10 разделов
Пока игра живёт на вашей машине, вопрос «как её запустить у игрока» кажется теоретическим. Практика наступает в день первой публикации: у кого-то Windows, у кого-то MacBook на Apple Silicon, кто-то откроет ссылку с телефона, а друг потребует браузерную версию без установки. Ebitengine закрывает все четыре цели из одного и того же исходного кода: десктоп собирается go build, мобильные платформы — gomobile, браузер — компиляцией в WebAssembly. Кода под каждую платформу почти нет: платформенность прячется внутри движка, и в ваших файлах не появляются ветки «если Windows, то иначе». Автору остаётся освоить команды сборки, знать требования каждой цели и не наступить на грабли, о которых при разработке не подозреваешь: MIME-тип сервера, dev-пакеты Linux, политика звука iOS. Все четыре маршрута разделяют один Update и один Draw — различия платформ живут в командах сборки, а не в коде игры. В этом уроке маршрут проходит целиком — от первой команды до чек-листа CI, который собирает игру сразу везде. И полезная рамка на весь урок: команды сборки — это код проекта в самом буквальном смысле, они живут в репозитории рядом с исходниками и проверяются тем же способом — запуском.
Десктоп: go build и графические библиотеки#
Десктоп — самый короткий маршрут: команда go build собирает исполняемый файл текущей платформы, и игре больше ничего не нужно. Кросс-сборка задаётся переменными GOOS и GOARCH: с Linux-машины собирается exe для Windows, с Mac — бинарник для Linux, и C-тулчейн для этого не нужен, потому что на Windows и macOS движку не требуются C-зависимости. Исключение по окружению — Linux: перед первой сборкой понадобятся dev-пакеты OpenGL и X11, вроде libgl1-mesa-dev и libx11-dev, — это заголовки для компиляции, а не то, что должен ставить игрок: раздавать можно уже собранный файл. Под капотом Ebitengine сама выбирает графическую библиотеку: DirectX на Windows, Metal на macOS и iOS, OpenGL или WebGL в остальных случаях; выбор можно форсировать переменной EBITENGINE_GRAPHICS_LIBRARY, если конкретный драйвер ведёт себя странно. Проверка десктопного маршрута честная: собранный файл стоит запустить на чистой машине или у друга — ничего, кроме бинарника, с собой тащить не нужно. Подводный камень macOS — архитектура: Intel и Apple Silicon это разные значения GOARCH, и релиз собирается в двух экземплярах. WASM игры Go собираются той же парой переменных, о ней ниже.
Команды десктопной сборки
GO# текущая платформа — просто go build
go build -o mygame .
# кросс-сборка: цель задаётся GOOS и GOARCH
GOOS=windows GOARCH=amd64 go build -o mygame.exe .
GOOS=darwin GOARCH=arm64 go build -o mygame .
# графическую библиотеку можно выбрать явно
EBITENGINE_GRAPHICS_LIBRARY=opengl ./mygameПервая команда — сборка под то, на чём вы сидите; вторая и третья — кросс-сборка без C-тулчейна: цель целиком описывается парой GOOS и GOARCH, и один и тот же исходник даёт разные исполняемые файлы. Суффикс .exe важен для Windows-цели. Переменная EBITENGINE_GRAPHICS_LIBRARY переключает бэкенд — DirectX, Metal или OpenGL — без пересборки.
С Linux-машины нужен бинарник для Windows x86-64. Какая команда верна?
Android и iOS: gomobile build и bind#
Мобильные цели собираются через gomobile — официальный инструмент Go для Android и iOS. Установка короткая: go install golang.org/x/mobile/cmd/gomobile@latest, затем gomobile init. Дальше два маршрута. gomobile build упаковывает игру целиком: APK для Android или бандл для iOS — быстрый путь для теста на телефоне и раздачи вне магазинов. gomobile bind собирает библиотеку — aar для Android или xcframework для iOS, — которую встраивают в нативный проект; это путь для релиза в магазинах, где нативный код ведёт рекламу, покупки и экраны публикации. Инфраструктура тяжёлая, но ставится один раз: JDK и Android SDK, Xcode для iOS. Обе утилиты работают поверх обычного кода: пакеты, тесты и структура из прошлого урока не меняются ни на строчку. Особенность исполнения: RunGame обязан стартовать на главном потоке интерфейса, и на мобильных цикл ведёт обвязка пакета ebiten/v2/mobile — ваши Update и Draw вызываются как обычно, просто их запуск устроен хитрее. Проверяйте управление на телефоне заранее: тач-ввод — единственный ввод платформы, и клавиатурные схемы на нём мертвы. Отдельно держите в уме запуск с телефона: подключите устройство, соберите пакет и посмотрите на реальную частоту кадров — жесты и размеры экрана сбивают предположения, сделанные за десктопом.
Браузер: GOOS=js GOARCH=wasm#
Браузерная версия собирается компилятором Go в WebAssembly: GOOS=js GOARCH=wasm go build — и на выходе файл game.wasm. Открыть его двойным щелчком нельзя: модулю нужна HTML-страница и обвязка wasm_exec.js из каталога GOROOT/lib/wasm, которая соединяет код с браузерным API. Страница тривиальная: канвас, тег script с обвязкой и десяток строк вызова — она пишется один раз и живёт во всех проектах курса без изменений. Дальше — веб-сервер, и здесь главный подводный камень маршрута: game.wasm обязан отдаваться с Content-Type application/wasm. С текстовым MIME браузер откажется инстанцировать модуль, и игра не запустится, хотя файлы лежат правильно и пути не битые. Браузеру нужен WebGL 2, а WASM-бинарник весит несколько мегабайт: рантайм Go и движок едут вместе с игрой. Это лечится сжатием на сервере — gzip или brotli ужимает модуль в разы, и после распаковки игрок ждёт секунды, а не минуты. Размер проверяется локально одним взглядом на файл: если модуль растёт с каждым релизом, посмотрите, что нового появилось в вшитых ассетах. Зато обновление игры — перезагрузка страницы: никакой установки, согласований и подписей, поэтому браузерная версия становится витриной проекта раньше всех остальных сборок.
Сборка и раздача WASM
GO# компиляция модуля для браузера
GOOS=js GOARCH=wasm go build -o build/web/game.wasm .
# обвязка syscall/js из установки Go
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" build/web/
# index.html подключает wasm_exec.js и грузит game.wasm;
# сервер обязан отдавать правильный Content-Type:
w.Header().Set("Content-Type", "application/wasm")Три шага маршрута: компиляция под js/wasm, копирование обвязки из GOROOT и раздача с правильным MIME-типом. Страница подключает wasm_exec.js, инстанцирует модуль и запускает игру; без заголовка application/wasm браузер отвергнет файл ещё до выполнения первой строки кода. Проверить маршрут проще всего локальным сервером из пары строк Go.
Игра не запускается в браузере — что случилось?
Прочитайте код и выберите, что он выведет:
# файлы на месте, пути в index.html верные:
# build/web/game.wasm, build/web/wasm_exec.js
# но сервер отдаёт статике с MIME-типом text/plain
#
# в консоли браузера: instantiateStreaming failedЭмбед ассетов: ресурсы внутри бинарника#
Ассеты — тихая причина сломанной сборки. Пока разработка идёт в папке проекта, чтение файлов с диска работает; но собранный бинарник запускают не из исходников, и папка sprites может остаться дома. Решение — директива //go:embed: компилятор вшивает файлы в бинарник, и они доступны через embed.FS в любом месте кода. Ассетам отводится пакет internal/assets из прошлого урока: там объявляется переменная FS с перечислением папок, и весь остальной код берёт ресурсы только через неё. Выигрыш не только в удобстве: эмбед работает одинаково на десктопе, в мобильном бандле и в WASM-модуле, потому что байты лежат внутри исполняемого файла, а не рядом с ним. Спрайты из урока про спрайты и звуки из урока про звук читаются из FS без изменений игрового кода. Важно правило одного источника: чтение с диска в пакете с эмбедом смешивать нельзя — иначе половина ассетов поедет в бинарнике, а половина останется дома, и упадёт только собранная версия. Ограничение одно: паттерн не может выходить за каталог пакета — папки обязаны лежать внутри internal/assets, что естественно совпадает со скелетом проекта. Плата — размер: вшитые ресурсы увеличивают бинарник на своих мегабайт, и для WASM это надо помнить при выборе набора файлов.
Пакет ассетов с эмбедом
GO// internal/assets/assets.go
package assets
import "embed"
//go:embed sprites sounds fonts
var FS embed.FS
// спрайт: байты из FS грузятся в ebiten.Image,
// звук: те же байты -> NewPlayerF32FromBytes
// из урока про звук — один источник для всех платформДиректива перечисляет папки, компилятор вшивает их содержимое, а переменная FS становится единственным входом к ресурсам. Игровой код не знает, откуда взялись байты, — на десктопе, телефоне и в браузере это одни и те же значения, и папки рядом с бинарником больше не нужны. Пропущенная папка в перечислении ловится на сборке: ресурса нет в FS — значит, читаем не оттуда.
Что меняет //go:embed по сравнению с чтением файлов с диска?
Платформа — команда — подводный камень#
| Платформа | Команда | Подводный камень |
|---|---|---|
| Windows | go build, кросс: GOOS=windows GOARCH=amd64 | не забыть суффикс .exe у выходного файла |
| macOS | GOOS=darwin с GOARCH=amd64 и arm64 | Intel и Apple Silicon собираются по отдельности |
| Linux | go build на месте | перед первой сборкой нужны dev-пакеты OpenGL и X11 |
| Android и iOS | gomobile build или gomobile bind | первый запуск требует gomobile init, JDK и Xcode |
| Браузер | GOOS=js GOARCH=wasm go build | сервер обязан отдавать application/wasm |
Ограничения платформ и CI-матрица#
У каждой цели есть потолок, о котором лучше узнать до релиза, а не после. WASM: мегабайтный бинарник, требование WebGL 2 и медленный первый запуск без сжатия на сервере. Android и iOS: тяжёлая инфраструктура SDK, подписи, и маленькая ловушка звука — на iOS аудио оживает после первого касания, политика автовоспроизведения запрещает играть звук без жеста пользователя, поэтому стартовый экран, где игрок тапает «начать», полезен и для запуска аудиоконтекста. Десктоп: раздача без установщика требует, чтобы бинарнику ничего не понадобилось рядом, — ещё один аргумент за эмбед. Всё это собирает CI-матрица: джоба перебирает пары GOOS и GOARCH, для мобильных гоняет gomobile, для браузера — сборку с обвязкой, и каждая сборка хотя бы запускается на смоук-тест. Достаточный минимум для игр курса — три джобы: десктоп текущей платформы, веб и один мобильный билд; остальное подключается по мере роста проекта. Кэш модулей делает матрицу быстрой, артефакты складываются в релиз одной кнопкой. Полезное правило матрицы: каждая джоба воспроизводима локально — любая строка CI должна запускаться с вашей машины той же командой, иначе отладка красной сборки превращается в угадывание. Итоговый проект курса публикуется именно так — одним пушем, четырьмя артефактами, без ручных повторов. Отдельный плюс матрицы — история: артефакт прошлой сборки остаётся доступным, и regression можно отличить от поломки окружения.
Классика чтения с диска: рядом с бинарником папки нет — спрайты не грузятся, а ошибки видны только в консоли. Эмбед из этого урока закрывает вопрос навсегда: байты едут внутри исполняемого файла, и собранная копия неотличима от рабочей.
Соберите пакет ассетов
Восстановите два пропуска: пакет стандартной библиотеки и имя переменной.
Программы песочницы сайта выполняются в браузере — им сборка не нужна. Всё из этого урока относится к локальному Go: команды проверяются на вашей машине, а песочница продолжает тренировать Update и Draw, которые на всех платформах выглядят одинаково.
Шаги CI-джобы сборки
Соберите шаги релизной джобы в рабочий порядок.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Чек-лист сборки#
- Десктоп: go build, кросс-сборка через GOOS и GOARCH без C-тулчейна
- Linux: dev-пакеты OpenGL и X11 нужны компилятору, а не игроку
- Мобильные: gomobile build для готового пакета, gomobile bind для нативного проекта
- Браузер: GOOS=js GOARCH=wasm плюс wasm_exec.js и HTML-страница
- Сервер отдаёт game.wasm с Content-Type application/wasm — иначе отказ запуска
- Ассеты вшиваются //go:embed и едут внутри бинарника на всех платформах
- iOS: звук оживает после первого касания — экран «начать» обязателен
- CI-матрица собирает все цели и гоняет смоук-тест каждой сборки
Сборка — мост между кодом и игроком, и проверяется он только проходом: соберите каждую цель руками хотя бы раз, пока проект маленький и правки дёшевы. Домашнее задание проведёт по всем маршрутам, и после него команды перестанут быть заклинаниями: каждая строчка будет отвечать на вопрос, который вы уже решали своими руками. Дальше курс разбирает последнее перед итоговым проектом: где брать ассеты и как не нарушить лицензии, — и папки, которые сегодня вшивались в бинарник, наконец обретут легальный источник. Запомните главное: одна кодовая база — четыре цели, эмбед делает ассеты переносимыми, а правильный Content-Type — половина успеха браузерной версии. Платформы не требуют разветвления кода — только дисциплины сборки. Один и тот же Update работает везде, а различия платформ остаются там, где им место: в командах и настройках серверов.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Одна кодовая база — четыре цели: десктоп, Android, iOS и браузер без веток в коде.
- Десктоп собирается go build; кросс-цель задаётся парой GOOS и GOARCH.
- На Linux компилятору нужны dev-пакеты OpenGL и X11, игроку — ничего.
- Мобильные цели идут через gomobile: build для пакета, bind для нативной обвязки.
- WASM-сборка требует wasm_exec.js, HTML-страницы и Content-Type application/wasm.
- Эмбед вшивает ассеты в бинарник — ресурсы едут вместе с игрой на всех платформах.
- CI-матрица перебирает цели, гоняет смоук-тесты и складывает артефакты в релиз.
Домашнее задание
Соберите игру для всех целей урока. Десктоп: go build под текущую платформу и кросс-сборка GOOS=windows GOARCH=amd64 — проверьте, что exe запускается без исходников рядом. Браузер: соберите build/web с game.wasm и wasm_exec.js, напишите минимальный сервер с заголовком application/wasm и убедитесь, что игра стартует и без заголовка — падает. Затем вшейте ассеты: перенесите спрайты и звуки в internal/assets, объявите FS с //go:embed и переведите загрузку на него, после чего удалите папки ресурсов рядом с бинарником — игра обязана работать. Проверьте живое место iOS-маршрута: на старте игры ставьте экран «тап, чтобы начать», заодно запускающий аудиоконтекст. В конце опишите CI-матрицу в виде скрипта из порядка джобы: тест, десктоп, веб, gomobile — и запустите его целиком, зафиксировав время прохождения: если матрица идёт дольше минуты, включите кэш модулей.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему WASM-бинарник весит мегабайты?
В модуль вшивается весь рантайм Go плюс графический движок и ваши эмбеднутые ассеты — сборщик не умеет выбрасывать то, что может понадобиться. Практическое лечение — сжатие на сервере: gzip или brotli ужимает файл в разы, а кэш браузера делает повторные визиты почти мгновенными.
Можно ли публиковать на Android без gomobile?
Для магазина — нет: Google Play и App Store принимают пакеты, которые собирает именно gomobile или нативный проект с библиотекой от gomobile bind. Обходной маршрут — браузерная версия: она работает в мобильном браузере без магазина, но лишается полноэкранного ввода и выглядит демо, а не релизом. Третий путь — привлечение соавтора с нативным опытом: библиотека от gomobile bind отдаётся со скелетом вызова, и чужой код дополняет игру, не трогая её.
Почему на iOS звук молчит до первого касания?
Так работает политика автовоспроизведения мобильных платформ: аудиоконтекст оживает только после жеста пользователя. Игровой код не меняется — нужен стартовый экран с тапом «начать», который заодно запускает контекст звука, и музыка из урока про звук стартует сразу после него.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript