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

Визуальная глубина и атмосфера — ключевые элементы для вовлечения игрока. Этот пример демонстрирует, как создать динамическую сцену с параллакс-скроллингом и сложной системой освещения, используя возможности WebGL рендерера Phaser. Вы научитесь работать с объектами `Stamp`, настраивать источники света, управлять камерой и создавать бесконечный фон, что полезно для платформеров, раннеров или любой игры с боковым скроллингом.

Версия 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('cavern2', 'assets/skies/cavern2.png');
        this.load.image('parsec', [ 'assets/sprites/parsec.png', 'assets/normal-maps/parsec_n.png' ]);
        this.load.image('palm-tree-left', 'assets/sprites/palm-tree-left.png');
        this.load.image('orb', 'assets/particles/orb.png');
    }

    create ()
    {
        const halfWidth = this.scale.width / 2;
        const halfHeight = this.scale.height / 2;

        // Create scrolling background.

        this.add.image(halfWidth, halfHeight, 'cavern2')
        .setScrollFactor(0)
        .setScale(1280 / 800);

        this.palms = this.add.tileSprite(halfWidth, this.scale.height * 0.65, this.scale.width, 168, 'palm-tree-left')
        .setScrollFactor(0)
        .setTint(0x888888, 0x888888, 0x444488, 0x444488);

        // Add a Stamp with lighting.
        // This will not move with the camera, but will still receive lighting.
        this.add.stamp(halfWidth, this.scale.height * 0.75, 'parsec')
        .setLighting(true);

        // Add a caption displaying camera offset.
        this.text = this.add.text(16, 16, 'Camera Offset: 0', { fontSize: '16px', fill: '#ffffff' })
        .setScrollFactor(0);

        this.textUpdateTimer = this.time.addEvent({
            delay: 100,
            repeat: -1,
            callback: () => {
                this.text.setText('Camera Offset: ' + Math.round(this.cameras.main.scrollX));
            }
        });

        // Add scene lighting.
        this.lights.enable();
        this.lights.setAmbientColor(0x222244);
        this.streetLights = [];
        this.streetLightFlares = [];
        const streetLightCount = 6;
        this.streetLightLeftMargin = -512;
        this.streetLightRightMargin = this.scale.width + 512;
        const streetLightSpacing = (this.streetLightRightMargin - this.streetLightLeftMargin) / streetLightCount;
        for (let i = 0; i < streetLightCount; i++)
        {
            const x = this.streetLightLeftMargin + i * streetLightSpacing;
            const y = 256;
            const light = this.lights.addLight(x, y, 512, 0xddffaa, 2);
            light.setZNormal(i % 2 === 0 ? 0.25 : -0.5);
            this.streetLights.push(light);
            const flare = this.add.image(x, y, 'orb');
            flare.setBlendMode(Phaser.BlendModes.ADD);
            flare.setScale(0.5);
            this.streetLightFlares.push(flare);
        }
    }

    update (time, delta)
    {
        const cam = this.cameras.main;
        cam.scrollX -= delta;

        this.palms.tilePositionX = cam.scrollX * 0.5;

        for (let i = 0; i < this.streetLights.length; i++)
        {
            const light = this.streetLights[i];
            const flare = this.streetLightFlares[i];
            if (light.x > this.streetLightRightMargin + cam.scrollX)
            {
                light.x -= (this.streetLightRightMargin - this.streetLightLeftMargin);
            }
            flare.x = light.x;
        }
    }
}

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

const game = new Phaser.Game(config);

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

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

this.load.image('parsec', [ 'assets/sprites/parsec.png', 'assets/normal-maps/parsec_n.png' ]);

В create вычисляются центральные координаты экрана, которые будут использоваться для позиционирования объектов. Сразу создаётся фоновое изображение, которое закрепляется (setScrollFactor(0)) и масштабируется под размер окна. Это статичный задний план.

Создание скроллящегося слоя и объекта Stamp

Для создания эффекта глубины используется TileSprite — спрайт, текстурой которого можно «управлять», меняя его смещение (tilePosition). В примере это пальмы на среднем плане. Они также закреплены, но их текстура будет сдвигаться в update, создавая параллакс-эффект.

this.palms = this.add.tileSprite(halfWidth, this.scale.height * 0.65, this.scale.width, 168, 'palm-tree-left')
.setScrollFactor(0)
.setTint(0x888888, 0x888888, 0x444488, 0x444488);

Ключевой объект сцены — Stamp. Это игровой объект, оптимизированный для статичных элементов, которые должны реагировать на освещение, но не двигаться вместе с камерой. Он создаётся с помощью this.add.stamp() и сразу включает поддержку освещения методом .setLighting(true).

this.add.stamp(halfWidth, this.scale.height * 0.75, 'parsec')
.setLighting(true);

Настройка системы освещения и источников света

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

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

Далее в цикле создаются точечные источники света — «фонари». Каждый свет (this.lights.addLight()) имеет позицию, радиус, цвет и интенсивность. Важный параметр — setZNormal(). Он задаёт «глубину» источника относительно карты нормалей объекта, влияя на то, как будет падать световой блик. Значения 0.25 и -0.5 создают разнонаправленные блики, добавляя реалистичности.

const light = this.lights.addLight(x, y, 512, 0xddffaa, 2);
light.setZNormal(i % 2 === 0 ? 0.25 : -0.5);

Для визуального отображения источников создаются спрайты «сияния» (flare), которым устанавливается аддитивный режим смешивания (Phaser.BlendModes.ADD). Это делает их яркими и реалистичными на тёмном фоне.

Динамика: движение камеры и бесконечный цикл света

Вся динамика сцены обрабатывается в методе update. Основная камера плавно движется влево, имитируя продвижение игрока.

cam.scrollX -= delta;

Смещение текстуры пальм со скоростью, равной половине скорости камеры (cam.scrollX * 0.5), создаёт классический параллакс-эффект, усиливая ощущение глубины.

Источники света должны создавать иллюзию бесконечного ряда фонарей. Для этого проверяется, не ушёл ли фонарь за правую границу видимой зоны (с учётом смещения камеры). Если ушёл — его позиция сбрасывается в начало ряда.

if (light.x > this.streetLightRightMargin + cam.scrollX)
{
    light.x -= (this.streetLightRightMargin - this.streetLightLeftMargin);
}

Спрайты «сияния» синхронно следуют за своими источниками света.

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

Пример комплексно демонстрирует, как собрать живую, динамичную 2D-сцену с продвинутыми визуальными эффектами. Использование Stamp для статичных, но освещаемых объектов — отличная практика для производительности. Экспериментируйте: попробуйте изменить параметры setZNormal у источников света, чтобы увидеть, как меняются блики; добавьте больше слоёв TileSprite с разной скоростью параллакса; или заставьте Stamp объекты реагировать на взаимодействие игрока, временно меняя их материал.