2D Game Development in Golang — Part 2

Chris Andrews
5 min readNov 6, 2019

--

So in Part 1 of this mini tutorial series on making 2D games using the Go programming language, we installed the Ebiten library and made our first Hello World game. This was designed to give beginners an entry point into making a game. Part 2 assumes that you have followed along with the first part as it will show you how to set everything up: Part 1

In the first Part of this series, we drew some text at the top left of the screen. Normally this method is used to print debug text, in order to get updated information such as the x and y position of a particular image or pixel. Instead of this, we will rewrite our main.go file to draw a nice space image instead.

Drawing the background Image

Before we continue, lets make a new folder in our project directory called “assets”. So now our project structure should look like the following:

/yourGameName
/assets
main.go

Inside of this folder, we are going to place all of our images that we want to render on the screen. Now you can use any image you want as a background, but to make things easy we can start by using this lovely image found on Free PNG Img.

For readability, lets rename our file to space.png. In order to render our image as a background in our space shooter game, we should first understand a few simple commands from the Ebiten library.

The first function that we will use is:

func NewImageFromFile(path string, filter ebiten.Filter)

This function takes a string file path to an image as its first argument, and a ebiten.Filter type as the second. For the first argument, we are going to provide the filepath to our space.png image, and for the second argument we will simply use the default filter.

The NewImageFromFile function actually returns 3 different values: which are *ebiten.Image, image.Image and error types. For the sake of our game, we will only use the first and last. Also, because of the nature of our main loop and how it functions, we only really want to load this image into memory once, instead of recreating it 60 times per second. First lets create 2 empty vars somewhere near the top of our main.go file, the first one will be called background and will be of type *ebiten.Image, which is a pointer address to the image file, and secondly a var called err of type error which we will use to catch any errors:

var (
err error
background *ebiten.Image
)

Now we can assign a new image, and catch an error message if necessary. You will notice that we are using ebiten.FilterDefault as our second argument in NewImageFromFile, this is basically telling the method to use the default filter setting which relates to how the pixels will be rendered (not really necessary to go deeper into, but if you are curious, check out the docs).

Lets create a new function called init() below our newly created vars. This function will be called once at run time. Inside of this init() function we will call the NewImageFromFile method.

Our init() function should look like this:

You will notice that we are passing an underscore _ as the second argument to NewImageFromFile, this is basically telling the Go compiler to ignore the return value, as we do not need this for our current scope.

So now that we are loading our space.png into our background var at run time, we should make use of this and draw it to the screen. Lets go down to our update function, and make some adjustments. Because we no longer want to draw “Hello, World!” to our screen, lets delete that line. In its place we will draw the space.png.

To enable us to draw the image to our render target, we need to invoke the following Ebiten method:

func (i *Image) DrawImage(img *Image, options *DrawImageOptions)

This method is a receiver for the *Image type, which is our screen *ebiten.Image (our render target). We are going to pass our newly created background var as the first argument, and in the second we will pass in a *DrawImageOptions type.

A DrawImageOptions struct represents options to render an image on an image (in our case, rendering the space.png to the screen). Before we can draw our image, we must first create a new instance of DrawImageOptions. For simplicity and readability lets call it op:

op := &ebiten.DrawImageOptions{}

You might notice the ampersand symbol prefixing the DrawImageOptions struct, and this is because we need to use the pointer address to the actual resource.

From that, we can now use the GeoM.Translate() receiver method to set the starting x, and y coordinates on our screen. Its good to note that 0,0 starts from the top left of our screen.

op.GeoM.Translate(0, 0)

Finally we can call screen.DrawImage(background, op) and pass in our background var and our op var so our main.go file should look something like this:

Lets run our go file and check that everything works:

go run main.go

If all is well, you should see something like this:

You can probably tell that our image looks a bit pixelated, why do you think that is? In our main() function, you might remember we are passing the number 2 as our argument for scale in the ebiten.Run() command, this is basically telling Ebiten to draw the screen at a scale of 2. To mitigate this, we can change do a few things:

  • Change the scale to 1, and double the screen width & height
ebiten.Run(update, 640, 480, 1, "Hello, World!")

Now it will draw the screen size at 640x480 and our background image will be scaled at 1, which is default. Scaling the screen size by 2, more or less doubles the resolution of the screen but it also scales the image up, which can lead to unwanted side effects. Now our image should look a bit nicer:

Now we have made a beautiful space image as our background, creating a little bit of immersion for our player. In Part 3 of this series, we are going to use what we learned here to draw our space ship onto the screen, and also accept inputs from the keyboard in order to move our ship on the screen.

Engage!

Useful references:

--

--

Chris Andrews

Experimenting with Python, Go and various frameworks for learning.