Adding touch pan and zoom controls to a Phaser game

Search for a command to run...

No comments yet. Be the first to comment.
Hello. I'm writing a Lambda "tick" function to simulate the universe for my game. It's a different architecture than my previous idea of maintaining a 24/7 server, which simulates a busy real-time game loop. I wanted to give it a try. In this post, I...

A small tool that lets you put a "marker" on a song and have a button to recall that position with a click.

Hey there! I had a blast participating in the Appwrite hackathon and I want to share my journey with you. So, buckle up and get ready for some timer magic! Team Details Okay, before we dive in, let's get to know the team (well, it's just me actually)...

Hello. I've decided to try something for the game I'm developing. The game is a long-running online solo/coop space strategy game with resource balancing and 4x elements, but that's not the subject of this post. I decided to separate game logic code ...

Hello. For this one, I ended up using Hammer.js. I did try Interact.js which is definitely newer and more updated, but that just... well... didn't work nicely. Judging by the examples on both libraries' pages you can see if the core functionality of these libraries works for you. Hammer.js, which I put an eye on for quite some time, nails it, even though it required some extra tweaking.
As my single/coop-player online space multiplayer 4X/resource managemnet game is targeting the mobile web, I wanted to make sure I got pan and pinch-zoom working properly before I settle down on the game framework, just to know I'm on the right track with that. These controls are important in the galaxy or solar system views, etc.
I will show this in parts and then post the "final" code I have for this. The quotation marks are because I am still missing mouse-wheel zooming but I believe it's rather easy to add and desktop is not the most important focus at this moment.
By creating a function that is external to any scene logic or class I can keep the scene classes clean and the touch code decoupled. This function accepts the target scene and an optional camera in case I don't want the main one to pan/zoom.
export function phaserPinchZoomPanCamera(
scene: Phaser.Scene,
camera?: Phaser.Types.Cameras.Scene2D.CameraConfig
) {
const hammer = new Hammer(scene.game.canvas);
hammer.get('pinch').set({ enable: true });
const chosenCamera = camera ?? scene.cameras.main;
This is pretty self-explanatory. I enable Hammer.js on the game's canvas itself and enable pinch.
With Hammer.js, I found out that the continuous events provide a delta (x, y) coordinate relative to the starting touch point. I therefore have to keep the starting coordinates of my camera (as well as the zoom as we'll see). This is in contrast to Interact.js which gave me the delta x and y's relative to the previous event which made the code a bit simpler, but it was buggy and thus ditched.
let startX: number;
let startY: number;
let startZoom: number;
hammer.on('pinchstart panstart', () => {
startZoom = chosenCamera.zoom ?? 1;
startX = chosenCamera.scrollX ?? 0;
startY = chosenCamera.scrollY ?? 0;
});
And for the actual touch-move events. It's as simple as that:
hammer.on('pinchmove panmove', (event) => {
chosenCamera.zoom = startZoom * event.scale;
chosenCamera.scrollX = startX - event.deltaX / chosenCamera.zoom;
chosenCamera.scrollY = startY - event.deltaY / chosenCamera.zoom;
});
}
It looks like this (YouTube link): https://www.youtube.com/shorts/h77OUO-6m14
export function phaserPinchZoomPanCamera(
scene: Phaser.Scene,
camera?: Phaser.Types.Cameras.Scene2D.CameraConfig
) {
const hammer = new Hammer(scene.game.canvas);
hammer.get('pinch').set({ enable: true });
const chosenCamera = camera ?? scene.cameras.main;
let startX: number;
let startY: number;
let startZoom: number;
hammer.on('pinchstart panstart', () => {
startZoom = chosenCamera.zoom ?? 1;
startX = chosenCamera.scrollX ?? 0;
startY = chosenCamera.scrollY ?? 0;
});
hammer.on('pinchmove panmove', (event) => {
chosenCamera.zoom = startZoom * event.scale;
chosenCamera.scrollX = startX - event.deltaX / chosenCamera.zoom;
chosenCamera.scrollY = startY - event.deltaY / chosenCamera.zoom;
});
}
My TypeScript external-types-foo is a bit lacking, but I got Hammer.js typing working with something like this:
After installing @types/hammerjs, in a lib/types/index.d.ts file (though you can probably put it in your regular code or something) I put:
import type Hammer from 'hammerjs';
Which seems to do the trick:

To use the function we defined above:
create() {
super.create();
// ...whatever
phaserPinchZoomPanCamera(this);
}
The super call is because I extend the game's actual scene classes for a custom base scene class that extends the original phaser Scene. Because I use Colyseus.js I have, for example, something like this:
// This is very early work of integrating between the Colyseus client and my scene,
// the real scenes extend this and implement `onStateChange`.
export abstract class RoomBasedScene extends Phaser.Scene {
protected room!: Room<GameRoomSchema>;
create() {
this.room = this.game.registry.get(
GameFramework.ROOM_DATA_KEY
) as Room<GameRoomSchema>;
this.room.onStateChange((state) => this.onStateChange(state));
}
abstract onStateChange(state: GameRoomSchema): void;
}
BTW, on the Phaser side of things, I recommend checking how to build a custom Phaser build to keep your bundle size much smaller than out-of-the-box Phaser.
An on a final note, I wanted to say that this blogging thing I do from time to time is amazing because when I paste my code, I fix things and improve to not post nonsense on the internet, so it makes my overall code a bit better and I learn extra stuff that I wouldn't have if I was not to publish these code snippets. 👏🏻