О чем этот пример
Изометрическая графика придает играм уникальный шарм и ощущение объема. В этом примере мы разберем, как динамически генерировать и изменять изометрический ландшафт, используя карту высот и геометрические примитивы Phaser. Вы научитесь работать с `add.isobox`, управлять высотой объектов и создавать интерактивный мир, реагирующий на ввод пользователя.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
waterHeight = 60;
maxHeight = 120;
offsetY = 90;
spacing = 12;
size = 20;
gridHeight = 46;
gridWidth = 39;
cursors;
py = 0;
px = 0;
land = [];
heightmap;
color = new Phaser.Display.Color();
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('noise', 'assets/tests/noise.png');
// this.load.image('noise', 'assets/tests/heightmap.png');
}
create ()
{
this.heightmap = this.textures.createCanvas('map', 512, 512);
this.heightmap.draw(0, 0, this.textures.get('noise').getSourceImage());
let ox = this.size;
let r = 0;
const h = this.size;
for (let y = 0; y < this.gridHeight; y++)
{
const row = [];
for (let x = 0; x < this.gridWidth - r; x++)
{
const tile = this.add.isobox(ox + x * this.size, this.offsetY + y * this.spacing, this.size, h, 0x8dcb0e, 0x3f8403, 0x63a505);
row.push(tile);
}
r++;
ox += this.size / 2;
if (r === 2)
{
r = 0;
ox = this.size;
}
this.land.push(row);
}
this.updateLand();
this.cursors = this.input.keyboard.createCursorKeys();
}
update ()
{
let down = false;
if (this.cursors.left.isDown)
{
this.px--;
if (this.px < 0)
{
this.px = 512;
}
down = true;
}
else if (this.cursors.right.isDown)
{
this.px++;
if (this.px === 512)
{
this.px = 0;
}
down = true;
}
if (this.cursors.up.isDown)
{
this.py--;
if (this.py < 0)
{
this.py = 512;
}
down = true;
}
else if (this.cursors.down.isDown)
{
this.py++;
if (this.py === 512)
{
this.py = 0;
}
down = true;
}
if (down)
{
this.updateLand();
}
}
updateLand ()
{
let r = 0;
for (let y = 0; y < this.gridHeight; y++)
{
for (let x = 0; x < this.gridWidth - r; x++)
{
const cx = Phaser.Math.Wrap(this.px + x, 0, 512);
const cy = Phaser.Math.Wrap(this.py + y, 0, 512);
this.heightmap.getPixel(cx, cy, this.color);
const h = (Math.max(this.color.r, this.color.g, this.color.b) / 255) * this.maxHeight;
if (h < this.waterHeight)
{
this.land[y][x].setFillStyle(0x00b9f2, 0x016fce, 0x028fdf);
}
else if (h === this.maxHeight)
{
this.land[y][x].setFillStyle(0xffe31f, 0xf2a022, 0xf8d80b);
}
else
{
this.land[y][x].setFillStyle(0x8dcb0e, 0x3f8403, 0x63a505);
}
this.land[y][x].height = h;
}
r++;
if (r === 2)
{
r = 0;
}
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#efefef',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка и загрузка ресурсов
Класс Example расширяет Phaser.Scene. В методе preload загружается изображение шума, которое будет использовано в качестве карты высот. Важно: изображение загружается с удаленного URL, что демонстрирует гибкость загрузчика Phaser.
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('noise', 'assets/tests/noise.png');
В методе create создается динамическая текстура heightmap на основе загруженного изображения. Это позволяет манипулировать пиксельными данными в реальном времени.
this.heightmap = this.textures.createCanvas('map', 512, 512);
this.heightmap.draw(0, 0, this.textures.get('noise').getSourceImage());
Построение изометрической сетки
Сетка ландшафта строится с помощью вложенных циклов. Ключевой метод — this.add.isobox, который создает изометрический параллелепипед (изобокс). Параметры: координаты X, Y, ширина, высота и три цвета для граней (светлая, темная и средняя).
const tile = this.add.isobox(ox + x * this.size, this.offsetY + y * this.spacing, this.size, h, 0x8dcb0e, 0x3f8403, 0x63a505);
Логика смещения ox и уменьшения ширины строки this.gridWidth - r создает характерный сдвиг изометрической сетки. Каждый ряд сохраняется в массив this.land для дальнейшего управления.
Динамическое обновление ландшафта
Метод updateLand — сердце примера. Для каждого изобокса в сетке вычисляется цвет пикселя на карте высот с помощью this.heightmap.getPixel. По значению цвета определяется высота и тип поверхности: вода, трава или вершина.
const h = (Math.max(this.color.r, this.color.g, this.color.b) / 255) * this.maxHeight;
В зависимости от высоты вызывается метод setFillStyle для изменения цветов граней изобокса. Высота самого объекта задается через свойство height. Это визуально изменяет его пропорции.
this.land[y][x].setFillStyle(0x00b9f2, 0x016fce, 0x028fdf);
this.land[y][x].height = h;
Обработка ввода и навигация
В методе update отслеживается состояние клавиш-стрелок с помощью this.cursors. При нажатии изменяются переменные this.px и this.py, которые представляют текущую позицию "камеры" на карте высот. Используется Phaser.Math.Wrap для зацикливания координат, создавая бесконечный ландшафт.
const cx = Phaser.Math.Wrap(this.px + x, 0, 512);
Любое изменение позиции приводит к вызову this.updateLand(), что мгновенно перерисовывает ландшафт, создавая эффект прокрутки.
Что попробовать дальше
Вы освоили создание динамического изометрического ландшафта в Phaser. Экспериментируйте: замените карту шума на собственное изображение, добавьте разные биомы с уникальными цветами, реализуйте плавную интерполяцию высот или создайте систему выделения ячеек при наведении мыши.
