Dev Thicket

Getting Started with 2D Game Development in Go

Learn how to build 2D games in Go using Ebitengine and Willow. Set up a project, draw sprites, handle input, and add structure with a scene graph.

Go is one of the most underrated languages for game development. Most developers associate it with web servers and CLI tools, but it has qualities that make it genuinely excellent for building games. Compilation is nearly instant, so your edit-run-test loop stays tight. The language produces single static binaries with no runtime dependencies, which makes distribution trivial. And Go's cross-compilation story is best-in-class; you can target Windows, macOS, and Linux from any platform with a single command.

The ecosystem has matured too. Ebitengine (formerly Ebiten) is a battle-tested 2D game engine for Go with a simple API, solid performance, and support for desktop, mobile, and WebAssembly. It handles the window, the render loop, input, and audio. You write Go; it draws frames.

This tutorial walks you through building a 2D game from scratch in Go using Ebitengine. By the end, you will have a window, a sprite on screen, and keyboard-driven movement. From there, we will look at how Willow can add structure as your project grows.

Prerequisites

Before you start, make sure you have:

  • Go 1.21+ installed. Download it here if you do not have it yet.
  • Basic Go knowledge. You should be comfortable with packages, structs, and interfaces. If you can write a "Hello, World" in Go, you are ready.
  • A text editor. VS Code with the Go extension, GoLand, or anything you prefer.

Setting up Ebitengine

Create a new directory for your project, initialize a Go module, and pull in Ebitengine:

Project setup

Now create a main.go file with a minimal game loop. In Ebitengine, your game is a struct that satisfies the ebiten.Game interface. That interface has three methods: Update(), Draw(), and Layout().

main.go

Run it:

Run the game

You should see a black 640x480 window. Nothing exciting yet, but a lot is happening behind the scenes. Ebitengine has created an OpenGL (or Metal, depending on your OS) context, started a render loop at 60 ticks per second, and is calling your Update() and Draw() methods every frame. Let's put something on screen.

Drawing your first sprite

Ebitengine does not have a built-in "rectangle" primitive. Instead, you create an off-screen image, fill it with a color, and then draw that image onto the screen. This is the same pattern used for real sprites loaded from PNG files; the only difference is the source of the pixel data.

Add a package-level variable for the sprite and create it in an init() function:

Sprite initialization

Now update the Draw() method to render it at a specific position:

Draw method

DrawImageOptions contains a GeoM field, which is a 2D geometry matrix. If you have used CSS transforms or canvas APIs, this is the same concept. It describes how to transform the image before placing it on screen. GeoM.Translate(100, 80) moves the image 100 pixels right and 80 pixels down from the top-left corner.

The geometry matrix also supports Scale() and Rotate(). The order matters: operations are applied in the order you call them. Translating then rotating produces a different result than rotating then translating.

Run the program again and you should see a blue 32x32 square at position (100, 80).

Adding movement

A static square is not much of a game. Let's make it move with the arrow keys. First, add position fields to the Game struct:

Game struct with position

Now update Update() to check for key presses and adjust the position. Ebitengine's ebiten.IsKeyPressed() returns true for as long as a key is held down, which is exactly what we want for smooth movement:

Keyboard movement

Update Draw() to use the dynamic position:

Dynamic draw position

Run it again. Your blue square now moves with the arrow keys. The movement is frame-locked at 3 pixels per tick (60 TPS), which gives 180 pixels per second. For a prototype this is fine. In a real game you would multiply by delta time for frame-rate-independent movement.

Where Willow fits in

The code above works, but notice how everything is manual. You track positions yourself. You call DrawImage for every object. You manage update order. Once your project grows past a few sprites, this becomes tedious and error-prone.

That is exactly the problem Willow solves. Willow is a scene graph library for Ebitengine. It gives you a display tree where every object has a position, scale, rotation, and parent-child relationships. Drawing and updating happen automatically by walking the tree.

Here is the same movable sprite in Willow:

Willow scene example

The difference becomes more obvious as complexity grows. Need to group sprites? Add them as children of a container node. Need to hide an entire HUD? Set the container's visibility to false. Need layered rendering? Willow handles draw order based on tree position. You describe what your scene looks like; Willow handles how it gets drawn.

Check out the Your First Scene guide to see it in action.

Next steps

You have a working foundation. Here is where to go from here:

Go is a fantastic language for game development. The tooling is fast, the language is readable, and the ecosystem keeps getting better. Start small, ship something, and iterate. That is the best way to learn.