О чем этот пример
Создание изометрической проекции — популярный подход в играх, от стратегий до RPG. Однако корректное отображение объектов, чтобы дальние блоки не перекрывали ближние, требует правильной сортировки по глубине (depth sorting). В этой статье мы разберем пример из официальной коллекции Phaser, который не только строит изометрическую сетку, но и анимирует её с сохранением правильного порядка отрисовки. Вы научитесь управлять глубиной спрайтов и создавать динамичные изометрические сцены.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('isoblocks', 'assets/atlas/isoblocks.png', 'assets/atlas/isoblocks.json');
}
create ()
{
var frames = this.textures.get('isoblocks').getFrameNames();
// blocks are 50x50
var mapWidth = 40;
var mapHeight = 40;
var tileWidthHalf = 20;
var tileHeightHalf = 12;
var centerX = (mapWidth / 2) * tileWidthHalf;
var centerY = -100;
var blocks = [];
for (var y = 0; y < mapHeight; y++)
{
for (var x = 0; x < mapWidth; x++)
{
var tx = (x - y) * tileWidthHalf;
var ty = (x + y) * tileHeightHalf;
var block = (x % 2 === 0) ? 'block-123' : 'block-132';
var tile = this.add.image(centerX + tx, centerY + ty, 'isoblocks', block);
tile.setData('row', x);
tile.setData('col', y);
tile.setDepth(centerY + ty);
blocks.push(tile);
}
}
this.tweens.add({
targets: blocks,
x: function (target, key, value) {
return (value - (30 - (target.getData('col')) * 4));
},
y: function (target, key, value) {
return (value - (target.getData('row') * 5));
},
yoyo: true,
ease: 'Sine.easeInOut',
repeat: -1,
duration: 700,
delay: function (target, key, value, targetIndex, totalTargets, tween) {
return (target.getData('row') * 60) + (target.getData('col') * 60);
}
});
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
zoomIn: cursors.up,
zoomOut: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.cameras.main.zoom = 0.62;
this.cameras.main.scrollX = -110;
}
update (time, delta)
{
this.controls.update(delta);
}
}
const config = {
type: Phaser.WEBGL,
width: 1024,
height: 768,
backgroundColor: '#0d0d0d',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ассетов
Класс Example расширяет Phaser.Scene. В методе preload загружается атлас текстур isoblocks. Атлас — это единое изображение, содержащее несколько отдельных спрайтов (фреймов), и JSON-файл с координатами этих фреймов. Такой подход оптимизирует загрузку и отрисовку.
this.load.atlas('isoblocks', 'assets/atlas/isoblocks.png', 'assets/atlas/isoblocks.json');
В методе create мы сначала получаем список всех имен фреймов из загруженной текстуры. Это может быть полезно для отладки, хотя в данном примере используются только два конкретных фрейма.
Математика изометрической проекции
Ключ к изометрии — преобразование координат сетки (x, y) в экранные координаты (tx, ty). В примере используется стандартная формула для проекции "ромбиком" (diamond-top).
var tx = (x - y) * tileWidthHalf;
var ty = (x + y) * tileHeightHalf;
Здесь tileWidthHalf и tileHeightHalf — это половины ширины и высоты базового тайла. Умножение на эти значения превращает шаги по сетке в корректные смещения на экране. Переменные centerX и centerY смещают всю сгенерированную карту относительно центра камеры.
Блоки выбираются по чередующемуся шаблону для создания шахматного узора.
var block = (x % 2 === 0) ? 'block-123' : 'block-132';
Управление глубиной спрайтов
Самая важная часть для корректного отображения — установка глубины (z-index) каждого спрайта. В изометрии объекты, находящиеся "выше" (имеющие большую сумму координат x+y), должны перекрывать те, что "ниже".
tile.setDepth(centerY + ty);
Глубина задается значением координаты ty (с учетом смещения centerY). Поскольку ty увеличивается при движении вниз и вправо по сетке, блоки, которые должны быть визуально дальше (ниже на экране), получают большее значение глубины и, следовательно, отрисовываются раньше. Более близкие блоки (с меньшим ty) отрисовываются позже и перекрывают их. Это гарантирует, что визуальный порядок всегда будет правильным. Каждому блоку также записываются в данные (setData) его исходные координаты в сетке для последующей анимации.
Сложная анимация с Tween
Пример оживляет сцену с помощью твина, который применяется ко всем блокам одновременно. Особенность в том, что конечные значения `xиy` для каждого блока вычисляются динамически на основе его позиции в сетке.
x: function (target, key, value) {
return (value - (30 - (target.getData('col')) * 4));
},
Функции-геттеры для `xиyиспользуют сохраненные ранее данные (rowиcol), чтобы сместить каждый блок на уникальное расстояние. Это создает эффект "волны" или перестроения сетки. Дополнительный параметрdelayтакже рассчитывается на основе координат, что приводит к последовательному, каскадному запуску анимации для разных блоков. Параметрyoyo: trueиrepeat: -1` заставляют анимацию непрерывно повторяться туда-обратно.
Управление камерой
Чтобы можно было рассмотреть всю большую сцену, реализовано плавное управление камерой с клавиатуры с помощью Phaser.Cameras.Controls.SmoothedKeyControl.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
zoomIn: cursors.up,
zoomOut: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
Этот контроллер предоставляет инерционное движение и зум. В методе update необходимо вызывать this.controls.update(delta), чтобы обработка управления работала каждый кадр. Камера также инициализируется с небольшим зумом и смещением для лучшего стартового обзора.
Что попробовать дальше
Пример наглядно демонстрирует два ключевых аспекта работы с изометрией в Phaser: преобразование координат и обязательную ручную сортировку глубины через setDepth(). Полученную сцену можно развивать: добавить статичный ландшафт и динамичные объекты (персонажей), сортируя их в едином порядке; реализовать укладку блоков разной высоты, учитывая её в формуле глубины; или применить шейдер для сложных визуальных эффектов на всей сетке.
