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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{

    light;
    offsets = [];
    player;
    layer;
    cursors;

    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.light = this.lights.addLight(0, 0, 200).setScrollFactor(0.0);

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

        // this.input.on('pointermove', function (pointer) {

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

        // });

        this.lights.addLight(0, 100, 100).setColor(0xff0000).setIntensity(3.0);
        this.lights.addLight(0, 200, 100).setColor(0x00ff00).setIntensity(3.0);
        this.lights.addLight(0, 300, 100).setColor(0x0000ff).setIntensity(3.0);
        this.lights.addLight(0, 400, 100).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)
            {
                index -= 1;
                currLight.x = 400 + Math.sin(this.offsets[index]) * 1000;
                this.offsets[index] += 0.02;
            }
        }, 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. Важно отметить загрузку двух версий тайловой текстуры: обычной (drawtiles1.png) и нормальной карты (drawtiles1_n.png). Нормальная карта (normal map) необходима для корректной работы освещения, чтобы свет правильно "ложился" на поверхность тайлов, создавая эффект объема.

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() происходит основная настройка игрового мира. Сначала создается тайловая карта (tilemap) и слой (layer). Ключевой момент — вызов метода .setLighting(true) для слоя. Это включает расчет освещения для каждого тайла на этом слое.

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

Затем создается главный источник света, который будет следовать за игроком, и включается глобальная система освещения с фоновым (ambient) светом.

this.light = this.lights.addLight(0, 0, 200).setScrollFactor(0.0);
this.lights.enable().setAmbientColor(0x555555);

Метод setScrollFactor(0.0) фиксирует свет относительно камеры, но в коде обновления его позиция привязывается к игроку, поэтому он движется вместе с миром. Также создаются четыре статических цветных источника света, которые позже будут анимированы. Игрок (player) создается как обычное изображение в центре первого тайла.

Логика передвижения и коллизии с тайлами

Метод update() обрабатывает управление. Движение происходит дискретно, по сетке тайлов, при нажатии стрелочных клавиш. Перед каждым шагом проверяется, свободен ли целевой тайл.

const tile = this.layer.getTileAtWorldXY(this.player.x + 32, this.player.y, true);
if (tile.index === 2) {
    //  Blocked, we can't move
}
Метод `getTileAtWorldXY()` получает тайл по мировым координатам. Аргумент `true` указывает, что нужно искать только на том слое, для которого вызван метод (в нашем случае — `this.layer`). Если индекс полученного тайла равен 2 (в тайлсете это, предположительно, блокирующий тайл, например, стена), движение отменяется. В противном случае координаты и угол поворота спрайта игрока обновляются.
Также в каждом кадре позиция основного света (`this.light`) синхронизируется с позицией игрока.

Анимация источников света

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

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

Цикл проходит по всем источникам света сцены, полученным через this.lights.lights. Главный свет игрока пропускается проверкой if (this.light !== currLight). Для каждого из четырех цветных огней берется свое значение смещения (offset) из массива this.offsets. Это значение увеличивается на 0.02 каждый кадр и подставляется в Math.sin(), чтобы рассчитать текущую позицию по X. Таким образом, огни совершают плавные колебательные движения с большой амплитудой (1000 пикселей) вокруг точки X=400.

Конфигурация игры: важность WEBGL и pixelArt

Система освещения в Phaser 3 работает только в рендерере WebGL. Поэтому в конфигурации игры обязательно должен быть указан type: Phaser.WEBGL.

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

Параметр pixelArt: true автоматически отключает сглаживание текстур при их масштабировании, что критично для сохранения четкого пиксельного стиля. Черный фон (backgroundColor: '#000000') хорошо контрастирует с цветными огнями и подчеркивает эффект освещения.

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

Вы создали сцену с динамическим освещением, которое следует за игроком, и фоном из анимированных цветных огней. Механика передвижения по сетке с проверкой тайлов — отличная основа для пошаговой RPG или головоломки. **Идеи для экспериментов:** 1. Добавьте больше типов блокирующих тайлов (например, воду или лаву) и разные реакции на них (урон, замедление). 2. Сделайте интенсивность или цвет основного света зависимыми от действий игрока (например, при получении бонуса). 3. Реализуйте источники света, которые можно включать/выключать, взаимодействуя с объектами на карте. 4. Используйте setScrollFactor(1.0) для основного света и посмотрите, как это изменит восприятие движения.