О чем этот пример
Визуальные эффекты, составленные из сотен анимированных частиц, способны превратить простую игру в захватывающее зрелище. В этой статье мы разберем пример из официальной коллекции Phaser, который показывает, как декомпозировать спрайт на отдельные пиксели и анимировать их, создавая гипнотическую «пиксельную волну». Вы научитесь работать с `CanvasTexture`, получать данные изображения и управлять цветом через `Phaser.Display.Color`, а также создавать сложные твин-анимации с задержками и повторами. Этот прием можно использовать для создания эпичных взрывов, магических заклинаний или плавных переходов между сценами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('sonic', 'assets/sprites/sonic.png');
this.load.image('pixel', 'assets/sprites/16x16.png');
}
create ()
{
const source = this.textures.get('sonic').source[0].image;
const canvas = this.textures.createCanvas('pad', 38, 42).source[0].image;
const ctx = canvas.getContext('2d');
ctx.drawImage(source, 0, 0);
const imageData = ctx.getImageData(0, 0, 38, 42);
let x = 0;
let y = 0;
const color = new Phaser.Display.Color();
for (var i = 0; i < imageData.data.length; i += 4)
{
const r = imageData.data[i];
const g = imageData.data[i + 1];
const b = imageData.data[i + 2];
const a = imageData.data[i + 3];
if (a > 0)
{
// var startX = 1024/2;
// var startY = 800;
const startX = Phaser.Math.Between(0, 1024);
const startY = Phaser.Math.Between(0, 768);
const dx = 200 + x * 16;
const dy = 64 + y * 16;
const image = this.add.image(startX, startY, 'pixel').setScale(0);
color.setTo(r, g, b, a);
image.setTint(color.color);
this.tweens.add({
targets: image,
duration: 2000,
x: dx,
y: dy,
scaleX: 1,
scaleY: 1,
angle: 360,
delay: i / 1.5,
yoyo: true,
repeat: -1,
repeatDelay: 6000,
hold: 6000
});
}
x++;
if (x === 38)
{
x = 0;
y++;
}
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
backgroundColor: '#1a1a1a',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка и загрузка: откуда берутся пиксели
Ключевая идея эффекта — взять исходное изображение и разобрать его на цветные составляющие. В методе preload загружаются два спрайта: оригинальное изображение Соника (sonic.png) и простая 16x16 пиксельная текстура (pixel.png), которая станет нашей частицей.
this.load.image('sonic', 'assets/sprites/sonic.png');
this.load.image('pixel', 'assets/sprites/16x16.png');
Далее, в create, мы получаем доступ к битмапу загруженного спрайта 'sonic' через менеджер текстур. Создается временный холст (CanvasTexture) с таким же размером, как у спрайта (38x42 пикселя). На этот холст отрисовывается исходное изображение, что позволяет нам затем работать с его пиксельными данными.
const source = this.textures.get('sonic').source[0].image;
const canvas = this.textures.createCanvas('pad', 38, 42).source[0].image;
const ctx = canvas.getContext('2d');
ctx.drawImage(source, 0, 0);
Извлечение данных: цвет каждого пикселя
Следующий шаг — получить массив данных каждого пикселя на холсте. Метод getImageData возвращает объект ImageData, содержащий одномерный массив (data). В этом массиве цвета каждого пикселя хранятся последовательно в формате RGBA (Red, Green, Blue, Alpha). Таким образом, для пикселя с индексом `iего компоненты будут находиться в ячейкахi,i+1,i+2,i+3`.
const imageData = ctx.getImageData(0, 0, 38, 42);
// imageData.data = [R, G, B, A, R, G, B, A, ...]
Мы инициализируем объект Phaser.Display.Color, который будет помогать нам управлять цветом в формате, понятном для Phaser. Также задаются переменные `xиy` для отслеживания позиции текущего пикселя в двумерной сетке спрайта (38x42).
Создание и раскраска частиц
Проходим циклом по массиву данных с шагом 4 (одна итерация — один пиксель). Если альфа-канал пикселя (`a`) больше нуля (пиксель не прозрачный), мы создаем для него частицу.
if (a > 0) {
const startX = Phaser.Math.Between(0, 1024);
const startY = Phaser.Math.Between(0, 768);
const image = this.add.image(startX, startY, 'pixel').setScale(0);
}
Частица — это обычный Image со спрайтом 'pixel'. Её начальная позиция (startX, startY) случайна в пределах игрового поля. Изначально масштаб установлен в 0, делая её невидимой. Цвет пикселя из исходного изображения применяется к частице через метод setTint. Объект color преобразует значения RGBA в единый числовой цвет, который понимает Phaser.
color.setTo(r, g, b, a);
image.setTint(color.color);
Сложная анимация: твины с задержкой и повтором
Магия движения создается с помощью системы твинов Phaser (this.tweens.add). Для каждой частицы создается анимация, которая управляет несколькими свойствами одновременно.
this.tweens.add({
targets: image,
duration: 2000,
x: dx,
y: dy,
scaleX: 1,
scaleY: 1,
angle: 360,
delay: i / 1.5,
yoyo: true,
repeat: -1,
repeatDelay: 6000,
hold: 6000
});
- **targets**: объект для анимации (наша частица).
- **x, y**: конечная точка (dx, dy) рассчитывается так, чтобы частицы в итоге выстроились в исходное изображение.
- **scaleX, scaleY**: изменение масштаба от 0 до 1 (появление).
- **angle**: полный оборот на 360 градусов.
- **delay**: задержка начала анимации, зависящая от индекса `i`. Это создает эффект волны, где пиксели начинают движение не одновременно.
- **yoyo**: если true, анимация проигрывается в обратном порядке после завершения.
- **repeat: -1**: бесконечное повторение.
- **repeatDelay, hold**: паузы в 6 секунд между повторениями и перед началом обратного движения, создающие ритмичный пульсирующий эффект.
Что попробовать дальше
Вы разобрали мощный паттерн для создания сложных визуальных эффектов из простых частиц. Комбинация Canvas API для чтения пикселей и гибкой системы твинов Phaser открывает огромный простор для творчества. Для экспериментов попробуйте: использовать другую текстуру-источник (например, логотип игры), изменить логику расчета конечных позиций частиц, чтобы они формировали не исходное изображение, а другую фигуру, или заменить твин на физическое тело с добавлением силы (setVelocity) для создания эффекта взрыва.
