Phaser Input & Audio — Keyboard, Touch, Gamepad & Sound (Practical Guide)
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.
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
| Input | Best For | Setup Complexity | Notes |
|---|---|---|---|
| Keyboard | Desktop games, menus | Simple | createCursorKeys() for arrows |
| Mouse | Point-and-click, strategy | Simple | Works as pointer by default |
| Touch | Mobile games | Simple | Add touch-action: none CSS |
| Gamepad | Console-style games | Moderate | Each 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
| Feature | Web Audio | HTML5 Audio |
|---|---|---|
| Latency | ~10ms | ~100ms+ |
| Simultaneous sounds | Hundreds (polyphonic) | Limited (varies by browser) |
| Effects | Pan, filter, spatial, convolution | None |
| Audio sprites | Yes | Yes |
| Browser support | Modern browsers | All browsers |
| Battery drain | Slightly higher | Lower |
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
Playing audio before user interaction — Browsers block autoplay audio. Always wrap the first
sound.play()in apointerdown,keydown, orpointeruphandler:this.input.once('pointerdown', () => { this.sound.play('bgm'); });.Not providing multiple audio formats — MP3 works everywhere except some Firefox builds prefer OGG. Always provide both:
['audio.mp3', 'audio.ogg'].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');.Using
JustDownon held keys —JustDownonly returns true on the first frame the key is pressed. If held, it returns false. UseisDownfor continuous hold detection.Overriding pointer event handlers with
this.input.on—this.input.on('pointerdown', ...)adds a listener — it doesn’t replace existing ones. Usethis.input.off('pointerdown')first to reset if needed.Touch input broken on mobile — Add
canvas { touch-action: none; }to your CSS to prevent browser scroll/zoom from hijacking touch events.Gamepad axis without dead zone — Analog sticks drift slightly from center. Always apply a threshold:
Math.abs(axis) > 0.15 ? axis : 0.
Practice Questions
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.What’s the difference between
isDownandJustDown()for keyboard input?isDownreturns true every frame the key is held.JustDown()only returns true on the first frame it’s pressed. UseisDownfor continuous movement,JustDownfor one-shot actions like jumping.How do you make a game object draggable with the mouse? Call
sprite.setInteractive({ draggable: true }), then listen fordragstart,drag, anddragendevents on the sprite.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.
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()andctx.createGain()in Web Audio. The echoed note should play 200ms after the original at 40% volume.
FAQ
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
| Topic | Description |
|---|---|
| Phaser Tilemaps | Level design with Tiled, tile layers, collision tiles, and procedural generation |
| Phaser Scenes & State | Review multi-scene architecture and data persistence |
| Phaser Sprites & Physics | Review Arcade physics foundations — collisions, velocity, groups |
| Phaser Getting Started | Review game config, scene lifecycle, and asset loading fundamentals |
| JavaScript Events | Review 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