---
title: "Сборка: десктоп, мобильные, WASM"
description: "Ebitengine сборка одной кодовой базы: go build для десктопа, gomobile для Android и iOS, GOOS=js GOARCH=wasm для браузера и эмбед ассетов в бинарник."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/22-sborka-platformy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Сборка: десктоп, мобильные, WASM

*Курс «Ebitengine: 2D-игры на Go», урок 22 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/ebitengine/lessons/22-sborka-platformy/*

Ebitengine сборка из одной кодовой базы даёт четыре цели: десктоп через go build, Android и iOS через gomobile, браузер через GOOS=js GOARCH=wasm — и платформенность почти не доходит до вашего кода. Разберём команды каждого маршрута, эмбед ассетов и подводные камни, которые проявляются только у игрока, а не у автора.

Пока игра живёт на вашей машине, вопрос «как её запустить у игрока» кажется теоретическим. Практика наступает в день первой публикации: у кого-то 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. Какая команда верна?

- ✅ GOOS=windows GOARCH=amd64 go build -o mygame.exe .
- gomobile build -target=windows -o mygame.exe .
- GOOS=amd64 GOARCH=windows go build -o mygame .
- Нельзя: Windows-бинарник собирается только на Windows

Цель задаётся парой GOOS=windows GOARCH=amd64, и кросс-сборка проходит без C-тулчейна. gomobile отвечает за Android и iOS, а не за десктоп, перепутанные местами значения GOOS и GOARCH не соберутся вовсе.

### 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.

### Игра не запускается в браузере — что случилось?

```go
# файлы на месте, пути в index.html верные:
# build/web/game.wasm, build/web/wasm_exec.js
# но сервер отдаёт статике с MIME-типом text/plain
#
# в консоли браузера: instantiateStreaming failed
```

**Что выведет программа?**

- ✅ Браузер отказался инстанцировать модуль: нужен Content-Type application/wasm
- Модуль соберётся, но будет работать вдвое медленнее обычного
- Браузер скачает файл как текст и покажет его в окне
- Ошибка компиляции: тип файла не совпал с Content-Type

Streaming-инстанцирование проверяет MIME-тип до исполнения: text/plain — отказ, и игра не стартует даже с правильными путями. Исправляется настройкой сервера или прокси, код игры менять не нужно.

### Эмбед ассетов: ресурсы внутри бинарника

Ассеты — тихая причина сломанной сборки. Пока разработка идёт в папке проекта, чтение файлов с диска работает; но собранный бинарник запускают не из исходников, и папка sprites может остаться дома. Решение — директива //go:embed: компилятор вшивает файлы в бинарник, и они доступны через embed.FS в любом месте кода. Ассетам отводится пакет internal/assets из прошлого урока: там объявляется переменная FS с перечислением папок, и весь остальной код берёт ресурсы только через неё. Выигрыш не только в удобстве: эмбед работает одинаково на десктопе, в мобильном бандле и в WASM-модуле, потому что байты лежат внутри исполняемого файла, а не рядом с ним. Спрайты из урока про [спрайты](/courses/ebitengine/lessons/04-sprajty/) и звуки из урока про [звук](/courses/ebitengine/lessons/18-zvuk/) читаются из 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 по сравнению с чтением файлов с диска?

- ✅ Ассеты вшиваются в бинарник и работают на всех платформах одинаково
- Ресурсы начинают грузиться быстрее за счёт GPU-кэша движка
- Файлы остаются на диске, но компилятор строит их индекс
- Директива работает только в WASM-сборке под браузер

Эмбед делает байты частью исполняемого файла: папка рядом с бинарником больше не нужна ни на одной платформе. Ни GPU-кэша, ни индексов на диске — просто данные внутри файла, доступные через embed.FS.

### Платформа — команда — подводный камень

| Платформа | Команда | Подводный камень |
| --- | --- | --- |
| 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 должна запускаться с вашей машины той же командой, иначе отладка красной сборки превращается в угадывание. Итоговый [проект](/courses/ebitengine/lessons/25-itogovyj-proekt/) курса публикуется именно так — одним пушем, четырьмя артефактами, без ручных повторов. Отдельный плюс матрицы — история: артефакт прошлой сборки остаётся доступным, и regression можно отличить от поломки окружения.

> **Живое место: игра работает у вас и без картинок у игрока.** Классика чтения с диска: рядом с бинарником папки нет — спрайты не грузятся, а ошибки видны только в консоли. Эмбед из этого урока закрывает вопрос навсегда: байты едут внутри исполняемого файла, и собранная копия неотличима от рабочей.

### Соберите пакет ассетов

Восстановите два пропуска: пакет стандартной библиотеки и имя переменной.

```go
package assets

import "___"

//go:embed sprites sounds
var ___ embed.FS
```

Подсказка: Первый — имя пакета стандартной библиотеки, второй — короткое имя переменной из примера выше.

Правильные ответы в пропусках: `embed`, `FS`

> **Про песочницу: программы сайта уже в браузере.** Программы песочницы сайта выполняются в браузере — им сборка не нужна. Всё из этого урока относится к локальному Go: команды проверяются на вашей машине, а песочница продолжает тренировать Update и Draw, которые на всех платформах выглядят одинаково.

### Шаги CI-джобы сборки

Соберите шаги релизной джобы в рабочий порядок.

Правильный порядок строк:

1. `mkdir -p build/web build/android`
2. `go test ./...`
3. `go build -o build/desktop/mygame .`
4. `GOOS=js GOARCH=wasm go build -o build/web/game.wasm .`
5. `cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" build/web/`
6. `gzip -9 build/web/game.wasm`
7. `gomobile build -target=android -o build/android/game.apk .`

### Чек-лист сборки

- Десктоп: 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-матрица собирает все цели и гоняет смоук-тест каждой сборки

Сборка — мост между кодом и игроком, и проверяется он только проходом: соберите каждую цель руками хотя бы раз, пока проект маленький и правки дёшевы. Домашнее задание проведёт по всем маршрутам, и после него команды перестанут быть заклинаниями: каждая строчка будет отвечать на вопрос, который вы уже решали своими руками. Дальше курс разбирает последнее перед итоговым проектом: где брать [ассеты](/courses/ebitengine/lessons/23-assety-i-lisenzii/) и как не нарушить лицензии, — и папки, которые сегодня вшивались в бинарник, наконец обретут легальный источник. Запомните главное: одна кодовая база — четыре цели, эмбед делает ассеты переносимыми, а правильный Content-Type — половина успеха браузерной версии. Платформы не требуют разветвления кода — только дисциплины сборки. Один и тот же Update работает везде, а различия платформ остаются там, где им место: в командах и настройках серверов.

### Термины урока

- **GOOS и GOARCH** — Переменные кросс-сборки: целевая операционная система и архитектура. GOOS=windows GOARCH=amd64 собирает exe с любой машины.
- **gomobile** — Инструмент Go для мобильных целей: build упаковывает APK или бандл, bind собирает библиотеку для нативного проекта.
- **wasm_exec.js** — Обвязка из GOROOT/lib/wasm, соединяющая WASM-модуль с браузерным API. Подключается на HTML-странице перед загрузкой игры.
- **application/wasm** — Content-Type, с которым сервер обязан отдавать game.wasm. С текстовым MIME браузер отказывается инстанцировать модуль.
- **//go:embed** — Директива, вшивающая файлы в бинарник; доступ через embed.FS. Ассеты едут внутри исполняемого файла на всех платформах.

## Коротко: что запомнить

- Одна кодовая база — четыре цели: десктоп, 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 звук молчит до первого касания?

Так работает политика автовоспроизведения мобильных платформ: аудиоконтекст оживает только после жеста пользователя. Игровой код не меняется — нужен стартовый экран с тапом «начать», который заодно запускает контекст звука, и музыка из урока про звук стартует сразу после него.
