Quick answer: Add the Gamepad object to your project, read button presses with standard indices (0 for A, 1 for B, etc.), apply a dead zone of 0.15–0.25 on analog sticks to prevent drift, and duplicate your keyboard movement events with gamepad conditions. Track which input device was last used to show correct button prompts.

Controller support transforms a Construct 3 game from a browser toy into something that feels like a real console experience. The Gamepad plugin uses the standard Web Gamepad API, which means it works with Xbox, PlayStation, Switch Pro controllers, and most third-party gamepads out of the box. This guide covers everything from basic button detection to analog stick handling, dead zones, UI menu navigation, and displaying the right button prompts.

Setting Up the Gamepad Plugin

Right-click the project tree, choose Insert new object, and add a Gamepad object. Unlike sprites, the Gamepad object has no visual presence — it just listens for controller input. You only need one instance regardless of how many controllers are connected.

The Gamepad API uses a standard mapping for buttons and axes. The most common button indices are:

// Standard Gamepad Button Mapping
// Button 0  = A (Xbox) / Cross (PS) / B (Switch)
// Button 1  = B (Xbox) / Circle (PS) / A (Switch)
// Button 2  = X (Xbox) / Square (PS) / Y (Switch)
// Button 3  = Y (Xbox) / Triangle (PS) / X (Switch)
// Button 4  = Left Bumper (LB / L1)
// Button 5  = Right Bumper (RB / R1)
// Button 6  = Left Trigger (LT / L2)
// Button 7  = Right Trigger (RT / R2)
// Button 8  = Back / Select / Share
// Button 9  = Start / Menu / Options
// Button 12 = D-pad Up
// Button 13 = D-pad Down
// Button 14 = D-pad Left
// Button 15 = D-pad Right

// Standard Axis Mapping
// Axis 0 = Left stick horizontal (-1 left, +1 right)
// Axis 1 = Left stick vertical (-1 up, +1 down)
// Axis 2 = Right stick horizontal
// Axis 3 = Right stick vertical

Movement with Analog Sticks and Dead Zones

Reading the analog stick is straightforward — use Gamepad.Axis(0, axisIndex) where the first parameter is the gamepad index (0 for the first controller) and the second is the axis. The critical part is applying a dead zone:

// Movement with dead zone
// Global variable: DeadZone = 0.2
// Instance variable on Player: InputX, InputY (number)

System: Every tick
System: abs(Gamepad.Axis(0, 0)) > DeadZone
    → Player: Set InputX to Gamepad.Axis(0, 0)

System: Every tick
System: abs(Gamepad.Axis(0, 0)) ≤ DeadZone
    → Player: Set InputX to 0

// Apply input to movement
Player: InputX > 0
    → Player: Simulate Platform pressing Right

Player: InputX < 0
    → Player: Simulate Platform pressing Left

// Jump with A button (index 0)
Gamepad: On button 0 pressed (gamepad 0)
    → Player: Simulate Platform pressing Jump

For twin-stick shooters or games that need precise analog control, you may want to normalize the stick input and apply a non-linear curve for more precision near the center:

// In a script for more precise control
function applyDeadZone(value, deadZone) {
    const sign = Math.sign(value);
    const abs = Math.abs(value);
    if (abs < deadZone) return 0;
    // Remap so that just past the dead zone starts at 0
    return sign * (abs - deadZone) / (1 - deadZone);
}

Supporting Keyboard and Gamepad Simultaneously

Most players expect to switch between keyboard and controller seamlessly. The easiest approach is to check both input sources for every action:

// Unified input: move right with keyboard OR controller
Keyboard: Right arrow is downPlayer: Simulate Platform pressing Right

Gamepad: Axis(0, 0) > DeadZone
    → Player: Simulate Platform pressing Right

// Track last input device for button prompts
// Global variable: LastInputDevice = "keyboard"

Keyboard: On any key pressedSystem: Set LastInputDevice to "keyboard"

Gamepad: On any button pressed (gamepad 0)
    → System: Set LastInputDevice to "gamepad"

// Show correct prompt icon based on LastInputDevice
System: LastInputDevice = "keyboard"PromptIcon: Set animation to "keyboard-space"
System: ElsePromptIcon: Set animation to "gamepad-a"

Menu Navigation with D-pad

Menus need special handling because the D-pad fires discrete presses rather than continuous input. Track a selected index and move it on button press:

// Menu navigation with gamepad
// Global variable: SelectedIndex = 0
// Global variable: MenuItemCount = 4

Gamepad: On button 13 pressed (D-pad Down)
    → System: Set SelectedIndex to
      (SelectedIndex + 1) % MenuItemCountAudio: Play "menu-move"

Gamepad: On button 12 pressed (D-pad Up)
    → System: Set SelectedIndex to
      (SelectedIndex - 1 + MenuItemCount) % MenuItemCountAudio: Play "menu-move"

Gamepad: On button 0 pressed (A / Confirm)
    → Function: Call "ActivateMenuItem" (SelectedIndex)

// Highlight the selected menu item visually
System: For each MenuItem
MenuItem: Index = SelectedIndex
    → MenuItem: Set opacity to 100
System: ElseMenuItem: Set opacity to 50

Vibration and Multiple Controllers

The Gamepad API supports haptic feedback on controllers that have vibration motors. Use it sparingly for impacts and important events:

// Vibrate on damage (scripting)
const gamepads = navigator.getGamepads();
if (gamepads[0]?.vibrationActuator) {
    gamepads[0].vibrationActuator.playEffect("dual-rumble", {
        startDelay: 0,
        duration: 200,
        weakMagnitude: 0.5,
        strongMagnitude: 0.8
    });
}

For local multiplayer, use the gamepad index parameter (the first argument in Gamepad.Axis(gamepadIndex, axisIndex)) to distinguish between controllers. Gamepad 0 controls player 1, gamepad 1 controls player 2, and so on.

Related Issues

If touch input is not working alongside your controller setup, see Fix: Construct 3 Touch Input Not Working on Mobile. For performance issues that can cause input lag, check Fix: Construct 3 Performance Low FPS Lag. If exported builds handle input differently than the editor, see Fix: Construct 3 Exported Game Not Running.

Always apply dead zones. Without them, players will think your game is broken when their character drifts on its own.