Go: рендеринг изображений в 2D-играх на примере Ebiten

Kate

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

Анимация​

Ebiten рендерит анимацию из нескольких отдельных неподвижных изображений (кадров), что, конечно, не является инновационным подходом в мире видеоигр. Набор кадров, в свою очередь, группируется в более крупное изображение — так называемый “текстурный атлас”, или “спрайтшит”. Вот пример спрайтшита, представленный на сайте Ebiten:

fffbf0e95933b55e0052b5bce6d756fe.png

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

b60db545e0bca92ef77762ce755fd583.png
bdd89641b3f8654511e93bd80eefdbe4.png

Для рендеринга анимации нам потребуется переменная с текущим кадром, взятая из соответствующей части спрайтшита. 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
b4f266e9218868e4ffa12b899434e1c5.png

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

Тайловый фон​

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

2b061e3a40c2249fd7ee2062e3dfff26.png

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

022f2cbec17448b66289e3204432f695.png

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

7b64ad773205f92a6d8ef575b66be0bf.png

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

Теперь мы можем построить карту с массивом индексов:

e150001a22345e1c242169ab96c5a664.png

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

0c379d0de2b47d3e294b95f49a367ee4.png

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

5d3fb4eb4afe66ca6f051e4aa1367852.png

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

e63e879e78e6ad4e3c8bd013bcefb02e.png

Код этого примера доступен на сайте Ebiten.

После формирования изображения, задача Ebiten заключается в обработке обновления экрана и отправке сформированной инструкции в видеокарту.

Обновление экрана (update)​

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

a2af2da773da25dc7b7ffcd15d4d344e.png

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

510bf53a7e2056c35f51c601e4f3960b.png

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

ad89249367d02ca9eff41b7f4c782565.png

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

2656da946eaa93247e223fefda6d0102.png

Ebiten также обеспечивает контроль над обновлением на экране, что также помогает повысить производительность.

Управление TPS​

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

72af97d9a3b2b763cf984b9bbf48618f.png

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 секунды и снова проверяет фокус. Взгляните на пример сэкономленных ресурсов:

ff6083801c06c4c4d30a388b8a081862.png

Эту оптимизацию при желании можно отключить — игра может быть замедлена по тактам самой системой, на которой она запущена. Например, игра, которая запускается в браузере, имеет ограничения при запуске в фоновых вкладках как в Firefox, так и в Chrome или любых других браузерах.

Ebiten открыт для спонсорства на Github; смело вносите свой вклад, если хотите увидеть больше игр, написанных на Go.

 
Сверху