2D Game Development in Golang — Part 3

Chris Andrews
9 min readNov 7, 2019

--

Hello game dev’s and welcome back to Part 3 of this mini series on game development in Go! If you are reading this and have been following Part 1 and Part 2, then congratulations! In this part, we are going to start getting into the mechanics of our new game project. So far we have covered the basics; setting up Ebiten & Go, making the “Hello World” game and then we even drew a nice image to the screen as a background.

Today we are going to shake things up a little and start accepting keyboard inputs in order to control a space craft on the screen! Fortunately for us, the Ebiten library actually contains many functions that can handle keyboard inputs, mouse inputs and even touch inputs such as on a tablet or mobile phone (yes you can create games for mobile!). So lets take advantage of them!

Creating our “Player”

Before we go and draw our space ship onto the screen, lets think about what our player should really be and what it should have. Lets first assume that the player is going to need an *ebiten.Image type which in our case will be a space ship. We are also going to need to track and store its current position on the screen, and given that we are developing a 2D game, then we will store the X and Y position on the screen, lets call them xPos and yPos respectively. We also want to move our spaceship on the screen by a predetermined amount, which we will call its “speed”.

In order to accomplish this, we are going to use a Go builtin type called a struct. Go’s structs are basically typed collections of fields. They’re useful for grouping data together to form records. If you are coming from a pure object oriented programming language like Java or C++, then you may be familiar with the concept of a class. Structs are akin to the data fields that you would have in a typical class, and we can leverage this to create a new Go type called “player”.

In go, lets create our new struct:

type player struct {}

Inside of this we will specify 4 fields, that will cover all the attributes we saw earlier. The image will be of type *ebiten.Image, the xPos and yPos will be of type float64, and the speed will also be of type float64. Lets build it:

// Create the player class
type
player struct {
image *ebiten.Image
xPos, yPos float64
speed float64
}

Just as we created a new var for our background image, lets also create one called spaceShip, and give the type *ebiten.Image. Our var declarations at the top of our main.go should look like this:

// Create our empty vars
var
(
err error
background *ebiten.Image
spaceShip *ebiten.Image
)

And like the background we will use the NewImageFromFile to create our new space ship image asset. You can actually use whatever you want, you could use a simple rocket ship or engage warp 9 and use the USS Enterprise — totally up to you! I found this cute little rocket ship on www.flaticon.com and downloaded the 64px png size. Once you have it downloaded it, rename it to spaceship.png and place it into the assets folder next to your background.

Rocket provided by Flaticon

If you remember in Part 2, we created our background image object inside of the init() function. Lets go ahead and do the same now for our spaceship:

You may notice that we are duplicating functions unnecessarily here, and you would be right. In this case, I want to keep it as simple as possible for beginners to follow on, so for now we will keep it this way. In the coming Parts, we will perform a code review and see what we could possibly change to make our code more readable, maintainable and to remove redundancy. Lets move on.

So we have a basic data structure for our player and we have an image to represent our player. Now we are going to need to instantiate a new object of our new player type. Lets create another var at the top, and this one will be called playerOne, and we will give it the type of our newly created player struct:

// Create our empty vars
var
(
err error
background *ebiten.Image
spaceShip *ebiten.Image
playerOne player
)

As I said earlier, I want to keep this code more or less simple for beginners, so we are going to create a new instance of type player, and we are going to do this inside our init() function. Before we do that however, I would like to make one change to our code. Back down in our main() function where we call ebiten.Run() you will notice we are hard coding our screen width and screen height, this is OK when beginning but later you will want to keep these values as a constant as you don’t want to go changing the resolution all willy-nilly, otherwise your player might get a bit annoyed.

Lets use the const keyword and create two new constants called screenWidth and screenHeight and assign our resolution of 640 x 480 respectively:

// Our game constants
const
(
screenWidth, screenHeight = 640, 480
)

Now lets replace our hard coded values in the ebiten.Run function with our new constants:

Lets go ahead and create our new player object finally. For the first argument, lets use our newly created spaceShip image, for the xPos and yPos, lets use our new screenWidth and screenHeight constants to calculate a position on the screen. Just because, lets put our spaceship in the center of the screen. Now given that I know that the screen width = 640 and my height is 480, I can just pass 320 and 240 as my xPos and yPos. The problem with this is that if I do decide to change my screenWidth and screenHeight later, it would break our current placement. Lets instead use this formula:

// pseudo code
X Position = screenWidth/2
Y Position = screenHeight/2

So now, our space ship should always be in the center, no matter what the screen size is. The last value in our new player object is going to be speed, for now we will give this an arbitrary value of 4. Lets create our new player:

playerOne = player{spaceShip, screenWidth/2, screenHeight/2, 4}

The last thing we need to do now is to draw our spaceShip onto the screen. We are going to do exactly the same as we did with the background. Its important to note that the order that you draw the images affects which one will be on top. In the case of Ebiten, images drawn last will always be on top, so its important to draw our background images first, and anything on the screen that we want to move, should be drawn last. Lets create our DrawImageOptions and use the GeoM.Translate() function as before, except now we are going to use the xPos and yPos stored inside of our new player object:

playerOp := &ebiten.DrawImageOptions{}
playerOp.GeoM.Translate(playerOne.xPos, playerOne.yPos)
screen.DrawImage(playerOne.image, playerOp)

We are using the stored values of xPos and yPos so that if we ever change the players position, the position will be automatically re-drawn to the new position. If we hard coded this value, the ship would never be able to move. The same goes for the image, as we also want to update the players image should that ever be changed (for example if you want a “damaged ship” image in case of getting hit with enemy bullets).

So lets take a quick look now at our main.go file:

To recap, so far we have created a type called player, we have included a new space ship image to use as our players image, and we have instantiated a new player object with initial values. Lets run our code and see what happens:

go run main.go

If everything is successful, then we should have something like this:

Looks cool so far! But being the unusually OCD person that I am, I can notice that my ship does not appear to be in the dead center. Why is that? When drawing images in Ebiten, the x and y position of the image actually starts from the top left corner of the image, so it somehow looks a bit offset from the center. Lets leave this for now, as it is not so important. Now that we have drawn our space ship, lets make it interesting by allowing the player to move it with the keyboard!

Handling User Input

In Ebiten, there are many built in methods to listen for keyboard input, the one that we will use now is called:

ebiten.IsKeyPressed(key Key) (bool)

This method takes a key as an argument and returns true or false depending on if that key is, you guessed it, pressed. For more details about it, check out the docs. Before we go ahead and use this, lets determine what keys we want to use to facilitate player movement. For simplicity, lets use the directional keys on the keyboard. Keys in Ebiten are defined as constants, for example the Up directional key is called KeyUp, for a full list of all keys, check this out.

Lets create a new function that we will use to listen for new keyboard input, lets call this function movePlayer():

func movePlayer() {}

This function will be called from within our update() function, at the top above the ebiten.IsDrawingSkipped() method call, you may remember that this is due to how Ebiten behaves, and that we should place all methods that update the state of an object above the IsDrawingSkipped() call.

Inside of our movePlayer() function, we will use if statements to check which key is pressed. We need to check if our key is pressed, and then if that is true we would like to initiate some kind of action, in this case we want to change the position of our space ship by a number of pixels in either the x or y axis using our playerOne.speed attribute as the change:

if ebiten.IsKeyPressed(ebiten.KeyUp) {
playerOne.yPos -= playerOne.speed
}

Here we are checking if the Up key is pressed, and if true we want to subtract the speed value from our players yPos attribute. You will recall that the screens x and y position starts from the top left corner, so to move up on the screen, we need to subtract from the yPos of the player, and then we can add to the yPos in order to make the ship move down. Lets code the rest of our if statements:

if ebiten.IsKeyPressed(ebiten.KeyUp) {
playerOne.yPos -= playerOne.speed
}
if ebiten.IsKeyPressed(ebiten.KeyDown) {
playerOne.yPos += playerOne.speed
}
if ebiten.IsKeyPressed(ebiten.KeyLeft) {
playerOne.xPos -= playerOne.speed
}
if ebiten.IsKeyPressed(ebiten.KeyRight) {
playerOne.xPos += playerOne.speed
}

Now we should be able to make our ship move. There are many different ways that we can achieve this, but this is the most basic and easily understandable way to achieve this.

One last thing is to call our movePlayer() function inside of the update() function, lets place this call at the top above IsDrawingSkipped(). If all is well, then your main.go should look like this:

Simply run your code and try using the directional keys to move!

go run main.go

Captain, we have regained control of our ship! Move us out of standard orbit!

So now we have a controllable player on screen, but it still feels a little bit boring. In Part 4 of this series, we are going to create an enemy to shoot at, and of course we are going to ask La Forge in engineering to provide our ship with some cannons!

Useful references:

If there is anything here that is not clear, please feel free to comment!

Thank you!

--

--

Chris Andrews

Experimenting with Python, Go and various frameworks for learning.