logo
LiveTuner

ui

LiveTuner

Free

Install with the CLI:

bjs download LiveTuner

LiveTuner

Sliders on top of a running game, so you can tune it by feel, even from a phone.

What it does

Each knob is one number on one component of one entity: CarDrive.steerRate on the Player, say. Open the page with ?tune on the end of the address (or press the toggle key) and a panel shows each knob as a slider. Move one and the field changes at once, so the car feels it on the next frame.

  • Your values are saved in the browser and come back when you reload.
  • Copy puts the live values on the clipboard as JSON, ready to paste into the scene file.
  • Reset goes back to the scene's own values and forgets yours.

Use it in a scene

A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.

Build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add LiveTuner
    bjs download LiveTuner

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "Game": {
    "components": {
      "LiveTuner": {
        "target": "Player",
        "title": "Handling",
        "urlFlag": "tune",
        "toggleKey": "Backquote",
        "knobs": [
          {
            "component": "CarDrive",
            "field": "steerRate",
            "label": "Steering strength",
            "min": 1,
            "max": 8,
            "step": 0.1
          },
          {
            "component": "CarCollision",
            "field": "slideGrip",
            "label": "Grip while sliding",
            "min": 0,
            "max": 1,
            "step": 0.01
          }
        ]
      }
    }
  }
}

A knob can work on another entity: add "entity": "Road" and it moves the Road's component instead of the target's. The panel heads that section Road · HighwayRoad.

component is the name the scene file uses, so it still matches in a minified build. A knob for a component the entity lacks, or a field that is not a number, shows a dash and greys out. Every 0.3 seconds the panel reads the fields again, so it shows a value some other system changed, such as a refit.

Snapping a value

TypeScript
export function knobStep(k: TunerKnob): number {
  return k.step && k.step > 0 ? k.step : (k.max - k.min) / 100;
}

/** Clamp to the knob's range and snap to its step (from `min`). */
export function snapToKnob(k: TunerKnob, value: number): number {
  if (!Number.isFinite(value)) return k.min;
  const step = knobStep(k);
  const v = Math.min(k.max, Math.max(k.min, value));
  const snapped = k.min + Math.round((v - k.min) / step) * step;
  // Trim float noise from the multiply (0.1 * 3 = 0.30000000000000004).
  const places = Math.max(0, Math.min(6, Math.ceil(-Math.log10(step)) + 1));
  return Math.min(k.max, Number(snapped.toFixed(places)));
}

For the steering knob above, snapToKnob(k, 4.437) is 4.4 and snapToKnob(k, 12) is 8. A knob with no step moves in hundredths of its range. Values from the slider and from storage both go through it.

Saved and copied

Values travel in two shapes, and the core comment shows both:

TypeScript
/**
 * `{ 'CarDrive.grip': 0.8, 'Road/HighwayRoad.laneWidth': 4 }` →
 * `{ Player: { CarDrive: { grip: 0.8 } }, Road: { HighwayRoad: { laneWidth: 4 } } }`,
 * entities and components in knob order.
 */

The flat one, on the left, is what goes into localStorage under storageKey each time a slider moves. A knob on the target is keyed Component.field, and a knob on another entity gets its name in front. On load, parseSaved reads the string back. It keeps only numbers for knobs that still exist and snaps each into the knob's current range, so a knob you cut down or took out can't push an old value back in.

The nested one, on the right, is what Copy puts on the clipboard. It holds the values in the game right now, not the saved ones. Paste each component block under that entity's components in the scene. If the browser blocks the clipboard, a text box opens with the JSON selected.

Saved values stick

Saved values are put back on every load in that browser, whether or not the panel is open, until someone presses Reset. They apply the first time each entity appears, and again if that entity is removed and made anew. Before applying them, the tuner records the scene's own values, and Reset writes those back.

The shipped Endless Rampage scene keeps LiveTuner on purpose, so its owner can tune on the live site. That means any player can open the panel with ?tune or the backquote key, and their tune stays in their browser. To stop players tuning, remove the LiveTuner entry from the scene for the release build. Setting urlFlag and toggleKey to "" hides the panel, but values a browser saved earlier still apply.

Settings

FieldDefaultWhat it does
target"Player"Entity the knobs work on, unless a knob names its own entity.
urlFlag"tune"Address flag that opens the panel on load (?tune). Empty means never from the address.
toggleKey"Backquote"KeyboardEvent.code that shows and hides the panel. Empty means no key.
title"Tuning"Panel heading.
storageKey""localStorage key the values are saved under. Empty means liveTuner:<target>.
knobs[]The sliders. Each one is { entity?, component, field, label?, min, max, step? }. A missing entity means target, a missing label means the field name.

Events

LiveTuner emits and hears nothing on the event bus. It reads the toggle key from the page's own keydown, so it works without KeyboardInput.

Needs

Nothing. It finds each component by the type name in the scene file and writes the field directly.

More like this

AutoRunnerHUD
BilliardHUD
CardFan
ConicPath
HealthBar
PinballHUD
PokerHUD
PokerTableCards
RoadAudio
Score
Scoreboard
SpaceGolfHUD
SpaceShooterHUD
SpaceShooterScore
TrajectoryPreview
Waypoint

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search