Содержание урока · 10 разделов
- Установка: go get, версии Go и история с C-компилятором
- Чеклист первого запуска по шагам
- ebitengine игровой цикл: 60 тиков в секунду по договору
- RunGame: блокирующий запуск и три способа выйти
- Семейство функций запуска
- Окно потеряло фокус: почему игра замерла
- Что тикает в песочнице, а что требует окна
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пустая папка, терминал, один модуль Go — и через десять минут у вас на экране будет висеть окно игры, которое тикает 60 раз в секунду. Звучит слишком оптимистично, но именно так и происходит: у Ebitengine один из самых коротких путей от нуля до окна среди графических библиотек Go. В этом уроке мы пройдём его целиком: поставим движок командой go get, соберём каркас с методами Update и Draw, запустим и поймём, что движок делает за нас, а что остаётся нашей работой. К концу урока вы сможете ответить не только на вопрос «как запустить», но и на то, почему игра не ускоряется на мощном железе и почему окно «замирает», когда вы переключаетесь в браузер. Если вы ещё не читали первое знакомство с Ebitengine, вернитесь на минуту: там мы договорились, что игра — это три метода на структуре, и сегодня запустим этот контракт вживую.
Установка: go get, версии Go и история с C-компилятором#
Начнём с команды, которая и есть вся установка: go get github.com/hajimehoshi/ebiten/v2@latest. Суффикс /v2 в пути — не украшение, а правило Go-модулей для второй мажорной версии: импортировать вы будете ровно этот путь, и компилятор не перепутает старую ветку с новой. Движку нужен свежий Go, и требование растёт со временем: ветка 2.8 просила Go 1.22, а документация актуальных релизов требует Go 1.25 и новее — проверьте версию командой go version, прежде чем ругаться на странные ошибки компиляции.
Вторая тема — компилятор C, вокруг которой накопилось много устаревших советов. Исторически десктопные сборки Ebitengine шли через cgo, и на Windows вам был нужен gcc из MSYS2, на macOS — командные инструменты Xcode, на Linux — компилятор и заголовки системных библиотек. С версии 2.10 документация сообщает важное: десктопные сборки (Windows, macOS, Linux, FreeBSD) собраны в чистом Go, и C-компилятор для них больше не нужен — cgo остался требованием мобильных платформ. Если вы найдёте инструкцию 2023 года, которая заставляет ставить MinGW — она устарела: для актуальных версий достаточно Go и, на Linux, пакетов графической среды. Сделайте go mod tidy после первого импорта, чтобы go.mod и go.sum пришли в порядок, — стандартная гигиена любого Go-проекта. Если сам Go для вас пока в новинку, заложите фундамент в базовом курсе, а затем возвращайтесь сюда.
Чеклист первого запуска по шагам#
- Проверьте go version: актуальным релизам нужен Go 1.25+ (ветка 2.8 довольствовалась 1.22)
- go mod init game — создайте модуль проекта
- go get github.com/hajimehoshi/ebiten/v2@latest — поставьте движок
- Скопируйте каркас из этого урока в main.go
- go run . — должно открыться окно с заголовком и чёрным холстом
- Окно молчит и не тикает? Проверьте фокус: нефокусному окну Update не приходит
Движок ставится не в систему, а в модуль проекта: после go get в go.mod появляется строка require github.com/hajimehoshi/ebiten/v2 с актуальной версией, а go.sum фиксирует криптографические суммы зависимостей. Сами файлы библиотеки лежат в кеше модулей Go — каталоге pkg/mod рабочего окружения, — и в проект не копируются: сборка подтягивает их оттуда сама. Из этого следует практический вывод: машина без интернета соберёт проект, если кеш уже прогрет, а закоммиченный каталог vendor/ решает вопрос переносимости целиком. Если хотите проверить установку до первого окна, запустите go build: движок компилируется без графики, а вот запуск требует уже настоящего экрана. Загляните и в go.mod глазами: суффикс /v2 в require — тот же, что в импорте, и это лишний шанс заметить опечатку до компиляции.
Каркас: ebiten update draw в четырёх методах
GOpackage main
import (
"log"
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct{}
func (g *Game) Update() error { return nil }
func (g *Game) Draw(screen *ebiten.Image) {}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return 320, 240
}
func main() {
ebiten.SetWindowSize(640, 480)
ebiten.SetWindowTitle("My first game")
if err := ebiten.RunGame(&Game{}); err != nil {
log.Fatal(err)
}
}Это канонический старт из официального cheat sheet. Три метода образуют интерфейс ebiten.Game: Update продвигает состояние игры, Draw рисует кадр в переданный screen, Layout возвращает логический размер холста. SetWindowSize задаёт размер окна — в два раза больше холста, чтобы было видно растяжение, — а RunGame запускает цикл и блокирует main до закрытия окна. Ошибка из RunGame обрабатывается как обычная Go-ошибка.
ebitengine игровой цикл: 60 тиков в секунду по договору#
Теперь главное — что происходит, когда вы запустили программу. RunGame забирает управление и начинает крутить цикл: Update вызывается каждый тик, по умолчанию 60 раз в секунду, и движок гарантирует, что между двумя соседними вызовами проходит ровно 1/60 секунды — независимо от того, какой у вас монитор и как быстро процессор. Draw вызывается каждый кадр, и его частота определяется частотой обновления монитора: на 60-герцовом дисплее это те же 60 кадров в секунду, на 120-герцовом — вдвое чаще, хотя тики по-прежнему идут с частотой 60. Первым кадром движок обещает порядок: Layout вызывается до первого Update, а Update — хотя бы раз до первого Draw, поэтому состояние игры всегда готово к отрисовке.
Из этого контракта следует правило, которое записано прямо в документации метода Draw: не кладите в него игровую логику. Draw — функция от картинки, и его частота зависит от железа пользователя; логика, привязанная к кадрам, ускорится на игровом мониторе и замедлится на ноутбуке. Правильная привычка: Update меняет числа, Draw только переводит числа в пиксели. Если 60 тиков вам мало — скажем, для точной физики — есть ebiten.SetTPS со значением по частоте; а фактическую частоту тиков и кадров в живой программе показывают ebiten.ActualTPS и ActualFPS. Оба замера — для отладки: на них нельзя строить механику, только смотреть глазами.
Монитор игрока обновляется с частотой 144 Гц. Сколько тиков Update получит игра при настройках по умолчанию?
Первый признак жизни: счётчик тиков
GOtype Game struct {
ticks int
charge int
}
func (g *Game) Update() error {
g.ticks++
if ebiten.IsKeyPressed(ebiten.KeySpace) {
g.charge++
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
msg := fmt.Sprintf("TICKS %d CHARGE %d", g.ticks, g.charge)
ebitenutil.DebugPrint(screen, msg)
}Update инкрементирует счётчик каждый тик и, пока зажат пробел, копит заряд — это уже честная игровая логика с опросом клавиатуры, которую подробно разберём в уроке про ввод. Draw ничего не решает: он лишь печатает текущие числа отладочным шрифтом. Запустите, зажмите пробел и отпустите — вы увидите, как CHARGE догоняет и отстаёт от TICKS ровно на ритм 60 тиков в секунду.
Что случится в самом первом кадре?
Прочитайте код и выберите, что он выведет:
func (g *Game) Update() error {
log.Println("update", g.n)
g.n++
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
log.Println("draw")
}
func (g *Game) Layout(ow, oh int) (int, int) {
log.Println("layout")
return 320, 240
}RunGame: блокирующий запуск и три способа выйти#
Функция ebiten.RunGame(game) — точка невозврата: она блокирует goroutine main до закрытия окна, и любой код после неё выполнится только при завершении игры. Вызывать RunGame в одном процессе можно один раз; документация прямо запрещает повторные вызовы. Если Update вернёт обычную непустую ошибку, цикл остановится, и RunGame вернёт эту же ошибку — это штатный способ падать с осмысленным сообщением, например когда не загрузился ассет. А для добровольного выхода — скажем, по нажатию Escape — есть особое значение ebiten.Termination: верните его из Update, игра остановится, но RunGame вернёт nil, и вам не придётся отличать «игрок вышел» от аварии.
Семья функций запуска исторически менялась, и в чужом коде вы встретите три стиля. В первой версии движка вход был функцией ebiten.Run с замыканиями: она принимала два колбэка, ширину, высоту, масштаб и заголовок окна — на конце первой ветки её пометили устаревшей в пользу RunGame. Во второй версии основной вход — RunGame(game) из этого урока, а окно настраивают пакетными функциями вроде SetWindowSize и SetWindowTitle до запуска, как в каркасе выше. А с версии 2.6 рядом появился RunGameWithOptions(game, options), где в структуре опций настраивают графическую библиотеку, прозрачность окна и однопоточный режим: если вы не задаёте таких флагов, простого RunGame достаточно. Таблица ниже сводит всё семейство одной строкой на функцию.
Update вернул ebiten.Termination. Что получит код после RunGame?
Сколько раз в одном процессе разрешено вызывать RunGame?
Семейство функций запуска#
| Функция | Появилась | Что принимает |
|---|---|---|
| ebiten.Run | v1, на конце ветки помечена устаревшей | замыкания update и draw, ширину, высоту, масштаб и заголовок окна |
| ebiten.RunGame | v1.12, в v2 — основной путь | структуру с методами Update, Draw и Layout |
| ebiten.RunGameWithOptions | v2.6 | то же плюс структуру опций: графическая библиотека, прозрачность окна, SingleThread |
Окно потеряло фокус: почему игра замерла#
Сценарий, который сбивает каждого второго: вы запускаете игру, переключаетесь в браузер посмотреть документацию — и таймер в логах замирает. Это не баг и не зависание: по умолчанию Update и Draw не вызываются, пока окно Ebitengine не в фокусе. Движок экономит ресурсы системы — нечего тикать игре, которую никто не видит. Для отладки это изматывает: вы смотрите на лог и не понимаете, почему счётчик стоит.
Поведение управляется функцией ebiten.SetRunnableOnUnfocused: передайте true до RunGame, и игра продолжит тикать даже в фоне. Здесь стоит быть аккуратным с обещаниями: игра, которая считает ресурсы в фоновом режиме, может раздражать пользователя и сливать батарею на ноутбуке. Разумная стратегия — держать runnable-on-unfocused выключенным в релизе и включать по флагу отладки, когда нужно наблюдать за логами при свёрнутом окне. Соседняя настройка — InitUnfocused в RunGameOptions: запустить игру сразу в нефокусном окне, что полезно для демо и оверлеев. Запомните эту пару «замерло — проверь фокус», она сэкономит вам один бессмысленный вечер: к вводу и паузам мы вернёмся в уроках про клавиатуру и сцены.
На вашем 60-герцовом ноутбуке тест с логикой в Draw пройдёт идеально — и провалится у первого же игрока со 144-герцовым монитором: скорость игры вырастет в 2,4 раза. Что сломалось бы дальше: таймеры урона, физика прыжков, откат способностей. Держите Draw пустым от механики — числа меняет только Update.
Самая частая ошибка первого запуска на серверах и в CI: окно не открывается, потому что нет графической среды. На Linux движок говорит с системой через X11 или Wayland, и без них RunGame завершится ошибкой, а не покажет чёрный холст. В WSL2 на Windows 11 окна работают через WSLg, в чистом WSL2 без графической подсистемы — нет. Логике игры это не мешает: тесты и игровая математика идут и без экрана.
Что тикает в песочнице, а что требует окна#
Скажем честно про границы урока: весь код этой главы для запуска нуждается в локальном Go и графическом окне — песочница сайта не умеет открывать окна и рисовать спрайты, да и Ebitengine в браузерную сборку интерпретатора не влезет. Но сама суть цикла — числа, которые меняются 60 раз в секунду, — чистая математика, и она считается прямо на странице: тематическая программа в песочнице сайта эмулирует тики и подсчитывает шаги, как это сделал бы Update. Прогоните её до того, как соберёте настоящий проект: почувствовать ритм 1/60 секунды на числах полезнее, чем вчитываться в чёрное окно. Дальше по курсу мы рисуем: третий урок покажет, как Layout превращает ваши 320 на 240 в любое окно.
Заполните каркас запуска
Восстановите три пропуска: метод-тик, размер окна и обработку ошибки запуска.
Соберите последовательность запуска
Расставьте строки минимальной программы в правильном порядке.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Установка — это go get github.com/hajimehoshi/ebiten/v2@latest; актуальным релизам нужен свежий Go (2.8 требовал 1.22, новые — 1.25).
- Каркас игры — интерфейс ebiten.Game: Update меняет состояние, Draw рисует, Layout возвращает логический размер холста.
- Update тикает 60 раз в секунду с константной дельтой 1/60; Draw привязан к монитору, и логики в нём быть не должно.
- RunGame блокирует main, вызывается один раз; Termination из Update даёт штатный выход с nil, другие ошибки возвращаются как есть.
- Окно настраивают пакетными Set-функциями до запуска; RunGameWithOptions с версии 2.6 добавляет опции вроде графической библиотеки.
- В нефокусном окне цикл по умолчанию стоит: либо верните фокус, либо SetRunnableOnUnfocused(true) для отладки.
Домашнее задание
Соберите проект по чеклисту урока и доведите каркас до живого счётчика: добавьте в Game поле ticks и печатайте его в Draw через DebugPrint. Затем включите SetRunnableOnUnfocused(true), сверните окно и убедитесь по логу, что тики продолжились, — и наоборот. Поменяйте Layout, чтобы он возвращал 480 на 320, и сравните картинку с 320 на 240. Наконец, верните ebiten.Termination из Update после 300 тиков и посмотрите, что напечатает программа после выхода из RunGame, если поставить log.Println после него. Загляните в go.mod: найдите строку require с ebiten и сверьте версию с выводом go list -m github.com/hajimehoshi/ebiten/v2.
Личные заметкиПусто
Вопросы и ответы по уроку
go run работает, а окно не появляется — куда смотреть?
Сначала в консоль: ошибки компиляции и паники движка печатаются там, а окно легко открывается поверх и прячет вывод. Затем в фокус: нефокусному окну Update и Draw не приходят, и кажется, что программа висит. Наконец, проверьте версию Go — старый тулчейн на новых версиях движка даёт загадочные ошибки.
Обязательно ли называть структуру Game?
Нет: движку всё равно, как зовётся ваш тип, — важен набор методов Update, Draw и Layout, который делает его реализацией интерфейса ebiten.Game. Название Game — просто конвенция из примеров.
Можно ли запускать игру без окна, например для тестов?
Игровую логику стоит выносить в методы без зависимости от ebiten — тогда её покрывают обычными Go-тестами. Для полного прогона с рендером окно всё же нужно, поэтому GUI-часть тестируют на десктопе, а не в CI без графики: рендер выносят в отдельный модуль и гоняют его только локально — это экономит и CI-минуты, и нервы.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chromedp в Go: автоматизация Chrome без Selenium
Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.
- Запускать Chrome из Go и управлять страницами
- Находить элементы селекторами и ждать их появления
- Кликать, заполнять формы и извлекать данные
- Делать скриншоты, PDF и выполнять JavaScript