Say hello to Gonk, the game we are going to make. You can find the code at https://github.com/dbriemann/gonk. This first chapter will cover basic structures and functions and the use of composition for game objects.
Preface
The whole code for the game made in this tutorial can be found here. You should be able to use git at least a little bit. First of get the source code. The recommended way is to use go get:
go get -d -v -u github.com/dbriemann/gonk
This will put gonk in your gopath at $GOPATH/src/github.com/dbriemann/gonk. You can at any point switch to this directory to build and run gonk. Note that the -d flag skips installing the binary into your gopath's bin directory.
Just use git to pull the newest state or checkout a specific tag. I will add a tag for each chapter so you can use
git checkout chapterX.Y
to jump to the game code containing everything up to the tagged chapter. Each chapter will have a note for this. At any point you can go back to the current state with git checkout master. If the beginning of this tutorial is too easy for you, you can just jump in at any point in the series later on.
Note that I won't discuss all of the code in the tutorial. Things that I deem not interesting, not essential or too simple will not be covered. They will however be part of the code and I will try to comment everything so you can just browse the code and understand what's happening.
1.1 Basic Structure & Functions
git checkout chapter1.1
First we should note that Pixel expects us to provide a specific function that acts similar to a main function. This is necessary to ensure that all graphics calls originate in the main thread of the program. Pixel makes this easy by allowing us to bind such a function with pixelgl.Run().
We will from now on just use run as if it was main and can be sure that all calls come from the main thread.
Now let's define some basic functions which will be extended over the course of the tutorial. This section covers the core building blocks of the game flow.
Every game needs to setup and initialize a lot of things at startup. It has to load assets (sound and graphics), select and apply the correct screen mode, check for updates and possibly many more things. That's why we need some init functions.
We won't go into the details of the initScreen() function because it's just boilerplate code. The code retrieves the primary monitor and sets up a window with dimensions screenWidth x screenHeight. We will add the ability to switch to full screen mode later in the tutorial. The most important variables here are defined globally in the file globals.go.
NOTE: Games are a lot about mutating states. Thus it is often easier (and more productive) to define variables globally than passing them around from function to function.
Our setFPS() function can either set the maximum frames per second (by providing a positive value) or disable any maximal value (by providing 0). frameTick is a pointer to a time.Ticker from the Go std library and defined in globals.go. If initialized it will trigger every X seconds or in our case fractions of a second. E.g. if we set the FPS to 60 it will trigger every 1/60th second.
This brings us to the game loop. Each game has it. It is the heart of the game. Our game loop looks like this if we strip some things from it that are unimportant to its basic purpose. It is part of the mentioned run() function.
for !window.Closed() { // (1)
last := now // (2a)
now = time.Now() // (2b)
dt := now.Sub(last).Seconds() // (2c)
update(dt) // (3)
draw() // (4)
window.Update() // (5)
if frameTick != nil { // (6a)
<-frameTick.C // (6b)
}
}
What it does is actually not that complicated. The loop continues to run until the the window is closed via the x icon (1). At some point we will add a menu to the game which will offer the typical way to exit a game.
NOTE: Usually you want to create the game before the menu. If your game sucks you have saved time.
Lines (2*) calculate the duration of the last frame. The value is stored in the variable dt which is short for delta time. dt is passed to the update() function (3) which can then interpolate if the frames don't have a consistent duration.
NOTE: A game's update(..) function handles the logic. It is responsible for modifying the state of the game and of all its entities. Often input is handled here too.
TIP: Read more about game loops and delta time in the great article Fix Your Timestep!
Next the draw() function is called (4) which will draw everything visible onto the screen. At the end we have a call to window.Update() (5) which tells Pixel that the frame is finished.
Lines (6*) show how the previously mentioned ticker is used. If there is a ticker object the code will wait until it triggers. We have seen how the ticker is initialized in the setFPS() function. With this little snippet we can limit the FPS to a specific value or disable it and the code runs as fast as possible.
The draw() function of a game just draws everything you can see as a player to the screen. It will usually not draw objects outside of the screen or out of sight to save performance. It also should not alter any state in the game. That's what the update(..) function is there for.
For now draw() just clears the screen with black and draws the measured frames per second on the screen. This tutorial explains how to handle text in Pixel. We won't have a look at this topic here. The FPS counter is part of a heads up display we will add to the game. Soon we will add a camera to the game but the HUD will always be drawn outside the camera, directly in screen space.
1.2 Game Objects & Composition
git checkout chapter1.2
A lot of code has been added since the last chapter. As always we will have a look at the core parts. Let's start by avoiding misunderstandings. If you have used an Entity Component System before you may be a bit confused because we will use the terms entity and component here but not necessarily with the same meaning. For us a component is a minimal building block with the follwing properties:
it consists of mostly data but can also have small essential functions attached,
it doesn't make sense to divide it into smaller pieces,
it often is used as a part of an entity.
The entity differs from the component because of the follwing properties:
it represents something more complex and has usually more functionality,
it will usually have an update() and a draw() function,
it can contain multiple components, often by embedding (composition).
We differentiate between components and entities just logically. There are no types involved. The only physical separation is that we have one file for components (components.go) and one file for entities (entities.go). Because Gonk is a small game we won't need separate files for each component and entity.
Orb
Let's start with our orb component. We know that our game will have planets and ships (copied from the original qonk). Both planets and ships show a similar behaviour. They rotate around some kind of anchor. This maybe the origin of the star system (center / sun) or another planet. If we pick the core properties of both a planet and a ship we arrive at orb.
type orb struct {
anchor *pixel.Vec
vel pixel.Vec
pos pixel.Vec
dir float64
dist float64
}
func (o *orb) rotate(dt float64) (shift pixel.Vec) {
mat := pixel.IM // (1)
len := o.vel.Len() // (2)
omega := o.dir * len / o.dist // (3)
mat = mat.Rotated(*o.anchor, omega*dt) // (4)
npos := mat.Project(o.pos) // (5)
shift.X, shift.Y = npos.X-o.pos.X, npos.Y-o.pos.Y // (6)
o.pos.X, o.pos.Y = npos.XY() // (7)
return
}
You can see that an orb holds a pointer to an anchor vector. This way we can use any position of any orb specifically as anchor without having to copy the values every time the anchor moves. The other properties are simple:
vel(ocity): speed of the orb
pos(ition): ..
dir(ection): this is -1 for clockwise rotaten and 1 for counter-clockwise
dist(ance): distance to the anchor
Let's have a look at the rotate function. It takes one argument (dt) and returns the shift Vector. We know dt already. It is the delta time usually consumed by update() functions. It determines how far the orb rotates depending on the time passed. The returned vector shift just contains how the orb was moved by the rotation. We will see how this is used later on.
In line (1) we just create a new matrix by copying the identity matrix. In the next line we misuse the vel vector (2) by just calculating its length to act as a pseudo-speed. Because the orb only rotates on a set circle we don't need a vector here and thus we simplify and pretend to have a 1D velocity. Line (3) calculates the angle dependending on dir, dist and the speed.
We now use Pixel's functions (4,5) to rotate pos around anchor. As you can see dt is a factor for the angle. You can remember it like that: omega is rotation (in rad) per second. Now if dt equals 1/60th of a second the orb is only rotated by 1/60th of omega. The Rotated function just creates a matrix (4) to rotate something around anchor by the given angle. Afterwards Project applies this matrix to the pos vector. The result npos is the new position after rotation.
In line (6) we calculate the delta vector from the old and new position. This is the vector that is returned by the rotate function. In line (7) only the new position values are assigned to the current position and the position is now updated.
Planet and Ship
It's time for a little composition. Here are our planet and shipentities. First let me say that player is another component but we will not discuss it here. Have a look at it. It is pretty self-explanatory.
As you see both planet and ship embed an orb and a *player. We already talked about the shared properties of planet and ship. Luckily in Go we can embed structs in other structs. By doing this the embedded struct's properties become properties of the embedding struct. E.g. both planet and ship have now access to pos, vel, *anchor etc. You could also access them via planet.orb.pos and so on if you wanted / needed to.
The planet has additionally gained some more properties. Line (1) shows a recursive declaration of a slice of pointers to planets. The satellites slice lets us keep track of other planets orbiting this one. The slice holds pointers because all planets live in a global array called planets and we only want to know which ones are satellites.
Next there is the ships slice (2). Every planet has ships circling it but ships can move from one planet to another to attack or reinforce. Now if ships attack most of them are destroyed in the process. We don't want to create and destroy too many objects and thus rely too much on garbage collection. That's why there is a global slice called recycledShips. As the name suggests it acts like a pool that collects destroyed ships and releases them as new when needed. You can check the newShip() function to see how it is done.
The shipsProduced property in line (3) keeps track of how many ships were produced since the last update() call. In many frames this number is < 1 and has no visible impact. As soon as the value is >= 1 a new ship is produced. Line (4) contains a helper variable that stores the angle modifier for all ships circling a planet.
The ship doesn't have more properties currently but will certainly be extended at some point.
Camera
For convenience I want the center of our universe to be (0,0), which also shall be at the center of the screen. Usually in Pixel (0,0) is the lower left corner of the screen. That means we have to translate the visible area to the left and to the bottom by half the screen width and half the screen height respectively. Let's introduce two variables that define our camera.
camPos = pixel.ZV
cam pixel.Matrix
The position we are looking at is defined by camPos and is set to the zero vector. The matrix describing the actual camera transformations is named cam and is set and applied in the initScreen() function:
We see that the camera is not applied to window directly but to worldCanvas. The reason is that we want to draw HUD elements to window without camera transformations. They live in screen space. The canvas has the same size as the window and is drawn to the window before all HUD elements.
Worth noting is that we also have to translate the canvas when drawing it to the window:
worldCanvas.Draw(window, cam)
The reason is simple. We only applied the camera transformations to worldCanvas but not to window. So when drawing (anything) to window (0,0) is still the lower left corner. That means we also have to apply the translation when drawing the canvas to the window.
Update & Rotation
As last topic in this chapter we will look at rotating and updating planets. Here is the relevant code:
In the rotateGroup() method we see how the shift vector returned by orb.rotate() is used. First an orb (in this case a planet) is rotated and the delta is saved in dvec (1). Then every of its satellites (2) is moved by the same delta (3*). This is easier than rotating each satellite also around its anchor's anchor.
func (p *planet) update(dt float64) {
p.rotateGroup(dt)
// Ship production depends on planet size: production = sqrt(radius)/5
prod := math.Sqrt(p.size) * productionFactor // (1)
p.shipsProduced += prod * dt
// Add new ships to slice.
for i := 0; i < int(p.shipsProduced); i++ {
added := false
// Search a free spot and if there is none append.
nship := newShip(p, p.player)
for j := 0; j < len(p.ships); j++ {
if p.ships[i] == nil {
p.ships[i] = nship
added = true
}
}
if !added {
p.ships = append(p.ships, nship)
}
p.shipsProduced--
}
p.setShips(dt) // (2)
}
The planet'supdate() method first rotates the planet and all its satellites as shown above. The production of new ships in line (1) follows the simple equation f(x) = sqrt(x)/5 where x is the radius. We have defined the maximum radius of a planet as 9 (see globals.go). This is how the ship production per second looks as a graph:
As you can see it is slightly above 0.5 ships per second for the biggest planets. This function will probably tweaked over time but for now I like the production speed. After adding the ships to the planet we call setShips() in line (2). This method just distributes all the ships of a planet evenly around a planet and rotates them.
1.3 Afterthought
This was the first real tutorial I've written. I needed significantly longer to write this than to write the Gonk code up to this point. I am also not really satisfied with the result. I did not cover the whole code but it is still a lot of text and I am not sure if the topics covered were too easy. I am also pretty sure that the whole structure of this chapter could be improved.
Resulting from these thoughts I have decided that the coming chapters will (probably) a bit shorter than this one and also only cover 1 specific topic. I may add git tags for code that does not receive a separate tutorial chapter if it is about a single topic.
1.4 What's next?
At the moment the draw methods for planet and ship both just draw primitive circles. In the next chapter we will procedurally generate nice planet sprites and use them instead. This not only looks nicer but will be great for performance. If you just run Gonk for a few minutes you will see how the performance becomes horrible. This is because circles need a lot of resources to draw (many triangles) and currently each ship is a circle!
Say hello to Gonk, the game we are going to make. You can find the code at https://github.com/dbriemann/gonk. This first chapter will cover basic structures and functions and the use of composition for game objects.
Preface
The whole code for the game made in this tutorial can be found here. You should be able to use git at least a little bit. First of get the source code. The recommended way is to use
go get
:This will put gonk in your gopath at
$GOPATH/src/github.com/dbriemann/gonk
. You can at any point switch to this directory to build and run gonk. Note that the -d flag skips installing the binary into your gopath's bin directory.Just use git to pull the newest state or checkout a specific tag. I will add a tag for each chapter so you can use
to jump to the game code containing everything up to the tagged chapter. Each chapter will have a note for this. At any point you can go back to the current state with
git checkout master
. If the beginning of this tutorial is too easy for you, you can just jump in at any point in the series later on.Note that I won't discuss all of the code in the tutorial. Things that I deem not interesting, not essential or too simple will not be covered. They will however be part of the code and I will try to comment everything so you can just browse the code and understand what's happening.
1.1 Basic Structure & Functions
First we should note that Pixel expects us to provide a specific function that acts similar to a
main
function. This is necessary to ensure that all graphics calls originate in the main thread of the program. Pixel makes this easy by allowing us to bind such a function withpixelgl.Run()
.We will from now on just use
run
as if it wasmain
and can be sure that all calls come from the main thread.Now let's define some basic functions which will be extended over the course of the tutorial. This section covers the core building blocks of the game flow.
Every game needs to setup and initialize a lot of things at startup. It has to load assets (sound and graphics), select and apply the correct screen mode, check for updates and possibly many more things. That's why we need some init functions.
We won't go into the details of the
initScreen()
function because it's just boilerplate code. The code retrieves the primary monitor and sets up a window with dimensionsscreenWidth x screenHeight
. We will add the ability to switch to full screen mode later in the tutorial. The most important variables here are defined globally in the fileglobals.go
.Our
setFPS()
function can either set the maximum frames per second (by providing a positive value) or disable any maximal value (by providing 0).frameTick
is a pointer to atime.Ticker
from the Go std library and defined inglobals.go
. If initialized it will trigger every X seconds or in our case fractions of a second. E.g. if we set the FPS to 60 it will trigger every 1/60th second.This brings us to the game loop. Each game has it. It is the heart of the game. Our game loop looks like this if we strip some things from it that are unimportant to its basic purpose. It is part of the mentioned
run()
function.What it does is actually not that complicated. The loop continues to run until the the window is closed via the x icon (1). At some point we will add a menu to the game which will offer the typical way to exit a game.
Lines (2*) calculate the duration of the last frame. The value is stored in the variable
dt
which is short for delta time.dt
is passed to theupdate()
function (3) which can then interpolate if the frames don't have a consistent duration.Next the
draw()
function is called (4) which will draw everything visible onto the screen. At the end we have a call towindow.Update()
(5) which tells Pixel that the frame is finished.Lines (6*) show how the previously mentioned ticker is used. If there is a ticker object the code will wait until it triggers. We have seen how the ticker is initialized in the
setFPS()
function. With this little snippet we can limit the FPS to a specific value or disable it and the code runs as fast as possible.Let's have a look at the
draw()
function.The
draw()
function of a game just draws everything you can see as a player to the screen. It will usually not draw objects outside of the screen or out of sight to save performance. It also should not alter any state in the game. That's what theupdate(..)
function is there for.For now
draw()
just clears the screen with black and draws the measured frames per second on the screen. This tutorial explains how to handle text in Pixel. We won't have a look at this topic here. The FPS counter is part of a heads up display we will add to the game. Soon we will add a camera to the game but the HUD will always be drawn outside the camera, directly in screen space.1.2 Game Objects & Composition
A lot of code has been added since the last chapter. As always we will have a look at the core parts. Let's start by avoiding misunderstandings. If you have used an Entity Component System before you may be a bit confused because we will use the terms entity and component here but not necessarily with the same meaning. For us a component is a minimal building block with the follwing properties:
The entity differs from the component because of the follwing properties:
update()
and adraw()
function,We differentiate between components and entities just logically. There are no types involved. The only physical separation is that we have one file for components (components.go) and one file for entities (entities.go). Because Gonk is a small game we won't need separate files for each component and entity.
Orb
Let's start with our
orb
component. We know that our game will have planets and ships (copied from the original qonk). Both planets and ships show a similar behaviour. They rotate around some kind of anchor. This maybe the origin of the star system (center / sun) or another planet. If we pick the core properties of both a planet and a ship we arrive atorb
.You can see that an
orb
holds a pointer to an anchor vector. This way we can use any position of any orb specifically as anchor without having to copy the values every time the anchor moves. The other properties are simple:Let's have a look at the
rotate
function. It takes one argument (dt
) and returns theshift
Vector. We knowdt
already. It is the delta time usually consumed byupdate()
functions. It determines how far the orb rotates depending on the time passed. The returned vectorshift
just contains how the orb was moved by the rotation. We will see how this is used later on.In line (1) we just create a new matrix by copying the identity matrix. In the next line we misuse the
vel
vector (2) by just calculating its length to act as a pseudo-speed. Because the orb only rotates on a set circle we don't need a vector here and thus we simplify and pretend to have a 1D velocity. Line (3) calculates the angle dependending ondir
,dist
and the speed.We now use Pixel's functions (4,5) to rotate
pos
aroundanchor
. As you can seedt
is a factor for the angle. You can remember it like that: omega is rotation (in rad) per second. Now ifdt
equals 1/60th of a second the orb is only rotated by 1/60th of omega. TheRotated
function just creates a matrix (4) to rotate something aroundanchor
by the given angle. AfterwardsProject
applies this matrix to thepos
vector. The resultnpos
is the new position after rotation.In line (6) we calculate the delta vector from the old and new position. This is the vector that is returned by the
rotate
function. In line (7) only the new position values are assigned to the current position and the position is now updated.Planet and Ship
It's time for a little composition. Here are our
planet
andship
entities. First let me say thatplayer
is another component but we will not discuss it here. Have a look at it. It is pretty self-explanatory.As you see both
planet
andship
embed anorb
and a*player
. We already talked about the shared properties ofplanet
andship
. Luckily in Go we can embed structs in other structs. By doing this the embedded struct's properties become properties of the embedding struct. E.g. bothplanet
andship
have now access topos
,vel
,*anchor
etc. You could also access them viaplanet.orb.pos
and so on if you wanted / needed to.The
planet
has additionally gained some more properties. Line (1) shows a recursive declaration of a slice of pointers to planets. Thesatellites
slice lets us keep track of other planets orbiting this one. The slice holds pointers because all planets live in a global array calledplanets
and we only want to know which ones are satellites.Next there is the
ships
slice (2). Everyplanet
has ships circling it but ships can move from oneplanet
to another to attack or reinforce. Now if ships attack most of them are destroyed in the process. We don't want to create and destroy too many objects and thus rely too much on garbage collection. That's why there is a global slice calledrecycledShips
. As the name suggests it acts like a pool that collects destroyed ships and releases them as new when needed. You can check thenewShip()
function to see how it is done.The
shipsProduced
property in line (3) keeps track of how many ships were produced since the lastupdate()
call. In many frames this number is < 1 and has no visible impact. As soon as the value is >= 1 a new ship is produced. Line (4) contains a helper variable that stores the angle modifier for all ships circling a planet.The
ship
doesn't have more properties currently but will certainly be extended at some point.Camera
For convenience I want the center of our universe to be (0,0), which also shall be at the center of the screen. Usually in Pixel (0,0) is the lower left corner of the screen. That means we have to translate the visible area to the left and to the bottom by half the screen width and half the screen height respectively. Let's introduce two variables that define our camera.
The position we are looking at is defined by
camPos
and is set to the zero vector. The matrix describing the actual camera transformations is namedcam
and is set and applied in theinitScreen()
function:We see that the camera is not applied to
window
directly but toworldCanvas
. The reason is that we want to draw HUD elements towindow
without camera transformations. They live in screen space. The canvas has the same size as the window and is drawn to the window before all HUD elements.Worth noting is that we also have to translate the canvas when drawing it to the window:
The reason is simple. We only applied the camera transformations to
worldCanvas
but not towindow
. So when drawing (anything) towindow
(0,0) is still the lower left corner. That means we also have to apply the translation when drawing the canvas to the window.Update & Rotation
As last topic in this chapter we will look at rotating and updating planets. Here is the relevant code:
In the
rotateGroup()
method we see how theshift
vector returned byorb.rotate()
is used. First anorb
(in this case aplanet
) is rotated and the delta is saved indvec
(1). Then every of its satellites (2) is moved by the same delta (3*). This is easier than rotating each satellite also around its anchor's anchor.The planet's
update()
method first rotates the planet and all its satellites as shown above. The production of new ships in line (1) follows the simple equationf(x) = sqrt(x)/5
wherex
is the radius. We have defined the maximum radius of a planet as 9 (seeglobals.go
). This is how the ship production per second looks as a graph:As you can see it is slightly above 0.5 ships per second for the biggest planets. This function will probably tweaked over time but for now I like the production speed. After adding the ships to the planet we call
setShips()
in line (2). This method just distributes all the ships of a planet evenly around a planet and rotates them.1.3 Afterthought
This was the first real tutorial I've written. I needed significantly longer to write this than to write the Gonk code up to this point. I am also not really satisfied with the result. I did not cover the whole code but it is still a lot of text and I am not sure if the topics covered were too easy. I am also pretty sure that the whole structure of this chapter could be improved.
Resulting from these thoughts I have decided that the coming chapters will (probably) a bit shorter than this one and also only cover 1 specific topic. I may add git tags for code that does not receive a separate tutorial chapter if it is about a single topic.
1.4 What's next?
At the moment the draw methods for
planet
andship
both just draw primitive circles. In the next chapter we will procedurally generate nice planet sprites and use them instead. This not only looks nicer but will be great for performance. If you just run Gonk for a few minutes you will see how the performance becomes horrible. This is because circles need a lot of resources to draw (many triangles) and currently eachship
is a circle!