Skip to content
Phaser Input & Audio — Keyboard, Touch, Gamepad & Sound (Practical Guide)

Phaser Input & Audio — Keyboard, Touch, Gamepad & Sound (Practical Guide)

DodaTech Updated Jun 6, 2026 15 min read

Phaser Input & Audio — Keyboard, Touch, Gamepad & Sound (Practical Guide)

Handle keyboard, mouse, touch, and gamepad input plus audio playback in Phaser — build interactive games with sound effects, music, audio sprites, and stereo panning controls.

What You’ll Learn

  • How Phaser’s Input Manager unifies keyboard, mouse, touch, and gamepad
  • Reading keyboard input with cursor keys, WASD, and key events
  • Handling mouse clicks, touch taps, drag-and-drop, and multi-touch
  • Detecting gamepad buttons and analog sticks
  • Playing audio with volume, pan, rate, and loop controls
  • Using audio sprites to reduce HTTP requests
  • Building an interactive audio visualizer with waveform display

Phaser input is the bridge between the player and the game. Without responsive input and immersive audio, a game feels lifeless.

Why Input & Audio Matters

A game can have beautiful graphics and clever mechanics, but if the controls feel sluggish or the audio is missing, players notice immediately. Phaser’s Input Manager normalizes all input sources into one consistent API — whether the player uses a keyboard, taps a phone screen, or plugs in a gamepad.

At DodaTech, security training simulators use multi-input support for different training environments: keyboard shortcuts for desktop labs, touch controls for mobile security quizzes, and gamepad input for physical security walkthroughs. Audio feedback cues trainees when they enter restricted zones or trigger alerts. These same techniques are used in tools built by the developers of Doda Browser and Durga Antivirus Pro.

Prerequisites: Complete the Phaser tutorial first. You should understand scene lifecycle, game config, and how to load assets. Basic JavaScript event handling concepts assumed.

Learning Path

    flowchart LR
    A[Getting Started] --> B[Sprites & Physics]
    B --> C[Scenes & State]
    C --> D[Input & Audio]
    D --> E[Tilemaps & Level Design]
    D -->|You are here| D
  

The Input Manager — A Universal Translator

Think of the Input Manager as a universal translator. You press a key, tap the screen, or click a mouse — each produces different hardware signals. The Input Manager converts all of them into consistent Phaser events.

Access input through this.input in any scene:

// Enable input on a game object
const sprite = this.add.sprite(400, 300, 'texture');
sprite.setInteractive({ useHandCursor: true });

// Pointer events — work for mouse AND touch
sprite.on('pointerdown', (pointer) => { /* clicked or tapped */ });
sprite.on('pointerover', () => { /* hover (desktop only) */ });
sprite.on('pointerout', () => { /* unhover */ });
sprite.on('drag', (pointer, dragX, dragY) => { /* dragging */ });

Why setInteractive() is required: By default, sprites don’t listen for input. Calling setInteractive() tells Phaser to track the sprite’s hit area and fire events when the pointer interacts with it. Without it, pointerdown events never fire.

Keyboard Input — Three Approaches

Phaser gives you three ways to read keyboard input, each suited to different situations:

1. Cursor Keys (Quick Arrow Movement)

Pre-built object for arrow keys, space, and shift. Best for simple player movement:

this.cursors = this.input.keyboard.createCursorKeys();
// Available: .up, .down, .left, .right, .space, .shift
if (this.cursors.left.isDown) { /* move left */ }

2. Custom Key Codes (WASD, Jump, etc.)

For any key on the keyboard. Use addKey() with the key’s code:

this.keys = this.input.keyboard.addKeys({
  up: Phaser.Input.Keyboard.KeyCodes.W,
  down: Phaser.Input.Keyboard.KeyCodes.S,
  left: Phaser.Input.Keyboard.KeyCodes.A,
  right: Phaser.Input.Keyboard.KeyCodes.D,
  jump: Phaser.Input.Keyboard.KeyCodes.SPACE
});

3. Key Events (One-Shot Actions)

Use events for actions that should fire once per press (jumping, shooting, opening a menu):

this.input.keyboard.on('keydown-SPACE', () => {
  console.log('Space pressed — fire once');
});

this.input.keyboard.on('keyup-SPACE', () => {
  console.log('Space released');
});

// Global keydown listener
this.input.keyboard.on('keydown', (event) => {
  console.log(`Key pressed: ${event.key}`);
});

Which approach should you use? isDown for continuous actions (movement, accelerating). JustDown() for single-fire actions (jump, shoot). Key events for menu navigation and debug toggles.

Preventing Browser Defaults

Some keys trigger browser actions (Space scrolls the page, arrows scroll in Firefox). Prevent this:

this.input.keyboard.addCapture([
  Phaser.Input.Keyboard.KeyCodes.SPACE,
  Phaser.Input.Keyboard.KeyCodes.UP,
  Phaser.Input.Keyboard.KeyCodes.DOWN
]);

Pointer Input — Mouse and Touch

Phaser normalizes mouse and touch into a single “pointer” API. This means your game automatically works on desktop (mouse clicks) and mobile (touch taps) without any extra code.

Single Pointer (Mouse or One Finger)

// Click/tap anywhere
this.input.on('pointerdown', (pointer) => {
  console.log(`Clicked at (${pointer.x}, ${pointer.y})`);
  console.log(`World position: (${pointer.worldX}, ${pointer.worldY})`);
});

// Track if pointer is held
this.input.on('pointerdown', () => { this.isPointerDown = true; });
this.input.on('pointerup', () => { this.isPointerDown = false; });

// Current pointer state (check in update())
if (this.input.activePointer.isDown) { /* pointer is held */ }

pointer.x vs pointer.worldX: x and y are screen coordinates (where you clicked on the canvas). worldX and worldY account for camera scrolling — use these when clicking on a tilemap with a moving camera.

Multi-Touch (Multiple Fingers)

For mobile games that need two-finger controls (pinch to zoom, dual stick):

this.input.addPointer(3);  // Support up to 4 simultaneous touches

this.input.on('pointerdown', (pointer) => {
  console.log(`Touch ${pointer.id} started at (${pointer.x}, ${pointer.y})`);
});

Drag and Drop

Make objects draggable with minimal code:

const obj = this.add.sprite(400, 300, 'texture');
obj.setInteractive({ draggable: true, useHandCursor: true });

obj.on('dragstart', () => { /* start dragging */ });
obj.on('drag', (pointer, dragX, dragY) => {
  obj.x = dragX;
  obj.y = dragY;
});
obj.on('dragend', () => { /* released */ });

// Optional: minimum drag distance before it counts
obj.input.dragDistanceThreshold = 10;  // 10 pixels minimum

Mobile CSS Requirement: On mobile, add canvas { touch-action: none; } to your CSS. Without it, the browser intercepts touch events for scrolling and zooming, breaking your game’s input.

Gamepad Support

Gamepad input uses the W3C Gamepad API. Phaser wraps it into a clean interface:

update() {
  const pads = this.input.gamepad.gamepads;
  if (pads[0]) {
    const pad = pads[0];

    // Buttons
    if (pad.A) { /* A button pressed */ }
    if (pad.B) { /* B button */ }
    if (Phaser.Input.Gamepad.JustDown(pad, 0)) { /* button 0 just pressed */ }

    // Analog sticks
    const leftX = pad.leftStick.x;   // -1 (left) to 1 (right)
    const leftY = pad.leftStick.y;   // -1 (up) to 1 (down)
    const rightX = pad.rightStick.x;

    // D-pad
    if (pad.up) { }
  }
}

Dead zone for analog sticks: Analog sticks drift slightly from center even when untouched. Apply a threshold:

const threshold = 0.15;
const leftX = Math.abs(pad.leftStick.x) > threshold ? pad.leftStick.x : 0;

Comparing Input Methods

InputBest ForSetup ComplexityNotes
KeyboardDesktop games, menusSimplecreateCursorKeys() for arrows
MousePoint-and-click, strategySimpleWorks as pointer by default
TouchMobile gamesSimpleAdd touch-action: none CSS
GamepadConsole-style gamesModerateEach browser/OS maps buttons differently

Audio — Making Your Game Sound Alive

Phaser uses the Web Audio API by default, with HTML5 Audio as a fallback. Web Audio is vastly superior — lower latency (~10ms vs ~100ms+), supports effects (pan, filter, spatial), and can play hundreds of sounds simultaneously.

Loading and Playing Audio

// In preload()
this.load.audio('sfx', ['sfx.mp3', 'sfx.ogg']);
this.load.audio('music', ['music.mp3', 'music.ogg']);

// In create()
const sfx = this.sound.play('sfx');  // One-shot sound effect
const music = this.sound.play('music', {
  volume: 0.5,
  loop: true,       // Background music loops
  rate: 1.0,
  detune: 0,
  seek: 0,
  delay: 0
});

Why provide two formats? MP3 is universally supported, but Firefox prefers OGG (no patent licensing issues). Providing both ensures your game works in every browser.

Controlling Sound Instances

const snd = this.sound.add('sfx');

snd.play();
snd.pause();
snd.resume();
snd.stop();

snd.volume = 0.8;          // 0 (silent) to 1 (full)
snd.rate = 1.5;            // Playback speed (1.0 = normal)
snd.detune = 200;          // Pitch shift in cents (100 = 1 semitone)
snd.seek = 5;              // Jump to 5 seconds

snd.on('complete', () => { /* finished playing */ });

Volume, Pan, and Spatial Audio

// Master volume (affects ALL sounds)
this.sound.volume = 0.7;

// Per-sound volume
const sfx = this.sound.add('sfx');
sfx.setVolume(0.5);

// Stereo pan (Web Audio only) — -1 (left) to 1 (right)
sfx.setPan(-0.5);

// Spatial audio — position sounds in 3D space
const snd = this.sound.add('engine', { spatial: true });
snd.setPosition(x, y, z);

// Update listener to follow the player
this.sound.setListenerPosition(player.x, player.y, 0);

// Spatial config
snd.setRefDistance(50);      // Distance where volume halves
snd.setRolloffFactor(1);     // How quickly volume decreases
snd.setMaxDistance(500);     // Sound cutoff distance

When to use spatial audio: In top-down games, position sounds at the source (enemy gunfire, explosion, footsteps). As the player moves away, the sound gets quieter and pans to the correct ear. DodaTech security training simulators use spatial audio for directional alerts — a beep to the left means “threat on your left.”

Audio Sprites — One File, Many Sounds

An audio sprite is a single audio file with multiple labelled sections. Think of it as a spritesheet for audio. Instead of 10 HTTP requests for 10 sound effects, you make 1 request for 1 file:

// Load audio sprite
this.load.audioSprite('sfx', 'assets/audiosprite.json', [
  'assets/audiosprite.mp3',
  'assets/audiosprite.ogg'
]);

// Play a section
this.sound.playAudioSprite('sfx', 'coin_pickup');
this.sound.playAudioSprite('sfx', 'jump');
this.sound.playAudioSprite('sfx', 'death');

The JSON file defines the sections:

{
  "resources": ["audiosprite.mp3", "audiosprite.ogg"],
  "spritemap": {
    "coin_pickup": { "start": 0, "end": 0.5 },
    "jump":         { "start": 0.6, "end": 1.0 },
    "death":        { "start": 1.1, "end": 2.0 }
  }
}

When to use audio sprites: When you have more than 5 short sound effects. The trade-off is slightly more complex setup vs. dramatically faster loading.

Web Audio vs HTML5 Audio

FeatureWeb AudioHTML5 Audio
Latency~10ms~100ms+
Simultaneous soundsHundreds (polyphonic)Limited (varies by browser)
EffectsPan, filter, spatial, convolutionNone
Audio spritesYesYes
Browser supportModern browsersAll browsers
Battery drainSlightly higherLower

Phaser auto-detects: uses Web Audio when available, falls back to HTML5 Audio.

You Might Be Wondering…

“Why won’t my audio play on the first scene?” Modern browsers require a user gesture (click, tap, keypress) before playing audio. This is the autoplay policy. Wrap your first sound.play() in a pointerdown handler: this.input.once('pointerdown', () => { this.sound.play('bgm'); });.

“Why does my gamepad not work on some browsers?” The W3C Gamepad API support varies. Chrome has the best support. Firefox requires about:config changes for some controllers. Always provide keyboard fallbacks.

“Can I play multiple sounds at once?” Yes. Each this.sound.play() creates a new sound instance. Web Audio supports hundreds simultaneously.

Common Mistakes

  1. Playing audio before user interaction — Browsers block autoplay audio. Always wrap the first sound.play() in a pointerdown, keydown, or pointerup handler: this.input.once('pointerdown', () => { this.sound.play('bgm'); });.

  2. Not providing multiple audio formats — MP3 works everywhere except some Firefox builds prefer OGG. Always provide both: ['audio.mp3', 'audio.ogg'].

  3. Forgetting to stop looping sounds on scene change — Looping audio continues playing across scenes. Stop it in shutdown(): this.sound.stopByKey('bgm'); this.sound.removeByKey('sfx');.

  4. Using JustDown on held keysJustDown only returns true on the first frame the key is pressed. If held, it returns false. Use isDown for continuous hold detection.

  5. Overriding pointer event handlers with this.input.onthis.input.on('pointerdown', ...) adds a listener — it doesn’t replace existing ones. Use this.input.off('pointerdown') first to reset if needed.

  6. Touch input broken on mobile — Add canvas { touch-action: none; } to your CSS to prevent browser scroll/zoom from hijacking touch events.

  7. Gamepad axis without dead zone — Analog sticks drift slightly from center. Always apply a threshold: Math.abs(axis) > 0.15 ? axis : 0.

Practice Questions

  1. Why do modern browsers require a user gesture before playing audio? To prevent unwanted autoplay (ads, spam). The autoplay policy requires a click, tap, or keypress before sound.play() works.

  2. What’s the difference between isDown and JustDown() for keyboard input? isDown returns true every frame the key is held. JustDown() only returns true on the first frame it’s pressed. Use isDown for continuous movement, JustDown for one-shot actions like jumping.

  3. How do you make a game object draggable with the mouse? Call sprite.setInteractive({ draggable: true }), then listen for dragstart, drag, and dragend events on the sprite.

  4. What is an audio sprite and when should you use one? An audio sprite is a single audio file with multiple labelled segments. Use it when you have many short sound effects — it reduces HTTP requests from N to 1 and prevents per-file decode latency.

  5. Challenge: Extend the Audio Visualizer below to add a fifth note (T key, frequency 587.33 Hz — D5) and implement an “echo” effect using ctx.createDelay() and ctx.createGain() in Web Audio. The echoed note should play 200ms after the original at 40% volume.

FAQ

How do I mute all sounds at once?
Set this.sound.mute = true to toggle mute globally. The volume level is preserved when unmuted.
What audio formats should I use for browser games?
MP3 and OGG cover all major browsers. Optionally add M4A (AAC) for Safari. Phaser accepts [.mp3, .ogg] and picks the first supported format.
Can I play multiple sounds at the same time?
Yes. Each call to this.sound.play() creates a new sound instance. Phaser supports polyphonic playback — hundreds of simultaneous sounds with Web Audio.
How do I check if Web Audio is supported?
this.sound.locked is true if audio playback requires a user gesture. this.sound.context exists when Web Audio is active.
What is an audio sprite and when should I use one?
An audio sprite is a single audio file with multiple labelled segments. Use it when you have many short SFX — it reduces HTTP requests from N to 1.

Try It Yourself

This interactive audio visualizer generates tones procedurally (no external audio files needed), displays a waveform, and lets you control volume, pan, and playback rate:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Audio Visualizer</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      background: #0f0f23;
      font-family: monospace;
    }
    #game-container { position: relative; }
    canvas { touch-action: none; }
    #controls {
      position: absolute;
      bottom: 10px; left: 50%;
      transform: translateX(-50%);
      z-index: 10;
      background: rgba(0,0,0,0.8);
      padding: 12px 20px;
      border-radius: 8px;
      color: #fff;
      font-size: 13px;
      display: flex;
      gap: 20px;
      align-items: center;
    }
    #controls label { display: flex; align-items: center; gap: 6px; }
    #controls input[type="range"] { width: 80px; }
    .key-hint {
      background: #333; color: #ffd700;
      padding: 2px 8px; border-radius: 4px; font-weight: bold;
    }
  </style>
</head>
<body>
<div id="game-container">
  <div id="controls">
    <label>Vol <input type="range" id="volume-slider" min="0" max="100" value="60"></label>
    <label>Pan <input type="range" id="pan-slider" min="-100" max="100" value="0"></label>
    <label>Rate <input type="range" id="rate-slider" min="25" max="200" value="100"></label>
    <span><span class="key-hint">Q</span> <span class="key-hint">W</span> <span class="key-hint">E</span> <span class="key-hint">R</span> — play</span>
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
<script>
class AudioVisScene extends Phaser.Scene {
  constructor() { super({ key: 'AudioVisScene' }); }
  preload() { this.generateAudioTextures(); }
  generateAudioTextures() {
    this.audioBuffers = {};
    const sampleRate = 44100;
    const duration = 0.5;
    const numSamples = sampleRate * duration;
    const notes = {
      'q': { freq: 261.63, color: '#ff6b35' },
      'w': { freq: 329.63, color: '#4ecdc4' },
      'e': { freq: 392.00, color: '#ffd700' },
      'r': { freq: 523.25, color: '#e63946' }
    };
    Object.entries(notes).forEach(([key, { freq, color }]) => {
      const ctx = this.sound.context;
      const buffer = ctx.createBuffer(1, numSamples, sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < numSamples; i++) {
        const t = i / sampleRate;
        let sample = Math.sin(2 * Math.PI * freq * t);
        sample += 0.3 * Math.sin(2 * Math.PI * freq * 2 * t);
        sample += 0.1 * Math.sin(2 * Math.PI * freq * 3 * t);
        const env = Math.min(1, (numSamples - i) / (sampleRate * 0.05));
        data[i] = sample * env * 0.3;
      }
      this.audioBuffers[key] = { buffer, color };
    });
  }
  create() {
    this.cameras.main.setBackgroundColor('#0a0a1a');
    this.add.text(400, 30, 'Audio Visualizer', {
      fontSize: '28px', fill: '#fff', fontFamily: 'monospace'
    }).setOrigin(0.5);
    this.add.text(400, 65, 'Press Q / W / E / R to play notes', {
      fontSize: '14px', fill: '#888', fontFamily: 'monospace'
    }).setOrigin(0.5);
    this.waveformGraphics = this.add.graphics();
    this.activeNotes = [];
    this.currentVolume = 0.6;
    this.currentPan = 0;
    this.currentRate = 1.0;
    this.setupKeyboard();
    this.setupControls();
    this.waveformData = new Float32Array(256);
    this.add.rectangle(400, 320, 700, 250, 0x111133, 0.5).setStrokeStyle(1, 0x333366);
  }
  setupKeyboard() {
    ['q', 'w', 'e', 'r'].forEach(key => {
      this.input.keyboard.on(`keydown-${key.toUpperCase()}`, () => { this.playNote(key); this.showKeyPress(key); });
    });
  }
  setupControls() {
    document.getElementById('volume-slider').addEventListener('input', () => { this.currentVolume = parseInt(document.getElementById('volume-slider').value) / 100; });
    document.getElementById('pan-slider').addEventListener('input', () => { this.currentPan = parseInt(document.getElementById('pan-slider').value) / 100; });
    document.getElementById('rate-slider').addEventListener('input', () => { this.currentRate = parseInt(document.getElementById('rate-slider').value) / 100; });
  }
  playNote(key) {
    const note = this.audioBuffers[key];
    if (!note) return;
    const ctx = this.sound.context;
    const source = ctx.createBufferSource();
    const gainNode = ctx.createGain();
    const panNode = ctx.createStereoPanner();
    source.buffer = note.buffer;
    source.playbackRate.value = this.currentRate;
    gainNode.gain.value = this.currentVolume;
    panNode.pan.value = this.currentPan;
    source.connect(panNode);
    panNode.connect(gainNode);
    gainNode.connect(ctx.destination);
    source.start();
    this.activeNotes.push({ note: key, color: note.color, startTime: Date.now(), duration: 500 });
    for (let i = 0; i < 256; i++) {
      const t = i / 256;
      let sum = 0;
      this.activeNotes.forEach(n => {
        const elapsed = Date.now() - n.startTime;
        const decay = Math.max(0, 1 - elapsed / n.duration);
        const freq = { q: 261.63, w: 329.63, e: 392.00, r: 523.25 }[n.note] || 261.63;
        sum += Math.sin(2 * Math.PI * freq * t * (this.currentRate * 0.3)) * decay;
      });
      this.waveformData[i] = sum;
    }
  }
  showKeyPress(key) {
    const labels = { q: 'C4', w: 'E4', e: 'G4', r: 'C5' };
    const colors = { q: '#ff6b35', w: '#4ecdc4', e: '#ffd700', r: '#e63946' };
    const txt = this.add.text(400, 420, labels[key], {
      fontSize: '40px', fill: colors[key], fontFamily: 'monospace', stroke: '#000', strokeThickness: 4
    }).setOrigin(0.5).setAlpha(0.9);
    this.tweens.add({ targets: txt, alpha: 0, y: 380, duration: 600, ease: 'Cubic.easeOut', onComplete: () => txt.destroy() });
  }
  update() { this.renderWaveform(); }
  renderWaveform() {
    const g = this.waveformGraphics;
    g.clear();
    const centerX = 400, centerY = 320, width = 660, height = 200, startX = centerX - width / 2;
    if (this.activeNotes.length > 0) {
      g.lineStyle(3, 0x4ecdc4, 0.8);
      g.beginPath(); g.moveTo(startX, centerY);
      for (let i = 0; i < 256; i++) {
        const x = startX + (i / 256) * width;
        const sample = this.waveformData[i] || 0;
        g.lineTo(x, centerY + sample * height * 0.4);
      }
      g.strokePath();
      g.lineStyle(1, 0x4ecdc4, 0.2);
      g.fillStyle(0x4ecdc4, 0.05);
      g.beginPath(); g.moveTo(startX, centerY);
      for (let i = 0; i < 256; i++) {
        const x = startX + (i / 256) * width;
        const sample = this.waveformData[i] || 0;
        g.lineTo(x, centerY + sample * height * 0.4);
      }
      g.lineTo(startX + width, centerY); g.closePath(); g.fillPath();
    }
    this.activeNotes = this.activeNotes.filter(n => (Date.now() - n.startTime) < n.duration);
  }
}
const config = {
  type: Phaser.AUTO, width: 800, height: 600,
  parent: 'game-container',
  audio: { disableWebAudio: false },
  scene: [AudioVisScene]
};
new Phaser.Game(config);
</script>
</body>
</html>

This demonstrates: programmatic audio buffer generation (synthesis), keyboard-triggered note playback, real-time volume/pan/rate controls, waveform visualization using Phaser Graphics, Web Audio API stereo panning and gain nodes, and tween-based UI feedback.

What’s Next

TopicDescription
Phaser TilemapsLevel design with Tiled, tile layers, collision tiles, and procedural generation
Phaser Scenes & StateReview multi-scene architecture and data persistence
Phaser Sprites & PhysicsReview Arcade physics foundations — collisions, velocity, groups
Phaser Getting StartedReview game config, scene lifecycle, and asset loading fundamentals
JavaScript EventsReview event-driven programming patterns used in input handling

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Multi-input support and spatial audio are core to DodaTech’s security training simulators, enabling realistic scenario-based training across desktop, mobile, and VR environments.

What’s Next

Congratulations on completing this Phaser Input Audio tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro