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

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

Версия 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, 'Exponential Ease').setColor('#00ff00').setFontSize(32).setShadow(2, 2).setOrigin(0.5, 0);

        const types = [ 'expo.in', 'expo.out', 'expo.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);

Загрузка ресурсов и базовая настройка

Как и в любой сцене Phaser, работа начинается с методов preload и create. В preload мы загружаем фоновое изображение, а в create — создаем все необходимые объекты для демонстрации.

Мы добавляем фоновую картинку, заголовок и массив с названиями типов плавности, которые будем переключать. Также создаются объекты для визуализации: Graphics для рисования графика, маленький зеленый прямоугольник (rect), который будет двигаться, и RenderTexture (rt) для отрисовки его следа.

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, 'Exponential Ease').setColor('#00ff00').setFontSize(32).setShadow(2, 2).setOrigin(0.5, 0);

    const types = [ 'expo.in', 'expo.out', 'expo.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);
}

Логика анимации и отрисовки графика

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

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

Самое важное — создание твина для объекта rect. Мы задаем два свойства для анимации: `xиy. Движение по оси X использует линейную плавность ('linear'), чтобы перемещение было равномерным. Движение по оси Y использует выбранный тип экспоненциальной плавности из массиваtypes`. Это и создает кривую траекторию.

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();
        }
    });
}

Обработка обновлений и завершения твина

Ключевую роль в визуализации играют коллбэки твина onUpdate и onComplete.

Коллбэк onUpdate вызывается на каждом кадре анимации. Параметр key указывает, какое свойство объекта было обновлено. В нашем коде логика отрисовки срабатывает только при обновлении координаты `x. Это сделано для того, чтобы точка на графике добавлялась один раз за кадр, а не дважды (отдельно дляxиy`). При каждом таком обновлении мы: 1. Рисуем текущее состояние прямоугольника rect в RenderTexture (rt.draw(rect)). Это создает "шлейф" из точек. 2. Добавляем новую точку в путь объекта Graphics с помощью graph.lineTo(rect.x, rect.y).

Коллбэк onComplete срабатывает по окончании анимации. В нем мы вызываем graph.stroke(), чтобы окончательно отрисовать накопленный путь (график функции плавности) на экране.

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

Интерактивность: переключение типов плавности

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

После изменения типа вызывается функция graphEase(), которая перезапускает анимацию и отрисовку с новыми параметрами. Таким образом, пользователь может циклически перебирать expo.in, expo.out и expo.inout, наблюдая за изменением формы графика.

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

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

Этот пример — отличная отправная точка для работы с твинами в Phaser. Вы научились применять разные функции плавности, использовать коллбэки onUpdate и onComplete для сложной логики и визуализации, а также создавать интерактивные демонстрации. **Идеи для экспериментов:** 1. Замените types на другие функции плавности, например, 'sine.in', 'back.out' или 'bounce.inout'. 2. Попробуйте анимировать другие свойства, например, scale или alpha, и понаблюдайте за разницей в поведении. 3. Используйте RenderTexture для создания эффекта "светлячка" или рисования траектории снаряда в реальной игре. 4. Создайте интерфейс выбора плавности (кнопки, селект) вместо переключения по клику.