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

Эмиттеры частиц в Phaser — мощный инструмент для создания эффектов. Частицы могут лететь к заданной точке, используя параметры `moveToX` и `moveToY`. Однако иногда требуется, чтобы цель движения менялась динамически, например, следовала за курсором мыши. В этой статье мы разберем пример, который показывает, как реализовать такое поведение с помощью функций `onEmit` и `onUpdate`. Это полезно для создания интерактивных эффектов, например, магических сфер, преследующих игрока, или частиц, реагирующих на действия пользователя.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/gradient26.png');
        this.load.image('ball', 'assets/demoscene/green_ball.png');
    }

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

        let x = 400;
        let y = 570;

        this.input.on('pointermove', pointer => {
            x = pointer.worldX;
            y = pointer.worldY;
        });

        const balls = this.add.particles(0, 0, 'ball', {
            x: { min: 300, max: 500 },
            y: 0,
            advance: 2000,
            moveToX: {
                onEmit: () => {
                    return x;
                },
                onUpdate: () => {
                    return x;
                }
            },
            moveToY: {
                onEmit: () => {
                    return y;
                },
                onUpdate: () => {
                    return y;
                }
            },
            lifespan: 2000,
            sortProperty: 'lifeT',
            sortOrderAsc: true
        });
        // Workaround:
        // balls.moveTo = true;

        const text = this.add.text();

        this.events.on('update', () => {
            text.text = `moveTo: ${balls.moveToX}, ${balls.moveToY}`
        })
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и обработка ввода

В методе preload загружаются две текстуры: фон (bg) и изображение частицы (ball). Основная логика начинается в create. Сначала добавляется фон.

Затем объявляются две переменные, `xиy`, которые будут хранить текущие координаты цели. Изначально они установлены в точку (400, 570) — низ экрана.

Далее на событие движения указателя (pointermove) вешается обработчик. При каждом движении мыши или касании координаты `xиyобновляются в соответствии с позицией курсора в игровом мире (pointer.worldX,pointer.worldY`). Это создает основу для динамической цели.

let x = 400;
let y = 570;

this.input.on('pointermove', pointer => {
    x = pointer.worldX;
    y = pointer.worldY;
});

Создание эмиттера с динамической целью

Эмиттер частиц создается с помощью this.add.particles. Ключевая настройка — параметры moveToX и moveToY. Обычно им присваивают статичное числовое значение, к которому будут стремиться все частицы. Но в нашем примере вместо числа используются объекты с двумя функциями: onEmit и onUpdate.

* onEmit вызывается в момент создания (эмита) каждой новой частицы. Она возвращает текущее значение цели (`xилиy`), которое будет зафиксировано для этой конкретной частицы на весь срок ее жизни. * onUpdate вызывается при каждом обновлении существующей частицы. Она также возвращает текущее значение цели. Это позволяет уже летящим частицам скорректировать свой курс, если цель переместилась.

Таким образом, все частицы постоянно стремятся к актуальным координатам, хранящимся в переменных `xиy`.

const balls = this.add.particles(0, 0, 'ball', {
    x: { min: 300, max: 500 },
    y: 0,
    advance: 2000,
    moveToX: {
        onEmit: () => { return x; },
        onUpdate: () => { return x; }
    },
    moveToY: {
        onEmit: () => { return y; },
        onUpdate: () => { return y; }
    },
    lifespan: 2000,
    sortProperty: 'lifeT',
    sortOrderAsc: true
});

Остальные параметры: * `xиy` задают область появления частиц. * advance предварительно вычисляет частицы. * lifespan — время жизни частицы в миллисекундах. * sortProperty и sortOrderAsc управляют порядком отрисовки для правильного наложения.

Визуализация цели и важный нюанс

В примере также есть визуализация текущих координат цели. Создается текстовый объект, и на событие update сцены вешается обработчик, который обновляет текст, отображая текущие значения moveToX и moveToY эмиттера.

const text = this.add.text();

this.events.on('update', () => {
    text.text = `moveTo: ${balls.moveToX}, ${balls.moveToY}`
})

**Критически важный момент:** для того чтобы механика moveTo с функциями onEmit/onUpdate заработала, необходимо вручную установить свойство moveTo эмиттера в true. В исходном коде примера эта строка закомментирована как // Workaround:. Без этого частицы не будут двигаться к цели, несмотря на корректные функции.

// Раскомментируйте эту строку для работы!
balls.moveTo = true;

Это неочевидное требование API, о котором важно помнить при реализации подобных эффектов.

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

Использование функций onEmit и onUpdate в параметрах moveToX/moveToY открывает возможность создания "живых", интерактивных систем частиц в Phaser. Частицы могут динамически менять свою цель прямо в полете. Для экспериментов попробуйте: изменить логику внутри onUpdate, чтобы цель двигалась по сложной траектории (синусоиде, окружности); привязать цель не к курсору, а к спрайту другого игрока; или создать несколько эмиттеров с разными целями для масштабного визуального эффекта. Не забудьте про ключевой параметр balls.moveTo = true!