CONTROLLER TRY THE DEMO →

NO APP · NO ACCOUNTS · NO PAIRING

Every phone is a controller.

The game shows a code. Everyone scans it. Their phone becomes a gamepad: a d-pad, a stick, a buzzer, a steering wheel, whatever the game needs. Presses reach the game in tens of milliseconds over a direct WebSocket relay.

GOT A CODE? JOIN A GAME

Three steps. One of them is "play".

1

The game opens a room

It connects to /ws/host/CODE and puts a QR code and a five-letter code on the big screen.

2

Players scan it

Their phone opens a controller in the browser, tinted in their player colour, with the layout the game asked for.

3

Mash buttons

Every change is a 16-byte binary snapshot relayed straight to the game. Holds, rolls, sticks, taps: nothing is lost or stuck.

Controllers for every kind of game

These are live. Go on, press them.

Or describe your own

Set the layout to custom and send a list of controls: buttons, tiles, pills, shoulders, buzzers, d-pads, sticks, sliders and touchpads, each placed as a fraction of the screen with its own label and colour. The phone builds it on the spot. Change it mid-game and every phone follows.


          

Edit the JSON; the preview updates. Links work too: /j?preview=custom#layout=…

For game developers

In the browser

import { ControllerHost } from "https://controller.onewheelgeek.net/sdk.js";

const host = new ControllerHost({
  title: "My Game",
  look: { layout: "dpad-ab", labels: { A: "JUMP", B: "FIRE" } },
});
host.on("open", (code, url) => showQr(url));
host.on("join", (pad) => host.setPad(pad.pad, { color: "#3a86ff", label: "P" + (pad.pad + 1) }));

function frame() {
  for (const [n, p] of host.poll()) {
    if (p.pressed & (1 << 4)) jump(n);   // A, latched: never misses a tap
    move(n, p.state.lx, p.state.ly);      // analog, -1..1
  }
  requestAnimationFrame(frame);
}

Anything with a WebSocket

connect  wss://controller.onewheelgeek.net/ws/host/ABCDE?key=<your secret>
send     {"t":"config","title":"My Game","look":{"layout":"stick-ab"}}
receive  {"t":"welcome","code":"ABCDE","joinUrl":"https://…/j/ABCDE"}
receive  {"t":"join","pad":0,"device":"…"}
receive  <16 bytes> kind u8 · pad u8 · seq u16 · buttons u32
                 lx ly rx ry i8 · px py i16   (little endian)
send     {"t":"haptic","pad":0,"ms":40}

Buttons: Up 0, Down 1, Left 2, Right 3, A 4, B 5, X 6, Y 7, L 8, R 9, Start 10, Select 11. Sticks also set the d-pad bits, so a d-pad game works with any layout. Full spec: protocol.md.

Built for latency

Frames leave the phone inside the touch handler, never queued to the next paint. Binary, 16 bytes, on change only.

Next to your game

Each room is a Durable Object that lives in the Cloudflare location nearest whoever opened it: the big screen.

Nothing gets stuck

Every frame is a whole snapshot. Phones release everything when they lock or drop, and reconnect to the same seat.