О чем этот пример
Когда игра усложняется, управлять множеством игровых объектов по отдельности становится неудобно. Контейнеры в Phaser позволяют группировать объекты, обрабатывая их как единое целое, что упрощает трансформации и организацию сцены. Эта статья на практическом примере покажет, как создавать иерархию контейнеров, динамически добавлять в них спрайты и корректно обрабатывать события наведения мыши даже внутри сложных структур. Вы научитесь строить гибкую систему объектов, что особенно полезно для интерфейсов, меню или сложных игровых сущностей.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
this.load.image('grid', 'assets/pics/debug-grid-1920x1920.png');
}
create ()
{
this.add.image(0, 0, 'grid').setOrigin(0).setAlpha(0.5);
const bounds = new Phaser.Geom.Rectangle(0, 0, 1024, 768);
const containers = [];
let container = this.add.container(0, 0).setName('Container1');
containers.push(container);
window['Container1'] = container;
let c = 1;
for (let i = 0; i < 128; i++)
{
const x = Phaser.Math.Between(bounds.left, bounds.right);
const y = Phaser.Math.Between(bounds.top, bounds.bottom);
const s = this.add.sprite(x, y, 'eye').setName(`Sprite${i}`);
window[`Sprite${i}`] = s;
s.setInteractive();
s.setAngle(Phaser.Math.Between(0, 359));
s.setScale(0.1 + Math.random());
if (i > 0 && i % 8 === 0)
{
container = this.add.container(0, 0).setName(`Container${c}`);
if (c > 1)
{
const p = Phaser.Utils.Array.GetRandom(containers).add(container);
console.log(container.name, 'child of', p.name);
}
containers.push(container);
window[`Container${c}`] = container;
c++;
}
Phaser.Utils.Array.GetRandom(containers).add(s);
}
this.input.on('gameobjectover', (pointer, gameObject) =>
{
gameObject.setTint(0xff0000);
// console.log(gameObject.name);
// console.table(gameObject.getIndexList());
});
this.input.on('gameobjectout', (pointer, gameObject) =>
{
gameObject.clearTint();
});
/*
this.tweens.add({
targets: containers,
angle: 360,
duration: 20000,
ease: 'Linear',
repeat: -1
});
*/
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1024,
height: 768,
backgroundColor: '#000000',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание базового контейнера
В методе create мы начинаем с добавления фонового изображения сетки для визуальной ориентации. Затем создается прямоугольная область (bounds), которая будет использоваться для случайного размещения спрайтов.
Ключевой момент — создание первого контейнера. Контейнер (Phaser.GameObjects.Container) — это игровой объект, который может содержать в себе другие игровые объекты (спрайты, текст, другие контейнеры). Все его дети наследуют трансформации (позицию, масштаб, угол поворота) родительского контейнера.
Мы создаем массив containers для хранения всех контейнеров и добавляем в него первый. Также для отладки контейнер добавляется в глобальный объект window.
const bounds = new Phaser.Geom.Rectangle(0, 0, 1024, 768);
const containers = [];
let container = this.add.container(0, 0).setName('Container1');
containers.push(container);
window['Container1'] = container;
Цикл создания спрайтов и иерархии контейнеров
В цикле создается 128 спрайтов с изображением глаза. Каждому спрайту задаются случайные координаты в пределах bounds, угол поворота и масштаб. Важный шаг — вызов setInteractive(). Без него объект не будет генерировать события ввода (например, наведение мыши).
const s = this.add.sprite(x, y, 'eye').setName(`Sprite${i}`);
window[`Sprite${i}`] = s;
s.setInteractive();
s.setAngle(Phaser.Math.Between(0, 359));
s.setScale(0.1 + Math.random());
Каждые 8 спрайтов создается новый контейнер. Здесь реализуется иерархия: начиная со второго контейнера, каждый новый случайным образом (Phaser.Utils.Array.GetRandom) добавляется как ребенок к одному из уже существующих контейнеров. Таким образом, мы строим древовидную структуру. Все созданные спрайты также случайным образом распределяются по всем существующим контейнерам (включая вновь созданные).
if (i > 0 && i % 8 === 0)
{
container = this.add.container(0, 0).setName(`Container${c}`);
if (c > 1)
{
const p = Phaser.Utils.Array.GetRandom(containers).add(container);
}
containers.push(container);
c++;
}
Phaser.Utils.Array.GetRandom(containers).add(s);
Обработка событий наведения мыши (Hover)
Обработка событий ввода — сильная сторона Phaser. Мы навешиваем обработчики на глобальные события сцены gameobjectover (курсор наведен на объект) и gameobjectout (курсор убран с объекта).
Когда событие gameobjectover срабатывает, объект, на который навели (gameObject), получает красный оттенок с помощью setTint. При событии gameobjectout оттенок сбрасывается.
Крайне важно, что эта логика работает прозрачно для всей иерархии контейнеров. Событие будет возникать как для отдельного спрайта, так и для контейнера, если на него навести. Phaser сам определяет целевой объект под курсором.
this.input.on('gameobjectover', (pointer, gameObject) => {
gameObject.setTint(0xff0000);
});
this.input.on('gameobjectout', (pointer, gameObject) => {
gameObject.clearTint();
});
Анимация контейнеров (закомментированный код)
В исходном примере есть закомментированный блок, который демонстрирует мощь контейнеров в анимации. С помощью системной анимации (tweens) можно анимировать сразу все контейнеры в массиве containers.
Поскольку контейнеры образуют иерархию, анимация вращения родительского контейнера автоматически повлияет на все его дочерние элементы (спрайты и вложенные контейнеры). Это позволяет создавать сложные составные движения с минимальным кодом.
this.tweens.add({
targets: containers,
angle: 360,
duration: 20000,
ease: 'Linear',
repeat: -1
});
Раскомментируйте этот блок, чтобы увидеть, как вся созданная структура начнет плавно вращаться, сохраняя при этом возможность взаимодействия с каждым элементом.
Что попробовать дальше
Контейнеры в Phaser — это мощный инструмент для организации сложных игровых сцен. Они позволяют управлять группами объектов как единым целым, что упрощает анимацию, позиционирование и обработку событий. В рассмотренном примере мы создали динамическую иерархию объектов, где события ввода корректно обрабатываются на любом уровне вложенности.
**Идеи для экспериментов:**
1. Раскомментируйте анимацию и попробуйте анимировать не все контейнеры, а только корневые.
2. Измените логику распределения спрайтов, чтобы они добавлялись только в контейнеры нижнего уровня (листья иерархии).
3. Добавьте обработку клика (gameobjectdown) и реализуйте логику перетаскивания (drag) для контейнеров. Убедитесь, что при перетаскивании родительского контейнера двигаются и все его дети.
