• Stars
    star
    199
  • Rank 189,944 (Top 4 %)
  • Language
    Go
  • Created almost 6 years ago
  • Updated over 5 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Space invaders in an app

Create a simple cross-platform desktop Space Invaders game with Go

A few months ago I fiddled around with writing Space Invaders using Go. I had a lot of fun writing it but the output only worked on iTerm2 because it used a specific feature of that terminal. In order to play the game you'd really have to download iTerm2 and run it on the command line. I thought it'd be fun to muck around it a bit more to see if I can bring it out as a proper desktop game.

Space Invaders

Now, if you know much about Go at all you would know that there simply isn't any good GUI toolkit or library. Go wasn't designed with UI in mind so there certainly aren't any UI packages in the standary library. There is the usual GTK and QT bindings, but generally I try to avoid bindings if I can help it. The more popular way of writing desktop apps in Go is really to create a web app and then front it with a browser like interface. A common way of doing this is using Electron, a framework used to write cross-platform desktop apps using Javascript, HTML and CSS. Plenty of desktop apps have used Electron, including Slack and Github's desktop apps, and even the editor I'm using now (Visual Studio Code).

However, Electron is pretty heavy, uses quite a bit of Javascript and has tonnes of documentation to wade through. I was looking for something Go-oriented and simple to just kickstart. More importantly I simply wanted to use what I already created earlier, which is nothing much more than simply displaying a series of images rapidly such that it looks properly animated.

Then I stumbled on this little Go library called webview. Webview is tiny and simple library that wraps around webview (MacOS), MSHTML (Windows) and gtk-webkit2 (Linux). Its documentation for the Go part is just a page or so!

Let's take stock at what we need to do:

  1. Build a web app that will serve out the frames
  2. Show the web app on the webview
  3. Make changes to the game logic to make it work

That's all! Let's go.

Build the web app

We start off with spinning out a separate goroutine to run the web app, then displaying a static HTML page from the webview.

var frame string                         // game frames
var dir string                           // current directory
var events chan string                   // keyboard events
var gameOver = false                     // end of game
var windowWidth, windowHeight = 400, 300 // width and height of the window
var frameRate int                        // how many frames to show per second (fps)
var gameDelay int                        // delay time added to each game loop

func init() {
	// events is a channel of string events that come from the front end
	events = make(chan string, 1000)
	// getting the current directory to access resources
	var err error
	dir, err = filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		log.Fatal(err)
	}
	frameRate = 50                                         // 50 fps
	gameDelay = 20                                         // 20 ms delay
	sprites = getImage(dir + "/public/images/sprites.png") // spritesheet
	background = getImage(dir + "/public/images/bg.png")   // background image
}

// main function
func main() {
	// run the web server in a separate goroutine
	go app()
	// create a web view
	err := webview.Open("Space Invaders", "http://127.0.0.1:12346/public/html/index.html",
		windowWidth, windowHeight, false)
	if err != nil {
		log.Fatal(err)
	}
}

See how creating the webview is just a single line of code! If you compare this with Electron, this is pretty awesome. Electron has quite a bit more features of course, but for something simple and straightforward, you just can't beat this.

The web app is simple and straightforward.

func app() {
	mux := http.NewServeMux()
	mux.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(dir+"/public"))))
	mux.HandleFunc("/start", start)
	mux.HandleFunc("/frame", getFrame)
	mux.HandleFunc("/key", captureKeys)
	server := &http.Server{
		Addr:    "127.0.0.1:12346",
		Handler: mux,
	}
	server.ListenAndServe()
}

I didn't use anything fancy, it's just your typical Go web app with 3 handlers. It also serves out html and other assets through a directory named public. The start handler starts the game, the frame handler returns the frame and the key handler receives the keyboard events.

// start the game
func start(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles(dir + "/public/html/invaders.html")
	// start generating frames in a new goroutine
	go generateFrames()
	t.Execute(w, 1000/frameRate)
}

The start handler starts generating frames in a separate goroutine, then calls the invaders.html template, passing it the frame rate.

// capture keyboard events
func captureKeys(w http.ResponseWriter, r *http.Request) {
	ev := r.FormValue("event")
	// what to react to when the game is over
	if gameOver {
		if ev == "83" { // s
			gameOver = false
			go generateFrames()
		}
		if ev == "81" { // q
			os.Exit(0)
		}

	} else {
		events <- ev
	}
	w.Header().Set("Cache-Control", "no-cache")
}

The captureKeys handler receives keyboard events from the webview and takes action accordingly. If the game ended, you can restart or quit the game. Otherwise all keyboard events are placed into the events channel.

// get the game frames
func getFrame(w http.ResponseWriter, r *http.Request) {
	str := "data:image/png;base64," + frame
	w.Header().Set("Cache-Control", "no-cache")
	w.Write([]byte(str))
}

At every interval, the webview will call getFrame for the frame. The frame is a image data URI with the base64 encoded image. This is then passed on to the <img> tag in the HTML template (which we'll see later). Notice that we set the Cache-Control header to no-cache. This is a workaround for MSHTML (in Windows specifically) because otherwise the image frame will cached and the game will be stuck at the first frame.

Show the web app on the webview

The frames are shown on a HTML page that will be displayed on the webview. The start page, doesn't need to be animated (or can be animated through a video clip or a gif) so it can be a totally static HTML page.

<!doctype html><meta charset=utf-8>
<html>
    <head>
        <style>
        body {
            background-image: url("/public/images/start.png");
            background-repeat: no-repeat;
        }
        </style>
        <script src="/public/js/jquery-3.3.1.min.js"></script>
        <script type="text/javascript">
            $("body,html").keydown(function(event) {
                if ( event.which == 13 ) {
                    event.preventDefault();
                }
                if ( event.which == 83 ) {
                    window.location.href="/start";
                }
            });
        </script>
    </head>
</html>

It just captures keyboard events through JQuery, and redirects to start handler when s is pressed. This will start the game.

Once we start the game, the game loop is triggered and frames are created repeatedly. A gameDelay variable is introduced in the game loop to slow down the game if it becomes too fast. Displaying the frames is all about using JQuery and retrieving frame one at a time regular interval.

To do this, I simply used setInterval, with the frequency provided by the start handler. The function in setInterval uses the Jquery get method to retrieve the data URI from the frame handler, and changes the value in the src attribute of the <img> tag with the image ID.

I also monitor the keydown event and use the JQuery get method to send the value captured to the key handler.

<!doctype html><meta charset=utf-8>
<html>
    <head>
        <style>
        body {
            background-image: url("/public/images/background.jpg");
            background-repeat: no-repeat;
            margin: 0;
        }
        </style>
        <script src="/public/js/jquery-3.3.1.min.js"></script>
        <script type="text/javascript">
            setInterval(function() {
                $.get('/frame', function(data) {
                    $('#image').attr('src', data);
                });
            }, {{ . }});

            $("body,html").keydown(function( event ) {
                if ( event.which == 13 ) {
                    event.preventDefault();
                }
                $.get('/key?event='+event.which);
            });
        </script>
    </head>
    <body>
        <img id="image" src="" style="display: block;"/>
    </body>
</html>

This is how the start screen looks on Windows.

Space Invaders on a Windows machine

This is how it looks on a Mac.

Space Invaders on a Mac

Game logic changes

Let's look at the changes I need to make to the game logic next. Most of the code doesn't change. However the game is on a webview so the controls will be also be on the webview itself. This means I don't need to use termbox any more. Instead, I just capture keyboard events sent to the webview using the JQuery keydown method and send it to the key handler. The key handler in turn adds it into the event channel (previously I send the termbox keyboard event into the channel).

In the main game loop, instead of checking for the termbox keyboard events, I check for the keyboard events from the webview.

for !gameOver {
    // to slow up or speed up the game
    time.Sleep(time.Millisecond * time.Duration(gameDelay))
    // if any of the keyboard events are captured
    select {
    case ev := <-events:
        // exit the game
        if ev == "81" { // q
            gameOver = true
        }
        if ev == "32" { // space bar
            if beam.Status == false {
                beamShot = true
            }
            playSound("shoot")
        }
        if ev == "39" { // right arrow key
            laserCannon.Position.X += 10
        }
        if ev == "37" { // left arrow key
            laserCannon.Position.X -= 10
        }
    default:
    }
    ...
}

See how I play the sound after each time I detect the string 32 (captured from the keyboard event), which indicates the space bar being pressed.

Play some sound

Games work better with game sounds and effects. I got the Space Invaders special effect sounds from Classics United website and also used the Beep package to play them.

// play a sound
func playSound(name string) {
	f, _ := os.Open(dir + "/public/sounds/" + name + ".wav")
	s, format, _ := wav.Decode(f)
	speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/20))
	speaker.Play(s)
}

Playing the sound effect is simply getting the WAV file, decode it and play it back. Unfortunately the package closes the file after decoding it so I have to reopen the file every time, but it works well enough.

Show game scores at the end game

Something else that changes in the game is the way the scores are displayed. When the game ended previously I simply showed the scores on the terminal. Now that I don't have a terminal to display the scores on, the best to do it is on the screen. What I need to is write text on the end game frame.

To do this, I used the image/font package in the Go standard library sub-repositories. As a refresher, the Go standard library sub-repositories are experimental packages that are found under golang.org/x/*. In particular the golang.org/x/image/font package provides us with basic capabilities to create write lines of text on an image.

When I said basic I really meant basic. While it can be used to do more complicated stuff, I ended up using the basic features only (primarily to keep the code simple).

// print a line of text to the image
func printLine(img *image.RGBA, x, y int, label string, col color.RGBA) {
	point := fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}
	d := &font.Drawer{
		Dst:  img,
		Src:  image.NewUniform(col),
		Face: inconsolata.Bold8x16,
		Dot:  point,
	}
	d.DrawString(label)
}

The printLine function takes in an image, the coordinates to write the text, the text itself and the color of the text, then draws a lines of text with the given color at the specified coordinates. The font used is a ready-made one from the inconsolata package.

This is how it's used in the game code.

// show end screen and score
endScreen := getImage(dir + "/public/images/gameover.png").(*image.RGBA)
printLine(endScreen, 137, 220, fmt.Sprintf("Your score is %d", score), color.RGBA{255, 0, 0, 255})
printLine(endScreen, 104, 240, "Press 's' to play again", color.RGBA{255, 0, 0, 255})
printLine(endScreen, 137, 260, "Press 'q' to quit", color.RGBA{255, 0, 0, 255})

createFrame(endScreen)

Building the app

First, you'll need to install the dependencies, if you don't already have them:

go get github.com/disintegration/gift
go get github.com/faiface/beep/speaker
go get github.com/faiface/beep/wav

To build the app on Mac, just use the build-macOS script. It should build the app and then place it accordingly into the invaders.app application package. With that you can just double-click on the app and start playing!

To build the app on Windows, use this command:

go build -ldflags="-H windowsgui" -o invaders.exe

After that you should have an invaders.exe binary executable file which you can then double-click to start playing.

Source code

You can find the source code here.

https://github.com/sausheong/invadersapp

How it looks

That's all there is to it! I didn't go through the game code because I've already explained it in the previous blog post.

In the mean time, here's how it looks on Windows. The response is a bit shaky because I don't actually own a Windows machine and tested it and created the video on a VirtualBox VM.

Space Invaders game on a Windows machine

This is how it looks on a Mac.

Space Invaders game on a Mac

Have fun!

Thank yous

  • A shout-out to Ibrahim Wu, who helped me to debug the app on Windows and also discovered the problem with MSHTML caching.
  • Thanks to Serge Zaitsev for his amazing webview package!

More Repositories

1

gwp

Go Web Programming code repository
JavaScript
1,596
star
2

invaders

Space Invaders in Go
Go
637
star
3

polyglot

Polyglot is a distributed web framework that allows programmers to create web applications in multiple programming languages
Go
624
star
4

gonn

Building a simple neural network in Go
Go
330
star
5

everyday

code from the book "Exploring Everyday Things with R and Ruby"
Ruby
170
star
6

muse

A Ruby DSL for making music
Ruby
123
star
7

saushengine.v1

Simple Ruby-based search engine
Ruby
118
star
8

ga

Simple genetic algorithms in Go
Go
111
star
9

snip

Simple TinyURL clone
Ruby
98
star
10

chirp

Simple Sinatra-based micro-blog/Twitter clone
Ruby
90
star
11

naive-bayes

Simple naive bayesian classifier implemented in Ruby
Ruby
64
star
12

tanuki

Tanuki is a polyglot web framework that allows you to develop web applications and services in multiple programming languages.
Go
62
star
13

mosaic

A photo-mosaic generating program, also showcasing Go concurrency techniques using goroutines and channels.
Go
60
star
14

talkie

A voice-based ChatGPT clone that can search on the Internet and also in local files
CSS
52
star
15

hs1xxplug

Go library for TP-Link HS100 and HS110 WiFi smart plug
Go
47
star
16

persona

Talking head video AI generator
Python
43
star
17

polyblog

How to Create A Web App In 3 Different Programming Languages
Java
41
star
18

kancil

Simple web app to showcase LlamaIndex
Python
40
star
19

chitchat

Simple forum written in Go
Go
33
star
20

goids

Flocking simulation in Go
Go
28
star
21

breeze

A simple ChatGPT clone built using Go
HTML
25
star
22

goreplicate

This is a simple Go package for interacting with the Replicate (https://replicate.com) HTTP APIs. Replicate is an API service that allows developers to use machine learning models easily through calling APIs.
Go
25
star
23

blueblue

Bluetooth LE scanner and spelunking tool
Go
24
star
24

exblog

A blogging web application written with Elixir and Dynamo.
Elixir
21
star
25

modular

Examples uses to illustrate how to write modular Rack-based web applications
Ruby
21
star
26

tinyclone

TinyURL clone
Ruby
20
star
27

carpark-cgpt

ChatGPT plugin for Singapore HDB car park availability
Go
17
star
28

ruby-gpio

A Ruby DSL to interface with the Raspberry Pi GPIO.
Ruby
17
star
29

saushengine

Ruby
16
star
30

tanks

Tanks! is a Gosu-based simple, real-time online multiplayer game, based on the popular retro game, Tank Battalion.
Ruby
14
star
31

Colony

Facebook clone project for the Cloning Internet Applications with Ruby book
JavaScript
14
star
32

rbase

A minimalist NoSQL database written in pure Ruby.
Ruby
13
star
33

petri

Go framework for building simulations based on cellular automation
Go
13
star
34

netnet

Discover Wi-Fi clients using Raspberry Pi Zero W, airodump-ng and Go
Go
12
star
35

gocookbook

Code repository for the Go Cookbook
Go
12
star
36

utopia

Pure Ruby version of 'Money, Sex and Evolution' agent-based modeling simulations
Ruby
11
star
37

gonb

Go
11
star
38

Wavform

Generate MP3 waveforms using Ruby and R
Ruby
11
star
39

epidemic-sim

Epidemic simulation using Go and Python
Jupyter Notebook
10
star
40

auth

Examples of third-party authentication, using Sinatra and Shoes, with RPX, OpenID etc
Ruby
10
star
41

easyblog

A minimalist blog web app
Ruby
9
star
42

ghost

"What if a cyber brain could possibly generate its own ghost, create a soul all by itself?"
Python
9
star
43

snip-appengine

Snip! deployment on Google AppEngine
Ruby
9
star
44

gwp2

Code for Go Web Programming 2nd edition
Go
9
star
45

squall

Squall is a Question Answering chatbot that runs entirely on a laptop, using Llama-2 and a downloaded HuggingFace embedding model.
Python
9
star
46

monsoon

Monsoon is a simple ChatGPT clone built with Go. It uses Llama-compatible LLMs, through llama.cpp.
CSS
8
star
47

pynn

Building simple artificial neural networks with TensorFlow, Keras, PyTorch and MXNet/Gluon
Python
8
star
48

gost

Gost is a native Go data store for storing data in S3 compatible object storage services.
Go
8
star
49

maiad

My AI Assistant for Microsoft Word
HTML
7
star
50

merkato

E-Commerce application with Go
Go
7
star
51

sghazeserv

Singapore Haze Watch server
Go
7
star
52

ruby_complexity_simulations

Source code for Programming Complexity (Ruby version) talk
JavaScript
7
star
53

promptscript

PromptScript is an experimental prompting language created by GPT-4
Python
7
star
54

founders

Algorithms for Startup Founders
Jupyter Notebook
7
star
55

gomuse

Creating music with Go
JavaScript
7
star
56

house

House is a debate simulation between multiple participants, which can be represented by different large language models (LLMs). House is an experiment to use LLMs to debate and discuss a topic and get views from multiple perspectives.
Python
7
star
57

chirpy

Simple Twitter clone
Ruby
6
star
58

easyforum

A minimalist forum web application for Rubyists
Ruby
6
star
59

gotext

Random Text Generator written in Go
Go
6
star
60

tanksworld

Web-based Tanks game server management
Ruby
6
star
61

anthill

Simple workload distribution system
JavaScript
6
star
62

bookspeak

Create audio books in any language using Python
Python
5
star
63

complexity_simulations

Code repository for 'Programming Complexity' talk
Go
5
star
64

Utopia2

Sugarscape JRuby simulation clone
Ruby
5
star
65

qard

Qard is a simple QR Code business card generator.
HTML
4
star
66

pompoko

Pom Poko is a sample blog web app written using the Tanuki web framework.
Ruby
4
star
67

mosaicgo

Docker-deployed version of the concurrent mosaic web application
Go
4
star
68

sausheong.github.com

Saush's GitHub Homepage
4
star
69

bots

Simple library for controlling robots using Ruby
Ruby
3
star
70

complexity

Programming Complexity
3
star
71

newspaper

Faster way to read print newspapers
Go
3
star
72

easywiki

An all-in-a-file wiki web app
Ruby
3
star
73

goauthserv

A REST-based authentication service written in Go.
Go
3
star
74

myhaze

Haze monitoring for Malaysia
JavaScript
3
star
75

go-recipes

Code for the Go Recipes publication site
Go
3
star
76

gosearch

A simple search engine in Go
Go
3
star
77

pixelate

Simple Go web application that pixelates a given JPEG file
Go
3
star
78

vdb

Sample code to show how to create an in-memory RAG
Go
3
star
79

culture_sim

Model and simulate cultural dissemination with Go and Python
Jupyter Notebook
2
star
80

todayreader

An alternative reader for the Today newspaper
CSS
2
star
81

hist

Simple Go web app to create histograms
HTML
2
star
82

sausheong.github.io

HTML
2
star
83

mst

Code repository for the minimum spanning tree algorithms post
Go
2
star
84

server-frameworks

Ruby
2
star
85

simplerd

Server for the Simpler Chrome extension
Go
2
star
86

waldo

Waldo is a command-line AI assistant that wraps around local LLMs, Google's Gemini models and OpenAI's GPT models
Go
2
star
87

openai

Go package that wraps around OpenAI HTTP APIs
Go
1
star
88

gale

Gale is an AI chatbot used for question & answering over documents, built with Go
HTML
1
star
89

sghaze

Singapore Haze alert
JavaScript
1
star
90

perf-go

Testing the performance of the basic go web application
Ruby
1
star
91

tweetclone

Twitter clone
1
star
92

shado

Camera-based motion detector
Go
1
star
93

red

Go
1
star
94

minstrel

Using LLM to create stories
HTML
1
star
95

loca

Simple chatbot wrapping around a LLM.
Python
1
star
96

multipage-pdf

Sample code to show how to convert multi-page PDFs to a sequence of PNG images using RMagick
Ruby
1
star