Keyboard and Touch Controls in Phaser
Input handling is one of the most critical systems in any game. Players notice input problems before they notice visual glitches, because the connection between what they press and what happens on screen is the core of the gameplay experience. Phaser provides a comprehensive input system that abstracts away browser differences and gives you clean, frame-accurate access to every input device the player might use.
Set Up Keyboard Input
The fastest way to handle keyboard input is with cursor keys. Call this.input.keyboard.createCursorKeys() in your create method to get an object that tracks the state of the arrow keys, spacebar, and shift key. Each key has properties for isDown (currently held), isUp (currently not held), and duration (how long the key has been held in milliseconds).
In your update method, check these properties to control player movement. If cursors.left.isDown, move the player left. If cursors.right.isDown, move right. If cursors.up.isDown and the player is on the ground, jump. This polling approach runs every frame and provides the most responsive controls because there is no event propagation delay.
For keys beyond the cursor set, add individual key objects with this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W). This returns a key object with the same isDown, isUp, and duration properties. You can also use addKeys to create multiple key objects at once, passing a comma-separated string of key names or an object mapping names to key codes.
Key events provide an alternative to polling. Listen for keydown and keyup events with this.input.keyboard.on to trigger one-time actions like opening a menu, using an item, or pausing the game. Events are better than polling for actions that should happen once per key press rather than continuously while the key is held. Use the JustDown and JustUp methods if you prefer polling but need one-shot behavior.
Prevent certain keys from triggering browser default actions by calling this.input.keyboard.addCapture with the key codes you want to capture. Without this, pressing spacebar scrolls the page, arrow keys scroll the viewport, and tab switches focus to the address bar. Capturing these keys ensures they only affect your game.
Handle Mouse and Pointer Events
Phaser's pointer system unifies mouse and touch input into a single API. The primary pointer, accessed through this.input.activePointer, represents the mouse on desktop and the first finger on touch devices. It provides x and y coordinates relative to the game canvas, isDown for click or touch state, and worldX and worldY for coordinates in the game world accounting for camera scroll.
Make game objects clickable by enabling input on them with sprite.setInteractive(). This adds hit area detection, and you can listen for events like pointerdown, pointerup, pointerover, and pointerout on the sprite. Interactive objects support drag behavior through this.input.setDraggable(sprite), which fires drag, dragstart, and dragend events as the player moves the object with the mouse or finger.
For shooting mechanics, read the pointer position in the update loop and calculate the angle between the player and the pointer with Phaser.Math.Angle.Between. Use this angle to aim a weapon, rotate a turret, or launch a projectile in the direction of the click. The pointer's worldX and worldY properties give you the correct target position even when the camera has scrolled from the origin.
Custom hit areas let you define clickable regions that differ from the sprite's visual bounds. Pass a geometric shape and a callback to setInteractive to use a circle, polygon, or other shape as the hit area. This is useful for irregularly shaped buttons, map regions, or characters with detailed silhouettes where the rectangular default would register false clicks on transparent areas.
Implement Touch Controls for Mobile
Mobile games need on-screen controls because there is no physical keyboard. The simplest approach is to create button sprites fixed to the camera that respond to touch events. Place a directional pad on the left side of the screen and action buttons on the right, matching the layout players expect from mobile games. Make the buttons semi-transparent so they do not obscure too much of the game view.
Virtual joysticks provide analog movement where the player drags a thumb stick within a circular boundary. The distance and angle of the drag determine the movement direction and speed. Several Phaser community plugins implement virtual joysticks, including the popular rexUI plugin which provides a configurable joystick component. Building your own requires tracking the touch start position, calculating the offset on each move event, and clamping the offset to a maximum radius.
Swipe detection interprets quick finger movements as directional commands. Track the pointer's start position on pointerdown, its end position on pointerup, and calculate the direction and speed of the swipe. If the swipe distance exceeds a minimum threshold and the swipe duration is below a maximum time, register it as a swipe in the primary direction. Swipe controls work well for puzzle games, card games, and casual games where precision is less critical than simplicity.
Touch controls should have generous hit areas because fingers are less precise than mouse cursors. A button that looks like a 48-pixel square should have a hit area of at least 64 pixels to account for finger size and touch inaccuracy. Provide visual feedback when buttons are pressed by changing their tint, scale, or texture so the player can confirm their touch registered. Audio feedback, like a subtle click sound, further reinforces the connection between input and response.
Add Gamepad Support
Enable gamepad support in the game configuration by setting input.gamepad to true. Phaser listens for the browser's gamepadconnected event and creates a Gamepad object for each detected controller. Access the first connected gamepad with this.input.gamepad.getPad(0), which returns null if no gamepad is connected.
Gamepad buttons work like keyboard keys, with isDown, isUp, and value properties. The value property is useful for analog triggers that report pressure between 0 and 1 rather than a binary pressed or not-pressed state. Read analog stick positions through the pad's axes or the leftStick and rightStick objects, which provide x and y values between -1 and 1, with 0 being the center position.
Apply a deadzone to analog sticks to prevent drift, which is the slight movement registered when the stick is at rest due to hardware imprecision. A deadzone of 0.15 to 0.2 means stick values below that threshold are treated as zero. Without a deadzone, the player's character might slowly drift in one direction even when the stick is untouched.
Map gamepad buttons to game actions using the standard mapping layout defined by the browser Gamepad API. Button index 0 is typically the A or cross button, index 1 is B or circle, and so on. The standard mapping ensures consistent button assignments across different controller brands, though you should provide a control reference screen so players can see which button does what.
Build an Input Abstraction Layer
An input abstraction layer maps game actions to input bindings rather than checking specific keys directly. Instead of checking if the W key is down, you check if the "moveUp" action is active. This separation lets you support multiple input devices, offer remappable controls, and change bindings without touching game logic.
Create a simple action map as a JavaScript object where each key is an action name and each value is a function that returns whether the action is active. The function checks all relevant inputs: for "jump," it might check the spacebar, the gamepad A button, and a virtual button on mobile. In the update loop, you call the action function rather than checking individual keys, which keeps the game logic clean and device-agnostic.
For remappable controls, store the key bindings in a configuration object that the player can modify through a settings menu. When a player clicks "rebind jump," listen for the next key press and assign that key code to the jump action. Save the bindings to localStorage so they persist between sessions. This feature is expected in most PC games and significantly improves accessibility for players who use non-standard keyboard layouts or have physical limitations.
The abstraction layer also handles input priority when multiple devices are active simultaneously. If the player is using a keyboard and then picks up a gamepad, the layer can switch the displayed button prompts from "Press Space" to "Press A" automatically based on which device most recently received input. This adaptive behavior makes the game feel polished and considerate of how players actually interact with it.
Handle Edge Cases and Platform Differences
Focus loss is the most common input edge case. When the player clicks outside the game canvas, switches tabs, or receives a system notification, the browser stops sending input events to the game. If the player was holding a movement key when focus was lost, the game thinks the key is still held when focus returns. Handle this by listening for the blur event on the game canvas and resetting all active key states.
Mobile browsers handle touch events differently from desktop browsers. Some fire mouse events alongside touch events for compatibility, which can cause double-registering of input. Phaser handles this internally, but if you add custom DOM event listeners, you may need to call event.preventDefault() on touch events to suppress the simulated mouse events.
Input buffering records inputs during moments when the game cannot act on them, such as during an attack animation or a brief stun period, and replays them when the character becomes responsive again. A simple input buffer stores the last input event and its timestamp, then checks the buffer at the start of each frame. If the buffered input is recent enough (typically within 100-200 milliseconds), execute it. Input buffering makes controls feel more forgiving and reduces the frustration of inputs being "eaten" during animations.
Test your controls on actual devices rather than relying solely on desktop emulation. Browser developer tools can simulate touch events, but they cannot replicate the feel of real finger input, the latency of actual mobile hardware, or the screen size and DPI that affect how large touch targets appear. Testing on at least one iOS and one Android device catches platform-specific issues that emulation misses.
Use Phaser's unified pointer system for code that works on both desktop and mobile. Build an input abstraction layer that maps actions to keys, buttons, and touches, so your game logic stays clean while supporting every input device your players use.