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

Визуализация сложных, изменяющихся во времени эффектов — отличительная черта демосцены и инди-игр. Часто такие эффекты слишком динамичны или алгоритмически сложны, чтобы их можно было описать стандартными спрайтами или анимациями. В этом случае на помощь приходит программный Canvas. Эта статья покажет, как использовать `Phaser.Textures.CreateCanvas` для создания и анимирования текстуры прямо во время выполнения игры. Мы разберем конкретный пример, генерирующий гипнотический узор из движущихся квадратов, и объясним, как адаптировать этот подход для ваших проектов.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


const T = Math.tan;
const C = Math.cos;
const S = Math.sin;

class Example extends Phaser.Scene
{
    frame = 0;
    time = 0;
    x;
    c;

    create ()
    {
        const canvasTexture = this.textures.createCanvas('dwitter', 1920, 1080);

        this.c = canvasTexture.getSourceImage();
        this.x = this.c.getContext('2d');

        this.add.image(0, 0, 'dwitter').setOrigin(0).setScale(0.5);
    }

    update ()
    {
        this.time = this.frame / 60;

        if (this.time * 60 | this.frame - 1 === 0)
        {
            this.time += 0.000001;
        }

        this.frame++;

        this.u(this.time);
    }

    R (r, g, b, a)
    {
        a = a === undefined ? 1 : a;

        return `rgba(${r|0},${g|0},${b|0},${a})`;
    }

    u (t)
    {
        let c = this.c;
        let x = this.x;
        let i;
        let j;

        c.width = 1920; for (i = 0; i < 31; i++) { for (j = 25; j > -25; j--) { x.fillRect(960 + j * i * 0.5 * C(i * 0.2) + C(2 * t + i * 0.2) * 300,540 + j * i * 0.5 * S(i * 0.2) + S(2.2 * t + i * 0.2) * 200,9,9); } }
    }
}

const config = {
    width: 960,
    height: 540,
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    backgroundColor: '#ffffff',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка Canvas-текстуры

Вместо загрузки статичного изображения мы создадим текстуру программно. Это делается в методе create() сцены.

Сначала создается Canvas-текстура с помощью this.textures.createCanvas(). Этот метод принимает уникальный ключ текстуры и её размеры. Затем мы получаем ссылку на сам HTMLCanvasElement и его 2D-контекст для рисования.

Наконец, создается изображение (this.add.image), которое использует нашу новую текстуру 'dwitter'. Оно позиционируется в начале координат и масштабируется, чтобы соответствовать размеру игры.

create ()
{
    const canvasTexture = this.textures.createCanvas('dwitter', 1920, 1080);

    this.c = canvasTexture.getSourceImage();
    this.x = this.c.getContext('2d');

    this.add.image(0, 0, 'dwitter').setOrigin(0).setScale(0.5);
}

Цикл анимации и управление временем

Анимация происходит в методе update(), который вызывается каждый кадр. Здесь вычисляется текущее 'время' игры в секундах (this.time).

Особенность данного примера — проверка if (this.time * 60 | this.frame - 1 === 0). Это трюк для предотвращения деления на ноль или других артефактов в самый первый кадр, когда this.frame может быть равен нулю. К времени добавляется очень маленькое значение, чтобы гарантировать корректность вычислений.

После обновления времени вызывается основная функция отрисовки this.u(this.time).

update ()
{
    this.time = this.frame / 60;

    if (this.time * 60 | this.frame - 1 === 0)
    {
        this.time += 0.000001;
    }

    this.frame++;

    this.u(this.time);
}

Вспомогательная функция для цвета

Функция R() — это удобная утилита для форматирования цвета в строку CSS rgba. Она принимает компоненты красного, зелёного, синего и опционально альфа-канал (прозрачность). Если альфа не указана, по умолчанию используется 1 (полная непрозрачность).

Оператор |0 используется для быстрого приведения чисел к целым значениям (отбрасывается дробная часть). Это нужно для корректного формата CSS.

R (r, g, b, a)
{
    a = a === undefined ? 1 : a;

    return `rgba(${r|0},${g|0},${b|0},${a})`;
}

Ядро визуализации: функция u(t)

Вся магия происходит в функции u(t). Она перерисовывает всю Canvas-текстуру каждый кадр, используя переданное время `t` как параметр для анимации.

Строка c.width = 1920; — ключевой момент. Присвоение ширины (или высоты) canvas полностью очищает его. Это стандартный и производительный способ очистки Canvas перед новой отрисовкой кадра.

Далее идут вложенные циклы for, которые рисуют сетку из квадратов. Их позиция вычисляется динамически с использованием тригонометрических функций `C(косинус) иS(синус), которые были заранее сохранены как локальные синонимы дляMath.cosиMath.sin. Формулы960 + j * i * 0.5 * C(i * 0.2) + C(2 * t + i * 0.2) * 300и540 + ...создают сложную траекторию для каждого квадрата, зависящую от переменных цикловi,jи времениt. Квадраты рисуются методомx.fillRect()`.

u (t)
{
    let c = this.c;
    let x = this.x;
    let i;
    let j;

    c.width = 1920; for (i = 0; i < 31; i++) { for (j = 25; j > -25; j--) { x.fillRect(960 + j * i * 0.5 * C(i * 0.2) + C(2 * t + i * 0.2) * 300,540 + j * i * 0.5 * S(i * 0.2) + S(2.2 * t + i * 0.2) * 200,9,9); } }
}

Настройка экземпляра игры

Конфигурационный объект config определяет параметры запуска Phaser-игры. Обратите внимание на свойства: - width и height: задают размер игрового поля (в данном случае в 2 раза меньше Canvas-текстуры, так как изображение масштабировано в 0.5). - type: Phaser.CANVAS указывает, что рендеринг должен использовать Canvas API, а не WebGL. - backgroundColor: устанавливает белый фон. - scene: указывает класс нашей сцены Example.

После определения конфигурации создаётся экземпляр игры new Phaser.Game(config).

const config = {
    width: 960,
    height: 540,
    type: Phaser.CANVAS,
    parent: 'phaser-example',
    backgroundColor: '#ffffff',
    scene: Example
};

const game = new Phaser.Game(config);

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

Использование Canvas-текстур открывает двери для procedural graphics и сложных динамических эффектов в Phaser, которые сложно реализовать другими способами. Основные шаги: создать текстуру, получить контекст для рисования, очищать canvas присваиванием ширины и рисовать в нём каждый кадр в update(). **Идеи для экспериментов:** 1. Измените формулы внутри fillRect для создания других паттернов (спиралей, волн). 2. Используйте функцию R() для задания цвета заливки (x.fillStyle) перед рисованием фигур. 3. Вместо fillRect попробуйте strokeRect, arc (для кругов) или drawImage для комбинации с другими текстурами. 4. Управляйте параметрами (количеством фигур, амплитудой колебаний) с помощью переменных, которые можно менять в реальном времени.