О чем этот пример
Определение, находится ли курсор или объект внутри геометрической фигуры — фундаментальная задача для игровых интерфейсов, триггеров и механик. В этом примере показано, как использовать встроенные методы геометрических объектов Phaser для проверки попадания точки, а также как визуализировать этот процесс и управлять камерой. Освоив этот подход, вы сможете легко создавать интерактивные зоны любой формы, не прибегая к тяжёлой физике.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
hitShape = null;
py;
px;
triangle1;
circle2;
circle1;
rect2;
rect1;
bounds;
graphics;
controls;
gui;
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();
this.input.on('pointermove', function (pointer)
{
const p = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
this.px = p.x;
this.py = p.y;
this.hitShape = null;
if (this.rect1.contains(this.px, this.py))
{
this.hitShape = this.rect1;
}
else if (this.rect2.contains(this.px, this.py))
{
this.hitShape = this.rect2;
}
else if (this.circle1.contains(this.px, this.py))
{
this.hitShape = this.circle1;
}
else if (this.circle2.contains(this.px, this.py))
{
this.hitShape = this.circle2;
}
else if (this.triangle1.contains(this.px, this.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 = (Phaser.Cameras.Controls.Smoothed) ? new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig) : 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);
}
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);
Подготовка геометрических фигур
В начале сцены создаются различные геометрические объекты, которые будут использоваться для проверки попадания. Каждый объект — это экземпляр класса из модуля Phaser.Geom.
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);
Создаются две прямоугольные области (Rectangle), два круга (Circle) и равносторонний треугольник (Triangle). Важно понимать, что пока это лишь математические представления форм — они не отрисовываются на экране автоматически. Их отрисовкой займётся объект Graphics.
Обработка движения курсора и проверка попадания
Ключевая логика находится в обработчике события pointermove. При каждом движении мыши вычисляются мировые координаты курсора и проверяется, находится ли точка внутри одной из заранее созданных фигур.
const p = this.cameras.main.getWorldPoint(pointer.x, pointer.y);
this.px = p.x;
this.py = p.y;
this.hitShape = null;
if (this.rect1.contains(this.px, this.py))
{
this.hitShape = this.rect1;
}
else if (this.circle1.contains(this.px, this.py))
{
this.hitShape = this.circle1;
}
// ... и так далее для других фигур
Метод this.cameras.main.getWorldPoint() преобразует координаты с экрана (относительно окна игры) в мировые координаты. Это критически важно, если камера перемещается, масштабируется или вращается. Каждый геометрический объект имеет метод .contains(x, y), который возвращает true, если точка находится внутри его границ. В данном примере мы просто сохраняем ссылку на «задетую» фигуру в переменную this.hitShape.
Визуализация с помощью Graphics
Все фигуры отрисовываются с помощью объекта this.graphics. В методе drawScene() происходит очистка холста и отрисовка всех фигур заново. Цвет фигуры зависит от того, является ли она «задетой» (this.hitShape).
if (this.hitShape === this.rect1)
{
this.graphics.fillStyle(0xff0000); // Красный для активной фигуры
this.graphics.fillRectShape(this.rect1);
}
else
{
this.graphics.fillStyle(0xffff00); // Жёлтый для неактивной
this.graphics.fillRectShape(this.rect1);
}
Используются методы fillRectShape, fillCircleShape и fillTriangleShape, которые принимают готовые геометрические объекты. Такой подход отделяет логику форм (геометрию) от их представления (отрисовки).
Плавное управление камерой
Чтобы продемонстрировать важность перевода координат в мировую систему, в примере реализовано полноценное управление камерой. Используется Phaser.Cameras.Controls.SmoothedKeyControl для плавного перемещения и зума.
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);
Клавиши Z и X добавляют вращение камеры. Без преобразования координат через getWorldPoint() проверка попадания работала бы некорректно при любом движении или повороте камеры.
Инструменты для отладки с dat.GUI
Пример использует библиотеку dat.GUI для создания панели отладки в реальном времени. Это позволяет отслеживать ключевые параметры камеры и курсора.
this.gui = new dat.GUI();
const f1 = this.gui.addFolder('Camera');
f1.add(cam, 'x').listen();
f1.add(cam, 'y').listen();
f1.add(cam, 'zoom', 0.1, 2).step(0.1).listen();
f1.open();
Метод .listen() заставляет элементы интерфейса автоматически обновлять свои значения. Такая панель невероятно полезна при отладке сложных взаимодействий, связанных с камерой и координатами.
Что попробовать дальше
Пример наглядно демонстрирует мощь и простоту модуля Geometry в Phaser для проверки попаданий. Вы можете использовать этот паттерн для создания невидимых триггеров сложной формы, интерактивных областей в интерфейсе или для упрощённой коллизии. Для экспериментов попробуйте: добавить больше фигур (например, эллипсы или многоугольники), реализовать проверку попадания не курсора, а другого геометрического объекта (используя методы вроде Intersects), или привязать логику к игровым объектам (Sprite), используя их мировые координаты.
