OKONAONLINE
Developer Docs
OkonaPad
The Unity SDK for Okona Console games. Import it and every player's controller shows up as a standard Unity Gamepad — your existing Input System bindings just work. No setup, no custom device, no networking code.
Introduction
OkonaPad is the Unity SDK for games published to the Okona Console — a living-room device that boots into a library, lets a player pick a game, and runs it as a Unity WebGL build with local USB/Bluetooth gamepads. The SDK's whole job is to make those controllers appear inside your game as ordinary Unity Gamepad devices.
OkonaPadDevice derives from UnityEngine.InputSystem.Gamepad, so Gamepad.all, buttonSouth, leftStick, rightTrigger, the <Gamepad>/… binding paths, and PlayerInputManager all work with zero Okona-specific code. Build your game against the standard Input System and it runs on Okona.Key Features
- Controllers arrive as standard Unity
Gamepaddevices - Up to 6 players, each in their own slot
- Full controller surface: face buttons, bumpers, both sticks (incl. L3/R3), D-pad, triggers
- Zero-allocation input via WebAssembly memory (no GC hitches)
- Standard rumble through
Gamepad.SetMotorSpeeds() - Self-bootstrapping — no GameObject, no component, no scene wiring
- Works in the Editor with any pad; on the console with the real controllers
What changed in v2
SDK v2 is a clean break from v1. If you integrated against v1, re-import the package and re-bind to the standard Gamepad paths — it's simpler now.
| Area | v1 (retired) | v2 |
|---|---|---|
| Device type | Custom OkonaPadControllerDevice (not a Gamepad) | OkonaPadDevice : Gamepad |
| Binding paths | <OkonaPad>/buttonA | <Gamepad>/buttonSouth |
| Transport | SendMessage + JSON + Base64 per frame | Binary snapshot read from HEAPU8 (zero alloc) |
| Setup | Add an OkonaInputBridge GameObject named exactly | None — the bridge self-bootstraps |
| Controls | A/B/X/Y, Start/Select, D-pad, left stick | Everything a Gamepad has, both sticks, analog triggers |
| Rumble | Custom static event | Standard Gamepad.SetMotorSpeeds() |
<Gamepad>/ bindings for Editor testing, but on the console every pad arrives as an OkonaPadDevice — which is a Gamepad, so your bindings are unaffected.Requirements
Unity
- Unity 6 (6000.3.x) or newer
- Input System package (
com.unity.inputsystem) 1.17+ - Build target: WebGL
Project Settings
- Install the Input System package via Package Manager.
Edit > Project Settings > Player > Other Settings→ set Active Input Handling to Input System (New) (or Both).- That's it. The SDK registers its layout and starts pumping input automatically at load — there is nothing to add to a scene.
Quick Start
Import the SDK
Download OkonaPad.unitypackage and import it (Assets > Import Package > Custom Package…). It drops a single Assets/OkonaPad/ folder into your project. There is no component to add and no GameObject to create — the bridge self-bootstraps via [RuntimeInitializeOnLoadMethod].
Bind to the standard Gamepad
In your Input Actions asset, bind to <Gamepad>/ paths exactly as you would for any Unity game — <Gamepad>/buttonSouth, <Gamepad>/leftStick, <Gamepad>/rightTrigger, and so on. An OkonaPadDevice is a Gamepad, so these match on the console and in the Editor with any pad you plug in.
<Gamepad>, not <OkonaPad>. A control scheme that requires <OkonaPad> will reject native pads in the Editor, so a joined player is silently dropped. Use <Gamepad> and everything works in both places.Read input
Use the Input System exactly as normal — PlayerInput / PlayerInputManager (recommended, see below), an InputActions asset, or direct polling:
using UnityEngine;
using UnityEngine.InputSystem;
public class Player : MonoBehaviour
{
void Update()
{
var pad = Gamepad.current; // an OkonaPadDevice on the console
if (pad == null) return;
if (pad.buttonSouth.wasPressedThisFrame) Jump();
Vector2 move = pad.leftStick.ReadValue();
float gas = pad.rightTrigger.ReadValue();
}
}
Build for WebGL & publish
File > Build Settings > WebGL→ Build.- Upload the build through the Developer Portal and submit it for review.
- Once published, the console loads it and feeds your game the connected controllers.
Recommended Pattern: PlayerInputManager
For both single- and multi-player, the cleanest setup is Unity's PlayerInputManager with "Join Players When Button Is Pressed". Each controller that presses a button spawns one player; when a controller is removed, that player's device is lost. Because OkonaPads are real Gamepads, this is identical to how you'd support local multiplayer in any Unity game — and it runs unchanged in the Editor with whatever pads you have on your desk.
Setup
- Create an Input Actions asset with a control scheme that requires
<Gamepad>, and bind your actions to<Gamepad>/…paths. - Make a Player prefab with a
PlayerInputcomponent pointing at that asset (set its default scheme + default map). - Add a
PlayerInputManagerto a scene object; set Join Behavior to Join Players When Button Is Pressed and point Player Prefab at your prefab. - If you subscribe to join/leave in C#, set the manager's Notification Behavior to Invoke C# Events (the
onPlayerJoined/onPlayerLeftevents only fire in that mode).
using UnityEngine;
using UnityEngine.InputSystem;
using OkonaPad;
// One of these is spawned per joined controller.
[RequireComponent(typeof(PlayerInput))]
public class PlayerController : MonoBehaviour
{
PlayerInput _input;
void Awake()
{
_input = GetComponent<PlayerInput>();
// The Gamepad this player owns (an OkonaPadDevice on the console).
var pad = _input.GetDevice<Gamepad>();
// Optional: the 0-based Okona slot (P1 = 0 … P6 = 5).
if (pad is OkonaPadDevice okona)
Debug.Log($"Joined as player slot {okona.okonaSlot}");
}
// Bound to a "Fire" action in the Input Actions asset.
public void OnFire(InputAction.CallbackContext ctx)
{
if (ctx.performed) Fire();
}
void Rumble()
{
// Standard Gamepad rumble — forwarded to the physical pad.
(_input.GetDevice<Gamepad>())?.SetMotorSpeeds(0.4f, 0.8f);
}
}
To clean up a player whose controller is removed, handle device loss on the PlayerInput:
_input.onDeviceLost += pi => Destroy(pi.gameObject);
API Reference
Everything lives in the OkonaPad namespace. For most games you never touch these types directly — you read input through the standard Gamepad / PlayerInput API. They're here for when you want the Okona slot, manual access, or deterministic tests.
OkonaPadDevice
OkonaPad.OkonaPadDevice : UnityEngine.InputSystem.Gamepad
One connected Okona controller, surfaced as a standard Unity Gamepad. Every normal Gamepad control and binding works out of the box. The only addition is the player slot.
Members
| Member | Type | Description |
|---|---|---|
okonaSlot | int | 0-based player slot (P1 = 0 … P6 = 5); -1 until assigned |
SetMotorSpeeds(low, high) | void | Standard Gamepad rumble; forwarded to the physical pad |
PauseHaptics() / ResetHaptics() | void | Stop rumble (sends 0,0) |
Inherited from Gamepad: buttonSouth/East/West/North, leftShoulder, rightShoulder, leftTrigger, rightTrigger, select, start, leftStick, rightStick, leftStickButton, rightStickButton, dpad, plus Gamepad.all and Gamepad.current.
OkonaInputBridge
static class OkonaPad.OkonaInputBridge
The static driver that pulls the input snapshot from the web layer each frame and adds/removes OkonaPadDevices as players connect and disconnect. It bootstraps itself at load — you do not create or reference it for normal use.
Members
// The connected device for a 0-based slot, or null.
public static OkonaPadDevice GetDevice(int slot);
// Pull the current snapshot and push it to the Input System. Runs every
// frame automatically (InputSystem.onBeforeUpdate); also callable directly
// for deterministic tests.
public static void Pump();
// Editor/test only: inject a snapshot as if it came from the web layer.
#if UNITY_EDITOR
public static void SimulateSnapshot(byte[] snapshot);
#endif
window.__okonaPad), so no OkonaPadDevices are created and Pump() no-ops. You test with native Unity gamepads, which work because your bindings target <Gamepad>. In a WebGL build the bridge manufactures OkonaPadDevices from the live snapshot.Controls & Bindings
OkonaPad maps the standard controller to the standard Unity Gamepad controls. Bind to the <Gamepad>/ paths below.
| Control | Binding path | Notes |
|---|---|---|
| A (bottom) | <Gamepad>/buttonSouth | |
| B (right) | <Gamepad>/buttonEast | |
| X (left) | <Gamepad>/buttonWest | |
| Y (top) | <Gamepad>/buttonNorth | |
| Left bumper | <Gamepad>/leftShoulder | |
| Right bumper | <Gamepad>/rightShoulder | |
| Left trigger | <Gamepad>/leftTrigger | Analog 0–1 |
| Right trigger | <Gamepad>/rightTrigger | Analog 0–1 |
| Select / View | <Gamepad>/select | |
| Start / Menu | <Gamepad>/start | |
| Left stick | <Gamepad>/leftStick | Vector2 |
| Right stick | <Gamepad>/rightStick | Vector2 |
| L3 (stick press) | <Gamepad>/leftStickPress | |
| R3 (stick press) | <Gamepad>/rightStickPress | |
| D-pad | <Gamepad>/dpad | /dpad/up etc. |
<Gamepad>/leftTrigger and /rightTrigger to actions typed Value / Axis (with Initial State Check on), then read the live 0–1 pull with ReadValue<float>() and apply your own threshold. If you type the action as a Button, ReadValue<float>() stays 0 until the pull crosses the press point (default 0.5) — so light presses do nothing and some pads (e.g. an Xbox controller over Bluetooth) may never register at all. A face button mapped to the same action still fires, which hides the bug. If you reuse a trigger Value action as a menu “confirm,” subscribe to started (one edge), not performed — performed re-fires every frame the trigger is held.Input Snapshot advanced
You never read this directly — it's how input physically reaches your game, documented for the curious. The shell keeps a fixed-layout binary snapshot of every player's current state in window.__okonaPad. Each frame the SDK copies it straight out of WebAssembly memory (HEAPU8) into a reused buffer and decodes it — no SendMessage, no JSON, no Base64, zero per-frame allocation.
Layout is little-endian, 106 bytes (a 4-byte header + 6 × 17-byte pad records). It is defined identically in bridge/okona-padstate.js (web) and OkonaPad/OkonaPadState.cs (Unity).
Header (4 bytes)
[0] magic 0x4F ('O')
[1] version 1
[2] maxPads 6
[3] reserved 0
Per pad (17 bytes), one per slot 0..5
[0] flags bit0 = connected
[1..4] buttons uint32 LE — bit i = button i pressed
[5..6] leftX int16 LE (-32767..32767 => -1..1)
[7..8] leftY int16 LE
[9..10] rightX int16 LE
[11..12] rightY int16 LE
[13..14] leftTrigger uint16 LE (0..65535 => 0..1)
[15..16] rightTrigger uint16 LE
The buttons bit positions are the standard Web Gamepad indices:
| Bit | Button |
|---|---|
| 0–3 | A, B, X, Y |
| 4–5 | LB, RB |
| 6–7 | LT, RT (digital) |
| 8–9 | Select, Start |
| 10–11 | L3, R3 |
| 12–15 | D-pad Up, Down, Left, Right |
| 16 | Guide (reserved) |
How Input Flows
On the Okona Console, a USB or Bluetooth pad is paired at the OS level and surfaced into the game page; the shell tracks it in one of six player slots and writes its state into the snapshot; the SDK reads the snapshot from WebAssembly memory and drives a Gamepad for it.
Slots & reconnection
The shell manages six slots. A pad is adopted into the lowest free slot the moment a button is pressed on it (a single pad attached at load is adopted automatically). If a pad disconnects — including a brief Bluetooth blip — its slot is held for 30 seconds and the same player resumes when it returns, so a momentary drop never turns into a new join. After the reserve expires the slot is freed and the OkonaPadDevice is removed.
Rumble
Call SetMotorSpeeds(low, high) on the player's Gamepad. The SDK forwards it to the web layer, which vibrates the physical pad (best-effort — silently ignored on pads that don't support haptics). No Okona-specific API to call.
Reference Project
The SDK ships with a Unity 6 reference project that demonstrates the recommended setup end-to-end:
- Input Tester — a
PlayerInputManagerscene that spawns one on-screen controller widget per joined pad and lights up every input (face buttons, bumpers, sticks, L3/R3, D-pad, analog triggers). Drop in up to six pads and watch them light independently. - Player prefab + Input Actions — a bare
PlayerInputprefab and an actions asset with a<Gamepad>control scheme you can copy straight into your game.
It's the fastest way to confirm the SDK works with your controllers — open the Input Tester scene, press Play, and press a button on each pad.
Testing Your Game for Okona
The Okona Dev Harness runs your WebGL build on your own machine exactly as the console runs it — the same game shell, the same controller input path, the same in-game menu — so you can validate your game before you publish it. Download the harness — it's a small tool that needs only Python 3.7+, nothing to install.
Run it
- Build your game for WebGL with Brotli compression (see Requirements).
- Download the harness and unzip it anywhere.
- From the unzipped
okona-harness/folder, point it at your build — the folder Unity produced, containingindex.htmland aBuild/subfolder:python devtools/okona-test/okona-serve.py <path-to-your-WebGL-build> - A browser opens with your game running. Plug in a USB or Bluetooth controller and play. Press ` (backtick) to toggle the diagnostics overlay.
The serve script sets the HTTP headers Unity WebGL requires — the reason a plain static server usually fails to load a Brotli build.
Confirm your controls
The strip along the bottom shows one card per controller slot (P1–P6), decoded live from the input snapshot the SDK reads: pressed buttons, stick positions, trigger pull. Use it to confirm every control maps the way you expect, and that multiple pads each drive their own player.
Check performance against the console
The Okona Console is a Raspberry Pi 5. A game that hits 60 fps on your development machine can run far slower on the Pi, so the harness does not rely on desktop framerate. Instead it grades your game's GPU workload — which predicts console behaviour no matter how fast your machine is — against budgets measured on real console hardware:
| Metric (per frame) | Budget | Why it matters |
|---|---|---|
| Draw calls | ≤ 80 | Each has fixed CPU cost; the Pi's CPU is the bottleneck |
| Buffer uploads | ≤ 2 MB | Per-frame dynamic vertex/index data stalls the GPU |
| Vertices | ≤ 100K | Raw geometry throughput |
| Render-target switches | ≤ 8 | Tiler GPUs pay heavily per pass (shadows, post stacks) |
Green is comfortable, amber is at budget, red is over. If you're in the red, reduce draw calls (batch/atlas), cut per-frame mesh rebuilds, and avoid realtime shadows and full-screen post-processing.
Pre-submit checklist
- Game loads and is playable in the harness (Brotli build).
- Every control reads correctly in the input visualizer.
- Multiple controllers each drive their own player (P1–P6).
- Home opens the Game Menu; Continue resumes, Exit leaves.
- Workload metrics are green or amber — not red.
- Rumble fires on supported pads (if your game uses it).
Troubleshooting
No controller in the Editor
- In the Editor the OkonaPad bridge is dormant — you test with native Unity gamepads, so plug one in and press a button.
- Confirm Active Input Handling is Input System (New) (or Both) in Player settings.
- Check the pad shows up:
Window > Analysis > Input Debuggershould list a Gamepad.
Input doesn't respond on the console
- Make sure your actions bind to
<Gamepad>/paths (not a custom device). AnOkonaPadDeviceis aGamepad. - If you use
PlayerInputManagerwith C# join/leave events, set its Notification Behavior to Invoke C# Events — the events don't fire otherwise. - Make sure your control scheme requires
<Gamepad>, not<OkonaPad>.
A pad controls two players (or joins twice)
- This is the native-gamepad-double-up problem. The SDK disables Unity's browser-gamepad backend in WebGL automatically; don't re-enable it or join players from a backend you've re-added.
- Never build join logic off
InputSystem.devicesthat you've manually re-enabled on WebGL — letPlayerInputManagerhandle joins.
Rumble not working
- Call
SetMotorSpeeds(low, high)on the player'sGamepad(values 0–1). - Not every physical pad supports haptics; the request is silently ignored on those.
WebGL build issues
- Ensure Input System (New) is enabled in Project Settings.
- Don't include server-side / networking code — WebGL can't run sockets, and the SDK needs none.
Use with AI Assistants
If you build with an AI coding assistant, connect it to the Okona docs MCP server and it can read this documentation directly — the full SDK reference, the testing guide, and the dev-harness manual — instead of guessing at the OkonaPad API. It's a public, read-only Model Context Protocol endpoint; no account or API key needed.
https://okonaonline.com/mcp
Claude Code
claude mcp add --transport http okona-docs https://okonaonline.com/mcp
Cursor
Add to .cursor/mcp.json in your project (or the global one):
{
"mcpServers": {
"okona-docs": { "url": "https://okonaonline.com/mcp" }
}
}
Other clients
Any MCP client that supports the Streamable HTTP transport works — point it at the URL above with no authentication. The server exposes three tools:
list_doc_sections— the documentation catalogread_doc_section— one section (or everything) as markdownsearch_docs— keyword search with snippets
Build Your Own Console
The Okona Console is a Raspberry Pi 5 appliance. We publish the exact pre-provisioned image we flash ourselves, so you can build your own: flash one SD card, plug it into a TV, and it boots straight into Okona — the same setup, library, and gameplay as a retail unit. No install steps, no command line.
What you'll need
- Raspberry Pi 5 (recommended). A Pi 4 also works; Pi 3 and older can't run Unity WebGL.
- microSD card, 16 GB or larger.
- Official USB-C power supply — 27 W for the Pi 5, 15 W for the Pi 4. (Underpowered supplies cause hard-to-diagnose instability.)
- micro-HDMI → HDMI cable, into the HDMI port next to the USB-C jack (HDMI0).
- A TV with an HDMI input.
- A USB or Bluetooth gamepad (Xbox, PlayStation, 8BitDo, Switch Pro…). A phone on the same Wi-Fi can also act as a controller.
Flash the card
- Download the image above — it's a
.img.gz. Don't unzip it; the flashing tool reads it directly. - Install Raspberry Pi Imager (or balenaEtcher).
- In Imager: Choose Device (your Pi) → Choose OS → Use custom → pick the downloaded
.img.gz→ Choose Storage (your microSD) → Write.
Verify the download optional
Confirm the file arrived intact by checking its SHA-256 against the published value (—):
# Windows (PowerShell)
Get-FileHash -Algorithm SHA256 okona-console.img.gz
# macOS / Linux
shasum -a 256 okona-console.img.gz
First boot
Insert the card, connect HDMI and power, and the Pi boots into the Okona setup screen.
- Wi-Fi: the console raises its own
Okona-XXXXhotspot and shows a QR code on the TV. Scan it with your phone, pick your home network, and type its password. The TV flips to the game library once it's online. - Controllers: hold a Bluetooth pad's pairing button to pair it — there's no pairing screen to find. USB pads just work when plugged in.
- Pick a game and play. Subsequent boots skip setup and land on the library in seconds.
Ready to build?
Download the SDK, bind to the standard Gamepad, and ship to the Okona Console.