CTRLLR

Getting Started

Get up and running with CTRLLR in a few minutes.

Installation

Install the core package using your preferred package manager:

# pnpm
pnpm add @ctrllr/core

# npm
npm install @ctrllr/core

# yarn
yarn add @ctrllr/core

Quick Start

1. Initialize the Manager

import { CtrllrManager } from '@ctrllr/core';

const ctrllr = new CtrllrManager({
  signalingUrl: 'wss://your-signaling-server.com',
});

2. Connect to the Signaling Server

await ctrllr.connect();

3. Display a QR Code

Generate a QR code for players to scan with the CTRLLR mobile app:

const qrImg = document.getElementById('qr') as HTMLImageElement;
qrImg.src = await ctrllr.getQRCodeDataURL();

4. Handle Controller Connections

ctrllr.on('controllerconnected', (e) => {
  console.log(`Player ${e.controller.index} joined!`);
  console.log(`Username: ${e.controller.username}`);
});

ctrllr.on('controllerdisconnected', (e) => {
  console.log(`Player ${e.controller.index} left`);
});

Controller Input

Each controller has a joystick and 4 aimable buttons (A, X, Y, Z):

interface ControllerState {
  joystick: InputState; // Left analog stick
  a: InputState; // Aimable button A
  x: InputState; // Aimable button X
  y: InputState; // Aimable button Y
  z: InputState; // Aimable button Z
}

interface InputState {
  pressed: boolean; // Is the input active
  x: number; // Horizontal axis (-1 to 1)
  y: number; // Vertical axis (-1 to 1)
}

Reading Input

Game Loop (Polling)

Read state directly in your game loop for continuous input like movement:

function gameLoop() {
  for (const controller of ctrllr.controllers.values()) {
    const { joystick } = controller.state;
    player.move(joystick.x, joystick.y);

    // Aimable buttons have direction too
    if (controller.state.a.pressed) {
      player.aim(controller.state.a.x, controller.state.a.y);
    }
  }
  requestAnimationFrame(gameLoop);
}

Events (Discrete Actions)

Use events for button presses — the SDK handles edge detection:

controller.on('buttondown', (e) => {
  if (e.input === 'a') player.jump();
  if (e.input === 'x') player.shoot(e.x, e.y); // e.x, e.y = aim direction
});

controller.on('buttonup', (e) => {
  if (e.input === 'a') player.endJump();
});

// State change (useful for React setState)
controller.on('statechange', (e) => {
  setControllerState(e.state);
});

Manager-Level Events

Listen on the manager for "any controller" scenarios:

// Pause menu - any controller can pause
ctrllr.on('buttondown', (e) => {
  if (e.input === 'a') {
    game.togglePause();
  }
});

// "Press A to join" lobby
ctrllr.on('buttondown', (e) => {
  if (e.input === 'a') {
    lobby.playerReady(e.controller);
  }
});

React Integration

Create a custom hook to sync controller state with React:

import { useState, useEffect } from 'react';
import { Controller, ControllerState } from '@ctrllr/core';

function useController(controller: Controller) {
  const [state, setState] = useState<ControllerState>(controller.state);

  useEffect(() => {
    const handler = (e: { state: ControllerState }) => setState(e.state);
    controller.on('statechange', handler);
    return () => controller.off('statechange', handler);
  }, [controller]);

  return state;
}

Then use it in your components:

function PlayerHUD({ controller }: { controller: Controller }) {
  const state = useController(controller);

  return (
    <div>
      <div>
        Joystick: {state.joystick.x.toFixed(2)}, {state.joystick.y.toFixed(2)}
      </div>
      <div>A Button: {state.a.pressed ? 'Pressed' : 'Released'}</div>
    </div>
  );
}

QR Code Options

Customize the QR code appearance:

const qrDataUrl = await ctrllr.getQRCodeDataURL({
  width: 256, // Size in pixels (default: 256)
  margin: 2, // Margin in modules (default: 2)
  darkColor: '#000000', // Foreground color
  lightColor: '#ffffff', // Background color
});

// Or get SVG
const qrSvg = await ctrllr.getQRCodeSVG();

Full Example

Here's a complete setup example:

import { CtrllrManager } from '@ctrllr/core';

async function init() {
  const ctrllr = new CtrllrManager({
    signalingUrl: 'wss://your-signaling-server.com',
  });

  await ctrllr.connect();

  // Show QR code
  const qrImg = document.getElementById('qr') as HTMLImageElement;
  qrImg.src = await ctrllr.getQRCodeDataURL();

  // Handle connections
  ctrllr.on('controllerconnected', ({ controller }) => {
    console.log(`${controller.username} joined as Player ${controller.index}`);

    // Listen to this controller's events
    controller.on('buttondown', (e) => {
      console.log(`Button ${e.input} pressed at (${e.x}, ${e.y})`);
    });

    controller.on('statechange', (e) => {
      // Access full state
      console.log('Joystick:', e.state.joystick);
    });
  });

  ctrllr.on('controllerdisconnected', ({ controller }) => {
    console.log(`Player ${controller.index} disconnected`);
  });

  // Game loop
  function gameLoop() {
    for (const controller of ctrllr.controllers.values()) {
      const { joystick, a } = controller.state;

      // Move with joystick
      if (joystick.x !== 0 || joystick.y !== 0) {
        player.move(joystick.x, joystick.y);
      }

      // Aim with A button
      if (a.pressed) {
        player.aim(a.x, a.y);
      }
    }
    requestAnimationFrame(gameLoop);
  }

  gameLoop();
}

init();

Next up: explore the full API Reference (coming soon).