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

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

Версия 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/textures/gold.png', 'assets/textures/gold-n.png' ]);
        this.load.spritesheet('face', [ 'assets/sprites/metalface78x92.png', 'assets/sprites/metalface78x92-n.png' ], { frameWidth: 78, frameHeight: 92 });
    }

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

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

        this.anims.create({
            key: 'eyes',
            frames: this.anims.generateFrameNumbers('face', { start: 0, end: 3 }),
            frameRate: 14,
            repeat: -1
        });

        for (let i = 0; i < 6; i++)
        {
            const x = Phaser.Math.Between(100, 700);
            const y = Phaser.Math.Between(100, 500);

            const face = this.physics.add.sprite(x, y, 'face');

            face.setLighting(true);
            face.play('eyes');
            face.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
            face.setBounce(1, 1);
            face.setCollideWorldBounds(true);
        }

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

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

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

const game = new Phaser.Game(config);

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

Для работы с динамическим освещением в WebGL-режиме Phaser требуются не только обычные текстуры, но и нормальные карты (normal maps). Нормальная карта кодирует информацию о поверхности объекта (неровности, углы), что позволяет свету реалистично "обтекать" его, создавая иллюзию объема.

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

this.load.image('bg', [ 'assets/textures/gold.png', 'assets/textures/gold-n.png' ]);
this.load.spritesheet('face', [ 'assets/sprites/metalface78x92.png', 'assets/sprites/metalface78x92-n.png' ], { frameWidth: 78, frameHeight: 92 });

Спрайтшит face загружается с указанием размеров кадра. Нормальная карта для него (-n.png) загружается автоматически тем же вызовом.

Включение освещения и настройка сцены

В начале метода create необходимо активировать систему освещения для сцены. Без этого все последующие манипуляции со светом не будут иметь эффекта.

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

this.lights.enable() включает менеджер освещения. this.lights.setAmbientColor(0x555555) устанавливает цвет окружающего (фонового) света. Это базовый уровень освещенности, который виден даже в тени. Темно-серый цвет (0x555555) создает контрастную основу для динамического света.

Затем мы добавляем фоновый спрайт и настраиваем его взаимодействие со светом.

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

Метод setLighting(true) сообщает движку, что этот объект должен реагировать на источники света. setAlpha(0.5) уменьшает непрозрачность, чтобы фон не перетягивал на себя внимание.

Создание анимации и физических объектов

Перед созданием спрайтов определяем анимацию. Phaser использует систему анимаций на основе ключей.

this.anims.create({
    key: 'eyes',
    frames: this.anims.generateFrameNumbers('face', { start: 0, end: 3 }),
    frameRate: 14,
    repeat: -1
});

generateFrameNumbers генерирует массив кадров из спрайтшита face с индексами от 0 до 3. repeat: -1 задает бесконечное повторение анимации.

Далее в цикле создаются шесть физических спрайтов со случайными параметрами.

for (let i = 0; i < 6; i++)
{
    const x = Phaser.Math.Between(100, 700);
    const y = Phaser.Math.Between(100, 500);
    const face = this.physics.add.sprite(x, y, 'face');
    face.setLighting(true);
    face.play('eyes');
    face.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
    face.setBounce(1, 1);
    face.setCollideWorldBounds(true);
}

this.physics.add.sprite создает спрайт с телом Arcade Physics. setLighting(true) включает для него реакцию на свет. setVelocity задает случайную начальную скорость по осям X и Y. setBounce(1, 1) определяет идеальный отскок (коэффициент 1) от любых поверхностей. setCollideWorldBounds(true) включает столкновение с границами мира, определенными в конфигурации игры.

Динамический источник света и привязка к курсору

Сердцевина интерактивности — создание источника света и его привязка к движению указателя мыши.

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

this.lights.addLight(x, y, radius) создает точечный источник света с начальными координатами и радиусом воздействия в пикселях. Метод setIntensity(2) удваивает интенсивность свечения по сравнению со значением по умолчанию.

Чтобы свет следовал за курсором, мы подписываемся на событие pointermove.

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

В обработчике события мы просто обновляем координаты `xиyсозданного объекта света, используя координаты указателя из объектаpointer`.

Конфигурация игры: WebGL и физика

Для работы освещения и нормальных карт критически важен рендерер. Необходимо использовать WebGL.

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

Ключевой параметр — type: Phaser.WEBGL. Освещение не работает в режиме Phaser.CANVAS. В блоке physics активируется система Arcade Physics, которую мы использовали для спрайтов с методами setVelocity и setBounce.

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

Вы создали интерактивную сцену, где анимированные физические объекты оживают под динамическим светом. Для экспериментов попробуйте: добавить несколько источников света разного цвета с помощью setColor, изменить setAmbientColor на более темный или цветной оттенок, управлять светом не курсором, а через this.input.keyboard, создавая световые вспышки, или привязать источник света к одному из движущихся спрайтов, сделав его своего рода фонариком.