---
title: "Установка и первый игровой цикл"
description: "Установка Ebitengine и первый игровой цикл: go get, методы Update и Draw, Layout, 60 тиков в секунду, RunGame и ошибки первого запуска. Разбор построчно."
course: "Ebitengine: 2D-игры на Go"
url: "https://golangmaster.ru/courses/ebitengine/lessons/02-ustanovka-i-cikl/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Установка и первый игровой цикл

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

Установка Ebitengine сводится к одной команде go get — но за первым же запуском стоит целый ebitengine игровой цикл, и его стоит понять до того, как вы напишете строчку геймплея. В этом уроке мы ставим движок, собираем минимальную программу из Update и Draw и разбираем, что происходит в первые 60 тиков после двойного клика по окну.

Пустая папка, терминал, один модуль Go — и через десять минут у вас на экране будет висеть окно игры, которое тикает 60 раз в секунду. Звучит слишком оптимистично, но именно так и происходит: у Ebitengine один из самых коротких путей от нуля до окна среди графических библиотек Go. В этом уроке мы пройдём его целиком: поставим движок командой go get, соберём каркас с методами Update и Draw, запустим и поймём, что движок делает за нас, а что остаётся нашей работой. К концу урока вы сможете ответить не только на вопрос «как запустить», но и на то, почему игра не ускоряется на мощном железе и почему окно «замирает», когда вы переключаетесь в браузер. Если вы ещё не читали первое знакомство с [Ebitengine](/courses/ebitengine/lessons/01-znakomstvo-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 для вас пока в новинку, заложите фундамент в [базовом курсе](/courses/go-basics/), а затем возвращайтесь сюда.

### Чеклист первого запуска по шагам

1. Проверьте go version: актуальным релизам нужен Go 1.25+ (ветка 2.8 довольствовалась 1.22)
2. go mod init game — создайте модуль проекта
3. go get github.com/hajimehoshi/ebiten/v2@latest — поставьте движок
4. Скопируйте каркас из этого урока в main.go
5. go run . — должно открыться окно с заголовком и чёрным холстом
6. Окно молчит и не тикает? Проверьте фокус: нефокусному окну 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 в четырёх методах

```go
package 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 получит игра при настройках по умолчанию?

- 144 — по тику на кадр
- ✅ 60 — частота тиков от монитора не зависит
- 0, пока игрок не двинет мышь
- Зависит от процессора: сколько успеет

Тики идут с фиксированной частотой TPS, по умолчанию 60 в секунду, независимо от частоты монитора. Чаще станет только Draw. Именно поэтому логика в Update даёт одинаковую скорость игры на разных машинах, а логика в Draw — нет.

### Первый признак жизни: счётчик тиков

```go
type 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 тиков в секунду.

### Что случится в самом первом кадре?

```go
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
}
```

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

- Сначала draw: движок должен что-то показать до первого обновления
- ✅ Сначала layout, затем update, и только потом draw
- Порядок не определён и может меняться от запуска к запуску
- Layout вызовется один раз, и update с draw пойдут параллельно в горутинах

Документация гарантирует: Layout вызывается до первого Update, а Update — хотя бы один раз до первого Draw. Это сознательная гарантия движка: к моменту первой отрисовки размеры и состояние игры уже рассчитаны. Опираться на порядок после первого кадра в числах не стоит, но стартовая последовательность стабильна.

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

- Панику движка: Termination — это ошибка сборки
- Ошибку Termination, которую надо обработать
- ✅ nil — игра завершилась штатно, без ошибки
- Ничего: RunGame не возвращается, процесс зависает

Termination — специальный сигнал добровольного выхода: цикл останавливается, а RunGame возвращает nil, отличая «игрок вышел» от реальной аварии. Любая другая непустая ошибка из Update доезжает до вызывающего кода как есть.

**Вопрос:** Сколько раз в одном процессе разрешено вызывать RunGame?

- ✅ Один раз — повторный вызов запрещён документацией
- Два: по разу на каждое окно игры
- Сколько угодно — каждый вызов открывает новый цикл
- Ни разу: RunGame вызывается только внутри main

Документация RunGame требует не вызывать её дважды и более в одном процессе. Один запуск, один цикл; весь жизненный цикл игры живёт внутри переданной структуры Game, а не вокруг нескольких 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: запустить игру сразу в нефокусном окне, что полезно для демо и оверлеев. Запомните эту пару «замерло — проверь фокус», она сэкономит вам один бессмысленный вечер: к вводу и паузам мы вернёмся в уроках про клавиатуру и сцены.

> **Живое место: логика в Draw — ошибка, которая не видна сразу.** На вашем 60-герцовом ноутбуке тест с логикой в Draw пройдёт идеально — и провалится у первого же игрока со 144-герцовым монитором: скорость игры вырастет в 2,4 раза. Что сломалось бы дальше: таймеры урона, физика прыжков, откат способностей. Держите Draw пустым от механики — числа меняет только Update.

> **Первый запуск на Linux без графической среды.** Самая частая ошибка первого запуска на серверах и в CI: окно не открывается, потому что нет графической среды. На Linux движок говорит с системой через X11 или Wayland, и без них RunGame завершится ошибкой, а не покажет чёрный холст. В WSL2 на Windows 11 окна работают через WSLg, в чистом WSL2 без графической подсистемы — нет. Логике игры это не мешает: тесты и игровая математика идут и без экрана.

### Что тикает в песочнице, а что требует окна

Скажем честно про границы урока: весь код этой главы для запуска нуждается в локальном Go и графическом окне — песочница сайта не умеет открывать окна и рисовать спрайты, да и Ebitengine в браузерную сборку интерпретатора не влезет. Но сама суть цикла — числа, которые меняются 60 раз в секунду, — чистая математика, и она считается прямо на странице: тематическая программа в песочнице сайта эмулирует тики и подсчитывает шаги, как это сделал бы Update. Прогоните её до того, как соберёте настоящий проект: почувствовать ритм 1/60 секунды на числах полезнее, чем вчитываться в чёрное окно. Дальше по курсу мы рисуем: [третий урок покажет](/courses/ebitengine/lessons/03-okno-i-holst/), как Layout превращает ваши 320 на 240 в любое окно.

### Заполните каркас запуска

Восстановите три пропуска: метод-тик, размер окна и обработку ошибки запуска.

```go
func (g *Game) ___() error {
        g.ticks++
        return nil
}

func main() {
        ebiten.SetWindowSize(640, ___)
        ebiten.SetWindowTitle("My first game")
        if err := ebiten.___(&Game{}); err != nil {
                log.Fatal(err)
        }
}
```

Подсказка: Тик — это Update, окно из примера было 640 на 480, а запускает игру RunGame.

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

### Соберите последовательность запуска

Расставьте строки минимальной программы в правильном порядке.

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

1. `package main`
2. `import "github.com/hajimehoshi/ebiten/v2"`
3. `type Game struct{}`
4. `func (g *Game) Update() error { return nil }`
5. `func (g *Game) Draw(screen *ebiten.Image) {}`
6. `func main() {`
7. `ebiten.SetWindowSize(640, 480)`
8. `ebiten.SetWindowTitle("My first game")`
9. `if err := ebiten.RunGame(&Game{}); err != nil { log.Fatal(err) }`
10. `}`

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

- **ebiten.Game** — Интерфейс игры из трёх методов: Update (логика по тикам), Draw (кадр в screen), Layout (логический размер холста).
- **Тик и TPS** — Тик — один вызов Update. TPS — тиков в секунду, по умолчанию 60; меняется SetTPS, фактический замер — ActualTPS.
- **RunGame** — Запускает игровой цикл и блокирует main до выхода. Вызывается один раз на процесс; возвращает ошибку Update или nil при ebiten.Termination.
- **ebiten.Termination** — Специальное значение ошибки: верните из Update, чтобы штатно остановить игру — RunGame вернёт nil, как при обычном закрытии окна.
- **SetRunnableOnUnfocused** — По умолчанию Update и Draw стоят в нефокусном окне; SetRunnableOnUnfocused(true) разрешает тикать в фоне — включайте для отладки.
- **Почему не Draw** — Частота Draw зависит от монитора, поэтому игровая логика в нём делает скорость игры зависимой от железа. Логика живёт только в Update.

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

- Установка — это 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-минуты, и нервы.
