Build a Cat Canvas Game with Go and WebAssembly: Step‑by‑Step Guide
Learn how to create a simple interactive cat‑themed canvas game for mobile browsers using Go compiled to WebAssembly, covering environment setup with Docker, Go code for DOM manipulation, event handling, rendering, audio, and deployment, while explaining WASM concepts and differences from Service and Web Workers.
Story Begins 📝
The goal is to build a tiny cat‑themed game that moves a red dot across the screen, plays Hi‑Fi music and vibrates on mobile devices. The entire project is written in Go, compiled to WebAssembly, and runs in the browser without any JavaScript.
Understanding WebAssembly
WebAssembly (WASM) is a universal virtual machine and a compilation target, not a language. It allows you to write code once—in Go, Rust, C++, etc.—and run it anywhere the browser supports WASM. It does not replace JavaScript but gives you an alternative runtime.
WASM vs. Service Workers & Web Workers
Service Workersand Web Workers provide background execution, offline capability and thread‑like isolation, but they cannot access the DOM directly. WASM can be executed inside these workers, offering a low‑level, high‑performance layer that is language‑agnostic.
Setting Up the Development Environment
We use Go, JavaScript, and optionally Docker. Install the Go toolchain locally or use the golang:1.12-rc Docker image. Compile the Go source with the WASM flags:
$ GOOS=js GOARCH=wasm go build -o game.wasm main.goOr with Docker:
docker run --rm \
-v `pwd`/src:/game \
--env GOOS=js --env GOARCH=wasm \
golang:1.12-rc \
/bin/bash -c "go build -o /game/game.wasm /game/main.go; cp /usr/local/go/misc/wasm/wasm_exec.js /game/wasm_exec.js"The wasm_exec.js runtime provided by the Go team handles the glue code between the compiled WASM module and the browser’s JavaScript environment.
HTML Boilerplate
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<style>body{height:100%;width:100%;margin:0;padding:0;background:#000;color:#fff;font-family:Arial;}</style>
<script src="./wasm_exec.js"></script>
<script type="text/javascript">
async function run(fileUrl) {
const file = await fetch(fileUrl);
const buffer = await file.arrayBuffer();
const go = new Go();
const {instance} = await WebAssembly.instantiate(buffer, go.importObject);
go.run(instance);
}
setTimeout(() => run("./game.wasm"));
</script>
</head>
<body></body>
</html>Go Source – Main and Setup
package main
import (
// https://github.com/golang/go/tree/master/src/syscall/js
"syscall/js"
)
var (
// js.Value can hold any JS object, type or constructor
window, doc, body, canvas, laserCtx, beep js.Value
windowSize struct{ w, h float64 }
)
func main() { setup() }
func setup() {
window = js.Global()
doc = window.Get("document")
body = doc.Get("body")
windowSize.h = window.Get("innerHeight").Float()
windowSize.w = window.Get("innerWidth").Float()
canvas = doc.Call("createElement", "canvas")
canvas.Set("height", windowSize.h)
canvas.Set("width", windowSize.w)
body.Call("appendChild", canvas)
// Red dot canvas context
laserCtx = canvas.Call("getContext", "2d")
laserCtx.Set("fillStyle", "red")
// Simple audio beep (data‑uri)
beep = window.Get("Audio").New("data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjI1LjEwMQAAAAAAAAAAAAAA/...")
}Rendering and Event Handling
func main() {
setup()
// Declare renderer callback (similar to JS requestAnimationFrame)
var renderer js.Func
renderer = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
updateGame()
window.Call("requestAnimationFrame", renderer)
return nil
})
window.Call("requestAnimationFrame", renderer)
// Mouse / touch event handler
var mouseEventHandler js.Func = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
updatePlayer(args[0])
return nil
})
window.Call("addEventListener", "pointerdown", mouseEventHandler)
}
func updatePlayer(event js.Value) {
mouseX := event.Get("clientX").Float()
mouseY := event.Get("clientY").Float()
go log("mouseEvent", "x", mouseX, "y", mouseY)
if isLaserCaught(mouseX, mouseY, gs.laserX, gs.laserY) {
go playSound()
}
}
func playSound() {
beep.Call("play")
window.Get("navigator").Call("vibrate", 300)
}Game Loop and State Management
type gameState struct {
laserX, laserY, directionX, directionY, laserSize float64
}
var gs = gameState{laserSize:35, directionX:3.7, directionY:-3.7, laserX:40, laserY:40}
func updateGame() {
// Boundary check
if gs.laserX+gs.directionX > windowSize.w-gs.laserSize || gs.laserX+gs.directionX < gs.laserSize {
gs.directionX = -gs.directionX
}
if gs.laserY+gs.directionY > windowSize.h-gs.laserSize || gs.laserY+gs.directionY < gs.laserSize {
gs.directionY = -gs.directionY
}
// Move dot
gs.laserX += gs.directionX
gs.laserY += gs.directionY
// Clear canvas
laserCtx.Call("clearRect", 0, 0, windowSize.w, windowSize.h)
// Draw red dot
laserCtx.Call("beginPath")
laserCtx.Call("arc", gs.laserX, gs.laserY, gs.laserSize, 0, 3.14159*2, false)
laserCtx.Call("fill")
laserCtx.Call("closePath")
}
func isLaserCaught(mouseX, mouseY, laserX, laserY float64) bool {
// Use Pythagorean theorem; enlarge hit radius by 15px for easier tapping
return (math.Pow(mouseX-laserX, 2) + math.Pow(mouseY-laserY, 2)) < math.Pow(gs.laserSize+15, 2)
}To keep the program running forever, a blocking empty channel is used:
func main() {
// Create empty channel that never receives
runGameForever := make(chan bool)
setup()
<-runGameForever // block forever
}Conclusion
WebAssembly is now a mature MVP; browsers show full support on CanIUse. By compiling Go to WASM you can build interactive, offline‑capable web games without writing a single line of JavaScript. The compiled .wasm bytecode can be shared across languages that target the same runtime.
The project referenced throughout this guide is go-wasm-cat-game-on-canvas-with-docker, which contains the full source code and Dockerfile for reproducible builds.
Further Resources
WebAssembly is more than the web
WebAssembly and Go: A look at the future (plus HN comments)
Mozilla Hacks and Hacker News articles
Awesome‑wasm lists: goawesome‑wasm, wasm‑weekly, etc.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
