
В статье речь идет о Ebiten 1.10.
Ebiten — это хорошо продуманная библиотека для создания 2D-игр, написанная Хадзиме Хошем на языке Go. С ее помощью созданы движки ряда мобильных и десктопных игр, как например зарелиженная в Apple Store Bear's Restaurant, или OpenDiablo2 — реализации Diablo 2 с открытым исходным кодом на Go. В этой статье я предлагаю вам познакомиться с несколькими фундаментальными концепциями видеоигр и их реализацией в Ebiten.
Анимация
Ebiten рендерит анимацию из нескольких отдельных неподвижных изображений (кадров), что, конечно, не является инновационным подходом в мире видеоигр. Набор кадров, в свою очередь, группируется в более крупное изображение — так называемый “текстурный атлас”, или “спрайтшит”. Вот пример спрайтшита, представленный на сайте Ebiten:

Данное изображение загружается в память и с применением достаточно простой математики рендерится кадр за кадром. В предыдущем примере размер каждого кадра равен 32 пикселям. Рендеринг каждого кадра анимации — это по сути просто процесс перемещения координаты X на 32 пикселя. Вот как выглядят его первые шаги:


Для рендеринга анимации нам потребуется переменная с текущим кадром, взятая из соответствующей части спрайтшита. Ebiten предоставляет весь необходимый API, позволяющий легко справиться с этой задачей. Вот пример первого шага для изображенного выше спрайтшита:
screen.DrawImage(
// draw a sub image of the sprite from the coordinates
// x=0 to x=32 and y=32 to y=64
runnerImg.SubImage(image.Rect(0, 32, 32, 64)).(*ebiten.Image),
// variable declared somewhere before
// that defines the position on the screen
op,
)
Хадзиме Хош, создатель Ebiten, также разработал инструмент “file2byteslice”, который преобразует любое изображение в строку, позволяя встроить любой файл в Go. Он легко интегрируется инструментами Go с помощью аннотации go:generate
, автоматически выгружая изображение в файл Go. Вот небольшой пример:
//go:generate file2byteslice
-package=img
-input=./images/sprite.png
-output=./images/sprite.go -var=Runner_png

Перед загрузкой в Ebiten и рендерингом в игре изображение должно быть декодировано. Далее мы рассмотрим способ рендеринга больших фоновых изображений, состоящих из повторяющихся элементов.
Тайловый фон
При рендеринге фонового пейзажа (бэкграунда) используется та же техника - большой атлас делится на множество небольших изображений, называемых “тайлами”. Пример текстурного атласа, представленный на сайте:

Этот атлас можно разделить на тайлы размером по 16 пикселей. Вот набор тайлов, созданный с помощью программы Tiled:

Каждому тайлу будет присвоен номер начиная от нуля с шагом в единицу. Поскольку в каждой линии тайлов содержится одинаковое их количество, координаты тайла можно получить с помощью операций деления и деления по модулю. Например, для синих цветов:

Порядковый номер для синих цветов - 303, что означает, что они находятся в 4-м столбце (303 делим по модулю на количество тайлов в строке, т.е. 303%25 = 3) и в 13-й строке (303 делим на количество тайлов в строке и смотрим на целую часть, т.е. 303/25 = 12).
Теперь мы можем построить карту с массивом индексов:

Отрисовка изображений по этой карте даст нам следующее:

Однако на предыдущем изображении отсутствует фон. Теперь нам нужно построить аналогичный массив, отвечающий за фон. Вот результат:

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

Код этого примера доступен на сайте Ebiten.
После формирования изображения, задача Ebiten заключается в обработке обновления экрана и отправке сформированной инструкции в видеокарту.
Обновление экрана (update)
Ebiten представляет собой абстракцию для драйверов, используемых iOS, которая в свою очередь использует Metal, и Android, которая использует OpenGL ES, что несомненно упрощает разработку. Она также позволяет вам определять функцию, которая обновляет экран и отрисовывает все ваши изменения - update. Однако из соображений повышения производительности перед отправкой драйверу библиотека будет упаковывать исходники вместе и аккумулировать изменения в буфере:

Буфер способен объединять инструкции отрисовки, чтобы уменьшить количество обращений к графическому процессору. На предыдущем рисунке три инструкции могут быть объединены в одну:

Это оптимизация очень важна, поскольку может значительно снизить накладные расходы при отправке инструкций. Вот как изменилась производительность после объединения инструкций:

При последовательной отправке инструкций без оптимизации, производительность сильно снижается:

Ebiten также обеспечивает контроль над обновлением на экране, что также помогает повысить производительность.
Управление TPS
По умолчанию Ebiten работает со скоростью 60 тактов в секунду. Однако это можно легко изменить с помощью API, предоставляемого Ebiten, а именно — метода ebiten.SetMaxTPS()
. С помощью этого можно снизить нагрузку на железо. Вот та же простая программа только уже с 25 TPS:

TPS (ticks per second) — количество тактов в секунду, не путайте с FPS (frames per second) — количество кадров в секунду. Различия между ними хорошо описаны Хадзиме Хошем:
Кадры (фреймы) — это обновление графики, что зависит от частоты обновления дисплея пользователя. Таким образом значение FPS может быть выставлено на 60, 70, 120 и так далее. Это число в принципе неконтролируемо. Что Ebiten может, так это просто включить или выключить vsync. Если vsync выключен, Ebiten пытается обновлять графику как можно чаще — тогда значение FPS может достигать даже 1000 (ну или около того).
Такт представляет собой обновление, относящийся больше к логике. TPS говорит нам, сколько раз в секунду вызывается функция обновления (update). По умолчанию установлено значение равное 60. Разработчик игры может установить значение TPS с помощью функции SetMaxTPS. Если установлен UncappedTPS, Ebiten будет пытаться максимально часто вызывать функцию обновления.
Ebiten предлагает еще одну оптимизацию для рендеринга, когда окно работает в фоновом режиме. При потере фокуса игра будет находиться в режиме сна до тех пор, пока фокус не вернется. Если вдаваться в подробности, она засыпает на 1/60 секунды и снова проверяет фокус. Взгляните на пример сэкономленных ресурсов:

Эту оптимизацию при желании можно отключить — игра может быть замедлена по тактам самой системой, на которой она запущена. Например, игра, которая запускается в браузере, имеет ограничения при запуске в фоновых вкладках как в Firefox, так и в Chrome или любых других браузерах.
Ebiten открыт для спонсорства на Github; смело вносите свой вклад, если хотите увидеть больше игр, написанных на Go.
Материал подготовлен в рамках курса «Golang Developer. Professional». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
Комментарии (3)
sergeymolchanovsky
09.11.2021 21:26-6Вообще так делать игры в наше время - это полное уродство. Есть же GameMaker и Unity.
GospodinKolhoznik
09.11.2021 22:29+6Для такой 2д игры, как в статье, написать код, который будет отрисовывать локацию и анимацию персонажей это меньше 1% всех трудозатрат. И что же, из за какого то 1% придется ограничить себя только юнити?
sedyh
Приятно видеть, что библиотека становится популярной. Правда, оригинальная статья уже сильно устарела.