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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    light;
    offsets = [];
    player;
    layer;

    preload() {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', [ 'assets/tilemaps/tiles/drawtiles1.png', 'assets/tilemaps/tiles/drawtiles1_n.png' ]);
        this.load.image('car', 'assets/sprites/car90.png');
        this.load.tilemapCSV('map', 'assets/tilemaps/csv/grid.csv');
    }

    create ()
    {
        const map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });

        const tileset = map.addTilesetImage('tiles', null, 32, 32, 1, 2);

        this.layer = map.createLayer(0, tileset, 0, 0).setLighting(true);

        this.player = this.add.image(32+16, 32+16, 'car');

        this.cursors = this.input.keyboard.createCursorKeys();

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

        this.light = this.lights.addLight(0, 0, 200);

        this.lights.addLight(0, 100, 140).setColor(0xff0000).setIntensity(3.0);
        this.lights.addLight(0, 250, 140).setColor(0x00ff00).setIntensity(3.0);
        this.lights.addLight(0, 400, 140).setColor(0xff00ff).setIntensity(3.0);
        this.lights.addLight(0, 550, 140).setColor(0xffff00).setIntensity(3.0);

        this.offsets = [ 0.1, 0.3, 0.5, 0.7 ];
    }
    update ()
    {
        if (this.input.keyboard.checkDown(this.cursors.left, 100))
        {
            const tile = this.layer.getTileAtWorldXY(this.player.x - 32, this.player.y, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                this.player.x -= 32;
                this.player.angle = 180;
            }
        }
        else if (this.input.keyboard.checkDown(this.cursors.right, 100))
        {
            const tile = this.layer.getTileAtWorldXY(this.player.x + 32, this.player.y, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                this.player.x += 32;
                this.player.angle = 0;
            }
        }
        else if (this.input.keyboard.checkDown(this.cursors.up, 100))
        {
            const tile = this.layer.getTileAtWorldXY(this.player.x, this.player.y - 32, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                this.player.y -= 32;
                this.player.angle = -90;
            }
        }
        else if (this.input.keyboard.checkDown(this.cursors.down, 100))
        {
            const tile = this.layer.getTileAtWorldXY(this.player.x, this.player.y + 32, true);

            if (tile.index === 2)
            {
                //  Blocked, we can't move
            }
            else
            {
                this.player.y += 32;
                this.player.angle = 90;
            }
        }

        this.light.x = this.player.x;
        this.light.y = this.player.y;

        this.lights.lights.forEach(function (currLight, index)
        {
            if (this.light !== currLight)
            {
                currLight.x = 400 + Math.sin(this.offsets[index]) * 300;
                this.offsets[index] += 0.01;
                index += 1;
            }
        }, this);
    }
}

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

const game = new Phaser.Game(config);

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

Класс Example наследуется от Phaser.Scene и описывает нашу игровую сцену. В методе preload() загружаются все необходимые ресурсы: тайловая текстура с двумя вариантами (обычным и для нормалей), спрайт машины и сама карта в формате CSV.

this.load.image('tiles', [ 'assets/tilemaps/tiles/drawtiles1.png', 'assets/tilemaps/tiles/drawtiles1_n.png' ]);
this.load.image('car', 'assets/sprites/car90.png');
this.load.tilemapCSV('map', 'assets/tilemaps/csv/grid.csv');

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

В методе `create()` происходит основная настройка. Сначала создается тайловая карта (`this.make.tilemap`), к ней добавляется набор тайлов (`map.addTilesetImage`). Ключевой момент — создание слоя (`map.createLayer`) с параметром `.setLighting(true)`. Это говорит движку, что на данный слой должно влиять освещение.
Затем включается система света (`this.lights.enable()`) и задается цвет окружающего освещения (`this.lights.setAmbientColor`). Без него области вне зоны действия источников света были бы полностью черными.
this.layer = map.createLayer(0, tileset, 0, 0).setLighting(true);
this.lights.enable();
this.lights.setAmbientColor(0x808080);

Расстановка источников света

Далее создаются источники света с помощью this.lights.addLight(x, y, radius). Первый свет привязан к игроку. Еще четыре статичных источника создаются с разными цветами и увеличенной интенсивностью (setIntensity(3.0)). Интенсивность влияет на яркость и радиус свечения.

this.light = this.lights.addLight(0, 0, 200);
this.lights.addLight(0, 100, 140).setColor(0xff0000).setIntensity(3.0);

Логика движения и навигации по карте

Метод update() обрабатывает ввод с клавиатуры для перемещения спрайта машины по сетке. Важная деталь — проверка на возможность хода. С помощью метода слоя getTileAtWorldXY() определяется тайл в точке, куда хочет переместиться игрок. Если индекс этого тайла равен 2, движение блокируется. Это простой способ реализовать препятствия на тайловой карте.

const tile = this.layer.getTileAtWorldXY(this.player.x + 32, this.player.y, true);
if (tile.index === 2) { /* Blocked */ } else { this.player.x += 32; }

Динамика освещения: светильник и плавающие огни

Основной источник света (this.light) постоянно следует за координатами игрока (this.player.x, this.player.y), имитируя, например, фонарик. Остальные четыре источника света не статичны — их положение по оси X плавно меняется с помощью синусоидальной функции, создавая эффект мерцания или плавающего движения. Для этого используется массив смещений offsets.

this.light.x = this.player.x;
this.light.y = this.player.y;
currLight.x = 400 + Math.sin(this.offsets[index]) * 300;
this.offsets[index] += 0.01;

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

Этот пример демонстрирует, как легко система освещения Phaser 3 интегрируется с тайловыми картами, добавляя визуальную глубину и интерактивность. Для экспериментов попробуйте изменить setAmbientColor на более темный оттенок, чтобы усилить контраст, или заставьте источники света реагировать на события (например, мигать при столкновении). Можно также поиграть с интенсивностью и радиусом света, чтобы создать разные типы освещения — от тусклого факела до яркого прожектора.