О чем этот пример
В процессе разработки игры часто возникает необходимость менять визуальный стиль уровня на лету — например, для эффектов времени суток, повреждений окружения или смены локаций. Phaser позволяет делать это без перезагрузки сцены, просто подменяя текстуру тайлсета. Эта техника экономит ресурсы и открывает простор для геймдизайнерских находок. В статье разберём пример, где по нажатию клавиш 1, 2, 3 весь уровень мгновенно перекрашивается, используя разные наборы тайлов. Мы изучим, как загрузить несколько текстур, создать тайловую карту, назначить ей динамический тайлсет и менять его изображение в реальном времени.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
currentTileset = 1;
showDebug = false;
player;
helpText;
debugGraphics;
cursors;
map;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/tiles/catastrophi_tiles_16.png');
this.load.image('tiles_red', 'assets/tilemaps/tiles/catastrophi_tiles_16_red.png');
this.load.image('tiles_blue', 'assets/tilemaps/tiles/catastrophi_tiles_16_blue.png');
this.load.tilemapCSV('map', 'assets/tilemaps/csv/catastrophi_level2.csv');
this.load.spritesheet('player', 'assets/sprites/spaceman.png', { frameWidth: 16, frameHeight: 16 });
}
create ()
{
// When loading a CSV map, make sure to specify the tileWidth and tileHeight
this.map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 16 });
const tileset = this.map.addTilesetImage('tiles_red');
const layer = this.map.createLayer(0, tileset, 0, 0);
layer.setScale(2);
// This isn't totally accurate, but it'll do for now
this.map.setCollisionBetween(54, 83);
this.input.keyboard.on('keydown-ONE', function (event)
{
const texture = this.sys.textures.get('tiles_red');
this.currentTileset = 1;
tileset.setImage(texture);
this.updateHelpText();
}, this);
this.input.keyboard.on('keydown-TWO', function (event)
{
const texture = this.sys.textures.get('tiles_blue');
this.currentTileset = 2;
tileset.setImage(texture);
this.updateHelpText();
}, this);
this.input.keyboard.on('keydown-THREE', function (event)
{
const texture = this.sys.textures.get('tiles');
this.currentTileset = 3;
tileset.setImage(texture);
this.updateHelpText();
}, this);
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('player', { start: 8, end: 9 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('player', { start: 1, end: 2 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'up',
frames: this.anims.generateFrameNumbers('player', { start: 11, end: 13 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'down',
frames: this.anims.generateFrameNumbers('player', { start: 4, end: 6 }),
frameRate: 10,
repeat: -1
});
this.player = this.physics.add.sprite(100, 100, 'player', 1)
.setScale(2);
this.player.setSize(10, 10, false);
// Set up the player to collide with the tilemap layer. Alternatively, you can manually run
// collisions in update via: this.physics.world.collide(player, layer).
this.physics.add.collider(this.player, layer);
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('down_67', event =>
{
this.showDebug = !this.showDebug;
this.drawDebug();
});
this.cursors = this.input.keyboard.createCursorKeys();
this.helpText = this.add.text(16, 16, '', {
fontSize: '20px',
fill: '#ffffff'
});
this.helpText.setScrollFactor(0);
this.updateHelpText();
}
update (time, delta)
{
this.player.body.setVelocity(0);
// Horizontal movement
if (this.cursors.left.isDown)
{
this.player.body.setVelocityX(-200);
}
else if (this.cursors.right.isDown)
{
this.player.body.setVelocityX(200);
}
// Vertical movement
if (this.cursors.up.isDown)
{
this.player.body.setVelocityY(-200);
}
else if (this.cursors.down.isDown)
{
this.player.body.setVelocityY(200);
}
// Update the animation last and give left/right animations precedence over up/down animations
if (this.cursors.left.isDown)
{
this.player.anims.play('left', true);
}
else if (this.cursors.right.isDown)
{
this.player.anims.play('right', true);
}
else if (this.cursors.up.isDown)
{
this.player.anims.play('up', true);
}
else if (this.cursors.down.isDown)
{
this.player.anims.play('down', true);
}
else
{
this.player.anims.stop();
}
}
drawDebug ()
{
this.debugGraphics.clear();
if (this.showDebug)
{
// Pass in null for any of the style options to disable drawing that component
this.map.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.updateHelpText();
}
updateHelpText ()
{
this.helpText.setText(
`Arrow keys to move.\nPress 1/2/3 to change the tileset texture.\nCurrent texture: ${this.currentTileset}`
);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
pixelArt: true,
physics: {
default: 'arcade',
arcade: { gravity: { y: 0 } }
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка ресурсов и создание карты
В методе preload загружаются три варианта одного тайлсета (основной, красный и синий) в виде отдельных изображений, CSV-файл с картой и спрайтшит игрока.
Ключевой момент: загруженные изображения тайлов имеют одинаковые размеры и расположение элементов, что позволяет их взаимозаменять.
В create создаётся тайловая карта из CSV. Важно явно указать размер тайла. Затем создаётся тайлсет, в который передаётся одно из изображений (например, tiles_red). Этот тайлсет используется для создания слоя карты.
this.map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 16 });
const tileset = this.map.addTilesetImage('tiles_red');
const layer = this.map.createLayer(0, tileset, 0, 0);
layer.setScale(2);
Механика переключения текстур
Динамическая смена внешнего вида уровня реализована через метод setImage() объекта Tileset. Этот метод заменяет исходное изображение тайлсета на другое, используя текстуру, полученную из кэша.
Обработчики клавиш 1, 2, 3 получают текстуру по её ключу и передают её в тайлсет. Вся геометрия карты (индексы тайлов, коллизии) остаётся неизменной — меняется только отображаемая картинка.
this.input.keyboard.on('keydown-ONE', function (event) {
const texture = this.sys.textures.get('tiles_red');
this.currentTileset = 1;
tileset.setImage(texture);
this.updateHelpText();
}, this);
Физика, анимации и отладка
Карта настраивается на коллизии: setCollisionBetween(54, 83) помечает диапазон индексов тайлов как твёрдые. Физический спрайт игрока создаётся с помощью this.physics.add.sprite, а коллайдер с картой — через this.physics.add.collider(this.player, layer). Это обеспечивает автоматическое управление столкновениями.
Анимации движения игрока создаются стандартным образом через this.anims.create. В методе update в зависимости от нажатых стрелок задаётся скорость и проигрывается соответствующая анимация.
Для визуализации коллизий в примере есть отладочный режим по клавише C, который рисует поверх карты с помощью map.renderDebug.
this.physics.add.collider(this.player, layer);
this.input.keyboard.on('down_67', event => {
this.showDebug = !this.showDebug;
this.drawDebug();
});
Советы по практическому использованию
1. **Оптимизация:** Загружайте только необходимые тайлсеты. Если текстуры очень большие, динамическая замена может вызвать кратковременный фриз. Рассмотрите предзагрузку.
2. **Гибкость:** Метод setImage можно вызывать не только по клавиатуре, но и по таймеру, триггеру на карте или событию в игровой логике.
3. **Коллизии:** Помните, что смена текстуры не влияет на маску коллизий. Если новая текстура должна изменить физику уровня (например, сделать проходимыми ранее твёрдые тайлы), вам потребуется обновить вызовы setCollision или setTileIndexCallback.
4. **Камера:** В примере камера следует за игроком и ограничена размерами карты — это стандартный подход для top-down игр.
Что попробовать дальше
Динамическая замена тайлсетов в Phaser — мощный и не ресурсоёмкий приём для оживления игрового мира. Он позволяет создавать динамические окружения, менять атмосферу уровня и реализовывать визуальные эффекты без дублирования данных карты. Для экспериментов попробуйте: добавить плавный переход между тайлсетами через шейдеры, привязать смену текстур к состоянию игрока (например, низкому здоровью) или создать систему "раскрашивания" уровня, где игрок активирует разные текстуры для разных зон.
