О чем этот пример
При создании игр с тайловыми картами часто возникает необходимость работать с отдельными слоями: менять их прозрачность, проверять наличие тайлов и динамически переключаться между ними. В этом примере наглядно показано, как Phaser управляет выбранным слоем карты и чем отличаются методы проверки тайлов у самого слоя и у карты в целом. Понимание этих механизмов позволит вам создавать более сложные взаимодействия с окружением, реализовывать систему подсветки активного слоя и точно определять, на какой слой кликает игрок — что особенно полезно для редакторов уровней или игр с переключением между планами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
tileInfoText;
stuffLayer;
platformLayer;
waterLayer;
rockLayer;
map;
controls;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('kenny_platformer_64x64', 'assets/tilemaps/tiles/kenny_platformer_64x64.png');
this.load.tilemapTiledJSON('multiple-layers-map', 'assets/tilemaps/maps/multiple-layers.json');
}
create ()
{
this.map = this.make.tilemap({ key: 'multiple-layers-map' });
const tiles = this.map.addTilesetImage('kenny_platformer_64x64');
this.rockLayer = this.map.createLayer('Rock Layer', tiles, 0, 0);
this.waterLayer = this.map.createLayer('Water Layer', tiles, 0, 0);
this.platformLayer = this.map.createLayer('Platform Layer', tiles, 0, 0);
this.stuffLayer = this.map.createLayer('Stuff Layer', tiles, 0, 0);
// When you create a layer, that becomes the currently 'selected' layer within the map. That
// means any tile operation on the map right now will be operating on 'Stuff Layer'.
// Let's change that:
this.selectLayer(this.platformLayer);
this.cameras.main.setScroll(0, 1000);
this.input.keyboard.on('keydown-ONE', event =>
{
this.selectLayer(this.rockLayer);
});
this.input.keyboard.on('keydown-TWO', event =>
{
this.selectLayer(this.waterLayer);
});
this.input.keyboard.on('keydown-THREE', event =>
{
this.selectLayer(this.platformLayer);
});
this.input.keyboard.on('keydown-FOUR', event =>
{
this.selectLayer(this.stuffLayer);
});
this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
this.tileInfoText = this.add.text(16, 16, '', {
fontSize: '18px',
padding: { x: 10, y: 5 },
backgroundColor: '#000000',
fill: '#ffffff'
});
this.tileInfoText.setScrollFactor(0);
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
speed: 0.5
};
this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
}
update (time, delta)
{
this.controls.update(delta);
const cam = this.cameras.main;
const worldPoint = this.input.activePointer.positionToCamera(cam);
const mapHasTile = this.map.hasTileAtWorldXY(worldPoint.x, worldPoint.y);
const platformLayerHasTile = this.platformLayer.hasTileAtWorldXY(worldPoint.x, worldPoint.y);
// If you want to use the map and be specific, the last parameter is a layer id. All of the
// following are valid ways to get something from the rock layer:
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, rockLayer)
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, 'Rock Layer')
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, 0)
this.tileInfoText.setText(
`Press 1/2/3/4 to change the map's selected layer\nMap's selected layer: ${this.map.layer.name}\nMap hasTileAt pointer: ${mapHasTile ? 'yes' : 'no'}\nPlatform layer hasTileAt pointer: ${platformLayerHasTile ? 'yes' : 'no'}`
);
}
selectLayer (layer)
{
// You can use map.setLayer(...) or map.layer. Either can be set using a layer name, layer
// index, StaticTilemapLayer/DynamicTilemapLayer.
this.map.setLayer(layer);
this.rockLayer.alpha = 0.5;
this.waterLayer.alpha = 0.5;
this.platformLayer.alpha = 0.5;
this.stuffLayer.alpha = 0.5;
layer.alpha = 1;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#1b262c',
parent: 'phaser-example',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Загрузка карты и создание слоёв
В методе preload загружаются тайлсет и JSON-карта, созданная в Tiled. Важно, что в JSON-файле уже определены несколько слоёв с разными типами объектов.
В create происходит инициализация карты и создание слоёв в определённом порядке. Порядок создания влияет на отрисовку: слои, созданные позже, будут отображаться поверх предыдущих.
this.map = this.make.tilemap({ key: 'multiple-layers-map' });
const tiles = this.map.addTilesetImage('kenny_platformer_64x64');
this.rockLayer = this.map.createLayer('Rock Layer', tiles, 0, 0);
this.waterLayer = this.map.createLayer('Water Layer', tiles, 0, 0);
this.platformLayer = this.map.createLayer('Platform Layer', tiles, 0, 0);
this.stuffLayer = this.map.createLayer('Stuff Layer', tiles, 0, 0);
После создания последнего слоя (Stuff Layer) он автоматически становится выбранным в объекте карты (this.map). Все последующие операции с картой по умолчанию будут применяться к этому слою, если явно не указать другой.
Управление выбранным слоем карты
Phaser отслеживает активный слой внутри объекта тайловой карты. Для переключения между слоями можно использовать метод this.map.setLayer() или напрямую присваивать this.map.layer. В примере это инкапсулировано в метод selectLayer.
selectLayer(layer)
{
this.map.setLayer(layer);
this.rockLayer.alpha = 0.5;
this.waterLayer.alpha = 0.5;
this.platformLayer.alpha = 0.5;
this.stuffLayer.alpha = 0.5;
layer.alpha = 1;
}
Метод не только меняет выбранный слой в карте, но и визуально выделяет его, устанавливая полную непрозрачность (alpha = 1), тогда как остальные слои затемняются (alpha = 0.5). Это наглядно демонстрирует, какой слой сейчас активен.
Переключение привязано к клавишам 1-4, что позволяет быстро тестировать функциональность.
Проверка наличия тайла: карта vs слой
Одна из ключевых демонстраций примера — разница между использованием метода hasTileAtWorldXY у карты и у отдельного слоя.
В методе update координаты указателя мыши преобразуются в мировые координаты с учётом камеры. Затем выполняются две проверки:
const mapHasTile = this.map.hasTileAtWorldXY(worldPoint.x, worldPoint.y);
const platformLayerHasTile = this.platformLayer.hasTileAtWorldXY(worldPoint.x, worldPoint.y);
Первая проверка (this.map.hasTileAtWorldXY) ищет тайл на **текущем выбранном слое карты**. Какая именно это будет проверка, зависит от того, какой слой выбран клавишами 1-4.
Вторая проверка (this.platformLayer.hasTileAtWorldXY) всегда проверяет наличие тайла на конкретном слое platformLayer, независимо от того, какой слой выбран в карте.
Это важное различие: метод карты зависит от состояния this.map.layer, а метод слоя — нет.
Тонкости использования `map.hasTileAtWorldXY`
Если вам нужна проверка на определённом слое, но вы хотите использовать метод карты, вы можете явно передать слой последним параметром. Это демонстрируется в комментарии к коду.
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, rockLayer)
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, 'Rock Layer')
// map.hasTileAtWorldXY(worldPoint.x, worldPoint.y, cam, 0)
Параметр может быть:
- Ссылкой на объект слоя (rockLayer).
- Именем слоя, заданным в Tiled ('Rock Layer').
- Индексом слоя (`0` для первого слоя в массиве слоёв карты).
Такой подход полезен, когда у вас есть общая логика, работающая с разными слоями, и вы хотите избежать дублирования кода, перебирая слои в цикле.
Настройка камеры и элементов интерфейса
Для навигации по большой карте используется Phaser.Cameras.Controls.FixedKeyControl, который привязывает стрелки клавиатуры к перемещению камеры.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
speed: 0.5
};
this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
Текстовая панель создаётся для отображения служебной информации: - Какая клавиша переключает слои. - Имя текущего выбранного слоя в карте. - Результаты двух проверок наличия тайла.
this.tileInfoText.setText(
`Press 1/2/3/4 to change the map's selected layer\nMap's selected layer: ${this.map.layer.name}\nMap hasTileAt pointer: ${mapHasTile ? 'yes' : 'no'}\nPlatform layer hasTileAt pointer: ${platformLayerHasTile ? 'yes' : 'no'}`
);
Установка setScrollFactor(0) фиксирует текст относительно экрана, а не мира.
Что попробовать дальше
Этот пример чётко разделяет понятия "слой как объект для отрисовки" и "слой как текущий контекст для операций с картой". Для игровой логики (например, проверки, может ли игрок стоять здесь) чаще надежнее использовать методы конкретного слоя. А для инструментов (например, редактора) удобнее переключать контекст карты. Поэкспериментируйте: попробуйте добавить функцию удаления или размещения тайлов по клику на активном слое. Или реализуйте систему, где разные слои блокируют движение разных типов персонажей. Также можно создать мини-редактор, который сохраняет изменения слоёв обратно в JSON.
