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

Визуальные эффекты — ключ к захватывающему геймплею. Один из мощных, но неочевидных приёмов — обрезка (кроп) анимированных спрайтов. Эта техника позволяет создавать динамические маски, эффекты "взгляда через подзорную трубу" или интерактивное раскрытие контента, когда игрок управляет видимой областью. В статье разберём пример, где анимированный скат отображается не целиком, а лишь в области, следующей за курсором мыши, и поймём, как управлять этим механизмом.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    cropHeight;
    cropWidth;
    py;
    px;
    offset;
    graphics;
    bob;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('sea', 'assets/animations/seacreatures_json.png', 'assets/animations/seacreatures_json.json');
    }

    create ()
    {
        this.anims.create({ key: 'stingray', frames: this.anims.generateFrameNames('sea', { prefix: 'stingray', end: 23, zeroPad: 4 }), repeat: -1 });

        this.add.sprite(400, 300, 'sea').setAlpha(0.3).play('stingray');

        this.bob = this.add.sprite(400, 300, 'sea').play('stingray');

        this.graphics = this.add.graphics();

        this.cropWidth = 64;
        this.cropHeight = 64;
        this.px = 0;
        this.py = 0;

        this.bob.setCrop(0, 0, this.cropWidth, this.cropHeight);

        this.offset = this.bob.getTopLeft();

        this.input.on('pointermove', pointer =>
        {

            this.px = pointer.x - this.offset.x;
            this.py = pointer.y - this.offset.y;

        });
    }

    update ()
    {
        this.bob.setCrop(
            this.px - this.cropWidth / 2,
            this.py - this.cropHeight / 2,
            this.cropWidth,
            this.cropHeight
        );

        this.graphics.clear();
        this.graphics.lineStyle(1, 0x00ff00);
        this.graphics.strokeRect(this.offset.x + this.bob._crop.x, this.offset.y + this.bob._crop.y, this.bob._crop.width, this.bob._crop.height);
    }
}

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

const game = new Phaser.Game(config);

Подготовка анимации и спрайтов

В методе preload загружается атлас анимации с морскими существами. В create создаётся сама анимация stingray на основе кадров из атласа.

Затем добавляются два спрайта с одной и той же текстурой 'sea' и анимацией 'stingray'. Первый спрайт служит фоновым, полупрозрачным (setAlpha(0.3)) ориентиром, чтобы видеть полную анимацию. Второй, this.bob, — это основной спрайт, к которому будет применён кроп. Также создаётся объект Graphics для отладки — он будет рисовать зелёную рамку вокруг области кропа.

this.anims.create({ key: 'stingray', frames: this.anims.generateFrameNames('sea', { prefix: 'stingray', end: 23, zeroPad: 4 }), repeat: -1 });
this.add.sprite(400, 300, 'sea').setAlpha(0.3).play('stingray');
this.bob = this.add.sprite(400, 300, 'sea').play('stingray');
this.graphics = this.add.graphics();

Инициализация кропа и отслеживание курсора

Задаются начальные размеры области кропа — 64x64 пикселя. Изначально кроп применяется к левому верхнему углу спрайта с помощью метода setCrop. Важно получить точку offset — это мировые координаты верхнего левого угла спрайта this.bob. Они понадобятся для корректного перевода координат курсора.

Обработчик события pointermove обновляет переменные this.px и this.py. В них записываются координаты указателя мыши, но не мировые, а относительно верхнего левого угла самого спрайта this.bob. Это достигается вычитанием this.offset.x и this.offset.y из координат курсора.

this.cropWidth = 64;
this.cropHeight = 64;
this.bob.setCrop(0, 0, this.cropWidth, this.cropHeight);
this.offset = this.bob.getTopLeft();
this.input.on('pointermove', pointer => {
    this.px = pointer.x - this.offset.x;
    this.py = pointer.y - this.offset.y;
});

Динамическое обновление кропа и отладка

В методе update, который вызывается каждый кадр, происходит магия. Позиция области кропа пересчитывается так, чтобы её центр совпадал с координатами курсора относительно спрайта. Метод setCrop принимает координаты X и Y левого верхнего угла области кропа (относительно текстуры спрайта), а также её ширину и высоту. Вычитание половины размера из this.px и this.py как раз центрирует область на курсоре.

Для наглядности поверх спрайта рисуется зелёная рамка, точно соответствующая области кропа. Координаты для рисования рассчитываются как сумма мирового смещения спрайта (this.offset) и внутренних координат кропа, которые после применения setCrop хранятся в свойстве this.bob._crop.

this.bob.setCrop(
    this.px - this.cropWidth / 2,
    this.py - this.cropHeight / 2,
    this.cropWidth,
    this.cropHeight
);
this.graphics.clear();
this.graphics.lineStyle(1, 0x00ff00);
this.graphics.strokeRect(this.offset.x + this.bob._crop.x, this.offset.y + this.bob._crop.y, this.bob._crop.width, this.bob._crop.height);

Конфигурация игры и запуск

Стандартная конфигурация игры Phaser. Обратите внимание, что в config.type указан Phaser.CANVAS. Это важно, потому что некоторые эффекты, связанные с отрисовкой, могут по-разному работать в режиме WebGL. Сцена Example передаётся в конфиг, после чего создаётся экземпляр игры.

const config = {
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d88',
    scene: Example
};
const game = new Phaser.Game(config);

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

Метод setCrop — это гибкий инструмент для управления отображением текстуры спрайта. Он работает даже с анимированными спрайтами, обрезая текущий отображаемый кадр. Для экспериментов попробуйте: изменить размер области кропа в зависимости от скорости движения мыши, привязать кроп не к курсору, а к другому игровому объекту, создать эффект постепенного «проявления» спрайта, увеличивая размер кропа, или использовать несколько областей кропа на одном спрайте для сложных эффектов разбиения.