Go lang course

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.

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.

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)
}

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.

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.

https://github.com/egonelbre/gophers

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()
    }
}

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()
    }

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) {

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()

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()
    }

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.

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":