Ensure every generated game works with Steam Deck controls out of the box. The Steam Deck exposes its controller as a standard XInput (Xbox-layout) gamepad via the W3C Gamepad API in browsers.
Steam Deck Physical Layout
[L1/LB] [R1/RB] Buttons 4, 5
[L2/LT] [R2/RT] Buttons 6, 7 (analog 0.0-1.0)
[L4] [L5] [R4] [R5] (back grip buttons, NOT in standard mapping)
┌─────────────────┐
LS │ [View] [Menu] │ RS Buttons 8, 9
(10) │ [Steam] │ (11) Button 16 │ │ D-pad │ │ [Y] (3) 12-15 │ Screen │ [X] [B] 2, 1 │ (Touchscreen) │ [A] (0) │ │ └─────────────────┘
[Left Trackpad] [Right Trackpad] (mapped as mouse or additional axes)
## W3C Standard Gamepad Button Index Mapping
The Steam Deck presents as a standard gamepad. These indices are guaranteed:
| Index | Button | Phaser Property | Use In Games |
|-------|--------|-----------------|-------------|
| 0 | A (bottom) | `gamepad.A` | Primary action (jump, select, confirm) |
| 1 | B (right) | `gamepad.B` | Secondary action (cancel, back, dodge) |
| 2 | X (left) | `gamepad.X` | Tertiary action (use item, interact) |
| 3 | Y (top) | `gamepad.Y` | Quaternary action (inventory, special) |
| 4 | LB / L1 | `gamepad.L1` | Left bumper (cycle left, previous) |
| 5 | RB / R1 | `gamepad.R1` | Right bumper (cycle right, next) |
| 6 | LT / L2 | `gamepad.L2` | Left trigger (aim, brake) - analog 0.0-1.0 |
| 7 | RT / R2 | `gamepad.R2` | Right trigger (shoot, accelerate) - analog 0.0-1.0 |
| 8 | View/Back | N/A (index) | Pause menu, scoreboard |
| 9 | Menu/Start | N/A (index) | Start game, open menu |
| 10 | Left Stick Press | N/A (index) | Sprint, lock-on |
| 11 | Right Stick Press | N/A (index) | Zoom, camera reset |
| 12 | D-pad Up | `gamepad.up` | Menu navigate up |
| 13 | D-pad Down | `gamepad.down` | Menu navigate down |
| 14 | D-pad Left | `gamepad.left` | Menu navigate left |
| 15 | D-pad Right | `gamepad.right` | Menu navigate right |
| 16 | Steam/Home | N/A | System button (do not use) |
## Axes Mapping
| Index | Axis | Phaser Property | Range |
|-------|------|-----------------|-------|
| axes[0] | Left Stick X | `gamepad.leftStick.x` | -1.0 (left) to 1.0 (right) |
| axes[1] | Left Stick Y | `gamepad.leftStick.y` | -1.0 (up) to 1.0 (down) |
| axes[2] | Right Stick X | `gamepad.rightStick.x` | -1.0 (left) to 1.0 (right) |
| axes[3] | Right Stick Y | `gamepad.rightStick.y` | -1.0 (up) to 1.0 (down) |
**IMPORTANT**: Y-axis is inverted from screen coordinates. Up on the stick = negative value.
## Steam Deck Specifics
### What IS available via Gamepad API in browser:
- All 17 standard buttons (indices 0-16)
- 4 axes (2 analog sticks)
- D-pad as buttons (not axes)
- Triggers as analog buttons (0.0-1.0)
### What is NOT available via standard Gamepad API:
- Back grip buttons (L4, L5, R4, R5) - only available via Steam Input remapping
- Trackpads - can be mapped to mouse via Steam Input, not to gamepad buttons
- Gyroscope/accelerometer - not exposed to browser Gamepad API
- Touchscreen - use standard DOM touch events, not gamepad
### Steam Input Remapping
Users can remap back grips and trackpads via Steam Input overlay. Do NOT rely on these for core gameplay. Treat them as optional convenience bindings only.
---
## Phaser 3 Gamepad Integration
### Enable Gamepad Input in Config
```typescript
const config: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO,
width: 1280,
height: 800,
input: {
gamepad: true, // REQUIRED: enable gamepad support
},
// ... rest of config
};
Detect Gamepad Connection
class GameScene extends Phaser.Scene {
private pad: Phaser.Input.Gamepad.Gamepad | null = null;
create(): void {
if (this.input.gamepad) {
// If already connected
if (this.input.gamepad.total > 0) {
this.pad = this.input.gamepad.getPad(0);
}
// Listen for new connections
this.input.gamepad.once(
"connected",
(pad: Phaser.Input.Gamepad.Gamepad) => {
this.pad = pad;
}
);
}
}
}
Read Input in Update Loop
update(time: number, delta: number): void {
if (!this.pad) return;
// Analog sticks (apply deadzone)
const DEADZONE = 0.15;
const lx = Math.abs(this.pad.leftStick.x) > DEADZONE ? this.pad.leftStick.x : 0;
const ly = Math.abs(this.pad.leftStick.y) > DEADZONE ? this.pad.leftStick.y : 0;
// Face buttons (boolean)
if (this.pad.A) { /* primary action */ }
if (this.pad.B) { /* secondary action */ }
if (this.pad.X) { /* tertiary action */ }
if (this.pad.Y) { /* quaternary action */ }
// Shoulder buttons
if (this.pad.L1) { /* left bumper */ }
if (this.pad.R1) { /* right bumper */ }
// Triggers (analog 0.0 to 1.0)
const leftTrigger = this.pad.L2; // float
const rightTrigger = this.pad.R2; // float
// D-pad (boolean)
if (this.pad.up) { /* d-pad up */ }
if (this.pad.down) { /* d-pad down */ }
if (this.pad.left) { /* d-pad left */ }
if (this.pad.right) { /* d-pad right */ }
// Buttons by index (for View/Menu/Stick press)
if (this.pad.isButtonDown(8)) { /* View/Back */ }
if (this.pad.isButtonDown(9)) { /* Menu/Start */ }
if (this.pad.isButtonDown(10)) { /* Left Stick Press */ }
if (this.pad.isButtonDown(11)) { /* Right Stick Press */ }
}
Dual-Input System (Keyboard + Gamepad)
Every game MUST support both keyboard and gamepad simultaneously. Use this unified input pattern:
InputState Interface
The InputState type is exported directly from @sdr/engine. Import and use it:
import { InputManager } from "@sdr/engine";
import type { InputState } from "@sdr/engine";
// In create():
this.inputManager = new InputManager(this);
this.inputManager.setup(); // handles gamepad, keyboard, AND touch (virtual joystick)
// In onUpdate():
const input: InputState = this.inputManager.getState();
The InputState interface:
interface InputState {
moveX: number; // -1.0 to 1.0 (horizontal movement)
moveY: number; // -1.0 to 1.0 (vertical movement)
aimX: number; // -1.0 to 1.0 (right stick horizontal)
aimY: number; // -1.0 to 1.0 (right stick vertical)
action1: boolean; // A / Space / touch-A — primary action
action2: boolean; // B / Shift / touch-B — secondary action
action3: boolean; // X / E — tertiary action
action4: boolean; // Y / Q — quaternary action
bumperLeft: boolean; // LB / Tab
bumperRight: boolean; // RB / R
triggerLeft: number; // LT (0.0 to 1.0)
triggerRight: number; // RT (0.0 to 1.0)
pause: boolean; // Start / Escape
lastDevice: "keyboard" | "gamepad" | "touch";
}
Touch Controls (Mobile)
InputManager.setup() automatically detects touch devices and creates:
- Virtual joystick in the lower-left corner (provides
moveX/moveY) - A button (lower-right, provides
action1) - B button (upper-right of A, provides
action2)
No extra setup needed — works transparently alongside keyboard/gamepad.
Default Control Scheme
| Action | Gamepad | Keyboard (Player 1) | Description |
|---|---|---|---|
| Move | Left Stick | WASD | Character movement |
| Aim/Look | Right Stick | Mouse (if available) | Camera or aim direction |
| Primary Action | A | Space | Jump, select, confirm |
| Secondary Action | B | Shift / Right-Click | Dodge, cancel, back |
| Tertiary Action | X | E | Interact, use item |
| Quaternary Action | Y | Q | Inventory, special ability |
| Bumper Left | LB | Tab | Cycle left, previous weapon |
| Bumper Right | RB | R | Cycle right, next weapon |
| Trigger Left | LT | (none) | Aim down sights, brake |
| Trigger Right | RT | Left-Click | Shoot, accelerate |
| Pause | Menu/Start | Escape | Pause menu |
| D-pad | D-pad | Arrow keys | Menu navigation, quick select |
Using InputManager (ALWAYS use this, never roll your own)
InputManager from @sdr/engine handles all three input sources and returns a unified InputState. It also creates the virtual joystick on touch devices automatically.
// In create():
this.inputManager = new InputManager(this);
this.inputManager.setup();
// In onUpdate():
const input = this.inputManager.getState();
// input.moveX, input.moveY, input.action1, input.lastDevice, etc.
Do NOT write your own readInput function or manage raw gamepad/keyboard objects. Use InputManager.
Required Patterns for All Generated Games
1. Always Apply Deadzone to Analog Sticks
// BAD: Raw stick values drift when idle
const x = gamepad.leftStick.x;
// GOOD: Apply deadzone
const DEADZONE = 0.15;
const x = Math.abs(gamepad.leftStick.x) > DEADZONE ? gamepad.leftStick.x : 0;
2. Always Handle Missing Gamepad Gracefully
// BAD: Assumes gamepad exists
const x = this.pad!.leftStick.x;
// GOOD: Null-safe access
const x = this.pad?.leftStick.x ?? 0;
3. Always Support Both Input Methods
Every game must be fully playable with EITHER keyboard OR gamepad. Never require both. Never require ONLY gamepad.
4. Use Delta Time for All Movement
// BAD: Frame-rate dependent
player.x += speed;
// GOOD: Frame-rate independent
player.x += speed * dt;
5. Screen Resolution
Always target 1280x800 (Steam Deck native resolution). Use Phaser.Scale.FIT with CENTER_BOTH to handle different aspect ratios.
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
width: 1280,
height: 800,
}
6. Readable UI at Steam Deck Size
The Steam Deck has a 7" 1280x800 screen. Minimum text size should be 18px. Buttons and interactive elements should have at least 48x48px touch targets. Use high-contrast colors.
7. Normalize Diagonal Movement
// Prevent faster diagonal movement
let dx = state.moveX;
let dy = state.moveY;
const mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 1) {
dx /= mag;
dy /= mag;
}
8. Button Prompts
When showing button prompts in-game, detect whether the last input was from gamepad or keyboard and show the appropriate icon/text:
// Track last input device
let lastDevice: "keyboard" | "gamepad" = "keyboard";
// In update:
if (pad && (Math.abs(pad.leftStick.x) > 0.1 || pad.A || pad.B)) {
lastDevice = "gamepad";
}
if (anyKeyDown) {
lastDevice = "keyboard";
}
// In UI:
const actionLabel = lastDevice === "gamepad" ? "[A]" : "[SPACE]";
Common Pitfalls
- Do not use button index 16 (Steam/Home) - This is the system button. It opens the Steam overlay.
- Do not require back grip buttons (L4/L5/R4/R5) - They are not exposed to the standard Gamepad API.
- Do not require trackpad input - It may be mapped to mouse, or disabled entirely.
- Do not forget the deadzone - Steam Deck sticks have slight drift. Always use >= 0.15 deadzone.
- Do not assume gamepad is always connected - The browser may not report it until the user presses a button.
- Do not use
gamepad.vibration- Steam Deck supports haptics but browser API support is inconsistent on Linux. - Do not hard-code button indices - Use Phaser's named properties (
.A,.B,.L1, etc.) when available.