

Create a simple cross-platform desktop game with Go
source link: https://www.tuicool.com/articles/hit/EriQfem
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.


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.
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:
- Build a web app that will serve out the frames
- Show the web app on the webview
- 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.

This is how it looks 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
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 previousblog 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.

This is how it looks 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!
Recommend
-
4
How do I create a cross-platform window in C#? Jan 2 ・1 min read ...
-
6
Build cross-platform desktop apps with JavaScript, HTML, and CSS. Build cross-platform desktop apps with JavaScript, HTML, and CSS
-
11
Driving Game Growth & Into the Metaverse Register today to connect with the games industry, join private networking sessions, and hear the latest on driving game growth and the metaverse....
-
9
GamesBeat x Oculus Venues Join Dean Takahashi, Ramez Naam, and Tim Chang online for "Science Fiction, Tech, and Games." Hosted in VR and 2D Feb...
-
4
Building Cross-Platform Desktop Applications with Electron Electron is a framework that allows you to develop native cross-platform desktop applications based on HTML, JavaScript and CSS. It uses...
-
99
Swar's Chia Plot Manager A plot manager for Chia plotting: https://www.chia.net/ Development Version: v0.0.1 This is a cross-platform Chia Plot Manager that will work on the major...
-
4
How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust Luke Edwards on Jul 26, 2021 Learn Development at
-
7
react-native-ridge-navigation Simple and performant cross platform navigation on iOS, Android and the web with simple and type-safe api for React 18 (alpha) ⚠️ This is beta software, things can break. Things which are n...
-
9
Homeward Simple Cross-Platform Shortcuts Homeward is a web app used to save URLs (including Deep Links) to a user's mobile home-screen. This is a technique used by several popular apps and documented on
-
9
React Native Responsive Scalability The package provides utility hooks that help React Native developers create responsive, cross-platform applications that are aware of orientation changes. About the Project Take a look...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK