О чем этот пример
При создании игр часто возникает необходимость не просто отобразить спрайт, а динамически управлять его видимой частью. Например, для эффектов постепенного появления, создания масок или интерактивного "окна" в текстуру. В 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 по таймеру.
