О чем этот пример
Создание бесконечного, разнообразного ландшафта — классическая задача в геймдеве. В этой статье мы разберем пример из официальной библиотеки Phaser, который генерирует такой ландшафт, используя шум `HashSimplex` для высот и цветовой градиент `ColorRamp` для визуализации. Вы научитесь работать с изометрическими примитивами (`isobox`), создавать плавный цветовой переход для разных биомов (океан, берег, горы) и реализовывать бесконечную прокрутку ландшафта с помощью клавиатуры. Этот подход отлично подходит для прототипирования карт, генерации фонов или создания тайловых миров.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
cursors;
isoWidth = 16;
isoHeight = 8;
isoboxes = [];
scroll = { x: 0, y: 0 };
scrollSpeed = 0.5; // Increase this to explore further!
simplexConfig = {
noiseCells: [ 4, 3 ],
noiseIterations: 6,
noiseContributionPower: 1.5,
noiseWarpAmount: 0.1,
noiseWarpIterations: 4
};
ramp;
tempColor = new Phaser.Display.Color();
lightBlue = new Phaser.Display.Color(167, 147, 255);
darkBlue = new Phaser.Display.Color(91, 127, 191);
create()
{
const { width, height } = this.scale;
let offsetX = 0;
for (let y = 0; y < height + 64; y += this.isoHeight)
{
offsetX = (y / this.isoHeight) % 2 == 0 ? this.isoWidth / 2 : 0;
for (let x = 0; x <= width; x += this.isoWidth)
{
const isobox = this.add.isobox(x + offsetX, y, this.isoWidth);
this.isoboxes.push(isobox);
}
}
this.ramp = new Phaser.Display.ColorRamp(this, [
{
// Ocean
colorStart: 0x00007f,
colorEnd: 0x3fbfff,
size: 0.5,
middle: 0.7
},
{
// Shoreline
colorStart: 0x8faf6f,
colorEnd: 0xafef4f,
size: 0.05
},
{
// Greenery
colorStart: 0x00af00,
colorEnd: 0x005f4f,
size: 0.3
},
{
// Stone
colorStart: 0x9f8f6f,
colorEnd: 0x8f9faf,
size: 0.1
},
{
// Snow
colorStart: 0xefefef,
colorEnd: 0xffffff,
size: 0.4
},
], false);
this.cursors = this.input.keyboard.createCursorKeys();
this.updateLand();
}
update (time, delta)
{
let down = false;
let { x, y } = this.scroll;
if (this.cursors.left.isDown)
{
x -= this.scrollSpeed;
down = true;
}
else if (this.cursors.right.isDown)
{
x += this.scrollSpeed;
down = true;
}
if (this.cursors.up.isDown)
{
y -= this.scrollSpeed;
down = true;
}
else if (this.cursors.down.isDown)
{
y += this.scrollSpeed;
down = true;
}
if (down)
{
this.scroll.x = x;
this.scroll.y = y;
this.updateLand();
}
}
updateLand ()
{
const { simplexConfig, ramp, isoWidth, tempColor, lightBlue, darkBlue } = this;
const { width, height } = this.scale;
const { x: sx, y: sy } = this.scroll;
for (const isobox of this.isoboxes)
{
let { x, y } = isobox;
x += sx;
y += sy;
let value = Phaser.Math.HashSimplex([ x / width / 8, y / height / 8 ], simplexConfig);
const col = ramp.getColor(Math.min(1, Math.max(0, value * 0.5 + 0.5)));
tempColor.setFromRGB(col);
const colTop = tempColor.color;
tempColor.redGL *= lightBlue.redGL;
tempColor.greenGL *= lightBlue.greenGL;
tempColor.blueGL *= lightBlue.blueGL;
const colLeft = tempColor.color;
tempColor.setFromRGB(col);
tempColor.redGL *= darkBlue.redGL;
tempColor.greenGL *= darkBlue.greenGL;
tempColor.blueGL *= darkBlue.blueGL;
const colRight = tempColor.color;
isobox.setSize(isoWidth, Math.max(0, 48 * value) + 4)
.setFillStyle(colTop, colLeft, colRight);
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 1280,
height: 720,
scene: Example
};
const game = new Phaser.Game(config);
Структура сцены и подготовка изобоксов
Класс Example расширяет Phaser.Scene. В его конструкторе инициализируются ключевые переменные: размер изобокса (isoWidth, isoHeight), массив для их хранения, объект прокрутки и конфигурация для генерации шума.
В методе create() создается сетка изобоксов, покрывающая весь экран и небольшой запас. Смещение по X (offsetX) на каждом втором ряду создает шахматный порядок, характерный для изометрического вида.
let offsetX = 0;
for (let y = 0; y < height + 64; y += this.isoHeight)
{
offsetX = (y / this.isoHeight) % 2 == 0 ? this.isoWidth / 2 : 0;
for (let x = 0; x <= width; x += this.isoWidth)
{
const isobox = this.add.isobox(x + offsetX, y, this.isoWidth);
this.isoboxes.push(isobox);
}
}
Генерация шума и настройка цветового градиента
Сердце процедурной генерации — функция Phaser.Math.HashSimplex. Она принимает координаты точки (нормализованные и масштабированные) и конфигурационный объект simplexConfig. Параметры конфига управляют детализацией и характером шума: количество ячеек, итераций, степень вклада и искажения.
Для превращения значения высоты (от -1 до 1) в цвет используется Phaser.Display.ColorRamp. Он описывает последовательность цветовых полос (биомов), каждая со своим начальным и конечным цветом, относительным размером (size) и опциональной точкой середины (middle).
this.ramp = new Phaser.Display.ColorRamp(this, [
{
// Ocean
colorStart: 0x00007f,
colorEnd: 0x3fbfff,
size: 0.5,
middle: 0.7
},
// ... другие биомы
], false);
Цикл обновления и управление камерой
В методе update() отслеживается состояние клавиш-стрелок. При нажатии изменяются координаты прокрутки scroll.x и scroll.y. Если было любое движение (down = true), вызывается метод updateLand(), который пересчитывает вид всех изобоксов.
if (this.cursors.left.isDown)
{
x -= this.scrollSpeed;
down = true;
}
// ... обработка других направлений
if (down)
{
this.scroll.x = x;
this.scroll.y = y;
this.updateLand();
}
Расчет высоты, цвета и отрисовка изобокса
Метод updateLand() — самая важная часть. Для каждого изобокса в массиве:
1. Его экранные координаты смещаются на значение прокрутки, создавая эффект движения камеры по бесконечному ландшафту.
2. Для новых мировых координат вычисляется значение шума HashSimplex.
3. Это значение преобразуется в диапазон от 0 до 1 и передается в ramp.getColor(), который возвращает цвет для верхней грани (colTop).
4. Используя временный объект tempColor типа Phaser.Display.Color, создаются затемненные версии этого цвета для левой (colLeft) и правой (colRight) граней, умножая компоненты redGL, greenGL, blueGL на коэффициенты из заранее заданных цветов lightBlue и darkBlue. Это создает эффект объемного освещения.
5. Высота изобокса (setSize) и его цвета (setFillStyle) обновляются.
let value = Phaser.Math.HashSimplex([ x / width / 8, y / height / 8 ], simplexConfig);
const col = ramp.getColor(Math.min(1, Math.max(0, value * 0.5 + 0.5)));
// ... расчет colTop, colLeft, colRight
isobox.setSize(isoWidth, Math.max(0, 48 * value) + 4)
.setFillStyle(colTop, colLeft, colRight);
Что попробовать дальше
Мы разобрали полный цикл создания процедурного ландшафта: от генерации высот с помощью шума Перлина-симплекса до наложения сложного цветового градиента и симуляции изометрического освещения. Код демонстрирует мощь встроенных инструментов Phaser для математики и работы с цветом.
Для экспериментов попробуйте:
1. Изменить параметры simplexConfig (например, noiseIterations или noiseWarpAmount), чтобы получить совершенно другой рельеф — от плавных холмов до резких скал.
2. Добавить новые биомы в ColorRamp или изменить цвета существующих для создания пустынного, вулканического или инопланетного мира.
3. Реализовать автоматическую прокрутку или привязать движение к персонажу, создав основу для простой exploration-игры.
