О чем этот пример
В 2D-платформерах и топ-даун играх часто нужно, чтобы одни тайлы были твердыми препятствиями, а другие — триггерами для событий, например, сбора предметов. Phaser позволяет легко настраивать такое поведение с помощью коллбэков для отдельных тайлов. В этой статье разберем пример, где игрок собирает монеты, натыкаясь на них, и может пройти через секретный блок в потолке. Вы научитесь использовать `setTileIndexCallback` и `setTileLocationCallback` для создания интерактивных тайловых карт.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
coinsCollected = 0;
coinLayer;
groundLayer;
showDebug = false;
player;
text;
debugGraphics;
cursors;
map;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('ground_1x1', 'assets/tilemaps/tiles/ground_1x1.png');
this.load.spritesheet('coin', 'assets/sprites/coin.png', { frameWidth: 32, frameHeight: 32 });
this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tile-collision-test.json');
this.load.image('player', 'assets/sprites/phaser-dude.png');
}
create ()
{
this.map = this.make.tilemap({ key: 'map' });
const groundTiles = this.map.addTilesetImage('ground_1x1');
const coinTiles = this.map.addTilesetImage('coin');
this.map.createLayer('Background Layer', groundTiles, 0, 0);
this.groundLayer = this.map.createLayer('Ground Layer', groundTiles, 0, 0);
this.coinLayer = this.map.createLayer('Coin Layer', coinTiles, 0, 0);
this.groundLayer.setCollisionBetween(1, 25);
// This will set Tile ID 26 (the coin tile) to call the function "hitCoin" when collided with
this.coinLayer.setTileIndexCallback(26, this.hitCoin, this);
// This will set the map location (2, 0) to call the function "hitSecretDoor" Un-comment this to
// be jump through the ceiling above where the player spawns. You can use this to create a
// secret area.
this.groundLayer.setTileLocationCallback(2, 0, 1, 1, this.hitSecretDoor, this);
this.player = this.physics.add.sprite(80, 70, 'player')
.setBounce(0.1);
// We want the player to physically collide with the ground, but the coin layer should only
// trigger an overlap so that collection a coin doesn'td kill player movement.
this.physics.add.collider(this.player, this.groundLayer);
this.physics.add.overlap(this.player, this.coinLayer);
this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
this.cameras.main.startFollow(this.player);
this.debugGraphics = this.add.graphics();
this.input.keyboard.on('keydown-C', event =>
{
this.showDebug = !this.showDebug;
this.drawDebug();
});
this.cursors = this.input.keyboard.createCursorKeys();
this.text = this.add.text(16, 16, '', {
fontSize: '20px',
fill: '#ffffff'
});
this.text.setScrollFactor(0);
this.updateText();
}
update (time, delta)
{
// Horizontal movement
this.player.body.setVelocityX(0);
if (this.cursors.left.isDown)
{
this.player.body.setVelocityX(-200);
}
else if (this.cursors.right.isDown)
{
this.player.body.setVelocityX(200);
}
// Jumping
if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor())
{
this.player.body.setVelocityY(-300);
}
}
hitCoin (sprite, tile)
{
this.coinLayer.removeTileAt(tile.x, tile.y);
this.coinsCollected += 1;
this.updateText();
// Return true to exit processing collision of this tile vs the sprite - in this case, it
// doesn't matter since the coin tiles are not set to collide.
return false;
}
hitSecretDoor (sprite, tile)
{
tile.alpha = 0.25;
// Return true to exit processing collision of this tile vs the sprite - here, we want to allow
// the player jump "through" the block and not collide.
return true;
}
drawDebug ()
{
this.debugGraphics.clear();
if (this.showDebug)
{
// Pass in null for any of the style options to disable drawing that component
this.groundLayer.renderDebug(this.debugGraphics, {
tileColor: null, // Non-colliding tiles
collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles
faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges
});
}
this.updateText();
}
updateText ()
{
this.text.setText(
`Arrow keys to move. Space to jump\nPress "C" to toggle debug visuals: ${this.showDebug ? 'on' : 'off'}\nCoins collected: ${this.coinsCollected}`
);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 576,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
pixelArt: true,
physics: {
default: 'arcade',
arcade: { gravity: { y: 300 } }
},
scene: Example
};
const game = new Phaser.Game(config);
Настройка сцены и загрузка ресурсов
Класс Example расширяет Phaser.Scene и содержит свойства для отслеживания состояния игры: количество собранных монет, слои тайлов, игрока и отладочной графики.
В методе preload загружаются необходимые ресурсы: изображения для тайлов земли и монет, спрайт игрока и файл карты в формате Tiled JSON.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('ground_1x1', 'assets/tilemaps/tiles/ground_1x1.png');
this.load.spritesheet('coin', 'assets/sprites/coin.png', { frameWidth: 32, frameHeight: 32 });
this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tile-collision-test.json');
this.load.image('player', 'assets/sprites/phaser-dude.png');
}
Создание карты, слоев и настройка коллизий
В методе create создается тайловая карта из загруженного JSON-файла. Затем добавляются tilesets и создаются три слоя: фоновый, слой земли (groundLayer) и слой монет (coinLayer).
Слой земли настраивается как сплошная коллизия для тайлов с ID от 1 до 25 с помощью метода setCollisionBetween. Это означает, что физическое тело игрока будет сталкиваться с этими тайлами.
this.groundLayer.setCollisionBetween(1, 25);
Для слоя монет используется другой подход. Мы не хотим, чтобы монеты физически останавливали игрока, поэтому для них не задается коллизия. Вместо этого мы назначаем коллбэк для тайла с ID 26 (монета) методом setTileIndexCallback. Этот метод принимает ID тайла, функцию обратного вызова и контекст (this). Теперь при пересечении игрока с этим тайлом будет вызвана функция hitCoin.
this.coinLayer.setTileIndexCallback(26, this.hitCoin, this);
Более точечный метод setTileLocationCallback позволяет назначить коллбэк для тайла по его координатам на карте (X, Y) и размеру области (ширина, высота). В примере он закомментирован, но если его раскомментировать, то тайл в позиции (2,0) вызовет функцию hitSecretDoor.
// this.groundLayer.setTileLocationCallback(2, 0, 1, 1, this.hitSecretDoor, this);
Далее создается физический спрайт игрока и настраиваются его взаимодействия со слоями. Для земли используется collider, что обеспечивает физическое столкновение. Для монет используется overlap, который только регистрирует факт пересечения, не влияя на движение.
this.physics.add.collider(this.player, this.groundLayer);
this.physics.add.overlap(this.player, this.coinLayer);
Логика обработки пересечений с тайлами
Когда игрок пересекается с тайлом монеты, движок вызывает функцию hitCoin. Ей передаются спрайт игрока и объект тайла.
Внутри функции мы удаляем этот тайл с карты с помощью removeTileAt, увеличиваем счетчик монет и обновляем текст на экране.
hitCoin (sprite, tile)
{
this.coinLayer.removeTileAt(tile.x, tile.y);
this.coinsCollected += 1;
this.updateText();
return false;
}
Возвращаемое значение (false) указывает движку, следует ли прекращать дальнейшую обработку коллизии для этого тайла. Поскольку для монет не задана физическая коллизия, это значение не играет роли.
Функция hitSecretDoor вызывается для конкретного тайла-блока. Она делает тайл полупрозрачным, устанавливая alpha = 0.25. Возвращаемое значение true критически важно: оно говорит движку, что столкновение с этим тайлом обработано и физическое взаимодействие (отталкивание) должно быть отменено. Это позволяет игроку "пропрыгнуть" сквозь блок.
hitSecretDoor (sprite, tile)
{
tile.alpha = 0.25;
return true;
}
Управление игроком и отладочный режим
В методе update обрабатывается ввод с клавиатуры для движения игрока влево-вправо и прыжка. Проверка this.player.body.onFloor() не позволяет прыгать в воздухе.
if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor())
{
this.player.body.setVelocityY(-300);
}
По нажатию клавиши 'C' переключается флаг showDebug и вызывается метод drawDebug. Этот метод очищает графический слой debugGraphics и, если отладка включена, рисует поверх тайлов земли цветные контуры: оранжевым выделяются тайлы с коллизией, серым — их грани.
drawDebug ()
{
this.debugGraphics.clear();
if (this.showDebug)
{
this.groundLayer.renderDebug(this.debugGraphics, {
tileColor: null,
collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200),
faceColor: new Phaser.Display.Color(40, 39, 37, 255)
});
}
this.updateText();
}
Что попробовать дальше
Используя setTileIndexCallback и setTileLocationCallback, вы можете превратить статичную тайловую карту в интерактивное игровое пространство. Возвращаемое значение true из коллбэка — ключ к созданию "проходимых" тайлов, как секретная дверь. Для экспериментов попробуйте: создать разные типы собираемых предметов с разными эффектами (здоровье, бонусы), сделать движущиеся платформы из тайлов или реализовать тайлы-переключатели, которые меняют свойства других тайлов на карте.
