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

Плавные анимации — ключ к отзывчивому и приятному геймплею. Phaser предоставляет богатый набор easing-функций, которые управляют скоростью изменения свойства во времени. Этот пример демонстрирует мощный инструмент для визуального подбора и комбинирования разных функций плавности для осей X и Y, позволяя создавать сложные и выразительные траектории движения объектов в вашей игре. Вы научитесь динамически управлять параметрами твинов и визуализировать их результат в реальном времени.

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

        const list1 = this.createSelectList('x', 250, 80);
        const list2 = this.createSelectList('y', 425, 80);
        const button = this.createButton(580, 80);

        button.addListener('click');

        let easeX = 'Linear';
        let easeY = 'Linear';

        list1.addEventListener('change', event => {

            easeX = event.target.selectedOptions[0].value;
            graphEase();

        });

        list2.addEventListener('change', event => {

            easeY = event.target.selectedOptions[0].value;
            graphEase();

        });

        button.on('click', () => {

            graphEase();

        });

        let tween;

        //  Draw the x/y bounds
        this.add.rectangle(150, 0, 1, 600, 0xe84dff).setOrigin(0, 0);
        this.add.rectangle(650, 0, 1, 600, 0xe84dff).setOrigin(0, 0);
        this.add.rectangle(0, 150, 800, 1, 0xe84dff).setOrigin(0, 0);
        this.add.rectangle(0, 500, 800, 1, 0xe84dff).setOrigin(0, 0);

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

        const graphEase = () => {

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

            rt.clear();

            rect.setPosition(150, 500);

            graph.clear();
            graph.lineStyle(3, 0x00ff00);
            graph.beginPath();
            graph.moveTo(rect.x, rect.y);

            tween = this.tweens.add({
                targets: rect,
                x: { value: 650, ease: easeX },
                y: { value: 150, ease: easeY },
                duration: 2000,
                onUpdate: (tween, target, key) => {
                    if (key === 'x')
                    {
                        rt.draw(rect);
                        graph.lineTo(rect.x, rect.y);
                    }
                },
                onComplete: () => {
                    graph.lineTo(rect.x, rect.y);
                    graph.stroke();
                }
            });
        }
    }

    createSelectList (id, x, y)
    {
        const eases = [
            'Linear',
            'Quad.in',
            'Cubic.in',
            'Quart.in',
            'Quint.in',
            'Sine.in',
            'Expo.in',
            'Circ.in',
            'Back.in',
            'Bounce.in',
            'Quad.out',
            'Cubic.out',
            'Quart.out',
            'Quint.out',
            'Sine.out',
            'Expo.out',
            'Circ.out',
            'Back.out',
            'Bounce.out',
            'Quad.inOut',
            'Cubic.inOut',
            'Quart.inOut',
            'Quint.inOut',
            'Sine.inOut',
            'Expo.inOut',
            'Circ.inOut',
            'Back.inOut',
            'Bounce.inOut'
        ];

        const div = document.createElement('div');

        const list = document.createElement('select');

        list.id = id;

        eases.forEach(ease => {

            const option = document.createElement('option');

            option.value = ease;
            option.innerText = ease;

            list.appendChild(option);

        });

        const label = document.createElement('label');

        label.for = id;
        label.innerText = id;
        label.style = 'color: #00ff00; font: 24px Courier; padding-right: 10px';

        div.appendChild(label);
        div.appendChild(list);

        this.add.dom(x, y, div);

        return list;
    }

    createButton (x, y)
    {
        const button = document.createElement('button');

        button.innerText = 'Display';

        return this.add.dom(x, y, button);
    }
}

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

const game = new Phaser.Game(config);

Структура примера и DOM-элементы

Пример создаёт интерактивную панель управления внутри игровой сцены. Это достигается за счёт системы DOM-элементов Phaser, которая позволяет встраивать обычные HTML-компоненты.

Ключевые элементы интерфейса создаются методами createSelectList и createButton. Они используют this.add.dom() для размещения HTML-элементов на канвасе.

// Создание выпадающего списка с функциями плавности
const list1 = this.createSelectList('x', 250, 80);

// Создание кнопки
const button = this.createButton(580, 80);
button.addListener('click');

Метод createSelectList генерирует список из всех встроенных в Phaser easing-функций, таких как 'Quad.inOut', 'Bounce.out' или 'Back.in'.

Механика миксования easing-функций

Сердце примера — функция graphEase, которая вызывается при изменении выбора в списках или нажатии кнопки. Она создаёт твин для маленького зелёного прямоугольника (rect), используя выбранные функции плавности для осей X и Y.

tween = this.tweens.add({
    targets: rect,
    x: { value: 650, ease: easeX }, // easeX из первого списка
    y: { value: 150, ease: easeY }, // easeY из второго списка
    duration: 2000,
    onUpdate: (tween, target, key) => {
        if (key === 'x') {
            rt.draw(rect);
            graph.lineTo(rect.x, rect.y);
        }
    },
    onComplete: () => {
        graph.lineTo(rect.x, rect.y);
        graph.stroke();
    }
});

Обратите внимание, что параметры ease задаются отдельно для свойств `xиy`. Это позволяет создать движение, где объект ускоряется и замедляется по горизонтали и вертикали по разным законам, например, имитируя прыжок с 'Bounce.out' по Y и плавное перемещение 'Sine.inOut' по X.

Визуализация траектории и рендер-текстура

Чтобы увидеть результат миксования, пример рисует траекторию движения и сохраняет след объекта.

1. **График (Graphics):** Объект graph рисует линию, которая обновляется в коллбеке onUpdate твина. Линия следует за прямоугольником, создавая интерактивный график пути. 2. **Рендер-текстура (RenderTexture):** Объект rt используется как холст для отрисовки следов (trails) движущегося прямоугольника. Каждый кадр в onUpdate прямоугольник отрисовывается в текстуру методом rt.draw(rect), создавая эффект шлейфа.

// Очистка предыдущей визуализации перед новым твином
rt.clear();
graph.clear();

// В onUpdate твина:
rt.draw(rect);          // Рисуем след
graph.lineTo(rect.x, rect.y); // Продолжаем линию графика

Фиолетовые прямоугольники, добавленные в create, задают границы области движения (от 150,500 до 650,150).

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

Phaser предоставляет гибкую систему твинов с поддержкой раздельных easing-функций для каждого свойства. Интерактивный подбор этих функций, как показано в примере, ускоряет разработку и позволяет находить идеальные параметры для игровой анимации. Для экспериментов попробуйте: добавить третий список для функции плавности масштаба (scale); реализовать сохранение понравившихся пресетов комбинаций; или привязать выбор ease-функции к физическим свойствам объекта, например, к его скорости.