Go: Image Rendering in 2D Video Games with Ebiten
By: Vincent Blanchon
Tags: Golang, Go, Package

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Ebiten 1.10.

Ebiten is a mature 2D game library, written in Go by Hajime Hosh. It is the engine in some mobile games on the Apple Store, such as Bear’s Restaurant, or desktop games like the OpenDiablo2, an open-source implementation of Diablo 2 with Go. Let’s now dive into some fundamental concepts in video games along with their implementation with Ebiten.

Animation

In the video game world, Ebiten renders animation thanks to separated still images. This collection of images are grouped together into a bigger image, usually called “texture atlas,” also called “sprite sheet.” Here is an example of it provided on the website:

This image is then loaded in the memory and rendered part by part where simple maths are sufficient. In the previous example, each part has a width of 32 pixels. Rendering each image is as simple as moving the X coordinates by 32 pixels. Here are the first steps:

Rendering the animation, it just requires the value of the current image to render the correct part of the sprite sheet. Ebiten provides all the API to render that easily, here is an example of the previous sprite sheet on the first step:

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,

)

Hajime Hosh, the creator of Ebiten, has also developed “file2byteslice,” a tool that transforms any image to a string, allowing it to embed any file into Go. It can be integrated easily with the Go tools, via the annotation go:generate , automatically dumping the image in a Go file. Here is an example:

//go:generate file2byteslice

-package=img

-input=./images/sprite.png

-output=./images/sprite.go -var=Runner_png

The sprite sheet is now available in the variable img.Runner_png from the code:

Then, the image has to be decoded before being loaded by Ebiten and rendered in the game. Let’s move to another way to render bigger background images, composed of recurrent elements.

Background by tiles

Rendering background landscape uses the same technique by dividing the main atlas into many small images, called “tiles.” Here is an example of texture atlas provided on the website:

This atlas can be divided by tiles of 16 pixels. Here is the tile set created with the software Tiled:

Each tile will be assigned a number, from zero to the last one incremented by one. Since each line of tiles has the same number of tiles, getting the coordinates of a tile can be done by division and modulo. Here is an example with the blue flowers:

The blue flowers got the number 303, meaning that they are in the 4th column (303 modulo the number of tiles per row, e.g., 303%25 = 3) and 13th row (303 divided by the number of tiles per row, e.g., 303/25 = 12).

We can now build the map with an array of the indexes:

Drawing the images from this map gives the main decoration:

However, the background is missing. We have to build a similar array that represents the background. Here is the result:

The layers are now ready and we just have to iterate on both to get the final result:

The code in this example is available on the Ebiten website.

Once the image is built, Ebiten has to manage the update of the screen and send the instruction to the graphic card.

Screen Update

Ebiten provides an abstraction to the drivers used by iOS, which uses Metal, and Android, which uses OpenGL ES, making the development easier. It also lets you define a function that allows you to update the screen and draw all your changes. However, for performance reasons, the library will pack the sources together and accumulate these changes in a buffer before sending to the driver:

The buffer is also able to merge the drawing instructions to reduce the number of calls done to the GPU. In the previous illustration, the three instructions can now be merged in one:

This improvement is important since it can reduce the overhead when sending the instructions. Here is the performance with the merge of the instructions:

By sending the instructions one by one, the performance is quite degraded:

Ebiten also provides control over the refreshment on the screen, which could help to tune the performance.

TPS management

Ebiten runs at 60 ticks per second by default. However, this is easily configurable by the API provided by Ebiten with the method ebiten.SetMaxTPS() . It helps to reduce the pressure on the machine. Here is the same simple program with 25 TPS:

The TPS — ticks per second — is different from the FPS — frames per second. These differences are well described by Hajime Hosh:

A frame represents a graphics update. This depends on the refresh rate on the user’s display. Then FPS might be 60, 70, 120, and so on. This number is basically uncontrollable. Ebiten can just turn on or off vsync. If vsync is turned off, Ebiten tries to update graphics as much as possible, then FPS can be 1000 or so. A tick represents a logical update. TPS means how many times the update function is called per second. This is fixed as 60 by default. The game developer can configure TPS by SetMaxTPS. If UncappedTPS is set, Ebiten tries to call the update function as much as possible.

Ebiten brings another optimization for the rendering when the window is running in the background. When losing the focus, the game will sleep until getting the focus back. It actually sleeps 1/60th of a second and checks the focus again. Here is an example of the resources saved:

Although this optimization can be turned be off, the game could be throttled by the running system. For example, a game that runs in a browser gets restrictions when running in background tabs, both on Firefox or Chrome or other browsers.

Ebiten is opened to sponsorship on Github; feel free to contribute if you wish to see more games in written Go.