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.

ITPUB
ITPUB
ITPUB
Build a Cat Canvas Game with Go and WebAssembly: Step‑by‑Step Guide

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 Workers

and 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.go

Or 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.

Cat game screenshot
Cat game screenshot
Happy cat animation
Happy cat animation
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Dockerfrontend developmentCanvasGoWebAssemblyGame Development
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.