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

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