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

Создание бесконечного, разнообразного ландшафта — классическая задача в геймдеве. В этой статье мы разберем пример из официальной библиотеки 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-игры.