See it running, not screenshotted.
Every pixel below is rendered by a real createGame instance — the same API you'd import in your project. Fixed-step physics, additive-blend particles, keyboard + pointer wired through gameplate's input layer.
Everything you need. Nothing you don't.
Tiny — ~3 KB gzipped
Smaller than a single sprite sheet. Side-effect free and fully tree-shakeable, so only what you import lands in your bundle.
TypeScript-first, strict
Source is TS with strict: true + every noUnchecked* flag. Inference does the work — no as any, ever.
Renderer-agnostic
Canvas 2D, WebGL, PIXI, Three.js, DOM, SVG, terminal — gameplate doesn't care. Bring whichever renderer you love.
Deterministic loop
Variable timestep by default; opt into a fixed-step accumulator with interpolation when physics needs to be reproducible.
First-class input
Normalized keyboard + pointer with target-relative coordinates. Headless? The Node build no-ops cleanly — same API, zero crashes.
Scene FSM built-in
Compile-time-checked finite state machine for menus, modes, and lifecycles. Send an event that doesn't exist — TypeScript stops you.
Memoized selectors
Reselect-style derived state in <30 LOC. Never recompute the visible-enemies list twice in the same frame.
Browser AND Node
Same code, two runtimes. Headless simulation, server-authoritative play, CI snapshot tests — all just work.
Dual ESM + CJS
ESM-first source, dual ESM/CJS publish. publint + @arethetypeswrong/cli clean. Provenance signed.
A game in 30 lines.
Typed state, deterministic loop, normalized input, scene FSM, memoized selectors — all batteries included, all tree-shakeable, all renderer-agnostic.
- ✅ Strict TypeScript inference — never type your state twice.
- ✅ Variable timestep by default, fixed-step on opt-in.
- ✅ Runs in the browser, Node, Bun, Deno, Web Workers.
- ✅ Zero runtime dependencies. Forever.
import { createGame, defineActions } from 'gameplate';
type State = { x: number; y: number; score: number };
const actions = defineActions<State>()({
moveBy: (s, dx: number, dy: number) => ({ ...s, x: s.x + dx, y: s.y + dy }),
addScore: (s, points: number) => ({ ...s, score: s.score + points }),
});
const game = createGame({
state: { x: 0, y: 0, score: 0 },
actions,
update: (state, dt, actions) => {
if (game.keyboard.isDown('ArrowRight')) actions.moveBy(200 * dt, 0);
if (game.keyboard.isDown('ArrowLeft')) actions.moveBy(-200 * dt, 0);
},
render: (state) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(state.x, state.y, 20, 20);
},
});
game.start();
Drop into any rendering stack.
gameplate never owns your render loop's pixels. Pass a render(state, alpha) callback to createGame — inside, use whichever renderer fits your project today. Swap it for a different one tomorrow.
You don't need another engine.
You need the glue.
vs. PIXI / Three / Phaser
Those are renderers (or full engines). gameplate is the glue between your game logic and any of them — bring whichever renderer you love.
vs. XState
XState is great for arbitrary statecharts. gameplate's FSM is small (~150 LOC), purpose-built for game scenes, and ships with the rest of the framework.
vs. rolling your own
You will. Eventually. gameplate is what your fifth from-scratch loop wants to become — typed, tested, headless-ready.