Ebiten runs on any platform Go runs on, including the browser via GOOS=js GOARCH=wasm. Ebiten handles canvas creation and the game loop, so you just write Update/Draw/Layout and call ebiten.RunGame.
Setup
The Go toolchain includes the js/wasm target out of the box. No extra install.
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o build/game.wasm .
You also need the wasm_exec.js shim that ships with your Go install:
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" build/wasm_exec.js
# (older Go versions keep it under misc/wasm/)
SDK integration
Go accesses browser globals through syscall/js. Ebiten's game loop is synchronous from Go's perspective — call the SDK just before RunGame:
Calling Wavedash.init() is required. Your game stays hidden behind the Wavedash loading screen until you do. Call it once your game is ready to play.
package main
import (
"syscall/js"
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct{}
func (g *Game) Update() error { return nil }
func (g *Game) Draw(screen *ebiten.Image) {}
func (g *Game) Layout(_, _ int) (int, int) { return 900, 600 }
func main() {
sdk := js.Global().Get("Wavedash")
sdk.Call("updateLoadProgressZeroToOne", 1)
sdk.Call("init")
if err := ebiten.RunGame(&Game{}); err != nil {
panic(err)
}
}
Call updateLoadProgressZeroToOne(...) with intermediate values during async asset loading (embedded files, HTTP fetches, etc.). init() automatically signals load completion, so call it last.
HTML shell
A minimal shell that loads wasm_exec.js, instantiates the module, and hands off to Ebiten:
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100%; height: 100%; outline: none; }
</style>
</head>
<body>
<script src="./wasm_exec.js"></script>
<script type="module">
const sdk = window.WavedashJS = await window.WavedashJS;
sdk.updateLoadProgressZeroToOne(0);
const response = await fetch("./game.wasm");
const total = +response.headers.get("Content-Length") || 0;
let bytes;
if (!total || !response.body) {
bytes = new Uint8Array(await response.arrayBuffer());
} else {
const reader = response.body.getReader();
const chunks = [];
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
sdk.updateLoadProgressZeroToOne((received / total) * 0.95);
}
bytes = new Uint8Array(received);
let pos = 0;
for (const c of chunks) { bytes.set(c, pos); pos += c.length; }
}
sdk.updateLoadProgressZeroToOne(0.95);
const go = new Go();
const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
go.run(instance);
</script>
</body>
</html>
Build script
#!/usr/bin/env sh
set -eu
mkdir -p build
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o build/game.wasm .
if [ -f "$(go env GOROOT)/lib/wasm/wasm_exec.js" ]; then cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" build/wasm_exec.js
elif [ -f "$(go env GOROOT)/misc/wasm/wasm_exec.js" ]; then cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" build/wasm_exec.js
fi
cp web/index.html build/index.html
wavedash.toml
game_id = "YOUR_GAME_ID_HERE"
upload_dir = "./build"
Other SDK features
Once initialized, Wavedash exposes leaderboards, achievements, stats, and user data. Call them the same way:
sdk.Call("setAchievement", "first_win", true)
user := sdk.Call("getUser")
See the SDK reference for the full API.