Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 2 из 25 ~18 мин +50 XP

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

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

Содержание урока · 10 разделов

Пустая папка, терминал, один модуль 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 для вас пока в новинку, заложите фундамент в базовом курсе, а затем возвращайтесь сюда.

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

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

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

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

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

Прочитайте код и выберите, что он выведет:

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.Runv1, на конце ветки помечена устаревшейзамыкания update и draw, ширину, высоту, масштаб и заголовок окна
ebiten.RunGamev1.12, в v2 — основной путьструктуру с методами Update, Draw и Layout
ebiten.RunGameWithOptionsv2.6то же плюс структуру опций: графическая библиотека, прозрачность окна, SingleThread

Окно потеряло фокус: почему игра замерла#

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

Поведение управляется функцией ebiten.SetRunnableOnUnfocused: передайте true до RunGame, и игра продолжит тикать даже в фоне. Здесь стоит быть аккуратным с обещаниями: игра, которая считает ресурсы в фоновом режиме, может раздражать пользователя и сливать батарею на ноутбуке. Разумная стратегия — держать runnable-on-unfocused выключенным в релизе и включать по флагу отладки, когда нужно наблюдать за логами при свёрнутом окне. Соседняя настройка — InitUnfocused в RunGameOptions: запустить игру сразу в нефокусном окне, что полезно для демо и оверлеев. Запомните эту пару «замерло — проверь фокус», она сэкономит вам один бессмысленный вечер: к вводу и паузам мы вернёмся в уроках про клавиатуру и сцены.

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

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

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

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

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) } }

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

Термин

ebiten.Game

нажмите, чтобы перевернуть
Определение

Интерфейс игры из трёх методов: Update (логика по тикам), Draw (кадр в screen), Layout (логический размер холста).

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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.

Личные заметкиПусто
Хранятся только в вашем браузере (Ebitengine · Установка и первый игровой цикл)

Вопросы и ответы по уроку

go run работает, а окно не появляется — куда смотреть?

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

Обязательно ли называть структуру Game?

Нет: движку всё равно, как зовётся ваш тип, — важен набор методов Update, Draw и Layout, который делает его реализацией интерфейса ebiten.Game. Название Game — просто конвенция из примеров.

Можно ли запускать игру без окна, например для тестов?

Игровую логику стоит выносить в методы без зависимости от ebiten — тогда её покрывают обычными Go-тестами. Для полного прогона с рендером окно всё же нужно, поэтому GUI-часть тестируют на десктопе, а не в CI без графики: рендер выносят в отдельный модуль и гоняют его только локально — это экономит и CI-минуты, и нервы.

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

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Chromedp25 уроков~11 чпрактика25 уроков уже доступны

Chromedp в Go: автоматизация Chrome без Selenium

Курс chromedp для Go: навигация, селекторы, клики и формы, скриншоты и PDF, JS-оценка, вкладки, прокси, пул браузеров и скрапинг. 25 уроков.

  • Запускать Chrome из Go и управлять страницами
  • Находить элементы селекторами и ждать их появления
  • Кликать, заполнять формы и извлекать данные
  • Делать скриншоты, PDF и выполнять JavaScript