О чем этот пример
Создание бесконечного или бесшовно скроллящегося мира — частый запрос в платформерах, раннерах или стратегиях. Генерация всей карты сразу может быть затратной по памяти. В этой статье разберем элегантный пример из официальной документации Phaser, который демонстрирует технику "бегущего окна" для тайловой карты. Вы научитесь динамически обновлять небольшую область тайлов, создавая иллюзию бесконечного и разнообразного ландшафта с минимальными вычислительными затратами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
tiles = [ 7, 7, 7, 6, 6, 6, 0, 0, 0, 1, 1, 2, 3, 4, 5 ];
distance = 0;
mapHeight = 37;
mapWidth = 51;
sx = 0;
text;
map;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/tiles/muddy-ground.png');
this.load.bitmapFont('nokia16', 'assets/fonts/bitmap/nokia16.png', 'assets/fonts/bitmap/nokia16.xml');
}
create ()
{
const mapData = [];
for (let y = 0; y < this.mapHeight; y++)
{
const row = [];
for (let x = 0; x < this.mapWidth; x++)
{
// Scatter the tiles so we get more mud and less stones
const tileIndex = Phaser.Math.RND.weightedPick(this.tiles);
row.push(tileIndex);
}
mapData.push(row);
}
this.map = this.make.tilemap({ data: mapData, tileWidth: 16, tileHeight: 16 });
const tileset = this.map.addTilesetImage('tiles');
const layer = this.map.createLayer(0, tileset, 0, 0);
this.text = this.add.bitmapText(0, 0, 'nokia16').setScrollFactor(0);
}
update (time, delta)
{
// Any speed as long as 16 evenly divides by it
this.sx += 4;
this.distance += this.sx;
this.text.setText(`Distance: ${this.distance}px`);
if (this.sx === 16)
{
// Reset and create new strip
let tile;
let prev;
for (let y = 0; y < this.mapHeight; y++)
{
for (let x = 1; x < this.mapWidth; x++)
{
tile = this.map.getTileAt(x, y);
prev = this.map.getTileAt(x - 1, y);
prev.index = tile.index;
if (x === this.mapWidth - 1)
{
tile.index = Phaser.Math.RND.weightedPick(this.tiles);
}
}
}
this.sx = 0;
}
this.cameras.main.scrollX = this.sx;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка данных: взвешенная случайность
Ключ к разнообразному миру — в алгоритме выбора тайлов. Вместо равномерного распределения используется взвешенный выбор, что позволяет контролировать частоту появления разных типов местности.
Массив tiles содержит индексы тайлов. Повторяющиеся значения увеличивают шанс их выбора. Например, индекс `7встречается три раза, а индекс5` — всего один. Это создает "грязную" основу с редкими вкраплениями камней.
В методе create создается двумерный массив mapData, который заполняется с помощью Phaser.Math.RND.weightedPick. Этот метод автоматически учитывает вес каждого элемента в массиве.
// Массив весов тайлов. Индекс 7 имеет самый большой вес (3), индекс 5 — самый маленький (1).
tiles = [ 7, 7, 7, 6, 6, 6, 0, 0, 0, 1, 1, 2, 3, 4, 5 ];
// Взвешенный выбор индекса тайла для конкретной ячейки (x, y)
const tileIndex = Phaser.Math.RND.weightedPick(this.tiles);
Создание и настройка тайловой карты
Phaser позволяет создавать карту напрямую из двумерного массива чисел, где каждое число — индекс тайла в tileset'е. Это идеально подходит для процедурной генерации.
Созданная карта this.map имеет фиксированные ширину и высоту в тайлах (mapWidth, mapHeight). Она отрисовывается один раз при создании сцены. Важно, что физический скролл камеры позже будет имитировать движение, но сами данные карты мы будем модифицировать.
// Создание тайловой карты из готового массива данных
this.map = this.make.tilemap({ data: mapData, tileWidth: 16, tileHeight: 16 });
// Добавление набора тайлов (tileset) и создание слоя для отрисовки
const tileset = this.map.addTilesetImage('tiles');
const layer = this.map.createLayer(0, tileset, 0, 0);
Движение и логика "бегущего окна"
Вся магия происходит в методе update. Переменная this.sx накапливает смещение камеры с каждым кадром. Когда это смещение достигает размера одного тайла (16 пикселей), карта "сдвигается" на один столбец тайлов влево, а самый правый столбец заполняется новыми случайными тайлами.
Это эффективно, потому что изменение индексов существующих тайлов (prev.index = tile.index) и генерация всего одного нового столбца требуют гораздо меньше ресурсов, чем пересоздание или перемещение всей графики.
// Скорость скролла. Должна нацело делить размер тайла (16).
this.sx += 4;
// Условие для сдвига данных карты
if (this.sx === 16)
{
// ... логика сдвига тайлов ...
this.sx = 0; // Сброс накопленного смещения
}
// Применение смещения к камере для визуального скролла
this.cameras.main.scrollX = this.sx;
Алгоритм сдвига столбцов тайлов
Сердце механизма — двойной цикл, который проходит по всем строкам карты. Для каждой ячейки (кроме самой левой) индекс тайла копируется из соседа справа. В результате все тайлы сдвигаются на одну позицию влево. Крайний правый столбец, который "освобождается", заполняется новыми случайными тайлами.
Операция выполняется только раз в несколько кадров (когда накопилось смещение в 16px), что делает ее очень производительной.
for (let y = 0; y < this.mapHeight; y++)
{
for (let x = 1; x < this.mapWidth; x++)
{
// Получаем текущий тайл и его левого соседа
tile = this.map.getTileAt(x, y);
prev = this.map.getTileAt(x - 1, y);
// Копируем индекс от текущего к предыдущему (сдвиг влево)
prev.index = tile.index;
// Для последнего столбца генерируем новый тайл
if (x === this.mapWidth - 1)
{
tile.index = Phaser.Math.RND.weightedPick(this.tiles);
}
}
}
Что попробовать дальше
Представленная техника — отличная основа для игр с боковым скроллингом. Она эффективно использует ресурсы, создавая бесконечный и уникальный мир. Для экспериментов попробуйте изменить логику генерации правого столбца: добавьте шаблоны для создания дорог, рек или пещер. Можно реализовать систему "чунков", где новый столбец подгружается из предварительно сгенерированных секций разной тематики. Также интересно будет привязать скорость скролла this.sx к игровому времени или действиям персонажа.
