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

Работая с тайловыми картами в Phaser 3, разработчики часто сталкиваются с объектами `Tilemap` и `TilemapLayer`. На первый взгляд, методы для заполнения или рандомизации тайлов доступны у обоих. В этом примере мы наглядно разберём ключевое отличие: объект карты (`map`) оперирует глобальными координатами сетки тайлов, в то время как слой (`layer`) работает в своём собственном пространстве, учитывая все применённые к нему трансформации, такие как масштабирование. Понимание этой разницы критически важно для корректного позиционирования элементов на экране, особенно когда слои масштабируются, прокручиваются или сдвигаются.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    controls;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tiles', 'assets/tilemaps/tiles/platformer_tiles.png');
    }

    create ()
    {
        // Creating a blank tilemap with the specified dimensions
        const map = this.make.tilemap({ tileWidth: 16, tileHeight: 16, width: 18, height: 13});

        const tiles = map.addTilesetImage('tiles');

        const layer = map.createBlankLayer('layer1', tiles);
        layer.setScale(3);

        // Add a simple scene with some random element. Since there is only one layer, we can use map or
        // layer interchangeably to access tile manipulation methods.
        map.fill(58, 0, 10, map.width, 1); // Surface of the water
        layer.fill(77, 0, 11, map.width, 2); // Body of the water
        map.randomize(0, 0, 8, 10, [ 44, 45, 46, 47, 48 ]); // Left chunk of random wall tiles
        layer.randomize(8, 0, 9, 10, [ 20, 21, 22, 23, 24 ]); // Right chunk of random wall tiles
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#00000',
    parent: 'phaser-example',
    pixelArt: true,
    scene: Example
};

const game = new Phaser.Game(config);

Создание карты и слоя

В методе create() мы начинаем с создания пустой тайловой карты. Карта определяет логическую сетку: её ширину, высоту и размер каждого тайла. Затем мы создаём слой, который является визуальным представлением этой сетки на экране.

const map = this.make.tilemap({ tileWidth: 16, tileHeight: 16, width: 18, height: 13});
const tiles = map.addTilesetImage('tiles');
const layer = map.createBlankLayer('layer1', tiles);
layer.setScale(3);

Ключевой момент здесь — вызов layer.setScale(3). Он увеличивает визуальный размер слоя (и всех его тайлов) в три раза. Однако важно помнить: сама логическая сетка карты (map) остаётся неизменной — по-прежнему 18x13 тайлов размером 16x16 пикселей каждый. Слой лишь отображает эту сетку в увеличенном виде.

Одинаковые методы, разная система координат

И map, и layer имеют методы для заполнения тайлами (fill) и их рандомизации (randomize). В примере они используются попеременно, и на первый взгляд результат одинаков. Это работает, потому что в данном конкретном случае слой не смещён и не обрезан, а просто равномерно масштабирован.

map.fill(58, 0, 10, map.width, 1);
layer.fill(77, 0, 11, map.width, 2);

Оба метода принимают координаты (x, y) в тайлах. Разница в том, что map.fill() использует координаты в **глобальной системе карты** (исходная сетка). layer.fill() использует координаты в **локальной системе слоя**. Если бы мы применили к слою не только масштаб, но и сдвиг (layer.setPosition), или изменили его область отображения (layer.setCullPadding), то тайлы, добавленные через map и layer, оказались бы в разных местах на экране.

Практическое правило: приоритет слоя

Чтобы избежать путаницы и потенциальных багов с позиционированием, рекомендуется всегда использовать объект слоя (layer) для любых операций с тайлами, если этот слой подвергается трансформациям.

// Предпочтительный и надёжный способ:
layer.fill(58, 0, 10, map.width, 1);
layer.randomize(0, 0, 8, 10, [ 44, 45, 46, 47, 48 ]);

Работая напрямую со слоем, вы гарантируете, что координаты тайлов интерпретируются корректно с учётом его текущего масштаба, позиции и других свойств отображения. Объект map лучше использовать для глобальных операций, не зависящих от отображения: загрузки данных, управления несколькими слоями или получения свойств базовой сетки.

Что происходит "под капотом" в примере

Давайте проследим за выполнением кода из статьи. Создаётся карта 18x13 тайлов. Создаётся слой и масштабируется в 3 раза. Теперь один логический тайл (16x16 пикселей) отрисовывается на экране как блок 48x48 пикселей.

// Эти вызовы дают визуально идентичный результат, потому что слой только масштабирован:
map.randomize(0, 0, 8, 10, [ 44, 45, 46, 47, 48 ]);
layer.randomize(8, 0, 9, 10, [ 20, 21, 22, 23, 24 ]);

Когда мы говорим map.randomize(0, 0, ...), мы заполняем тайлами левый верхний угол логической сетки. Слой, получая данные этой сетки, автоматически масштабирует результат. Вызов layer.randomize(8, 0, ...) делает то же самое, но координата x=8 уже вычисляется в системе масштабированного слоя. Так как других трансформаций нет, итоговое положение блоков на экране совпадает с ожидаемым.

Что попробовать дальше

Главный вывод: Tilemap — это абстрактная модель данных (сетка), а TilemapLayer — её визуальное представление с возможными трансформациями. Для манипуляций с тайлами всегда используйте методы слоя (layer), если он масштабируется, двигается или обрезается. Это сделает ваш код предсказуемым и устойчивым к изменениям. Для экспериментов попробуйте добавить сдвиг слоя layer.setPosition(100, 50) и снова выполнить заполнение через map.fill() и layer.fill() — вы сразу увидите разницу в поведении.