О чем этот пример
В Phaser камера — это ваш взгляд на игровой мир. Часто возникает задача: определить, над каким объектом в мире находится курсор мыши, особенно когда камера перемещается, масштабируется или поворачивается. Этот пример наглядно демонстрирует, как преобразовать координаты указателя с экрана в координаты игрового мира и использовать их для проверки столкновений с геометрическими фигурами. Вы научитесь создавать интерактивные области в большом мире, не зависящие от текущего состояния камеры.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
// var graphics;
class Example extends Phaser.Scene
{
constructor ()
{
super();
this.hitShape = null;
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
this.sprites = [];
}
create ()
{
this.graphics = this.add.graphics();
this.bounds = new Phaser.Geom.Rectangle(0, 0, 1600, 1200);
this.rect1 = new Phaser.Geom.Rectangle(200, 200, 600, 100);
this.rect2 = new Phaser.Geom.Rectangle(1010, 800, 60, 300);
this.circle1 = new Phaser.Geom.Circle(1200, 200, 160);
this.circle2 = new Phaser.Geom.Circle(400, 900, 80);
this.triangle1 = new Phaser.Geom.Triangle.BuildEquilateral(800, 500, 200);
this.drawScene();
for (let i = 0; i < 32; i++)
{
let x = Phaser.Math.Between(this.bounds.left, this.bounds.right);
let y = Phaser.Math.Between(this.bounds.top, this.bounds.bottom);
this.sprites.push(this.add.sprite(x, y, 'eye').setInteractive());
}
this.input.on('gameobjectover', function (pointer, gameObject) {
gameObject.setTint(0xff0000);
});
this.input.on('gameobjectout', function (pointer, gameObject) {
gameObject.clearTint();
});
this.input.on('pointermove', function (pointer) {
const p = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
const px = p.x;
const py = p.y;
this.hitShape = null;
if (this.rect1.contains(px, py))
{
this.hitShape = this.rect1;
}
else if (this.rect2.contains(px, py))
{
this.hitShape = this.rect2;
}
else if (this.circle1.contains(px, py))
{
this.hitShape = this.circle1;
}
else if (this.circle2.contains(px, py))
{
this.hitShape = this.circle2;
}
else if (this.triangle1.contains(px, py))
{
this.hitShape = this.triangle1;
}
this.drawScene();
}, this);
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.input.keyboard.on('keydown_Z', function (event) {
this.cameras.main.rotation += 0.01;
}, this);
this.input.keyboard.on('keydown_X', function (event) {
this.cameras.main.rotation -= 0.01;
}, this);
const cam = this.cameras.main;
this.gui = new dat.GUI();
const p1 = this.gui.addFolder('Pointer');
p1.add(this.input, 'x').listen();
p1.add(this.input, 'y').listen();
p1.open();
const help = {
line1: 'Cursors to move',
line2: 'Q & E to zoom',
line3: 'Z & X to rotate',
}
const f1 = this.gui.addFolder('Camera');
f1.add(cam, 'x').listen();
f1.add(cam, 'y').listen();
f1.add(cam, 'scrollX').listen();
f1.add(cam, 'scrollY').listen();
f1.add(cam, 'rotation').min(0).step(0.01).listen();
f1.add(cam, 'zoom', 0.1, 2).step(0.1).listen();
f1.add(help, 'line1');
f1.add(help, 'line2');
f1.add(help, 'line3');
f1.open();
}
update (time, delta)
{
this.controls.update(delta);
this.sprites.forEach(function(sprite, i) {
sprite.rotation += (i % 2) ? 0.005 : -0.005;
});
}
drawScene ()
{
this.graphics.clear();
// camera marker
this.graphics.lineStyle(1, 0x00ff00);
this.graphics.strokeRectShape(this.bounds);
this.graphics.lineBetween(0, 0, 1600, 1200);
this.graphics.lineBetween(1600, 0, 0, 1200);
// shapes
if (this.hitShape === this.rect1)
{
this.graphics.fillStyle(0xff0000);
this.graphics.fillRectShape(this.rect1);
}
else
{
this.graphics.fillStyle(0xffff00);
this.graphics.fillRectShape(this.rect1);
}
if (this.hitShape === this.rect2)
{
this.graphics.fillStyle(0xff0000);
this.graphics.fillRectShape(this.rect2);
}
else
{
this.graphics.fillStyle(0xffff00);
this.graphics.fillRectShape(this.rect2);
}
if (this.hitShape === this.circle1)
{
this.graphics.fillStyle(0xff0000);
this.graphics.fillCircleShape(this.circle1);
}
else
{
this.graphics.fillStyle(0xffff00);
this.graphics.fillCircleShape(this.circle1);
}
if (this.hitShape === this.circle2)
{
this.graphics.fillStyle(0xff0000);
this.graphics.fillCircleShape(this.circle2);
}
else
{
this.graphics.fillStyle(0xffff00);
this.graphics.fillCircleShape(this.circle2);
}
if (this.hitShape === this.triangle1)
{
this.graphics.fillStyle(0xff0000);
this.graphics.fillTriangleShape(this.triangle1);
}
else
{
this.graphics.fillStyle(0xffff00);
this.graphics.fillTriangleShape(this.triangle1);
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#000000',
scene: Example
};
const game = new Phaser.Game(config);
От экранных координат к мировым
Основная задача — понять, куда в игровом мире указывает курсор. Координаты события мыши (pointer.x, pointer.y) всегда даны относительно окна отрисовки (viewport) и не учитывают положение камеры. Метод this.cameras.main.getWorldPoint() решает эту проблему, преобразуя экранные координаты в мировые с учётом смещения (scroll), масштаба (zoom) и поворота (rotation) камеры.
В обработчике pointermove мы получаем мировую точку и проверяем, находится ли она внутри одной из заранее созданных геометрических фигур.
const p = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
const px = p.x;
const py = p.y;
this.hitShape = null;
if (this.rect1.contains(px, py)) {
this.hitShape = this.rect1;
}
Создание мира и управление камерой
Пример создаёт большой игровой мир (bounds размером 1600x1200) и размещает в нём несколько простых фигур (прямоугольники, круги, треугольник) с помощью классов Phaser.Geom. Для навигации по этому миру используется система Phaser.Cameras.Controls.SmoothedKeyControl. Она позволяет плавно перемещать камеру с помощью стрелок и изменять масштаб клавишами Q/E.
Клавиши Z и X добавляют поворот камеры, что наглядно демонстрирует, как преобразование координат работает даже при таком сложном преобразовании.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
Визуализация и интерактивность
Все фигуры отрисовываются с помощью Graphics. Если курсор находится над фигурой, она заливается красным, если нет — жёлтым. Отрисовка происходит в методе drawScene(), который вызывается при каждом движении мыши.
Дополнительно в мир добавлены 32 спрайта "глаза", которые реагируют на наведение стандартными событиями gameobjectover и gameobjectout. Это показывает разницу между проверкой попадания в геометрическую фигуру и во взаимодействующий игровой объект (Sprite). В update() спрайты медленно вращаются.
this.input.on('gameobjectover', function (pointer, gameObject) {
gameObject.setTint(0xff0000);
});
// В update:
this.sprites.forEach(function(sprite, i) {
sprite.rotation += (i % 2) ? 0.005 : -0.005;
});
Инструменты для отладки: dat.GUI
Для наблюдения за внутренним состоянием камеры в реальном времени пример использует библиотеку dat.GUI. Она создаёт панель управления, которая отображает ключевые свойства: координаты камеры в мире (`x,y), смещение (scrollX,scrollY), поворот (rotation) и масштаб (zoom). Также отображаются текущие экранные координаты курсора (pointer.x,pointer.y`). Это бесценный инструмент для отладки сложных преобразований.
const f1 = this.gui.addFolder('Camera');
f1.add(cam, 'x').listen();
f1.add(cam, 'y').listen();
f1.add(cam, 'scrollX').listen();
f1.add(cam, 'scrollY').listen();
f1.add(cam, 'rotation').min(0).step(0.01).listen();
f1.add(cam, 'zoom', 0.1, 2).step(0.1).listen();
Что попробовать дальше
Ключевой вывод: для любой интерактивной логики, связанной с миром (выбор объектов, размещение построек, AI), всегда используйте преобразованные мировые координаты из camera.getWorldPoint(). Это гарантирует корректную работу при любых трансформациях камеры.
Экспериментируйте: попробуйте добавить проверку попадания в несколько фигур одновременно, создайте сложный составной объект из простых фигур или реализуйте перетаскивание мировых объектов мышью с учётом поворота и зума камеры.
