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