← Back to all posts
Part 6: Player Interaction - Input Systems
February 25, 2026
Game Development
Phaser.js
Cross-platform controls that work everywhere
The Interface Between Player and Game
Input is where intent becomes action. A player thinks "jump" and presses a button. Bad input handling creates a disconnect—they pressed jump but nothing happened. Good input handling feels invisible —thought and action are one.
Phaser's input system unifies mouse, touch, keyboard, and gamepad into a single coherent API. Let's explore the 10 patterns that make controls responsive, accessible, and cross-platform.
Pattern 1: Pointer Events - Unified Mouse + Touch
// Make sprite interactive
const button = this.add.sprite(400, 300, 'button').setInteractive();
button.on('pointerdown', () => button.setTint(0xff0000));
button.on('pointerup', () => button.clearTint());
button.on('pointerover', () => button.setScale(1.1));
button.on('pointerout', () => button.setScale(1.0));
// Scene-level (anywhere on screen)
this.input.on('pointerdown', (pointer) => {
this.spawnEffect(pointer.worldX, pointer.worldY);
// Check buttons
if (pointer.leftButtonDown()) { /* primary action */ }
if (pointer.rightButtonDown()) { /* context menu */ }
});
Why Essential : One API for mouse and touch . Code once, works on desktop and mobile. No platform detection. No separate codepaths.
Pointer Properties : x, y (screen), worldX, worldY (camera-adjusted), velocity, distance, duration (for gesture detection).
Pattern 2: Drag and Drop - Intuitive Manipulation
const item = this.add.sprite(400, 300, 'item').setInteractive();
this.input.setDraggable(item);
item.on('drag', (pointer, dragX, dragY) => {
// Snap to grid
item.x = Math.round(dragX / 32) * 32;
item.y = Math.round(dragY / 32) * 32;
});
// Drop zones
const dropZone = this.add.zone(400, 300, 200, 200)
.setRectangleDropZone(200, 200);
item.on('drop', (pointer, dropZone) => {
item.x = dropZone.x;
item.y = dropZone.y;
this.sound.play('itemPlaced');
});
// Return to origin if not dropped
item.on('dragend', (pointer, dropped) => {
if (!dropped) {
this.tweens.add({
targets: item,
x: item.input.dragStartX,
y: item.input.dragStartY,
duration: 200
});
}
});
Why Essential : Inventory systems . Card games (hand → table). Building placement . Puzzle arrangement . Drag-and-drop is how modern interfaces work—players expect it.
Pattern 3: Keyboard Input - Responsive Controls
create() {
this.cursors = this.input.keyboard.createCursorKeys();
this.keySpace = this.input.keyboard.addKey('SPACE');
// Event-driven
this.keySpace.on('down', () => player.shoot());
}
update() {
// Polling (for continuous movement)
if (this.cursors.left.isDown) player.x -= 5;
if (this.cursors.right.isDown) player.x += 5;
// Diagonal movement
if (this.cursors.up.isDown && this.cursors.right.isDown) {
// Move northeast
}
}
Polling vs Events :
Polling (isDown in update): Continuous actions (movement)
Events (on('down')): Single actions (jump, shoot)
Why Essential : Precision control . Accessibility (keyboard-only players). Hotkeys (1-9 for abilities). Debug tools (F-keys for cheats).
Pattern 4: Gamepad Support - Console Feel
create() {
this.input.gamepad.once('connected', (pad) => {
this.gamepad = pad;
console.log('Gamepad connected:', pad.id);
});
}
update() {
if (!this.gamepad) return;
// Left stick (movement)
const leftStick = this.gamepad.leftStick;
if (leftStick.x !== 0 || leftStick.y !== 0) {
player.x += leftStick.x * 5;
player.y += leftStick.y * 5;
}
// Buttons
if (this.gamepad.A) player.jump(); // Xbox A / PS X
if (this.gamepad.B) player.dash(); // Xbox B / PS Circle
if (this.gamepad.X) player.attack(); // Xbox X / PS Square
}
Why Essential : Console players expect gamepad . Accessibility (motion limitations). Couch gaming . Steam Deck support .
Rumble Feedback : this.gamepad.vibrate(duration, weakMagnitude, strongMagnitude)
Pattern 5: Multi-Touch Gestures - Mobile Interaction
// Enable multi-touch
create() {
this.input.addPointer(2); // Support 3 fingers (default is 1)
}
update() {
const pointer1 = this.input.pointer1;
const pointer2 = this.input.pointer2;
// Pinch zoom
if (pointer1.isDown && pointer2.isDown) {
const distance = Phaser.Math.Distance.Between(
pointer1.x, pointer1.y,
pointer2.x, pointer2.y
);
if (!this.lastPinchDistance) {
this.lastPinchDistance = distance;
}
const delta = distance - this.lastPinchDistance;
this.cameras.main.zoom += delta * 0.001;
this.lastPinchDistance = distance;
} else {
this.lastPinchDistance = null;
}
}
Common Gestures :
Pinch : Zoom (2 fingers, distance change)
Swipe : Direction (1 finger, velocity direction)
Long press : Context menu (1 finger, duration > 500ms)
Rotate : Rotation (2 fingers, angle change)
Why Essential : Mobile is touch-first . Gestures are faster than buttons. Accessibility (large touch targets).
Pattern 6: Pixel-Perfect Interaction - Transparent Hits
const sprite = this.add.sprite(400, 300, 'character').setInteractive();
// Only click on opaque pixels
sprite.setInteractive(
this.input.makePixelPerfect(1) // Alpha threshold (0-255)
);
// Or custom hit area
sprite.setInteractive(
new Phaser.Geom.Circle(32, 32, 30), // Circle hitarea
Phaser.Geom.Circle.Contains
);
Why Essential : Irregular sprites (star shape, character silhouette). Overlapping objects . Precision clicks (don't trigger transparent areas).
Performance Note : Pixel-perfect is CPU-intensive. Use for <100 objects. For more, use geometric hit areas.
Pattern 7: Input Priority and Event Propagation
// High priority (checked first)
topSprite.setInteractive({ priority: 10 });
// Low priority (checked last)
bottomSprite.setInteractive({ priority: 1 });
// Stop propagation
topSprite.on('pointerdown', () => {
console.log('Top clicked');
return true; // Stop event from reaching lower sprites
});
Why Essential : Layered UI (modal dialogs over game). Z-order clicking (top sprite wins). Event control (prevent double-triggers).
Pattern 8: Touch Joystick - Virtual Controls
// Create virtual joystick (mobile)
const joystick = this.add.circle(100, 500, 50, 0x888888, 0.5);
const joystickThumb = this.add.circle(100, 500, 20, 0xffffff);
joystick.setInteractive();
this.input.setDraggable(joystickThumb);
joystickThumb.on('drag', (pointer, dragX, dragY) => {
const distance = Phaser.Math.Distance.Between(100, 500, dragX, dragY);
const maxDistance = 50;
if (distance < maxDistance) {
joystickThumb.x = dragX;
joystickThumb.y = dragY;
} else {
// Clamp to circle edge
const angle = Phaser.Math.Angle.Between(100, 500, dragX, dragY);
joystickThumb.x = 100 + Math.cos(angle) * maxDistance;
joystickThumb.y = 500 + Math.sin(angle) * maxDistance;
}
// Move player
const dx = (joystickThumb.x - 100) / maxDistance;
const dy = (joystickThumb.y - 500) / maxDistance;
player.setVelocity(dx * 200, dy * 200);
});
joystickThumb.on('dragend', () => {
joystickThumb.x = 100;
joystickThumb.y = 500;
player.setVelocity(0, 0);
});
Why Essential : Mobile games need virtual controls . Accessibility (customizable position/size). Cross-platform (hide on desktop).
Pattern 9: Context Menu - Right-Click Interactions
button.on('pointerdown', (pointer) => {
if (pointer.rightButtonDown()) {
// Show context menu
this.contextMenu.setPosition(pointer.x, pointer.y);
this.contextMenu.setVisible(true);
}
});
// Prevent browser context menu
this.input.mouse.disableContextMenu();
Why Essential : RTS games (right-click to move). Inventory (right-click to drop). RPG (right-click for quick-use).
Pattern 10: Input Accessibility - Inclusive Design
// Configurable key bindings
const config = {
moveLeft: 'A',
moveRight: 'D',
jump: 'SPACE',
attack: 'J'
};
// Create keys from config
Object.keys(config).forEach(action => {
this.keys[action] = this.input.keyboard.addKey(config[action]);
});
// Touch target sizes (WCAG minimum 44x44px)
button.setDisplaySize(88, 88); // 2x minimum for better usability
// Visual feedback (required for accessibility)
button.on('pointerover', () => button.setAlpha(0.8));
button.on('pointerout', () => button.setAlpha(1.0));
// Keyboard navigation
this.input.keyboard.on('keydown-TAB', () => {
this.focusNextButton();
});
Accessibility Checklist :
✓ Configurable key bindings
✓ Keyboard-only navigation
✓ Gamepad support
✓ Touch targets ≥ 44x44px
✓ Visual hover feedback
✓ Audio click feedback
✓ One-handed mode option
Why Essential : 10-15% of players need accessibility features . Legal requirements (ADA, CVAA). Broader audience .
Real-World Applications
E-Learning Simulations :
Touch-friendly : Large buttons, drag-and-drop interactions
Keyboard shortcuts : Power users navigate faster
Accessibility : Screen reader support, keyboard navigation
Training Scenarios :
Multi-touch : Pinch zoom on diagrams
Context menus : Right-click for help/hints
Gamepad : Equipment operation simulation
Input Best Practices
1. Support Multiple Input Methods :
Keyboard AND mouse
Touch AND gamepad
Never lock to one method
2. Provide Visual Feedback :
Hover states
Click states
Disabled states
Loading states
3. Handle Input Conflicts :
Gamepad connected? Hide virtual joystick
Modal open? Disable game input
Paused? Stop processing movement
4. Test on Real Devices :
Touch latency varies
Gamepad button mapping differs
Keyboard layouts change by region
Coming Up
In Part 7 , we master Audio Design - the other half of game feel. Learn the 10 patterns that make sound effects punchy, music immersive, and spatial audio realistic.
Patterns documented: 449 total (Input: 10, Previous: 439)
This is part of my daily developer log. Follow my journey as I learn new skills and build tools with Brian at Actyra.
📝 Edits & Lessons Learned
No edits yet - this is the initial publication.
← Back to all posts