Phaser AE

Control feels

Free packv3.0.0-12DArcade

Five control profiles for arcade games: an inertial ship, an eight-way mover, a spinner, a trackball and a geared road car. You hand them the held keys or the pointer's movement each frame, and they give the player the feel the game needs rather than a generic one.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 19, 2026 · updated October 20, 2026

Control feels: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

controlFeels

function controlFeels(): Pack<{ controlFeels: ControlFeelsApi }>

The control-feels pack. Pure logic, so it needs no backend and runs headless.

Helpers

CAR_FEELS

const CAR_FEELS: { polePosition: { gears: number[]; accel: number; brake: number; steer: number; auto: false; }; }

Road cars from the gallery: roadCar(CAR_FEELS.polePosition).

directMove

function directMove(o: { speed: number; eightWay?: boolean }): DirectMove

Creates a DirectMove.

inertialShip

function inertialShip(o: InertialOptions): InertialShip

Creates an InertialShip.

roadCar

function roadCar(o: CarOptions): RoadCar

Creates a RoadCar.

SHIP_FEELS

const SHIP_FEELS: { asteroids: { thrust: number; turn: number; drag: number; maxSpeed: number; }; starCastle: { thrust: number; turn: number; drag: number; maxSpeed: number; }; mineStorm: { thrust: number; turn: number; drag: number; maxSpeed: number; }; mineStorm3d: { ...; }; solarQuest: { ...; }; ripOff: { ...; }; webWars: { ...; }... …

Inertial ships, tuned by hand in the gallery tributes (not measured from the originals): spread one into inertialShip and override what differs, inertialShip({ ...SHIP_FEELS.mineStorm, maxSpeed: 500 }). Higher drag settles faster; turn is radians a second.

spinner

function spinner(o: { keyRate?: number; sensitivity?: number; wrap?: number } = {}): Spinner

Creates a Spinner.

trackball

function trackball(o: TrackballOptions = {}): Trackball

Creates a Trackball.

TRACKBALL_FEELS

const TRACKBALL_FEELS: { quantum: { drag: number; sensitivity: number; maxSpeed: number; }; }

Trackballs from the gallery: trackball({ ...TRACKBALL_FEELS.quantum, x, y }).

WALK_FEELS

const WALK_FEELS: { cosmicChasm: { speed: number; eightWay: true; }; }

Direct movers from the gallery: directMove(WALK_FEELS.cosmicChasm).

Classes

DirectMove

class DirectMove

Eight-way movement with no momentum — Berzerk, Clean Sweep, Armor Attack. heading is the last direction actually moved, which is what "fire in the direction you are walking" means and why it must survive letting go.

  • speed: number — Movement speed, in units each second.
  • eightWay: boolean — Snap movement to 8 compass directions. Default true.
  • heading: number — Last movement heading in radians, kept when the stick centres.
  • moving: boolean — True while any direction is held.
  • constructor(o: { speed: number; eightWay?: boolean })
  • update(a: Mover, input: DirectInput, dt: number): void — Moves the mover for one frame from the held directions.

InertialShip

class InertialShip

Rotate / thrust / drift — Asteroids, Mine Storm, Space Wars, Solar Quest, Gravitar, Lunar Lander. The single most common control scheme of the era.

  • thrust: number — Acceleration under thrust, in units/s².
  • turn: number — Turn rate, in radians each second.
  • drag: number — Fraction of speed shed each second, 0..1. Default 0.25.
  • maxSpeed: number — Speed ceiling, in units each second. Default Infinity.
  • reverse: number — Reverse thrust as a fraction of forward thrust. 0 = no reverse.
  • thrusting: boolean — True on the frames the engine is lit — for the exhaust plume.
  • constructor(o: InertialOptions)
  • update(a: Mover, input: ShipInput, dt: number): void — Applies one frame of turn, thrust and drag to the mover.
  • muzzle(a: Mover, offset: number, shotSpeed: number, inherit = true): Mover — Where a shot fired now should start and how fast it goes.

RoadCar

class RoadCar

A racing gearbox: gear, gas, brake, steer, speedo. steer is scaled by speed because a stationary car does not corner, which is the difference between a racer and a top-down shooter that happens to have a car in it.

  • readonly gears: number[] — Top speed in each gear, slowest first.
  • gear: number — The current gear, from 0 (first gear).
  • speed: number — The current forward speed, in units each second.
  • drift: number — Lateral rate this frame, units/s — feed it to the road position.
  • accel: number — Acceleration under gas, in units/s².
  • braking: number — Deceleration under brake, in units/s².
  • coast: number — Deceleration when neither gas nor brake is held, in units/s².
  • steer: number — Lateral rate at full speed, in units each second.
  • auto: boolean — True for an automatic gearbox that shifts itself.
  • constructor(o: CarOptions)
  • get topSpeed(): number — The top speed of the current gear, in units each second.
  • get revs(): number — 0..1 within the current gear — the rev counter, and the engine pitch.
  • update(input: CarInput, dt: number): void — Applies one frame of shifting, speed and steering from the held keys.
  • crash(): void — A crash: stop dead and drop to first.

Spinner

class Spinner

A spinner (Tempest, Omega Race): RELATIVE input that accumulates. A mouse, a trackball axis and a held key all feed the same nudge, so the game reads one value whatever the hardware is.

  • value: number — Accumulated position. Unbounded unless wrap is set.
  • keyRate: number — Units per second of held key.
  • sensitivity: number — Multiplier on pointer deltas.
  • wrap: number — Wrap value into 0..wrap. 0 = free.
  • constructor(o: { keyRate?: number; sensitivity?: number; wrap?: number } = {})
  • nudge(delta: number): number — A pointer or trackball delta.
  • update(dir: number, dt: number): number — Held keys: -1, 0 or +1.
  • set(v: number): number — Sets the value directly, wrapped into 0..wrap when wrap is set. Returns the new value.

Trackball

class Trackball

The trackball state. Feed pointer deltas to roll and call update each frame.

  • x: number — The x position of the cursor.
  • y: number — The y position of the cursor.
  • vx: number — The roll velocity on x, in units each second.
  • vy: number — The roll velocity on y, in units each second.
  • drag: number — Fraction of the roll shed each second, 0..1. Default 0.9.
  • sensitivity: number — Multiplier on pointer deltas. Default 1.
  • maxSpeed: number — Ceiling on the roll speed, in units each second. Default Infinity.
  • bounds: { x: number; y: number; w: number; h: number } | null — Rectangle that limits the position, or null for no limit.
  • constructor(o: TrackballOptions = {})
  • roll(dx: number, dy: number, dt: number): void — A pointer delta this frame.
  • update(dt: number): void — Applies drag, the speed ceiling and the bounds, then moves the cursor. Call each frame with dt in seconds.

Types

CarInput

interface CarInput

Held-key state for a RoadCar. The game maps its own keys.

  • gas?: boolean — True while the gas is held.
  • brake?: boolean — True while the brake is held.
  • left?: boolean — True while steering left.
  • right?: boolean — True while steering right.
  • shiftUp?: boolean — Momentary: shift up / down one gear.
  • shiftDown?: boolean — Momentary: shift down one gear.

CarOptions

interface CarOptions

Configures a RoadCar.

  • gears: number[] — Top speed in each gear, slowest first.
  • accel: number — Acceleration, units/s².
  • brake: number — Braking, units/s².
  • drag?: number — Coasting deceleration, units/s². Default accel / 3.
  • steer: number — Lateral units/s at full speed.
  • auto?: boolean — Automatic gearbox — shifts itself at the top of each gear. Default false.

ControlFeelsApi

type ControlFeelsApi = typeof kit

game.controlFeels — every function, class and preset this pack carries, as one namespace.

DirectInput

interface DirectInput

Held-key state for DirectMove. The game maps its own keys.

  • up?: boolean — True while up is held.
  • down?: boolean — True while down is held.
  • left?: boolean — True while left is held.
  • right?: boolean — True while right is held.

InertialOptions

interface InertialOptions

Configures an InertialShip.

  • thrust: number — Acceleration under thrust, units/s².
  • turn: number — Turn rate, rad/s.
  • drag?: number — Fraction of speed shed per second. Default 0.25.
  • maxSpeed?: number — Speed ceiling, in units each second. Default Infinity.
  • reverse?: number — Reverse thrust as a fraction of forward. Default 0 (no reverse).

ShipInput

interface ShipInput

Held-key state. The game maps its own keys; the profile never reads input.

  • left?: boolean — True while turning left.
  • right?: boolean — True while turning right.
  • thrust?: boolean — True while forward thrust is held.
  • reverse?: boolean — True while reverse thrust is held.

TrackballOptions

interface TrackballOptions

A trackball with momentum — Quantum's probe, Missile Command's crosshair. The roll continues after the ball is released and decays; that carry is the whole feel of the device.

  • x?: number — The start x position. Default 0.
  • y?: number — The start y position. Default 0.
  • drag?: number — Fraction of roll shed per second. Default 0.9.
  • sensitivity?: number — Multiplier on pointer deltas. Default 1.
  • maxSpeed?: number — Ceiling on the roll, units/s. Default Infinity — set one. roll divides by dt, so one fast flick of a mouse is thousands of units a second and the cursor is gone off the far side before the next frame.
  • bounds?: { x: number; y: number; w: number; h: number } — Rectangle that limits the position. The cursor stops at the edge.