О чем этот пример
Визуальная глубина и атмосфера — ключевые элементы для вовлечения игрока. Этот пример демонстрирует, как создать динамическую сцену с параллакс-скроллингом и сложной системой освещения, используя возможности 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 объекты реагировать на взаимодействие игрока, временно меняя их материал.
