Lately I’ve been building a lot of games, and for one of my mobile projects I wanted the controls to feel great. At some point I realized I needed an on-screen joystick.
My first idea was to use simple virtual buttons — but of course, they don’t offer any real tactile feedback. You just can’t feel them.
That’s when I remembered Brawl Stars, which I used to play years ago. Its joystick always felt incredibly smooth and responsive.
So I decided to study its behavior and rebuild something similar — but this time as an open-source library, so any Phaser developer can use and improve it.
Observations
One of the biggest challenges with virtual joysticks is maintaining touch consistency.
If the joystick always stays fixed in place, the player will inevitably touch different spots every time. That leads to confusion, you might intend to move left, but if your finger lands slightly to the right, your input ends up reversed.
The solution used by top-tier games like Brawl Stars is to reposition the joystick to the initial touch point. This way, movement always starts exactly where the player’s finger touches, creating a more natural and intuitive control feel.
Another important design choice is the active area limitation: since action buttons usually sit on the right side of the screen, the joystick only responds to touches on the left side. This prevents input conflicts and ensures smoother gameplay.
And there’s one more subtle UX detail: when the player’s finger moves too far away, the joystick follows the motion slightly, keeping a safe margin so it doesn’t interfere with direction control. That way, the player never has to drag the finger all the way back, the joystick adapts and moves gracefully with them.
From binary touches to analog movement
When I started integrating the Xbox controller, I realized that the analog stick isn’t binary (obviously), it’s not just “pressed or not pressed.”
Each axis (horizontal and vertical) actually provides a float value between -1 and 1, representing both direction and intensity.
That means if you tilt the stick slightly to the right, you might get 0.3; push it all the way, and it reaches 1.0. The same goes for up, down, and left — just with negative values.
I found that behavior fascinating — it’s what gives console games their smooth and precise control. So I decided to replicate the exact same logic in my virtual joystick.
Now, instead of just asking “which direction is the finger moving?”, the joystick calculates the distance and angle from the starting point to the current touch position, normalizing both axes to the [-1, 1] range.
The result feels much more natural: the character doesn’t just move or stop, it moves with varying speed and direction, depending on how far you drag your finger.
How I approached the development
I started by creating a container to represent the entire joystick. Inside it, I placed a small central circle, inspired by Brawl Stars, which acts as the neutral reference point — the center of movement.
Around that circle, I defined the movement area, the boundary within which the joystick can move.
On top of it, I added another movable circle, representing the stick itself — visually similar to an Xbox analog stick.
When the player touches the screen, the system stores the initial touch position.
Then, on every move event, it calculates the current finger position and uses that data to compute the movement angle, determining the direction the stick should point to.
To prevent the stick from drifting too far, I applied an angular limit, ensuring it stays within its defined movement radius.
Finally, with the angle in hand, I normalize the movement by converting the distance and direction into float values between -1 and 1 for both the horizontal and vertical axes.
The result is a virtual joystick that behaves just like a real analog stick — smooth, responsive, and with proportional control based on touch intensity.
Turning the concept into an open-source library
To make this logic reusable, I created three main events in PhaserVirtualJoystick:
press, move, and release.
Their names are self-explanatory — they represent the initial touch, the continuous movement, and the moment the player lifts their finger.
These three events make it simple to capture the full joystick behavior.
They allow you to react instantly to player input without worrying about angle calculations or normalization math.
Here’s a small usage example:
import { VirtualJoystick } from 'phaser-virtual-joystick';
class GameScene extends Phaser.Scene {
create() {
// Create a virtual joystick with default settings
const joystick = new VirtualJoystick({
scene: this
});
// ⚠️ IMPORTANT: Don't forget to add the joystick to the scene!
this.add.existing(joystick);
// Listen to joystick events
joystick.on('move', (data) => {
console.log(`Joystick position: ${data.x}, ${data.y}`);
// Move your character based on joystick input
this.moveCharacter(data.x, data.y);
});
joystick.on('press', () => {
console.log('Joystick pressed');
});
joystick.on('release', () => {
console.log('Joystick released');
});
}
moveCharacter(x: number, y: number) {
// Your character movement logic here
const speed = 200;
this.character?.setVelocity(x * speed, y * speed);
}
}
With just a few lines of code, you get a smooth, responsive virtual joystick ready to drop into any Phaser game.
Installation
You can install the lib Phaser-Virtual-Joystick using npm, yarn or pnpm.
$ npm i --save phaser-virtual-joystick
# or
$ yarn i phaser-virtual-joystick
# or
$ pnpm add phaser-virtual-joystick
Usage
In your scene, you must create the virtual joystick and add it.
import { VirtualJoystick } from 'phaser-virtual-joystick';
export class GameScene extends Phaser.Scene {
private joystick!: VirtualJoystick;
private player!: Phaser.Physics.Arcade.Sprite;
create() {
// Create the virtual joystick
this.joystick = new VirtualJoystick({
scene: this
});
// ⚠️ IMPORTANT: Don't forget to add the joystick to the scene!
this.add.existing(this.joystick);
// Create a player sprite
this.player = this.physics.add.sprite(400, 300, 'player');
// Listen to joystick events
this.joystick.on('move', (data) => {
// data.x and data.y are normalized between -1 and 1
this.player.setVelocity(
data.x * 200, // Move speed
data.y * 200
);
});
this.joystick.on('release', () => {
this.player.setVelocity(0, 0);
});
}
update() {
// Update joystick (required for smooth following behavior)
this.joystick?.update();
}
}
Convention over configuration
One of my favorite development principles is “Convention over Configuration”, and that’s exactly what I applied to PhaserVirtualJoystick.
The idea is simple: everything should work great out of the box, but still be easy to customize when needed.
For example, by default, the joystick is active only on the left half of the screen, which is the usual layout for mobile games.
But if you want to change that, just use the bounds parameter:
bounds?: {
topLeft: { x: number; y: number };
bottomRight: { x: number; y: number };
};
This lets you define exactly where the joystick can be triggered.
Want it active across the entire screen?
Just set topLeft: { x: 0, y: 0 } and bottomRight to match your game’s dimensions.
Another convention is styling.
If you don’t provide any configuration, the joystick defaults to a smooth blue theme (the one you’ve seen in the GIFs).
But every part of it can be customized.
The joystick is made up of three elements:
-
deadZone: the central neutral circle -
baseArea: the outer movement boundary -
stick: the movable analog itself
Each element accepts a StyleConfig object:
export type StyleConfig = {
alpha: number;
strokeColor: number;
strokeAlpha: number;
strokeWidth: number;
radius: number;
fillColor: number;
}
You can override just a few values or all of them — it’s up to you.
Here’s an example of a yellow custom joystick using the phaser-wind color palette:
👉 StackBlitz Demo
Conclusion
This project started out as a small curiosity — I just wanted a joystick that felt right on mobile.
But after tweaking the movement, normalizing the axes, and testing over and over again, it turned out so good that it became an open-source library: PhaserVirtualJoystick.
If you’re building a game with PhaserJS and want smooth, responsive controls, give it a try.
You can check out the full documentation here:
👉 PhaserVirtualJoystick Docs
Contributions are always welcome!
If you have ideas, improvements, or want to adapt it for different gameplay styles, open an issue or pull request.
My goal is for this library to keep evolving with the Phaser community.
And that’s when I finally went to bed… mission accomplished 😴.




Top comments (0)