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

Плавные переходы (tweens) — ключевой элемент игровой динамики. Phaser предлагает мощный встроенный твин-менеджер с множеством функций плавности (easing). Понимание их работы наглядно помогает создавать выразительную анимацию. В этой статье мы разберем пример, который визуализирует три варианта функции `bounce`, показывая график движения объекта в реальном времени.

Версия 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('bg', 'assets/tweens/background-crt.jpg');
    }

    create ()
    {
        this.add.image(400, 300, 'bg').setScale(0.78);

        this.add.text(400, 28, 'Bounce Ease').setColor('#00ff00').setFontSize(32).setShadow(2, 2).setOrigin(0.5, 0);

        const types = [ 'bounce.in', 'bounce.out', 'bounce.inout' ];
        let type = 0;
        let tween;

        const label = this.add.text(400, 530).setColor('#00ff00').setFontSize(22).setShadow(1, 1).setOrigin(0.5, 0).setAlign('center');

        const graph = this.add.graphics();
        const rect = this.add.rectangle(100, 400, 2, 2, 0x00ff00);
        const rt = this.add.renderTexture(400, 300, 800, 600);

        const graphEase = () => {

            if (tween)
            {
                tween.stop();
            }

            rt.clear();

            graph.clear();
            graph.lineStyle(3, 0x00ff00);
            graph.beginPath();

            rect.setPosition(50, 450);

            label.setText([
                types[type],
                'Click to change type'
            ]);

            tween = this.tweens.add({
                targets: rect,
                x: { value: 750, ease: 'linear' },
                y: { value: 100, ease: types[type] },
                duration: 4000,
                onUpdate: (tween, target, key) => {
                    if (key === 'x')
                    {
                        rt.draw(rect);
                        graph.lineTo(rect.x, rect.y);
                    }
                },
                onComplete: () => {
                    graph.stroke();
                }
            });
        }

        this.input.on('pointerdown', () => {

            type++;

            if (type === types.length)
            {
                type = 0;
            }

            graphEase();

        });

        graphEase();
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и ресурсов

Класс Example расширяет Phaser.Scene. В методе preload загружается фоновое изображение. Важно отметить, что setBaseURL задает базовый путь для всех последующих загрузок.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/tweens/background-crt.jpg');

В create сначала добавляется фон и заголовок. Затем создаются ключевые объекты для визуализации: массив типов bounce, текстовая метка label, графический объект graph для рисования линии, зеленый прямоугольник rect как движущаяся точка и RenderTexture (rt) для отрисовки следа движения.

const graph = this.add.graphics();
const rect = this.add.rectangle(100, 400, 2, 2, 0x00ff00);
const rt = this.add.renderTexture(400, 300, 800, 600);

Функция отрисовки графика

Основная логика заключена в функции graphEase. Она запускается при старте и при каждом клике мыши.

const graphEase = () => {
    if (tween) {
        tween.stop();
    }
    rt.clear();
    graph.clear();
    // ... настройка и запуск tween
}

Функция сначала останавливает предыдущий твин (если он был), затем очищает RenderTexture и графический контекст. Это необходимо для начала новой чистой анимации. После сброса состояния она настраивает и запускает новый твин.

tween = this.tweens.add({
    targets: rect,
    x: { value: 750, ease: 'linear' },
    y: { value: 100, ease: types[type] },
    duration: 4000,
    onUpdate: (tween, target, key) => { /* ... */ },
    onComplete: () => { graph.stroke(); }
});

Конфигурация твина и колбэки

Твин анимирует два свойства прямоугольника rect. Движение по оси X использует линейную (linear) функцию плавности, чтобы время на графике шло равномерно. Движение по оси Y использует выбранный тип bounce (bounce.in, bounce.out или bounce.inout), что создает эффект отскока.

Колбэк onUpdate — сердце визуализации. Он срабатывает на каждом кадре анимации.

onUpdate: (tween, target, key) => {
    if (key === 'x') {
        rt.draw(rect);
        graph.lineTo(rect.x, rect.y);
    }
}

Параметр key указывает, какое свойство обновилось. Код реагирует только на обновление `x. В этот момент текущая позиция прямоугольника отрисовывается вRenderTexture` (оставляя след), и к графическому контуру добавляется новая линия. Такой подход гарантирует, что точки графика синхронизированы по времени (X).

Колбэк onComplete вызывается по окончании анимации и обводит накопленный графический контур.

onComplete: () => {
    graph.stroke();
}

Обработка ввода и переключение типов

По клику мыши (pointerdown) увеличивается индекс type для перебора массива types. Когда индекс достигает конца массива, он сбрасывается в ноль.

this.input.on('pointerdown', () => {
    type++;
    if (type === types.length) {
        type = 0;
    }
    graphEase();
});

После изменения типа вызывается graphEase(), которая перезапускает анимацию с новой функцией плавности. Текстовая метка label обновляется внутри graphEase, чтобы отображать текущее активное имя функции.

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

Этот пример — отличный инструмент для понимания поведения функций плавности. Вы можете экспериментировать: подставить другие ease-функции (например, quad, cubic, elastic) в массив types, изменить длительность (duration) или траекторию движения, чтобы увидеть разницу. Попробуйте заменить RenderTexture на рисование частиц (this.add.particles) для создания более живописного следа.