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

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

Версия 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('light', 'assets/normal-maps/light.png');

        this.load.setPath('assets/tests/grave/');

        this.load.atlas('candle');
        this.load.image('background');
        this.load.image('clouds');
        this.load.image('fog');
        this.load.image('overlay');
        this.load.image('tombs', [ 'tombs.png', 'tombs_n.png' ]);
        this.load.image('tombsNormalMap', 'tombs_n.png');
    }

    create ()
    {
        this.add.image(512, 384, 'background').setAlpha(0.7);

        const clouds = this.add.image(1024, 32, 'clouds').setOrigin(0);

        this.tweens.add({
            targets: clouds,
            x: -1250,
            ease: 'Linear',
            duration: 400000,
            repeat: -1
        });

        const fog = this.add.image(1024, 200, 'fog').setOrigin(0);

        this.tweens.add({
            targets: fog,
            x: -3000,
            ease: 'Linear',
            duration: 300000,
            repeat: -1
        });

        const pic = this.add.image(512, 384, 'tombs');
        pic.setLighting(true);

        //  The 3 lights
        const dummy = this.add.image(900, 400, 'light').setVisible(false);

        const light1 = this.lights.addLight(280, 400, 200);
        const ellipse1 = new Phaser.Geom.Ellipse(light1.x, light1.y, 70, 100);

        const light2 = this.lights.addLight(650, 386, 200);
        const ellipse2 = new Phaser.Geom.Ellipse(light2.x, light2.y, 30, 40);

        const light3 = this.lights.addLight(900, 400, 200);

        this.time.addEvent({
            delay: 100,
            callback: function ()
            {
                Phaser.Geom.Ellipse.Random(ellipse1, light1);
                Phaser.Geom.Ellipse.Random(ellipse2, light2);
            },
            callbackScope: this,
            repeat: -1
        });

        this.tweens.add({
            targets: [ light3, dummy ],
            y: 150,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1,
            duration: 3000
        });

        // We must enable the light system. By default is disabled
        this.lights.enable();

        //  The 2 candle flames
        this.anims.create({
            key: 'flicker',
            frames: this.anims.generateFrameNames('candle', { prefix: 'candleFl', start: 1, end: 14 }),
            repeat: -1,
            frameRate: 16,
            repeatDelay: function ()
            {
                return Math.random() * 6;
            }
        });

        this.add.sprite(652, 386, 'candle').setScale(0.25).play('flicker');
        this.add.sprite(260, 400, 'candle').setScale(0.5).play('flicker');

        this.add.image(512, 384, 'overlay');
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 1024,
    height: 768,
    parent: 'phaser-example',
    backgroundColor: '#000000',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка ассетов: нормальные карты и атласы

Пример загружает как обычные текстуры, так и специальные ассеты для работы со светом. Ключевой элемент — нормальная карта (tombs_n.png), которая определяет, как поверхность объектов реагирует на освещение, создавая иллюзию объёма. Обратите внимание, что изображение 'tombs' загружается с двумя файлами: цветовой текстурой и её нормальной картой.

Также загружается атлас 'candle' для анимации пламени. Атлас — это один изображение-спрайтшит, содержащий несколько кадров анимации, что оптимизирует производительность.

this.load.image('tombs', [ 'tombs.png', 'tombs_n.png' ]);
this.load.image('tombsNormalMap', 'tombs_n.png');
this.load.atlas('candle');

Включение системы освещения и создание источников

По умолчанию система освещения в Phaser отключена. Её необходимо активировать с помощью метода this.lights.enable(). Только после этого созданные источники света будут влиять на объекты сцены.

Источники света создаются методом this.lights.addLight(x, y, radius). В примере создаётся три источника: два для свечей и один дополнительный для динамического перемещения. Каждому источнику можно задавать позицию и радиус свечения.

//  The 3 lights
const light1 = this.lights.addLight(280, 400, 200);
const light2 = this.lights.addLight(650, 386, 200);
const light3 = this.lights.addLight(900, 400, 200);

// We must enable the light system. By default is disabled
this.lights.enable();

Чтобы свет взаимодействовал с объектом, например, с изображением гробниц, этому объекту нужно явно разрешить освещение с помощью метода setLighting(true).

const pic = this.add.image(512, 384, 'tombs');
pic.setLighting(true);

Анимация источников света: дрожание и движение

Статичный свет выглядит скучно. В примере свет от свечей дрожит, а третий источник плавно перемещается вверх-вниз. Для реализации дрожания используются геометрические объекты Phaser.Geom.Ellipse, которые задают область случайного смещения. Событие по таймеру this.time.addEvent каждые 100 мс выбирает случайную точку внутри эллипса и перемещает в неё источник света.

const ellipse1 = new Phaser.Geom.Ellipse(light1.x, light1.y, 70, 100);

this.time.addEvent({
    delay: 100,
    callback: function ()
    {
        Phaser.Geom.Ellipse.Random(ellipse1, light1);
    },
    repeat: -1
});

Для плавного вертикального движения третьего источника используется твин. Твин анимирует свойство `yобъектовlight3и невидимого спрайта-заглушкиdummy`, создавая цикличное движение с эффектом 'йо-йо'.

this.tweens.add({
    targets: [ light3, dummy ],
    y: 150,
    ease: 'Sine.easeInOut',
    yoyo: true,
    repeat: -1,
    duration: 3000
});

Создание атмосферы: фон, туман и анимация свечей

Освещение работает в связке с другими слоями графики. Фоновые элементы, такие как медленно плывущие облака и туман, добавляют сцене динамики и глубины. Они двигаются с помощью твинов по горизонтали на огромной длительности, создавая едва заметное, но непрерывное движение.

this.tweens.add({
    targets: clouds,
    x: -1250,
    ease: 'Linear',
    duration: 400000,
    repeat: -1
});

Анимация пламени свечей создаётся из загруженного атласа. Особенность в использовании repeatDelay — случайной задержки между циклами анимации, что делает мерцание более естественным и неравномерным.

this.anims.create({
    key: 'flicker',
    frames: this.anims.generateFrameNames('candle', { prefix: 'candleFl', start: 1, end: 14 }),
    repeat: -1,
    frameRate: 16,
    repeatDelay: function ()
    {
        return Math.random() * 6;
    }
});

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

this.add.sprite(652, 386, 'candle').setScale(0.25).play('flicker');

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

Система освещения Phaser 3 — это гибкий инструмент для создания настроения и реализма в 2D-играх. Комбинируя нормальные карты, динамические источники света и традиционную анимацию, можно добиться впечатляющих визуальных эффектов с относительно небольшими усилиями. Для экспериментов попробуйте: изменить цвет источников света через свойство .color, добавить больше слоёв с разными режимами смешивания (blendMode), или привязать движение света к физическому телу персонажа, создав эффект фонарика.