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

Инверсная кинематика (IK) — мощный инструмент для создания реалистичных движений в играх, например, щупалец, хвостов или цепочек. В Phaser нет встроенной IK-системы, но её можно элегантно реализовать с помощью контейнеров (`Container`). Эта статья покажет, как создавать иерархические цепочки объектов, где вращение родителя влияет на всех потомков, что идеально подходит для procedural animation.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    containerTails = [];
    containers = [];

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');

        this.load.image('arrow', 'assets/sprites/arrow.png');
    }

    create ()
    {
        let lastContainer;
        const count = 40;

        for (let j = 0; j < 4; ++j)
        {
            for (let index = 0; index < count; ++index)
            {
                const image = this.make.image({x: 0, y: 0, key: 'arrow', add: false});
                if (index === 0)
                {
                    lastContainer = this.add.container(game.config.width / 2, game.config.height / 2);
                    this.containers.push(lastContainer);
                    lastContainer.rotation += (j * 90) * Math.PI / 180;
                }
                else
                {
                    let newContainer = this.make.container({x: image.width, y: 0, add: false});
                    lastContainer.add(newContainer);
                    lastContainer = newContainer;
                    newContainer.setScale(1.0 - index / (count));
                    newContainer.rotation = index / count * 2;
                }
                image.setOrigin(0, 0.5);
                lastContainer.add(image);

                if (index === 5 || index === 4 || index === 10)
                {
                    let leafContainer = lastContainer;
                    const direction = index === 5 ? 1 : -1;
                    for (let k = 0; k < 10; ++k)
                    {
                        const image2 = this.make.image({x: 0, y: 0, key: 'arrow', add: false});
                        let newContainer = this.make.container({x: image2.width, y: 0, add: false});
                        leafContainer.add(newContainer);
                        leafContainer = newContainer;
                        leafContainer.setScale(1.0 - k / 10);
                        leafContainer.rotation = 0.1 * direction;
                        image2.setOrigin(0, 0.5);
                        leafContainer.add(image2);
                    }
                }

                if (index === count - 1) { this.containerTails.push(lastContainer); }
            }
        }

    }

    update ()
    {
        for (let index = 0; index < this.containerTails.length; ++index)
        {
            const container = this.containerTails[index];
            this.rotateContainer(container, 0.02);
        }
    }

    rotateContainer (container, rotation)
    {
        if (container)
        {
            container.rotation += rotation;
            this.rotateContainer(container.parentContainer, rotation);
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Суть подхода: иерархия контейнеров

Вместо сложных математических расчётов мы используем вложенность. Каждый новый контейнер добавляется как дочерний элемент к предыдущему. При повороте родительского контейнера Phaser автоматически применяет это преобразование ко всем его дочерним элементам, включая другие контейнеры.

В примере создаётся 4 основные цепочки (по 40 звеньев в каждой), расходящиеся из центра под углом 90 градусов. Каждое звено — это контейнер с изображением стрелки.

Создание основной цепочки

Первое звено каждой цепочки — корневой контейнер, размещённый в центре экрана. Каждое последующее звено создаётся как дочерний контейнер предыдущего, смещённый по оси X на ширину изображения.

// Создание корневого контейнера для цепочки
lastContainer = this.add.container(game.config.width / 2, game.config.height / 2);
this.containers.push(lastContainer);
lastContainer.rotation += (j * 90) * Math.PI / 180;

// Создание и добавление дочернего контейнера
let newContainer = this.make.container({x: image.width, y: 0, add: false});
lastContainer.add(newContainer);
lastContainer = newContainer;

Масштаб и вращение каждого нового контейнера немного уменьшаются относительно его индекса, создавая плавное затухание цепочки.

Добавление ответвлений (ветвление)

На определённых звеньях основной цепочки (индексы 4, 5, 10) создаются боковые ветви. Это демонстрирует, как можно строить сложные иерархические структуры.

if (index === 5 || index === 4 || index === 10)
{
    let leafContainer = lastContainer;
    const direction = index === 5 ? 1 : -1;
    for (let k = 0; k < 10; ++k)
    {
        // Создание звена боковой ветви
        let newContainer = this.make.container({x: image2.width, y: 0, add: false});
        leafContainer.add(newContainer);
        leafContainer = newContainer;
        leafContainer.rotation = 0.1 * direction; // Ветви закручиваются в разные стороны
    }
}

Важно: leafContainer — это текущий контейнер основной цепочки, который становится родителем для всей боковой ветви.

Анимация с помощью рекурсии

В update() анимируются только последние (хвостовые) контейнеры каждой цепочки, сохранённые в массиве containerTails. Функция rotateContainer рекурсивно поднимается по иерархии родителей, применяя поворот к каждому из них.

rotateContainer (container, rotation)
{
    if (container)
    {
        container.rotation += rotation;
        this.rotateContainer(container.parentContainer, rotation);
    }
}

Поворот, применённый к «хвосту», передаётся вверх по цепочке, создавая волнообразное движение. Это и есть простая реализация принципа инверсной кинематики.

Ключевые API Phaser в примере

Код использует несколько важных методов фабрики (make) и менеджера игровых объектов:

// Создание изображения без автоматического добавления на сцену
const image = this.make.image({x: 0, y: 0, key: 'arrow', add: false});

// Создание контейнера без автоматического добавления на сцену
let newContainer = this.make.container({x: image.width, y: 0, add: false});

// Явное добавление одного объекта (контейнера) в другой
lastContainer.add(newContainer);

// Установка точки вращения/масштабирования для изображения
image.setOrigin(0, 0.5); // (0, 0.5) — левый край, центр по вертикали

Использование make с add: false даёт полный контроль над моментом добавления объекта в иерархию через метод add().

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

Контейнеры Phaser — гибкий инструмент для создания иерархических связей между объектами, что открывает путь к procedural-анимации и простым IK-системам. Для экспериментов попробуйте: изменить правило вращения в rotateContainer на зависимое от времени, привязать движение «хвоста» к курсору мыши, или добавить физические тела (physics body) к конечным контейнерам, чтобы цепочка реагировала на столкновения.