Let's Make a Simple Game in Go
Let’s get started making a simple game in Go. Our goal is to get the feeling for what it is like to make a brief Go program so understanding the fine details is not needed. Copying and pasting the code is fine, however typing it your self is good training.
Preparation
In this course we will use a game making library called pixel. We expect that pixel should already be installed from the previous step, however if it is not please return to “Go Programming Setup” and follow those instructions. The Mac and Windows instruction pages are different, so please choose the type of computer you will be using for programming in Go.
First move into your project directory using the cd
command on your computer. If you don’t have a specific directory prepared, use the $GOPATH/src
directory.
cd $GOPATH/src
Next make a new directory for this game and move into it. Execute the commands below in the terminal.
mkdir flappy cd flappy
Open this directory in Atom. Launching Atom from the Dock or Start Menu fine, but launching from the terminal with your current directory is convenient.
atom .
After Atom has launched, select File > File New from the menu to open a new file.
Make a Window
Once the file is open, we will make our program to display the window for our game. Please put the code below into the file.
package main
import (
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
)
func run() {
cfg := pixelgl.WindowConfig{
Title: "Game Screen",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
for !win.Closed() {
win.Update()
}
}
func main() {
pixelgl.Run(run)
}
Executing a Go Program
Let’s try executing the program we just made right away.
Please select File > Save from the menu or press Command+s (ctrl+s on Windows) to save your file. You will be asked to give it a file name, save it as “flappy.go”.
To execute a Go program, type “go run (filename)” into the terminal followed by the return key.
$ go run flappy.go
If a black window opens it was a success. To close the game window, click on the close window or in the terminal press Control+c.
Display the Character
Continuing, let’s try making the character we will control in our game appear on screen. We will use an image that has been prepared for the character. First click on the blue button and download the character image file.
Once the download is completed, move it to the same directory as the program source code. Next let’s try finishing up until the part where program loads and shows the character image.
package main
import (
"image"
"os"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
)
func loadPicture(path string) (pixel.Picture, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
return nil, err
}
return pixel.PictureDataFromImage(img), nil
}
func run() {
cfg := pixelgl.WindowConfig{
Title: "Game Screen",
Bounds: pixel.R(0, 0, 1024, 768),
VSync: true,
}
win, err := pixelgl.NewWindow(cfg)
if err != nil {
panic(err)
}
pic, err := loadPicture("surfing.png")
if err != nil {
panic(err)
}
sprite := pixel.NewSprite(pic, pic.Bounds())
for !win.Closed() {
sprite.Draw(win, pixel.IM.Moved(pixel.V(500, 300)))
win.Update()
}
}
Try Moving the Character
Did the character show up fine?
Next, try making this character move. In this game, when you press the space key the character will float up, if you do nothing it will fall. The character’s speed and position will be grouped together in a structure named “hero”. When the expression "hero.○○" appears, you should think of it as data belonging to character "hero".
import (
"time"
"image"
"os"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"golang.org/x/image/colornames"
)
sprite := pixel.NewSprite(pic, pic.Bounds())
// Set Hero
type Hero struct {
velocity pixel.Vec
rect pixel.Rect
hp int
}
hero := Hero{
velocity: pixel.V(200, 0),
rect: pixel.R(0, 0, 100, 100).Moved(pixel.V(win.Bounds().W() / 4, win.Bounds().H() / 2)),
hp: 100,
}
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
win.Clear(colornames.Skyblue)
if win.Pressed(pixelgl.KeySpace) {
hero.velocity.Y = 500
}
hero.rect = hero.rect.Moved(pixel.V(0, hero.velocity.Y * dt))
hero.velocity.Y -= 900 * dt
mat := pixel.IM
mat = mat.ScaledXY(pixel.ZV, pixel.V(hero.rect.W() / sprite.Frame().W(), hero.rect.H() / sprite.Frame().H()))
mat = mat.Moved(pixel.V(hero.rect.Min.X + hero.rect.W() / 2, hero.rect.Min.Y + hero.rect.H() / 2))
sprite.Draw(win, mat)
win.Update()
}
Display Obstacles
In this game, obstacles appear, to advance in the game you need to avoid them. The obstacles will appear as blue rectangles.
Multiple obstacles will appear at the same time, we will create a slice() called walls to handle them. There are a few difficult to understand expressions but let’s try and take care when writing the program.
import (
"time"
"math/rand"
"image"
"os"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"github.com/faiface/pixel/imdraw"
"golang.org/x/image/colornames"
)
// Set walls
type Wall struct {
X float64
Y float64
W float64
H float64
}
var walls []Wall
distance := 0.0
last := time.Now()
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
win.Clear(colornames.Skyblue)
if len(walls) <= 0 || distance - (walls[len(walls)-1].X - win.Bounds().W()) >= 400 {
new_wall := Wall{}
new_wall.X = distance + win.Bounds().W()
new_wall.W = win.Bounds().W() / 10
new_wall.H = rand.Float64() * win.Bounds().H() * 0.7
if rand.Intn(2) >= 1 {
new_wall.Y = new_wall.H
} else {
new_wall.Y = win.Bounds().H()
}
walls = append(walls, new_wall)
}
drawing := imdraw.New(nil)
for _, wall := range walls {
drawing.Color = colornames.Blue
drawing.Push(pixel.V(wall.X - distance, wall.Y))
drawing.Push(pixel.V(wall.X - distance + wall.W, wall.Y - wall.H))
drawing.Rectangle(0)
if wall.X - distance < - wall.W {
walls = walls[1:]
}
}
drawing.Draw(win)
distance += hero.velocity.X * dt
if win.Pressed(pixelgl.KeySpace) {
Collision Detection
Did the obstacles flow from the left to the right?
Right now, even if the character hits an obstacle, nothing at all happens. Next we will try to make it so that when the character hits an obstacle the HP decreases, and when it below 0, it will be game over.
The variable status will express the game’s current state. The program is split so that when status is “playing” the game is executing, and when it is “gameover” the program executes gameover instructions.
distance := 0.0
last := time.Now()
status := "playing"
for !win.Closed() {
dt := time.Since(last).Seconds()
last = time.Now()
win.Clear(colornames.Skyblue)
switch status {
case "playing":
if len(walls) <= 0 || distance - (walls[len(walls)-1].X - win.Bounds().W()) >= 400 {
drawing := imdraw.New(nil)
for _, wall := range walls {
drawing.Color = colornames.Blue
drawing.Push(pixel.V(wall.X - distance, wall.Y))
drawing.Push(pixel.V(wall.X - distance + wall.W, wall.Y - wall.H))
drawing.Rectangle(0)
if wall.X - distance <= hero.rect.Max.X && wall.X - distance + wall.W >= hero.rect.Min.X && wall.Y >= hero.rect.Min.Y && wall.Y - wall.H <= hero.rect.Max.Y {
drawing.Color = colornames.Red
drawing.Push(hero.rect.Min)
drawing.Push(hero.rect.Max)
drawing.Rectangle(0)
hero.hp -= 1
if hero.hp <= 0 {
status = "gameover"
}
}
if wall.X - distance < - wall.W {
walls = walls[1:]
}
}
drawing.Draw(win)
distance += hero.velocity.X * dt
if hero.rect.Max.Y < 0 || hero.rect.Min.Y > win.Bounds().H() {
status = "gameover"
}
sprite.Draw(win, mat)
case "gameover":
os.Exit(3)
}
win.Update()
Display Game Over and Score
Does the game now end when you hit too many obstacles or go too high or low?
Lastly, let’s display a score when game over happens and restart the game by pressing the space key.
import (
"time"
"math/rand"
"image"
"os"
"fmt"
_ "image/png"
"github.com/faiface/pixel"
"github.com/faiface/pixel/pixelgl"
"github.com/faiface/pixel/imdraw"
"github.com/faiface/pixel/text"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/colornames"
)
var walls []Wall
// Set text
basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
basicTxt := text.New(win.Bounds().Center(), basicAtlas)
distance := 0.0
last := time.Now()
status := "playing"
for !win.Closed() {
case "gameover":
basicTxt.Clear()
basicTxt.Color = colornames.Green
line := fmt.Sprintf("Game Over! Score: %d\n", int(distance))
basicTxt.Dot.X -= basicTxt.BoundsOf(line).W() / 2
fmt.Fprintf(basicTxt, line)
basicTxt.Draw(win, pixel.IM.Scaled(basicTxt.Orig, 4))
if win.Pressed(pixelgl.KeySpace) {
hero.hp = 100
hero.rect = pixel.R(0, 0, 100, 100).Moved(pixel.V(win.Bounds().W() / 4, win.Bounds().H() / 2))
status = "playing"
distance = 0.0
last = time.Now()
walls = walls[:0]
}
}
win.Update()
}
Summary
And with that the game is complete.
It’s a relatively small program, but it has become a proper working game.
We didn’t go over the fine details of the game, but having a faint understanding of what part of the program does what is enough at this stage.
Through continuing to make games let’s train ourselves little by little to become familiar with understanding the program and making our own.
Extra Credit: Try Making the Character Spin Around
This is a bit of extra code to make the character rotate based on their speed. Let's see how the character's movement changes after adding the code below.
import (
"time"
"math"
"math/rand"
mat := pixel.IM
mat = mat.ScaledXY(pixel.ZV, pixel.V(hero.rect.W() / sprite.Frame().W(), hero.rect.H() / sprite.Frame().H()))
mat = mat.Rotated(pixel.ZV, math.Atan(hero.velocity.Y / (hero.velocity.X * 3)))
mat = mat.Moved(pixel.V(hero.rect.Min.X + hero.rect.W() / 2, hero.rect.Min.Y + hero.rect.H() / 2))
sprite.Draw(win, mat)
case "gameover":