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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    iter = 0;
    numbers = [];
    frame = 'veg01';
    idx = 1;
    gravity = 0.5;
    blitter;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', [ 'assets/textures/gold.png', 'assets/textures/gold-n.png' ]);

        this.load.atlas({
            key: 'atlas',
            textureURL: 'assets/tests/fruit/veg2.png',
            normalMap: 'assets/tests/fruit/veg2-n.png',
            atlasURL: 'assets/tests/fruit/veg2.json'
        });
    }

    create ()
    {
        this.lights.enable();
        this.lights.setAmbientColor(0x808080);

        this.add.sprite(400, 300, 'bg')
        .setLighting(true)
        .setAlpha(0.5);

        this.blitter = this.add.blitter(0, 0, 'atlas').setLighting(true);

        for (let i = 0; i < 32; i++)
        {
            this.launch();
        }

        const light = this.lights.addLight(400, 300, 400);

        this.input.on('pointermove', pointer =>
        {

            light.x = pointer.x;
            light.y = pointer.y;

        });
    }

    update ()
    {
        if (this.input.activePointer.isDown)
        {
            for (let i = 0; i < 8; i++)
            {
                this.launch();
            }
        }

        for (let index = 0, length = this.blitter.children.list.length; index < length; ++index)
        {
            const bob = this.blitter.children.list[index];

            bob.data.vy += this.gravity;

            bob.y += bob.data.vy;
            bob.x += bob.data.vx;

            if (bob.x > 780)
            {
                bob.x = 780;
                bob.data.vx *= -bob.data.bounce;
            }
            else if (bob.x < 0)
            {
                bob.x = 0;
                bob.data.vx *= -bob.data.bounce;
            }

            if (bob.y > 568)
            {
                bob.y = 568;
                bob.data.vy *= -bob.data.bounce;
            }
        }
    }

    launch ()
    {
        //  Max of 1000 lit objects
        if (this.blitter.children.list.length >= 1000)
        {
            return;
        }

        this.idx++;

        if (this.idx === 38)
        {
            this.idx = 1;
        }

        if (this.idx < 10)
        {
            this.frame = `veg0${this.idx.toString()}`;
        }
        else
        {
            this.frame = `veg${this.idx.toString()}`;
        }

        const bob = this.blitter.create(0, 0, this.frame);

        bob.data.vx = Math.random() * 6;
        bob.data.vy = Math.random() * 2;
        bob.data.bounce = 1;
    }
}

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

const game = new Phaser.Game(config);

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

Класс Example расширяет Phaser.Scene и содержит несколько ключевых свойств для управления состоянием. В методе preload мы загружаем текстуры. Обратите внимание на загрузку фона ('bg'): передаётся массив из двух путей. Первый — обычная текстура, второй (-n) — нормал-мап, который необходим для корректной работы освещения, создавая иллюзию рельефа.

Затем загружается атлас спрайтов ('atlas'). Важно указать не только textureURL и atlasURL, но и normalMap. Без нормал-мапа объекты не будут корректно реагировать на направленный свет.

this.load.image('bg', [ 'assets/textures/gold.png', 'assets/textures/gold-n.png' ]);

this.load.atlas({
    key: 'atlas',
    textureURL: 'assets/tests/fruit/veg2.png',
    normalMap: 'assets/tests/fruit/veg2-n.png',
    atlasURL: 'assets/tests/fruit/veg2.json'
});

Включение света и создание объектов

В методе create активируется система освещения сцены. Метод this.lights.enable() включает её, а this.lights.setAmbientColor(0x808080) задаёт средний серый цвет окружающего света, чтобы тени не были абсолютно чёрными.

Фон ('bg') добавляется как спрайт. Вызов .setLighting(true) сообщает движку, что этот объект должен участвовать в световых расчётах. .setAlpha(0.5) делает его полупрозрачным.

Ключевой объект — this.blitter. Blitter — это оптимизированный контейнер для отрисовки множества однотипных спрайтов (bobs) из одного текстурового атласа, что идеально подходит для частиц. Ему также включается освещение.

Сразу создаётся 32 объекта через метод launch(). Затем создаётся источник света с помощью this.lights.addLight(400, 300, 400), где первые два аргумента — координаты, а третий — радиус свечения.

this.lights.enable();
this.lights.setAmbientColor(0x808080);

this.add.sprite(400, 300, 'bg')
.setLighting(true)
.setAlpha(0.5);

this.blitter = this.add.blitter(0, 0, 'atlas').setLighting(true);

const light = this.lights.addLight(400, 300, 400);

Источник света привязывается к движению курсора. Обработчик события 'pointermove' постоянно обновляет его координаты.

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

Логика создания и "запуска" объектов

Метод launch() отвечает за создание отдельных спрайтов (bob) внутри Blitter. Он ограничивает общее количество объектов 1000 для производительности.

Логика с this.idx циклически перебирает кадры (frames) из атласа, генерируя имена вида veg01, veg02, ... veg37. Это позволяет создавать разнообразные визуальные объекты (например, разные фрукты или овощи).

Создание объекта происходит через this.blitter.create(0, 0, this.frame). Каждому объекту в его пользовательских данных (bob.data) записываются начальные случайные скорости по осям X и Y, а также коэффициент отскока bounce.

const bob = this.blitter.create(0, 0, this.frame);

bob.data.vx = Math.random() * 6;
bob.data.vy = Math.random() * 2;
bob.data.bounce = 1;

Игровая физика и обновление состояния

В методе update происходит две важные вещи. Во-первых, если кнопка мыши нажата (this.input.activePointer.isDown), каждый кадр создаётся по 8 новых объектов, позволяя игроку генерировать их поток.

Во-вторых, в цикле перебираются все созданные объекты this.blitter.children.list для применения простой физики. К вертикальной скорости каждого объекта добавляется "гравитация" (this.gravity). Затем скорости прибавляются к координатам, заставляя объекты двигаться и падать.

Код также обрабатывает столкновения с границами сцены (0, 780 по X и 568 по Y). При столкновении координата объекта фиксируется у границы, а соответствующая скорость умножается на -bob.data.bounce, имитируя отскок. Поскольку bounce равен 1, отскок идеально упругий без потери энергии.

for (let index = 0, length = this.blitter.children.list.length; index < length; ++index)
{
    const bob = this.blitter.children.list[index];

    bob.data.vy += this.gravity;

    bob.y += bob.data.vy;
    bob.x += bob.data.vx;

    if (bob.x > 780)
    {
        bob.x = 780;
        bob.data.vx *= -bob.data.bounce;
    }
    // ... обработка других границ
}

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

Вы создали прототип с динамичным освещением и физикой для сотен объектов. Blitter в связке с системой света — это эффективный способ визуализации частиц, падающих предметов или интерактивного фона. Для экспериментов попробуйте: изменить gravity или bounce для другой физики; добавить несколько разных источников света с разными цветами (setColor); менять frame в update для анимации объектов; или использовать текстуры с альфа-каналом для более сложных эффектов.