О чем этот пример
При разработке игр часто возникает необходимость обрабатывать взаимодействие с сотнями или тысячами объектов. Нативный подход с отдельными обработчиками событий для каждого спрайта быстро приводит к падению производительности. В этой статье мы разберем пример из официальной документации Phaser, который демонстрирует, как эффективно и быстро обрабатывать наведение и клики по 10 000 игровым объектам. Мы рассмотрим ключевые техники: использование единой области взаимодействия (hitArea), группового создания объектов и камерного скроллинга.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
controls;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
}
create ()
{
// Create a little 32x32 texture to use to show where the mouse is
const graphics = this.make.graphics({ x: 0, y: 0, add: false, fillStyle: { color: 0xff00ff, alpha: 1 } });
graphics.fillRect(0, 0, 32, 32);
graphics.generateTexture('block', 32, 32);
const highlighted = this.add.image(16, 16, 'block');
// All the Images can share the same Shape, no need for a unique instance per one, a reference is fine
const hitArea = new Phaser.Geom.Rectangle(0, 0, 32, 32);
const hitAreaCallback = Phaser.Geom.Rectangle.Contains;
// Create 10,000 Image Game Objects aligned in a grid
// Change this to 2000 on MS Edge as it can't seem to cope with 10k at the moment
const group = this.make.group({
classType: Phaser.GameObjects.Image,
key: 'bobs',
frame: Phaser.Utils.Array.NumberArray(0, 399),
randomFrame: true,
repeat: 24,
max: 10000,
hitArea: hitArea,
hitAreaCallback: hitAreaCallback,
gridAlign: {
width: 100,
cellWidth: 32,
cellHeight: 32
}
});
// Camera controls
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.input.on('pointerover', (pointer, gameObjects) =>
{
highlighted.setPosition(gameObjects[0].x, gameObjects[0].y);
});
this.input.on('gameobjectdown', (pointer, gameObject) =>
{
gameObject.visible = false;
});
}
update (time, delta)
{
this.controls.update(delta);
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Создание массы объектов через группу (Group)
Вместо создания и позиционирования каждого спрайта вручную, код использует фабрику this.make.group. Это мощный метод Phaser для массового создания однотипных игровых объектов с заданными параметрами.
const group = this.make.group({
classType: Phaser.GameObjects.Image,
key: 'bobs',
frame: Phaser.Utils.Array.NumberArray(0, 399),
randomFrame: true,
repeat: 24,
max: 10000,
hitArea: hitArea,
hitAreaCallback: hitAreaCallback,
gridAlign: {
width: 100,
cellWidth: 32,
cellHeight: 32
}
});
Здесь происходит следующее:
- classType: Задает тип создаваемых объектов — Phaser.GameObjects.Image.
- key и frame: Указывает на ключ спрайтшита, загруженного в preload(). Phaser.Utils.Array.NumberArray(0, 399) создает массив из 400 индексов кадров, а randomFrame: true случайным образом назначает их объектам.
- repeat и max: Параметры управляют количеством. Объекты создаются до тех пор, пока их не станет max (10000).
- gridAlign: Автоматически выравнивает все созданные объекты в сетку шириной 100 колонок (width: 100) с размером ячейки 32x32 пикселя. Это избавляет от необходимости вручную рассчитывать координаты X и Y для каждого спрайта.
Единая область взаимодействия (Hit Area)
Ключ к производительности при работе с тысячами объектов — использование единой геометрической формы для проверки пересечения с курсором, вместо индивидуальных расчетов для каждого спрайта.
const hitArea = new Phaser.Geom.Rectangle(0, 0, 32, 32);
const hitAreaCallback = Phaser.Geom.Rectangle.Contains;
Эти две строки настраивают общую логику взаимодействия:
1. hitArea: Создает прямоугольник размером 32x32 пикселя. Этот прямоугольник будет использоваться как виртуальная «горячая зона» для каждого из 10 000 изображений.
2. hitAreaCallback: Это функция (Phaser.Geom.Rectangle.Contains), которая будет вызываться для проверки, находится ли точка (курсор) внутри этого прямоугольника. Она передается в конфиг группы, и Phaser применяет ее ко всем объектам.
Таким образом, система ввода проверяет не 10 000 сложных форм, а позицию курсора относительно простого прямоугольника, что в разы эффективнее.
Обработка событий наведения и клика
Phaser предоставляет глобальные события ввода, которые срабатывают при взаимодействии с любым игровым объектом, имеющим hitArea. В коде подписаны два таких события.
this.input.on('pointerover', (pointer, gameObjects) => {
highlighted.setPosition(gameObjects[0].x, gameObjects[0].y);
});
Событие 'pointerover' срабатывает, когда курсор попадает в hitArea объекта. Колбэк получает массив gameObjects — все объекты под курсором (обычно это один объект). Код берет первый (gameObjects[0]) и позиционирует спрайт-маркер (highlighted) на его координаты, визуализируя наведение.
this.input.on('gameobjectdown', (pointer, gameObject) => {
gameObject.visible = false;
});
Событие 'gameobjectdown' срабатывает при нажатии кнопки мыши на объекте. В колбэке конкретный gameObject, по которому кликнули, просто скрывается (visible = false). Обратите внимание, что здесь в колбэк передается одиночный объект, а не массив.
Плавное управление камерой для навигации
Чтобы можно было осмотреть все 10 000 объектов, реализовано управление камерой с клавиатуры с помощью SmoothedKeyControl. Этот контроллер обеспечивает плавное, инерционное движение.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
Конфиг связывает основные клавиши-стрелки с управлением главной камерой. Параметры acceleration (ускорение) и drag (сопротивление) создают эффект плавного разгона и замедления. maxSpeed ограничивает максимальную скорость перемещения.
Обновление состояния контроллера происходит каждый кадр в методе update:
update (time, delta) {
this.controls.update(delta);
}
Передача delta (время, прошедшее с предыдущего кадра) обеспечивает независимость скорости движения камеры от частоты кадров.
Что попробовать дальше
Этот пример наглядно демонстрирует, как Phaser позволяет работать с огромным количеством интерактивных объектов, оставаясь в рамках высокой производительности. Главные выводы: используйте Group для массового создания, назначайте общую hitArea для множества объектов и обрабатывайте взаимодействие через глобальные события ввода. Для экспериментов попробуйте: изменить форму hitArea на круг (Phaser.Geom.Circle), реализовать более сложную логику при клике (например, менять не visible, а tint), или увеличить maxSpeed камеры для быстрого перемещения по огромной сцене.
