6. Adding Text to Your Ebitengine Game

6. Adding Text to Your Ebitengine Game

Watch the associated video on YouTube or MakerTube

Watch on YouTube

Introduction #

In this tutorial we will learn two methods of drawing text onto the screen using the Ebitengine game engine.

The code for this tutorial is available here. Please consider donating if you find this tutorial helpful.

Bitmap Fonts #

Bitmap fonts are composed of bitmap images.

When drawing text using bitmap fonts, scaling the text size up or down will involve image filtering.

We should only use bitmap fonts when we want a pixelated result, because non-pixelated fonts will lose definition.

We can use either nearest neighbor or linear filtering when drawing an image onto the screen.

Nearest neighbor filtering does not blend color values together, producing a pixelated image. This is the default filter.

Linear filtering blends color values together, producing an anti-aliased image.

When scaling a bitmap font, the default filter will typically yield the best result.

Vector and Stroke Fonts #

Vector fonts are composed of vector images, and stroke fonts are composed of stroke paths.

When drawing text using vector and stroke fonts, scaling is applied within the text shaping engine.

Each character glyph is rasterized at the specified size according to a set of vector images or stroke paths.

Characters of any size are produced without loss of definition, producing a legible and aesthetic result.

Monospace vs. Variable-width Fonts #

Monospace fonts use a fixed amount of space to draw each character onto the screen.

Variable-width fonts use a varying amount of space to draw each character onto the screen.

Each has benefits and trade-offs, which will become clear as we learn about each and use them both in our game.

Methods of Drawing Text #

Ebitengine provides two methods for drawing text onto the screen:

  • text.Draw allows specifying the font, size and color of the text. This is best for text shown to the player.
  • ebitenutil.DebugPrint always uses a pre-configured font, size and color. This is best for text shown to developers.

text.Draw #

To draw text using text.Draw, we must first initialize a font face.

Initialize a Font Face #

To initialize a font face, we must first initialize a font source by loading a font file.

A single font source may then be used to initialize any number of font faces. In this example, we create a 16px font face.

type game struct {
	//..

	// Text font source.
	fontSource *text.GoTextFaceSource

	// Text font face.
	fontFace *text.GoTextFace
}

// initialize sets up the initial state of the game.
func (g *game) initialize() {
	// Load variable-width font embedded in Ebitengine.
	var err error
	g.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
	if err != nil {
		log.Panic(err)
	}

	// Create 16px font face from the above source.
	g.fontFace = &text.GoTextFace{
		Source: g.fontSource,
		Size:   16,
	}
}

Use a Font Face #

Here we provide text.Draw with a font face and a text.DrawOptions value.

We specify the position of the text by translating the geometry matrix.

We can specify the color of the text by calling ColorScale.ScaleWithColor. We will use the default white color in this game.

// Draw is where we draw the game state.
func (g *game) Draw(screen *ebiten.Image) {
    // ...

    // Draw variable-width text onto the screen.
    txtOp := &text.DrawOptions{}
    // Start drawing at the top center of the screen.
    txtOp.GeoM.Translate(screenW/2, 0)
    // By default, the text is white. We can call ScaleWithColor to specify a different color.
    //colorGreen := color.RGBA{0, 255, 0, 255}
    //txtOp.ColorScale.ScaleWithColor(colorGreen)
    text.Draw(screen, "Hello, world!", g.fontFace, txtOp)
}

ebitenutil.DebugPrint #

To draw text using ebitenutil.DebugPrint, we only need to provide the text.

// Draw is where we draw the game state.
func (g *game) Draw(screen *ebiten.Image) {
	// ...

	ebitenutil.DebugPrint(screen, "Hello, world!")
}

This draws the provided text onto the screen in white with a drop shadow.

ebitenutil.DebugPrint can only be used with the font Press Start 2P, which is automatically embedded.

Each character of text will occupy 6x10 pixels on the screen. Because each character is a fixed size, calculating how much screen space will be required to draw any text is a simple process.

To draw characters larger or smaller than 6x10 using ebitenutil.DebugPrint, we must first draw to an intermediate (off-screen) image. We can then draw the intermediate image onto the screen using a scaled geometry matrix.

type game struct {
	//..

	offscreenImg *ebiten.Image
}

// initialize sets up the initial state of the game.
func (g *game) initialize() {
    // ...

    offscreenImg = ebiten.NewImage(270, 152)
}

// Draw is where we draw the game state.
func (g *game) Draw(screen *ebiten.Image) {
	// ...

    // Clear intermediate image.
    g.offscreenImg.Clear()

    // Draw text onto intermediate image.
	ebitenutil.DebugPrint(g.offscreenImg, "Hello, world!")

    // Draw intermediate image onto the screen at 4x its size.
    op := &ebiten.DrawImageOptions{}
    op.GeoM.Scale(4, 4)
    screen.DrawImage(g.offscreenImg, op)
}

In the above example, we create an off-screen image which is 270 pixels wide and 152 pixels high.

To keep resource usage low, use as few and small of intermediate images as possible.

Creating many large off-screen images may exhaust the system’s video memory, especially on mobile devices.

When to Use ebitenutil.DebugPrint #

It’s easy to draw text using ebitenutil.DebugPrint, and its output can be wrangled to produce aesthetic results.

However, we can only change the size and color of the text by drawing to an off-screen image and then manipulating it.

Therefore, we should generally use text.Draw, and only use ebitenutil.DebugPrint to print debug information.


Stay tuned for the next tutorial, Adding Audio to Your Ebitengine Game.

Please consider donating if you found this tutorial helpful.