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

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

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

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

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

        this.bob = this.add.image(400, 300, 'atlas', 'hello').setFlipX(true);

        const cropWidth = 200;
        const cropHeight = 100;

        this.bob.setCrop(20, 20, 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() загружается текстурный атлас — эффективный способ хранения множества изображений в одном файле. Метод this.load.atlas() принимает ключ ресурса, путь к изображению и путь к JSON-файлу с данными о кадрах.

this.load.atlas('atlas', 'assets/atlas/megaset-2.png', 'assets/atlas/megaset-2.json');

В create() создается фоновое полупрозрачное изображение с ключом 'atlas' и конкретным кадром 'hello'. Метод setFlipX(true) зеркально отражает его по горизонтали. Это демонстрирует, как можно быстро модифицировать спрайт. Затем создается графический объект this.graphics для отладки и основной спрайт this.bob, с которым мы будем работать.

Инициализация обрезки (Crop)

Обрезка позволяет показывать только часть текстуры спрайта. Метод setCrop() объекта Image принимает четыре параметра: координаты X и Y верхнего левого угла области обрезки относительно текстуры, а также ее ширину и высоту.

const cropWidth = 200;
const cropHeight = 100;
this.bob.setCrop(20, 20, cropWidth, cropHeight);

Здесь мы устанавливаем начальную область обрезки размером 200x100 пикселей, смещенную на 20 пикселей от края текстуры. Важно: координаты обрезки (_crop.x, _crop.y) отсчитываются от внутренней системы координат текстуры, а не от позиции спрайта на сцене. Чтобы правильно связать их с мировыми координатами, нам нужна точка отсчета, которую мы получаем с помощью this.bob.getTopLeft() и сохраняем в this.offset.

Интерактивное управление обрезкой

Для создания интерактивности мы подписываемся на событие движения указателя pointermove. В обработчике события мы каждый раз пересчитываем позицию области обрезки так, чтобы ее центр следовал за курсором мыши.

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` и `cropHeight / 2` центрирует область обрезки под курсором.
3. Ширина и высота области остаются фиксированными. Таким образом, при движении мыши видимая часть текстуры `this.bob` меняется.

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

Чтобы визуально отображать текущие границы обрезки, в методе 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);

Ключевой момент: итоговые экранные координаты прямоугольника — это сумма смещения спрайта (this.offset) и внутренних координат обрезки (this.bob._crop). Свойства _crop (например, _crop.x) обновляются автоматически при каждом вызове setCrop(). Метод graphics.clear() вызывается каждый кадр, чтобы стереть предыдущий прямоугольник перед рисованием нового.

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

Объект конфигурации config определяет основные параметры игры: тип рендерера, родительский DOM-элемент, размеры холста, цвет фона и главную сцену.

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

Экземпляр Phaser.Game инициализируется с этой конфигурацией, запуская жизненный цикл сцены (preload, create, update). Установка backgroundColor помогает четко видеть границы игрового поля.

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

Методы setCrop() и setFlip() предоставляют мощный и производительный контроль над отображением спрайтов без необходимости редактирования исходных изображений. Вы можете экспериментировать: анимировать размер области обрезки для эффекта "проявления", использовать обрезку для отображения шкалы здоровья прямо на текстуре персонажа, комбинировать setCrop с масками для сложных эффектов или привязывать обрезку не к курсору, а к положению другого игрового объекта.