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

Работая со сложными Spine-анимациями в Phaser, разработчики часто сталкиваются с необходимостью ограничить область их отображения — например, для создания эффекта взгляда через подзорную трубу, оконного проёма или UI-элементов неправильной формы. Стандартный контейнер `SpineContainer` позволяет группировать анимации, но как наложить на него маску? В этой статье мы разберем реальный пример из официального репозитория Phaser, который демонстрирует неочевидный, но эффективный способ применения геометрической маски к контейнеру со Spine-объектами. Вы узнаете, как создать контейнер, добавить в него анимации и обрезать их видимую область с помощью `GeometryMask`, а также как отлаживать положение и размер маски.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


var config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d66',
    scene: {
        preload: preload,
        create: create,
        update: update,
        pack: {
            files: [
                { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/3.8.95/SpinePluginDebug.js', sceneKey: 'spine' }
            ]
        }
    }
};

var controls;

var game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('logo', 'assets/sprites/phaser.png');

    this.load.setPath('assets/spine/3.8/spineboy');

    this.load.spine('boy', 'spineboy-pro.json', 'spineboy-pro.atlas', true);

    this.load.setPath('assets/spine/3.8/coin');

    this.load.spine('coin', 'coin-pro.json', 'coin-pro.atlas');
}

function create ()
{
    const spineContainer = this.add.spineContainer(200, 400);

    const spineBoy = this.add.spine(0, 50, 'boy', 'walk', true).setScale(0.5);
    spineContainer.add(spineBoy)

    //add mask to spineContainer
    spineContainer.maskShape = this.add.graphics()
    .fillStyle(0x7fff00, 0.2) //for debugging purposes
    .setVisible(true) //set true for debugging purposes
    .setDepth(5)
    .fillRect(
      Math.ceil(100),
      Math.ceil(100),
      Math.ceil(300),
      Math.ceil(300)
    )
    const mask = new Phaser.Display.Masks.GeometryMask(this, spineContainer.maskShape)
    spineContainer.setMask(mask)

    // this.add.image(0, 0, 'logo');

    const coin = this.add.spine(200, 300, 'coin', 'rotate', true).setScale(0.3);

    var cursors = this.input.keyboard.createCursorKeys();

    var controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        acceleration: 0.06,
        drag: 0.0005,
        maxSpeed: 1.0
    };

    controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}

function update (time, delta)
{
    controls.update(delta);
}

Подготовка сцены и загрузка ресурсов

В примере используется конфигурация под WEBGL, так как плагин SpinePlugin требует именно этот рендерер. В секции pack сцены подключается сам плагин для работы со Spine.

var config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d66',
    scene: {
        preload: preload,
        create: create,
        update: update,
        pack: {
            files: [
                { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/3.8.95/SpinePluginDebug.js', sceneKey: 'spine' }
            ]
        }
    }
};

В функции preload загружаются необходимые анимации. Обратите внимание на использование setBaseURL и setPath для организации путей к ресурсам. Spine-анимации загружаются с помощью метода this.load.spine.

function preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('logo', 'assets/sprites/phaser.png');
    this.load.setPath('assets/spine/3.8/spineboy');
    this.load.spine('boy', 'spineboy-pro.json', 'spineboy-pro.atlas', true);
    this.load.setPath('assets/spine/3.8/coin');
    this.load.spine('coin', 'coin-pro.json', 'coin-pro.atlas');
}

Создание контейнера и добавление Spine-объектов

Ключевой объект для маскирования — SpineContainer. Он действует как группа для Spine-объектов, позволяя применять к ним трансформации и эффекты совместно.

В функции create мы создаем контейнер в точке (200, 400) и добавляем в него анимацию Spineboy. Объект spineBoy создается с анимацией 'walk' в режиме повтора и масштабируется.

const spineContainer = this.add.spineContainer(200, 400);
const spineBoy = this.add.spine(0, 50, 'boy', 'walk', true).setScale(0.5);
spineContainer.add(spineBoy)

Отдельно, вне контейнера, создается анимация монетки. Это демонстрирует, что маска, применяемая к контейнеру, не затрагивает другие объекты на сцене.

const coin = this.add.spine(200, 300, 'coin', 'rotate', true).setScale(0.3);

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

Маска в Phaser — это объект, который определяет видимую область другого объекта. Для создания маски на основе графики используется класс Phaser.Display.Masks.GeometryMask.

Сначала создается графический объект (Graphics), который будет формой маски. В примере это зеленый прямоугольник с полупрозрачной заливкой. Параметры fillStyle и setVisible(true) оставлены для отладки, чтобы видеть, где именно находится область маски.

spineContainer.maskShape = this.add.graphics()
    .fillStyle(0x7fff00, 0.2)
    .setVisible(true)
    .setDepth(5)
    .fillRect(
        Math.ceil(100),
        Math.ceil(100),
        Math.ceil(300),
        Math.ceil(300)
    );

Затем эта графика передается в конструктор GeometryMask. Важно: маска создается для конкретной сцены (контекста this) и формы.

const mask = new Phaser.Display.Masks.GeometryMask(this, spineContainer.maskShape);

Наконец, маска применяется к контейнеру с помощью метода setMask. Теперь видимыми будут только те части анимации Spineboy, которые попадают в границы зеленого прямоугольника.

spineContainer.setMask(mask);

Управление камерой для проверки результата

Чтобы можно было перемещать камеру и с разных ракурсов проверить работу маски, в примере реализовано управление с клавиатуры. Используется Phaser.Cameras.Controls.SmoothedKeyControl для плавного перемещения.

var cursors = this.input.keyboard.createCursorKeys();
var controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    acceleration: 0.06,
    drag: 0.0005,
    maxSpeed: 1.0
};
controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

В функции update вызывается метод controls.update(delta) для обработки ввода и обновления положения камеры.

function update (time, delta)
{
    controls.update(delta);
}

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

Применение GeometryMask к SpineContainer — мощный прием для контроля отображения сложных анимаций. Этот метод позволяет создавать динамические эффекты обрезки, которые остаются производительными благодаря работе на уровне контейнера. Для экспериментов попробуйте: 1. Заменить прямоугольник fillRect на более сложную форму, используя методы графики Graphics. 2. Анимировать положение или размер графического объекта maskShape в update, чтобы маска двигалась или меняла форму. 3. Использовать в качестве маски не Graphics, а спрайт с текстурой, создав BitmapMask. 4. Применить маску не к контейнеру, а к отдельному Spine-объекту и сравнить разницу в отображении.