О чем этот пример

Создание сложных игровых персонажей, состоящих из множества отдельных спрайтов — частая задача в 2D-разработке. Phaser 3 предлагает элегантное решение: класс `Container`. Он позволяет группировать игровые объекты, управляя ими как единым целым, сохраняя при этом возможность тонкой настройки каждого элемента. В этой статье мы разберем практический пример сборки анимированного персонажа девушки из частей атласа, что особенно полезно для создания персонажей с кастомизацией или пошаговой анимацией.

Версия 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.atlas('girl', 'assets/atlas/girl.png', 'assets/atlas/girl.json');
    }

    create ()
    {
        const girl = this.add.container(400, 450);

        const body = this.add.image(-30, 18, 'girl', 'Body').setName('body');
        const hair = this.add.image(110, -310, 'girl', 'HairTop').setName('hair');
        const head = this.add.image(0, -200, 'girl', 'Head').setName('head');
        const eyes = this.add.image(-34, -182, 'girl', 'EyesOpen').setName('eyes');
        const mouth = this.add.image(-58, -114, 'girl', 'MouthOpen').setName('mouth');
        const braidLeft = this.add.image(14, -74, 'girl', 'BraidLeft').setName('braidLeft');
        const braidRight = this.add.image(98, -72, 'girl', 'BraidRight').setName('braidRight');
        const armLeft = this.add.image(-48, -62, 'girl', 'ArmLeft').setName('armLeft').setOrigin(1, 0.5);
        const armRight = this.add.image(26, 28, 'girl', 'ArmRight').setName('armRight');
        const hand = this.add.image(18, 96, 'girl', 'HandRight').setName('hand');

        //  armLeft angle = -10 to 10

        girl.add([
            armLeft,
            body,
            braidLeft,
            braidRight,
            hair,
            head,
            eyes,
            mouth,
            armRight,
            hand
        ]);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#010101',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка атласа: основа для сборки

Перед сборкой необходимо загрузить ресурсы. В данном примере используется текстура с атласом — единое изображение, содержащее все части персонажа, и JSON-файл, описывающий координаты каждого фрейма.

Метод this.load.setBaseURL() задает базовый URL для загрузки, что удобно при хранении ресурсов в одном месте. Затем this.load.atlas() загружает сам атлас по его ключу 'girl'.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('girl', 'assets/atlas/girl.png', 'assets/atlas/girl.json');

Создание контейнера и его частей

Контейнер (Container) — это основной объект, который будет объединять все части. Он создается в точке (400, 450) на сцене. Это будет точка привязки (origin) для всей сборки.

Каждая часть тела — это отдельный спрайт (Image), созданный из фрейма атласа 'girl'. Ключевой момент: координаты каждого спрайта задаются относительно контейнера! Например, голова (head) помещается в точку (0, -200), то есть на 200 пикселей выше центра контейнера.

Метод .setName() задает уникальное имя каждому спрайту, что очень полезно для дальнейшего доступа к ним внутри контейнера.

const girl = this.add.container(400, 450);

const body = this.add.image(-30, 18, 'girl', 'Body').setName('body');
const head = this.add.image(0, -200, 'girl', 'Head').setName('head');
const eyes = this.add.image(-34, -182, 'girl', 'EyesOpen').setName('eyes');
// ... и так далее для hair, mouth, braidLeft, braidRight, armLeft, armRight, hand

Обратите внимание на руку armLeft: для нее задается нестандартная точка привязки .setOrigin(1, 0.5). Это смещает точку вращения спрайта к его правому краю по центру, что типично для вращения конечностей вокруг сустава.

Компоновка иерархии: добавление в контейнер

После создания всех спрайтов их необходимо добавить в контейнер, чтобы система отрисовки и трансформаций Phaser воспринимала их как группу. Это делается методом container.add(), который принимает массив объектов.

Порядок добавления имеет критическое значение — он определяет порядок отрисовки (z-index). Объекты, добавленные первыми, рисуются ниже тех, что добавлены позже. В нашем примере armLeft добавлена первой и будет на заднем плане, а hand — последней и будет поверх всех.

girl.add([
    armLeft,
    body,
    braidLeft,
    braidRight,
    hair,
    head,
    eyes,
    mouth,
    armRight,
    hand
]);

Теперь, применяя трансформации (позицию, масштаб, вращение) к объекту girl, мы будем двигать всю сборку целиком, сохраняя взаимное расположение частей.

Преимущества подхода и дальнейшее управление

Использование Container дает несколько преимуществ:

1. **Единое управление:** Вы можете двигать, вращать и масштабировать всего персонажа одной операцией. 2. **Сохранение иерархии:** Каждая часть сохраняет свою локальную позицию относительно контейнера. 3. **Простой доступ:** К любому дочернему элементу можно обратиться по индексу или имени, используя методы контейнера, например, container.getByName('eyes').

Для анимации можно изменять свойства отдельных частей, обращаясь к ним через контейнер. Например, чтобы качнуть левой рукой, как намекает комментарий в коде:

// Получаем ссылку на левую руку
const leftArm = girl.getByName('armLeft');
// Устанавливаем угол вращения
leftArm.angle = 5;

Контейнер также автоматически обновляет свою визуальную и физическую границу (bounds), учитывая всех своих детей.

Что попробовать дальше

Контейнеры в Phaser 3 — мощный инструмент для структурирования сложных игровых объектов. Показанный пример со сборкой персонажа — лишь отправная точка. Вы можете экспериментировать: анимировать части тела, меняя их angle или x/y; динамически заменять части (например, смена прически) через методы container.add() и container.remove(); или привязывать к контейнеру физическое тело, чтобы цельный персонаж взаимодействовал с миром. Это открывает путь к созданию сложных, детализированных и гибких в управлении персонажей для вашей игры.