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

При создании игр часто возникает необходимость не просто отобразить спрайт, а динамически управлять его видимой частью. Например, для эффектов постепенного появления, создания масок или интерактивного "окна" в текстуру. В Phaser для этого у объектов типа `Image` есть мощный метод `setCrop()`. В этой статье мы разберем практический пример, который показывает, как обрезать кадр из атласа текстур и управлять областью обрезки в реальном времени с помощью курсора мыши. Это полезно для создания кастомных интерфейсов, мини-карт или нестандартных анимаций.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    offset;
    graphics;
    bob;

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

    create ()
    {
        this.add.image(400, 300, 'atlas', 'phaser2').setAlpha(0.3);

        this.bob = this.add.image(400, 300, 'atlas', 'phaser2');

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

        const cropWidth = 200;
        const cropHeight = 100;

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

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

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

            this.bob.setCrop(
                (pointer.x - this.offset.x) - cropWidth / 2,
                (pointer.y - this.offset.y) - cropHeight / 2,
                cropWidth,
                cropHeight
            );
        });
    }

    update ()
    {
        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.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d88',
    scene: Example
};

const game = new Phaser.Game(config);

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

В методе preload() загружается атлас текстур. Атлас — это один большой PNG-файл (megaset-0.png) с набором спрайтов и соответствующий JSON-файл с координатами каждого кадра. Это позволяет эффективно использовать память и загружать множество текстур одной операцией.

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

В методе create() создаются два основных объекта. Первый — фоновое полупрозрачное изображение, которое служит ориентиром для исходного размера и положения кадра 'phaser2'.

this.add.image(400, 300, 'atlas', 'phaser2').setAlpha(0.3);

Второй объект this.bob — это основное изображение, с которым мы будем работать. Именно к нему будет применяться обрезка.

this.bob = this.add.image(400, 300, 'atlas', 'phaser2');

Также создается объект Graphics, который будет использоваться для отрисовки зеленой рамки, визуализирующей текущую область обрезки.

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

Инициализация обрезки и вычисление точки отсчета

Перед началом работы с обрезкой определяются константы cropWidth и cropHeight. Они задают фиксированный размер прямоугольника, который будет "вырезаться" из текстуры.

const cropWidth = 200;
const cropHeight = 100;

Метод setCrop() объекта Image принимает четыре аргумента: `x,y,width,height. Эти координаты (x,y) отсчитываются от верхнего левого угла самого кадра текстуры (не от его позиции на экране!). Изначально обрезка устанавливается в(0, 0)`, то есть показывается верхний левый угол кадра размером 200x100 пикселей.

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

Для корректного расчета позиции обрезки относительно перемещения курсора нам нужна точка отсчета. Метод getTopLeft() возвращает мировые координаты (Phaser.Math.Vector2) верхнего левого угла игрового объекта this.bob на экране. Эта точка сохраняется в свойстве this.offset.

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

Интерактивное управление областью обрезки

Для создания интерактивности на событие перемещения указателя (pointermove) вешается обработчик. При каждом движении мыши вычисляются новые координаты для метода setCrop().

this.input.on('pointermove', pointer =>
{
    this.bob.setCrop(
        (pointer.x - this.offset.x) - cropWidth / 2,
        (pointer.y - this.offset.y) - cropHeight / 2,
        cropWidth,
        cropHeight
    );
});

Логика расчета: 1. pointer.x - this.offset.x — это расстояние от точки клика до левого края изображения на экране. 2. Из этого расстояния вычитается cropWidth / 2. Это нужно для того, чтобы центр области обрезки (а не ее левый верхний угол) следовал за курсором. Без этого смещения курсор всегда находился бы в углу зеленой рамки. 3. Аналогично рассчитывается координата `y`. В результате мышь указывает на центр "окна", которое "плавает" по текстуре.

Визуализация области обрезки

Чтобы пользователь видел текущие границы обрезки, в методе update() каждый кадр рисуется зеленая рамка. Сначала очищается холст Graphics от рисунка предыдущего кадра.

this.graphics.clear();
this.graphics.lineStyle(1, 0x00ff00);
Координаты для рисования прямоугольника рассчитываются сложением:
1. `this.offset.x` — мировая координата левого края изображения.
2. `this.bob._crop.x` — текущее смещение обрезки внутри самого кадра текстуры.
Аналогично для `y`. Ширина и высота берутся напрямую из внутреннего объекта `_crop`.
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);

Важно: свойство _crop (с нижним подчеркиванием) является внутренним для Phaser. В продакшн-коде напрямую к нему обращаться не рекомендуется, но в демонстрационных целях оно идеально показывает состояние обрезки.

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

Метод setCrop() предоставляет прямой и эффективный способ управлять видимой частью изображения в Phaser. В этом примере мы связали его с движением мыши, создав интерактивный просмотрщик текстуры. Для экспериментов попробуйте изменить логику: привяжите обрезку не к курсору, а к движению другого спрайта, создавая эффект "фонаря". Или анимируйте размеры cropWidth/cropHeight для эффекта постепенного раскрытия изображения. Можно также использовать setCrop() для создания простой системы frame-by-frame анимации, переключая _crop.x по таймеру.