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

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

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

Живой запуск

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

Исходный код


class Bat extends Phaser.GameObjects.Container
{
    constructor (scene, x, y)
    {
        super(scene, x, y);

        const body = scene.add.image(0, 0, 'assets', 'Body02_01');
        const wing1 = scene.add.image(-50, 10, 'assets', 'Wing02_01').setOrigin(1, 0.5);
        const wing2 = scene.add.image(50, 10, 'assets', 'Wing02_02').setOrigin(0, 0.5);

        this.add([ wing2, body, wing1 ]);

        this.body = body;
        this.wing1 = wing1;
        this.wing2 = wing2;

        scene.add.existing(this);

        body.setInteractive(new Phaser.Geom.Circle(170, 170, 100), Phaser.Geom.Circle.Contains);

        body.once('pointerdown', () => this.kill());

        this.fly();
    }

    fly ()
    {
        const y = this.y - (Phaser.Math.Between(150, 280));

        this.scene.tweens.add({
            targets: this,
            y,
            ease: 'sine.inout',
            yoyo: true,
            repeat: -1,
            duration: Phaser.Math.Between(900, 1200)
        });

        this.scene.tweens.add({
            targets: this.wing1,
            angle: { start: -20, to: 20 },
            ease: 'sine.inout',
            yoyo: true,
            repeat: -1,
            duration: 200
        });

        this.scene.tweens.add({
            targets: this.wing2,
            angle: { start: 20, to: -20 },
            ease: 'sine.inout',
            yoyo: true,
            repeat: -1,
            duration: 200
        });
    }

    kill ()
    {
        this.scene.tweens.killTweensOf([ this, this.wing1, this.wing2 ]);

        this.wing1.setAngle(20);
        this.wing2.setAngle(-20);

        this.body.setFrame('Body02_02');

        this.scene.tweens.chain({
            targets: this,
            tweens: [
                {
                    y: '-=100',
                    angle: 270,
                    scale: 0.3,
                    duration: 500
                },
                {
                    angle: 180,
                    y: 800,
                    ease: 'power1',
                    duration: 500
                }
            ],
            onComplete: () => {

                const x = Phaser.Math.Between(100, 700);
                const y = Phaser.Math.Between(300, 500);
                const scale = Phaser.Math.FloatBetween(0.4, 1);

                new Bat(this.scene, x, y).setScale(scale);

                this.destroy();

            }
        });
    }
}

class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/spooky.png');
        this.load.atlas('assets', 'assets/atlas/tweenparts.png', 'assets/atlas/tweenparts.json');
    }

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

        new Bat(this, 600, 300).setScale(0.4);
        new Bat(this, 180, 400).setScale(0.7);
        new Bat(this, 440, 500);
    }
}

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

const game = new Phaser.Game(config);

Структура объекта: контейнер с частями тела

Летучая мышь создана как наследник класса Phaser.GameObjects.Container. Это позволяет группировать несколько игровых объектов (спрайтов) и управлять ими как единым целым. В конструкторе создаются три части: тело и два крыла, которые добавляются в контейнер.

const body = scene.add.image(0, 0, 'assets', 'Body02_01');
const wing1 = scene.add.image(-50, 10, 'assets', 'Wing02_01').setOrigin(1, 0.5);
const wing2 = scene.add.image(50, 10, 'assets', 'Wing02_02').setOrigin(0, 0.5);

this.add([ wing2, body, wing1 ]);

Крылья смещены относительно центра контейнера (координаты -50 и 50 по X). Важно настроить точку вращения (origin) для каждого крыла: левое крыло вращается вокруг своей правой границы (1, 0.5), а правое — вокруг левой (0, 0.5). Это обеспечивает естественное движение, как будто крылья крепятся к телу.

Полет: независимые твины для объекта и его частей

Метод fly() запускает три параллельные анимации с помощью this.scene.tweens.add. Каждая настройка твина — это объект конфигурации.

this.scene.tweens.add({
    targets: this,
    y,
    ease: 'sine.inout',
    yoyo: true,
    repeat: -1,
    duration: Phaser.Math.Between(900, 1200)
});

Первый твин двигает весь контейнер вверх-вниз по синусоидальной траектории (ease: 'sine.inout'). Параметры yoyo: true и repeat: -1 заставляют анимацию колебаться и повторяться бесконечно. Длительность (duration) выбирается случайно для каждой мыши, что добавляет реалистичности.

Два других твина анимируют углы поворота (angle) крыльев в противофазе. Это создает иллюзию махания.

targets: this.wing1,
angle: { start: -20, to: 20 },

Интерактивность: обработка клика и завершение анимаций

Тело мыши сделано интерактивным с областью в форме круга. При первом клике срабатывает метод kill().

body.setInteractive(new Phaser.Geom.Circle(170, 170, 100), Phaser.Geom.Circle.Contains);
body.once('pointerdown', () => this.kill());

Первым делом в kill() все текущие твины объекта и его крыльев останавливаются методом killTweensOf. Это предотвращает конфликты анимаций.

this.scene.tweens.killTweensOf([ this, this.wing1, this.wing2 ]);

Крылья мгновенно устанавливаются в крайнее положение (setAngle), а тело меняет кадр спрайта на "мертвый" ('Body02_02').

Цепочка твинов: эффектное падение и респаун

Основная магия происходит с помощью this.scene.tweens.chain. Этот метод позволяет запускать твины последовательно, один за другим.

this.scene.tweens.chain({
    targets: this,
    tweens: [
        {
            y: '-=100',
            angle: 270,
            scale: 0.3,
            duration: 500
        },
        {
            angle: 180,
            y: 800,
            ease: 'power1',
            duration: 500
        }
    ],
    onComplete: () => { /* Код создания новой мыши */ }
});

Первая анимация в цепочке немного приподнимает мышь, вращает и уменьшает её. Вторая — отправляет её вниз за пределы экрана с ускорением (ease: 'power1').

Колбэк onComplete выполняется после окончания всей цепочки. Он создает новую летучую мышь в случайной позиции и со случайным масштабом, а старую уничтожает через this.destroy(). Это создает бесконечный цикл интерактивных объектов.

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

Пример наглядно показывает, как комбинировать базовые твины в сложные поведенческие цепочки. Для экспериментов попробуйте: изменить параметры ease для более драматичного падения, добавить твин прозрачности (alpha) при исчезновении, или создать цепочку из более чем двух анимаций для сложного death-эффекта. Используйте tweens.chain для любых последовательных действий: открытие сундука → вылет предмета → его подбор.