0011. Creating and Loading Tilemaps Using Ebitengine

0011. Creating and Loading Tilemaps Using Ebitengine

Watch the associated video on YouTube or MakerTube

Watch on YouTube

Introduction #

In this tutorial we will learn how to create and load tilemaps using the the Ebitengine game engine.

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

Tilemaps #

In tile-based video games, tilemaps are level/world maps made up of tiles.

The image data for each tile is located in a tileset. A tileset will typically have a unique ID assigned to it.

Each tile in a tileset will also have an ID assigned. Because tilemaps are mostly ID references, they are resource efficient.

They are also easy to work with as a developer, as we can build and layer complexity over a simple tilemap foundation.

Tiled Map Editor #

Tiled is a free and open source map editor for tile-based video games.

It has an intuitive interface and an array of features, making it a great choice for simple and complex video games.

In this tutorial we will learn some of the basics of using Tiled. Additional documentation is available in the Tiled manual.

Download Tiled #

Download and install the Tiled map editor by following the instructions on the Tiled website.

If you are using Linux, you may wish to install Tiled using your system package manager instead.

Create a Project #

Open Tiled and create a new project by clicking File > New > New Project.

In the newly opened dialog, create a directory and enter it. Click Save.

Whenever we use Tiled to work on our game, we will load this project file.

This project file describes which folders Tiled should search for tilesets and tilemaps.

A Tiled session file is also created in this directory, which allows Tiled to restore its previous state when reopened.

Create a Tileset #

Tileset

In this tutorial we will create a 64x64 tileset composed of 8x8 tiles.

Create a Tileset Image #

Using your favorite image editor, such as Krita or GIMP, create a 64x64 image.

To visualize the boundaries of each tile, enable the display of a grid over the image in the editor’s preferences.

Set the editor’s grid size to match the tile size: 8 pixels wide by 8 pixels high.

Color the Tiles #

Fill in the first three tiles with three different colors. Any three colors will do, we just want to be able to tell them apart.

Fill in the other tiles with a fourth color, such as bright pink. If we see this color in our game, we know something is wrong.

Save the Tileset Image #

Save the image as a PNG file in the /asset/image/ directory.

In an effort to work with the smallest possible asset files, consider using a tool such as oxipng to optimize the image.

Create a Tiled Tileset #

In Tiled, create a new tileset by clicking File > New > New Tileset.

Enter a name for the tileset, then click the Browse button next to Source. Choose the image file we just created.

Do not check the “Embed in map” option. When enabled, the tileset is embedded within tilemap files referencing it.

We don’t need to use this feature here. In fact, sharing a tileset between multiple maps results in better performance.

Create a Tilemap #

In Tiled, create a new tilemap by clicking File > New > New Map.

Leave the first few options as their defaults: Orthogonal, CSV and Right Down.

Set the map size to 32 tiles wide by 18 tiles high. Set the tile size to 8 pixels wide by 8 pixels high.

Click OK, and the newly created tilemap will be shown.

The tileset we just created should be shown in the sidebar on the right. If not, open it using the sidebar on the left.

Select the first tile in the tileset shown in the sidebar on the right.

This activates the brush tool, which is used to paint tiles onto the tilemap. Click on the tilemap to paint.

Repeat this process to paint using the next two tiles in the tileset. Click and drag to paint multiple tiles.

Save Files #

Click File > Save All to save the tileset, tilemap and project.

Save the tileset as /asset/tileset/tileset.tsx. Save the tilemap as /asset/map/map.tmx.

Load a Tilemap #

Import the go-tiled package, then run go mod tidy to record relevant dependency version information.

go-tiled parses data stored in Tiled tileset and tilemap files. A rendering system is also included in the package.

Because we are working with tile and map sizes which will not change as the game runs, we first define a few constants:

const (
	tileW, tileH       = 8, 8
	tilesetW, tilesetH = 64, 64
	mapW, mapH         = 32, 18
	screenW, screenH   = mapW * tileW, mapH * tileH
)

We then define three fields in our game struct: a Tiled tileset, Tiled tilemap and slice of tile images.

type game struct {
	// Tileset.
	t *tiled.Tileset

	// Tilemap.
	m *tiled.Map

	// Tileset image.
	tileImages []*ebiten.Image

    // ...
}

We also define a loadMap function which parses an embedded Tiled tilemap file by name:

//go:embed asset
var assetFS embed.FS

// loadMap loads the map with the specified name.
func loadMap(name string) (*tiled.Map, error) {
	// Build map file path.
	path := filepath.Join("asset", "map", name+".tmx")

	// Parse map file.
	gameMap, err := tiled.LoadFile(path, tiled.WithFileSystem(assetFS))
	if err != nil {
		return nil, fmt.Errorf("failed to load map %s: %s", path, err)
	}
	return gameMap, nil
}

This function will be called in the initialize method we will define later.

Load a Tileset #

We define a loadTileset method which first ensures one tileset is referenced by the tilemap we loaded.

We then load the source image of the tileset as img and create SubImage references for each tile within the tileset.

A SubImage reference isolates part of an image. The image is referenced, not copied. Changes to either affect the other.

func (g *game) loadTileset() {
	// Check for expected number of tileset references in tilemap.
	numTilesets := len(g.m.Tilesets)
	if numTilesets != 1 {
		log.Panicf("failed to load map: expected 1 tileset, got %d", numTilesets)
	}

	// Store reference to tileset.
	g.t = g.m.Tilesets[0]

	// Load tileset image.
	img := loadImage(filepath.Join("asset", "map", g.t.Image.Source))

	// Store sub-image references for each tile contained in the tileset. A
	// sub-image reference only isolates part of a source image. Thus, each
	// of the sub-image references point to the tileset image we just loaded.
	g.tileImages = make([]*ebiten.Image, g.t.TileCount)
	for i := uint32(0); i < uint32(g.t.TileCount); i++ {
		// Calculate sub-image rect for this tile.
		r := g.t.GetTileRect(g.t.FirstGID - 1 + i)

		// Store sub-image reference for this tile.
		g.tileImages[i] = img.SubImage(r).(*ebiten.Image)
	}
}

We also define an initialize method which loads a map and tileset at the start of the game:

// initialize sets up the initial state of the game.
func (g *game) initialize() {
	// Load map.
	var err error
	g.m, err = loadMap("map")
	if err != nil {
		log.Panic(err)
	}

	// Load tileset.
	g.loadTileset()

	// ...
}

Render a Tilemap #

In this tutorial we will implement our own tilemap rendering system. This allows us to make use of certain optimizations.

We can achieve the best possible performance by drawing tiles by their type, rather than by their screen location.

Ebitengine can typically batch successive draw calls together where the source and target image remain unchanged.

Thus, we will draw a single tile image as many times as it appears on the screen, without drawing anything in between.

Once all instances of a tile have been drawn, we then draw all instances of the next tile in the tileset, and so on.

// Draw is where we draw the game state.
func (g *game) Draw(screen *ebiten.Image) {
	op := &ebiten.DrawImageOptions{}

	// Draw tiles in groups based on tile ID.
	l := g.m.Layers[0]
	for drawTile := g.t.FirstGID - 1; drawTile < g.t.FirstGID+2; drawTile++ {
		for i, tile := range l.Tiles {
			if tile.ID != drawTile || tile.Nil {
				continue
			}

			// Calculate tile position in tilemap.
			tileX, tileY := i%mapW, i/mapW

			// Calculate tile position on screen.
			screenX, screenY := tileX*tileW, tileY*tileH

			// Draw tile.
			op.GeoM.Reset()
			op.GeoM.Translate(float64(screenX), float64(screenY))
			screen.DrawImage(g.tileImages[drawTile-g.t.FirstGID+1], op)
		}
	}
}

Stay tuned for the next tutorial, Tilemap Collision Detection Using Ebitengine.

Please consider donating if you found this tutorial helpful.