О чем этот пример
При создании процедурно генерируемых уровней однообразие — главный враг. Заполнение карты полностью случайными тайлами часто выглядит неестественно: сундуки могут оказаться на каждом шагу, а редкие артефакты потеряются среди обычного мусора. В этой статье разберём метод взвешенной рандомизации (`weightedRandomize`), который позволяет контролировать вероятность появления каждого элемента на тайловой карте. Вы научитесь создавать правдоподобные локации, где одни объекты встречаются часто, а другие становятся ценными находками, просто задав им разные 'веса'.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
map;
objectLayer;
groundLayer;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
// Credits! Michele "Buch" Bucelli (tilset artist) & Abram Connelly (tileset sponser)
// https://opengameart.org/content/top-down-dungeon-tileset
this.load.image('tiles', 'assets/tilemaps/tiles/buch-dungeon-tileset.png');
}
create ()
{
// Creating a blank tilemap with the specified dimensions
this.map = this.make.tilemap({ tileWidth: 16, tileHeight: 16, width: 23, height: 17 });
const tiles = this.map.addTilesetImage('tiles');
this.groundLayer = this.map.createBlankLayer('Ground Layer', tiles);
this.objectLayer = this.map.createBlankLayer('Object Layer', tiles);
this.groundLayer.setScale(2);
this.objectLayer.setScale(2);
this.cameras.main.setScroll(-27, -27);
// Walls & corners of the room
this.groundLayer.fill(39, 0, 0, this.map.width, 1);
this.groundLayer.fill(1, 0, this.map.height - 1, this.map.width, 1);
this.groundLayer.fill(21, 0, 0, 1, this.map.height);
this.groundLayer.fill(19, this.map.width - 1, 0, 1, this.map.height);
this.groundLayer.putTileAt(3, 0, 0);
this.groundLayer.putTileAt(4, this.map.width - 1, 0);
this.groundLayer.putTileAt(23, this.map.width - 1, this.map.height - 1);
this.groundLayer.putTileAt(22, 0, this.map.height - 1);
this.randomizeRoom(); // Initial randomization
this.input.on('pointerdown', this.randomizeRoom, this);
const help = this.add.text(16, 16, 'Click to re-randomize.', {
fontSize: '18px',
padding: { x: 10, y: 5 },
backgroundColor: '#ffffff',
fill: '#000000'
});
help.setScrollFactor(0);
}
randomizeRoom ()
{
// Fill the floor with random ground tiles
this.groundLayer.weightedRandomize([
{ index: 6, weight: 4 }, // Regular floor tile (4x more likely)
{ index: 7, weight: 1 }, // Tile variation with 1 rock
{ index: 8, weight: 1 }, // Tile variation with 1 rock
{ index: 26, weight: 1 } // Tile variation with 1 rock
],
1,// - The left most tile index (in tile coordinates) to use as the origin of the area.
1,// - The top most tile index (in tile coordinates) to use as the origin of the area.
this.map.width - 2, // - How many tiles wide from the `tileX` index the area will be.
this.map.height - 2 // - How many tiles tall from the `tileY` index the area will be.
);
// Fill the floor of the room with random, weighted tiles
this.objectLayer.weightedRandomize([
{ index: -1, weight: 50 }, // Place an empty tile most of the tile
{ index: 13, weight: 3 }, // Empty pot
{ index: 32, weight: 2 }, // Full pot
{ index: 127, weight: 1 }, // Open crate
{ index: 108, weight: 1 }, // Empty crate
{ index: 109, weight: 2 }, // Open barrel
{ index: 110, weight: 2 }, // Empty barrel
{ index: 166, weight: 0.25 }, // Chest
{ index: 167, weight: 0.25 } // Trap door
], 1, 1, this.map.width - 2, this.map.height - 2);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка карты и слоёв
В примере создаётся пустая тайловая карта фиксированного размера. Ключевой момент — использование двух отдельных слоёв: один для пола (groundLayer), другой для объектов (objectLayer). Это стандартный подход в Phaser для разделения логики отрисовки и взаимодействия.
this.map = this.make.tilemap({ tileWidth: 16, tileHeight: 16, width: 23, height: 17 });
const tiles = this.map.addTilesetImage('tiles');
this.groundLayer = this.map.createBlankLayer('Ground Layer', tiles);
this.objectLayer = this.map.createBlankLayer('Object Layer', tiles);
После создания слои масштабируются, и камера немного сдвигается, чтобы всё поместилось в поле зрения. Затем вручную расставляются тайлы стен и углов комнаты методом fill (для линий) и putTileAt (для отдельных угловых тайлов). Это создаёт замкнутое пространство для дальнейшей рандомизации.
Принцип работы weightedRandomize
Метод weightedRandomize доступен для объектов TilemapLayer. Его сила — в использовании весов (weight). Вместо равной вероятности для каждого тайла, вы задаёте список объектов с числовым весом. Чем больше вес, тем выше шанс, что этот конкретный тайл будет выбран для ячейки.
this.groundLayer.weightedRandomize([
{ index: 6, weight: 4 }, // Обычный пол (в 4 раза вероятнее)
{ index: 7, weight: 1 }, // Вариация с камнем
{ index: 8, weight: 1 },
{ index: 26, weight: 1 }
], 1, 1, this.map.width - 2, this.map.height - 2);
Последние четыре аргумента определяют область применения: начальные координаты (tileX, tileY) и размеры (width, height) в тайлах. В коде область смещена на 1 тайл от края, чтобы не перезаписывать стены.
Создание правдоподобного разброса объектов
Второй вызов weightedRandomize заполняет слой объектов. Здесь демонстрируется продвинутая техника: использование индекса -1 для обозначения пустой ячейки.
this.objectLayer.weightedRandomize([
{ index: -1, weight: 50 }, // Пустая ячейка (самый высокий вес)
{ index: 13, weight: 3 }, // Пустой горшок
{ index: 166, weight: 0.25 } // Сундук (очень редкий)
], 1, 1, this.map.width - 2, this.map.height - 2);
Благодаря весам, пустые ячейки (weight: 50) имеют огромный перевес. Это создаёт разреженное, естественное расположение объектов: несколько горшков в углах, случайные бочки и крайне редкий сундук. Дробные веса (как 0.25 для сундука) позволяют тонко настраивать редкость.
Интерактивность и перегенерация
Для демонстрации динамической природы метода к сцене привязано событие клика мыши. Каждый клик вызывает функцию randomizeRoom заново, полностью перегенерируя внутренность комнаты.
this.randomizeRoom(); // Первоначальная генерация
this.input.on('pointerdown', this.randomizeRoom, this);
Этот приём полезен не только для отладки, но и для игровых механик. Представьте комнату, которая 'перестраивается' при активации магического свитка, или локацию, где расположение препятствий меняется каждый раз, когда игрок входит в дверь.
Что попробовать дальше
Метод weightedRandomize — это мощный и элегантный инструмент для процедурной генерации в Phaser. Он избавляет от необходимости писать сложные логические цепочки для 'взвешенного' случайного выбора, инкапсулируя эту логику прямо в API работы с тайлами. Для экспериментов попробуйте: динамически менять веса в зависимости от типа комнаты (склад, алтарь, казармы); использовать его для генерации 'биомов' на большой карте; или комбинировать с другими методами тайлмапов, например, randomize или replaceByIndex, для создания ещё более сложных процедурных ландшафтов.
