О чем этот пример
Обрезка изображений (crop) — мощный инструмент в арсенале игрового разработчика. Она позволяет не загружать десятки отдельных текстур, а работать с атласами, вырезая из них нужные области. Однако в Phaser 3 есть нюанс: координаты обрезки рассчитываются относительно внутренней системы отсчёта спрайта, а не глобальных координат сцены. Эта статья на реальном примере из репозитория Phaser показывает, как корректно работать с методом `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-2.png', 'assets/atlas/megaset-2.json');
}
create ()
{
this.add.image(400, 300, 'atlas', 'hello').setAlpha(0.3);
this.graphics = this.add.graphics();
this.bob = this.add.image(400, 300, 'atlas', 'hello');
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);
Суть проблемы: система координат для cropping
Основная сложность при работе с setCrop() заключается в точке отсчёта. Метод ожидает координаты не относительно левого верхнего угла сцены или даже самого спрайта на сцене, а относительно внутренней текстуры (фрейма) этого спрайта.
Визуализируем: ваш спрайт bob находится на сцене в позиции (400, 300). Но его внутреннее изображение (фрейм hello из атласа) имеет свою собственную систему координат, где точка (0,0) — это левый верхний угол этого фрейма. Метод setCrop(x, y, width, height) работает именно в этой системе.
Поэтому просто передать в него координаты курсора (pointer.x, pointer.y) — ошибка. Нужно перевести глобальные координаты курсора в локальные координаты фрейма спрайта.
// Так делать НЕЛЬЗЯ. Это вызовет непредсказуемую обрезку.
this.bob.setCrop(pointer.x, pointer.y, cropWidth, cropHeight);
Решение: корректный расчёт offset
В предоставленном примере используется верный подход. В момент создания (create) вычисляется и сохраняется точка offset — это глобальные координаты левого верхнего угла *текстурной области* спрайта. Именно от этой точки нужно вести расчёт.
Ключевые строки кода:
// Получаем мировые координаты верхнего левого угла спрайта
this.offset = this.bob.getTopLeft();
// В обработчике движения курсора пересчитываем координаты
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: переводим глобальную координату X курсора в локальную для фрейма.
2. - cropWidth / 2: смещаем точку обрезки так, чтобы курсор оказался в центре вырезаемого прямоугольника. Это делает интерфейс более интуитивным.
3. Аналогично для координаты Y.
Таким образом, область обрезки будет следовать за курсором, оставаясь корректно привязанной к изображению спрайта.
Визуализация области crop
Чтобы видеть, какая именно область изображения сейчас обрезана, в методе 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.bob._crop` — внутренний объект, содержащий текущие параметры обрезки (`x`, `y`, `width`, `height`). Эти значения — локальные для фрейма.
- Чтобы нарисовать рамку на сцене в нужном месте, мы берём мировую точку отсчёта (`this.offset`) и прибавляем к ней локальные координаты обрезки.
- `this.graphics.clear()` вызывается каждый кадр, чтобы старая рамка стиралась перед отрисовкой новой.
**Важно:** Использование внутреннего свойства _crop (с подчёркиванием) указывает на то, что оно не является частью публичного API и может измениться в будущих версиях. В продакшн-коде для получения этих данных стоит использовать публичные геттеры, если они доступны.
Практическое применение и вариации
Понимание механики cropping открывает несколько практических путей для использования:
1. **Интерактивные элементы:** Создание подсветки или "увеличения" части спрайта при наведении.
2. **Анимация появления:** Можно анимировать ширину или высоту обрезки (cropWidth/cropHeight), чтобы изображение "проявлялось" по горизонтали или вертикали.
3. **Маска для диалогового окна:** Использовать обрезку для создания эффекта портрета говорящего персонажа в диалоговом окне, динамически меняя область обрезки в зависимости от эмоции.
Пример анимации появления спрайта сверху вниз:
// В create()
this.bob.setCrop(0, 0, this.bob.width, 0);
// В update() - увеличиваем высоту обрезки до полной
if (this.bob._crop.height < this.bob.height) {
this.bob.setCrop(0, 0, this.bob.width, this.bob._crop.height + 2);
}
Главное правило — всегда помнить о переводе координат из глобальной системы сцены в локальную систему фрейма спрайта при вызове setCrop().
Что попробовать дальше
Динамический cropping в Phaser 3 — это не просто обрезка картинки, а инструмент для создания интерактивности и анимации непосредственно в рантайме. Ключ к успеху — правильный расчёт offset через getTopLeft(). Для экспериментов попробуйте заставить область обрезки следовать не за курсором, а за другим движущимся игровым объектом, или создайте эффект "дыхания", циклически меняя размеры обрезанной области. Также стоит изучить связку setCrop с setOrigin, чтобы менять точку отсчёта для обрезки.
