Coding A Simple Game In Golang And Fyne

This article will explore how to code a character sprite that can walk around on an image background using Golang, the Image package, and the Fyne GUI toolkit

Some time ago, I followed some of the interesting tutorials by Frank’s Laboratory on how to code games in JavaScript. I have decided to code one of his simple games in Golang using the Fyne GUI toolkit for this article instead of JavaScript.

The structure of the code that I present to you here will also be somewhat different from the original by Frank’s Laboratory, given that Golang works differently from JavaScript.

As in his video tutorial, we won’t be building a complete game, though. You’ll be able to move a character around on a background image using the arrow keys on your keyboard. Interest, this could be the start of an actual game.

Below, you can see the two-game elements used in this project.

We’ll use a Starwars background and a sprite sheet featuring Chewbacca, aka. “Chewie.” The original links to these assets can be found on the YouTube page of the tutorial by Frank’s Laboratory.

The background image (left) and the character sprite sheet (right)

Once you have downloaded them, these PNG image game assets should be placed in a folder called ./assets below the folder in which the Go code is run.

The Player and Game models

In lines 1–16, we define the Player struct.

The xand y fields refer to the position of the player in the game.

The width and height are the size of each individual sprite on the sprite sheet.

The variables frameX and frameY Refer to the sprite position on the sprite sheet in the x and y directions, respectively.

The field cyclesX counts the number of sprites in the X direction on the sprite sheet. These sprites will repeatedly be cycled from beginning to end when the player walks.

The variables upY, downY, leftY, rightY refer to the specific rows of sprites that correspond to the orientation of the player character.

The “chewie” sprite sheet with 16 individual sprites

The variable speed is the amount that is added to the x or y position of the player.

The fields xMov and yMov are set during the game when pressing specific keys.

In lines 18–24, we define the Game struct.

The fields canvasWidth and canvasHeight refer to the size of the background.

The constant fps refers to the frames per second in which we wish to run the game.

Finally, then is used for timekeeping — more on that later on.

Loading the PNG files

The above code loads the PNG images that we use as game assets.

It was adapted from the code from this article. I changed it to work specifically with PNG images and make it return an image of the image.Image type.

Essentially it opens a file containing a PNG image and then decodes it. For each of these steps, there is some basic error handling.

Closing the file is deferred so that it will always happen when the program leaves the function — no matter whether errors have occurred or not.

Setting up the assets, the models, and the Fyne app

Create a Fyne app with the main window with “Game” as its title.

Loading the PNG files using the load() function defined above.

Find out what time it is right now in Unix Millisecond style — this is an int64 number. Save it as now.

Create a Game struct for holding some relevant variables. Set the width to 800 and the height to 500. The fps or frames per second is set to 10. The then field is set to the time right now.

Finally, we set the margin field to 4. This makes sure that our player does not touch the background border.

Then we calculate the fpsInterval using the fps variable. This variable will be used in the game loop — more on that later on.

When that’s done, we’ll create a Player struct with some important variables. The start position of the player will be at position (100, 200) with (0, 0) being in the upper left of the window.

The size of each player sprite will be 40px by 72px . The starting frameX and frameY will both be at 0. The sprite sheet rows that map onto the up-arrow, down-arrow, left-arrayand right-arrow keys will be 4, 3, 0, and 1, respectively. The speed is set to 9. The changes xMov and yMov in x and y position are both set to 0.

Create a Fyne canvas called img and make it hold the background image. Then set the size of the canvas to the original size of the background.

Now create an empty image called sprite at the same size as the background for drawing the sprite and put it in a Fyne canvas. This will be called playerImg.

We’ll also create a variable called spriteSize that will be used later on. This variable keeps the size of a single-player sprite.

Then add both the background canvas img and the sprite canvas playerImg to a Fyne container called c. This container will organize the canvasses according to the MaxLayout layout. The result of this is that both canvasses will be set to their max size. You can read more about this layout here.

Then we set the content of the window w to the container c.

The key events and the game loop will be discussed below.

The window w is set to show in the middle of the screen. Finally, it is told to be shown and run.

Defining the key events

Here we implement the four possible key events. Given that you should be able to move the player character in four directions on a part of the background, we need to implement each of these possibilities.

We first attach a Fyne key event handler to a canvas of the window w by using the functions Canvas().SetOnTypedKey().

A switch statement will take care of each of the four cases. You can find all the definitions of each keyboard key here.

We check if the player character’s position is within some bounds for each of the possibilities. Here we remember to include the game.margin when applicable.

Depending on the direction of movement, player.xMov or player.yMov are set to a positive or negative value of player.speed. In addition, the correct row of sprites is selected by setting player.frameY to the specific row index.

The game loop

This is the game loop. This part of the program contains an infinite loop.

For every iteration of the loop, there will be a pause of 1 millisecond. This could potentially be tweaked to be a longer pause if needed.

Then we get the time right now and convert it into int64 Unix Milliseconds. We’ll use it to find out how much time has elapsed since we last set game.then.

If the time that has elapsed is greater than the fpsInterval then we’ll enter into the actual game mechanics branch. We only enter this branch when fpsInterval has passed to achieve the correct game speed.

Then we set the game.then to now.

To extract a sprite from the sprite sheet, we’ll need a starting point spriteDP and a rectangle called sr with a size of spriteSize that covers that specific sprite.

Example of a rectangle around a specific sprite

To place that sprite on top of the background, we analogously need a reference point dp for the player — based on the x and y coordinates of the player — and a rectangle where the sprite needs to be drawn. This rectangle is also of size spriteSize.

Now, let’s first clear the sprite image by filling it with image.Transparent. Once this has been done, we can transfer the specific sprite from the playerSprites sprite sheet to the sprite image and create a new raster from this using canvas.NewRasterFromImage().

In the last part of the game loop, we’ll determine the player x and y coordinates and the player.frameX for the next cycle, if either the player.xMov or player.yMov are non-zero. In addition, we’ll also cycle through the sprites in the row by changing player.frameX.

If the movement was zero, then the player.frameX is set to zero, which is the first sprite in the row. This sprite also represents the stationary stance of the character.

Finally, the container c is refreshed to ensure that the images are updated in the window.

Leave a Comment