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

Easing-функции — ключ к созданию плавной и естественной анимации в играх. Они определяют, как объект ускоряется или замедляется во время твина. В этой статье мы разберем наглядный пример из официальной документации Phaser, который рисует график движения для разных вариантов синусоидального easing. Вы научитесь визуализировать и понимать поведение функций `sine.in`, `sine.out` и `sine.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, 'Sine Ease').setColor('#00ff00').setFontSize(32).setShadow(2, 2).setOrigin(0.5, 0);

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

Разбор сцены и загрузки ресурсов

Код начинается с объявления класса сцены, который загружает фоновое изображение в методе preload. Обратите внимание на использование setBaseURL — это удобный способ задать базовый путь для всех загружаемых ассетов в этой сцене.

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

В методе create мы сначала добавляем фон и заголовок. Затем инициализируем ключевые переменные и графические объекты для визуализации.

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

Здесь types — массив строковых идентификаторов easing-функций, которые мы будем перебирать. type — индекс текущей функции, а tween — ссылка на активный твин, чтобы мы могли им управлять.

Графические объекты для отрисовки

Для визуализации пути движения используются три основных объекта Phaser: Graphics, Rectangle и RenderTexture.

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

* graph — объект Graphics, который будет рисовать непрерывную зеленую линию, представляющую траекторию движения. * rect — маленький зеленый прямоугольник, который является целью (target) для твина. Он будет двигаться, а его положение — использоваться для отрисовки. * rtRenderTexture. Это специальный текстурый объект, который действует как холст в памяти. Мы будем «штамповать» на него движущийся прямоугольник на каждом кадре, создавая след из точек. Это альтернативный способ визуализации пути.

Сердце примера: функция graphEase и твин

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

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();
    }
});
Это конфигурация твина:
*   `targets: rect` — анимируемый объект.
*   `x: { value: 750, ease: 'linear' }` — движение по горизонтали от начальной позиции (100) до 750 с **линейной** (`linear`) функцией. Это значит, что скорость по оси X постоянна.
*   `y: { value: 100, ease: types[type] }` — движение по вертикали от 400 до 100 с выбранной синусоидальной функцией (`sine.in`, `.out` или `.inout`). Именно эта настройка создает характерную кривую.
*   `onUpdate` — колбэк, вызываемый на каждом кадре обновления твина. Параметр `key` указывает, какое свойство обновилось. Мы обновляем графику только при изменении `x`, чтобы избежать дублирования. На `RenderTexture` ставится отпечаток прямоугольника (`rt.draw`), а в `Graphics` добавляется новая точка линии (`graph.lineTo`).
*   `onComplete` — после завершения твина мы вызываем `graph.stroke()`, чтобы отрисовать накопленную за время анимации линию.

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

Пример становится интерактивным благодаря обработчику клика. При каждом клике мы меняем индекс типа easing и заново запускаем визуализацию.

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

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

Что показывают графики?

Визуализация четко демонстрирует разницу: * **sine.in (Slow Start)**: Движение начинается медленно и плавно ускоряется к концу. На графике это видно по более пологой кривой в начале. * **sine.out (Slow End)**: Движение начинается быстро и плавно замедляется в конце. График круто поднимается вначале и выходит на плато. * **sine.inout (Slow Start and End)**: Комбинация двух предыдущих. Движение плавно ускоряется в первой половине и плавно замедляется во второй. Кривая симметрична.

Горизонтальная ось (X) — это время (т.к. движение по X линейное), а вертикальная ось (Y) — значение анимируемого свойства. Таким образом, наклон кривой показывает скорость изменения.

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

Этот пример — отличный инструмент для понимания принципов работы easing-функций в Phaser. Вы можете экспериментировать с ним: попробуйте заменить sine на другие функции, например quad, cubic или back. Измените начальные и конечные координаты прямоугольника или длительность анимации (duration), чтобы увидеть, как это влияет на график. Понимание этих кривых позволит вам создавать анимации, которые ощущаются физически правдоподобно и визуально приятно, будь то прыжок персонажа, исчезновение интерфейса или движение камеры.