О чем этот пример
При импорте тайловых карт из редактора Tiled или при использовании кастомных тайлсетов разработчики часто сталкиваются с проблемой некорректного отображения текстур. Тайлы "съезжают", отображается не тот фрагмент изображения, или появляются артефакты. Чаще всего причина кроется в неучтённых отступах (spacing) и полях (margin) внутри самого файла тайлсета. Эта статья на практическом примере покажет, как правильно передавать параметры `tileMargin` и `tileSpacing` в методы Phaser при загрузке тайлсетов из разных источников. Вы научитесь корректно работать как с картами из Tiled, так и с тайлсетами, загруженными отдельно, что избавит вас от часов отладки визуальных багов.
Версия 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.tilemapTiledJSON('map', 'assets/tilemaps/maps/desert.json');
// this.load.spritesheet('Desert', 'assets/tilemaps/tiles/tmw_desert_spacing.png', {frameWidth:32, frameHeight:32, spacing:1 , margin: 1});
this.load.image('Desert', 'assets/tilemaps/tiles/tmw_desert_spacing.png');
this.load.image('drawtiles-spaced', 'assets/tilemaps/tiles/drawtiles-spaced.png');
}
create ()
{
// 1 - Tilesets loaded from Tiled will having the spacing properties already set
const map = this.make.tilemap({ key: 'map' });
// console.log('addTilesetImage desert');
// const tiles = map.addTilesetImage('Desert');
const tiles = map.addTilesetImage('Desert', null, 32, 32, 1, 1);
// console.log('create layer');
// const layer = map.createLayer(0, 'Desert', 0, 0);
const layer = map.createLayer(0, tiles, 0, 0);
layer.setScrollFactor(0.5);
layer.setAlpha(0.75);
// 2 - Tilesets loaded outside of a Tiled JSON file will need the spacing properties set (if
// your tileset has a margin or padding)
const level = [
[ 2, 2, 2, 2 ],
[ 2, 2, 2, 2 ],
[ 2, 2, 2, 2 ],
[ 2, 2, 2, 2 ]
];
const map2 = this.make.tilemap({ tileWidth: 32, tileHeight: 32, data: level });
// addTilesetImage Parameters:
// tilesetName, key, tileWidth, tileHeight, tileMargin, tileSpacing
const tiles2 = map2.addTilesetImage('drawtiles-spaced', null, 32, 32, 1, 2);
const layer2 = map2.createLayer(0, tiles2, 200, 200);
layer2.setScale(2);
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 config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Проблема: откуда берутся spacing и margin?
Многие графические редакторы для тайлов, включая популярный Tiled, при экспорте листа тайлов добавляют между ними небольшие отступы (spacing) и поля по краям самого изображения (margin). Это сделано для предотвращения артефактов наложения текстур (texture bleeding) при рендеринге.
Phaser должен знать об этих отступах, чтобы правильно «нарезать» большое изображение на отдельные тайлы. Если параметры не указаны, движок будет делить картинку, предполагая, что тайлы идут вплотную друг к другу, что приведёт к смещению и ошибкам.
В исходном коде примера закомментирована строка, которая загружает тайлсет как спрайтшит с явным указанием параметров:
// this.load.spritesheet('Desert', 'assets/tilemaps/tiles/tmw_desert_spacing.png', {frameWidth:32, frameHeight:32, spacing:1 , margin: 1});
Вместо этого используется простая загрузка изображения, поэтому параметры нарезки нужно будет передать позже.
Способ 1: Тайлсеты из Tiled JSON
Когда вы загружаете карту в формате Tiled JSON через load.tilemapTiledJSON, информация об отступах уже содержится внутри файла карты. Phaser считывает её автоматически при создании тайлсета через map.addTilesetImage('Desert').
Однако в примере показан альтернативный, более явный способ, где параметры передаются вручную. Это полезно для понимания процесса или если автоматическое чтение по какой-то причине не сработало.
Создаём карту и тайлсет:
const map = this.make.tilemap({ key: 'map' });
const tiles = map.addTilesetImage('Desert', null, 32, 32, 1, 1);
Здесь:
- 'Desert' — имя тайлсета, как оно указано в JSON-файле карты.
- null — ключ изображения (если он совпадает с именем тайлсета, можно передать null).
- 32, 32 — ширина и высота одного тайла.
- 1, 1 — tileMargin и tileSpacing соответственно.
После этого создаём слой, используя созданный объект тайлсета tiles:
const layer = map.createLayer(0, tiles, 0, 0);
Способ 2: Внешние тайлсеты и динамические карты
Ситуация меняется, когда вы не используете готовую карту из Tiled, а создаёте её динамически из массива данных или используете тайлсет, загруженный отдельно от карты. В этом случае Phaser неоткуда узнать параметры отступов, и их нужно указать явно.
В примере создаётся простая карта 4x4 из массива level:
const map2 = this.make.tilemap({ tileWidth: 32, tileHeight: 32, data: level });
Ключевой момент — добавление тайлсета с указанием всех параметров. Для тайлсета drawtiles-spaced указан spacing 2 (отступ между тайлами) и margin 1 (поле по краям изображения):
const tiles2 = map2.addTilesetImage('drawtiles-spaced', null, 32, 32, 1, 2);
Обратите внимание на порядок аргументов в addTilesetImage: tilesetName, key, tileWidth, tileHeight, tileMargin, tileSpacing. Именно такой порядок используется при вызове метода с более чем двумя аргументами.
После этого слой создаётся стандартно и масштабируется для наглядности:
const layer2 = map2.createLayer(0, tiles2, 200, 200);
layer2.setScale(2);
Параметры `addTilesetImage`: разбираем сигнатуры
Метод Tilemap.addTilesetImage имеет несколько вариантов вызова, что может вызвать путаницу. Давайте их систематизируем на основе примера и документации Phaser.
1. **Автоматический вариант (для карт из Tiled):** Phaser сам читает параметры из JSON.
const tiles = map.addTilesetImage('Desert');
2. **Явное указание ключа изображения:** Если ключ загрузки изображения не совпадает с именем тайлсета в JSON.
const tiles = map.addTilesetImage('Desert', 'myDesertTextureKey');
3. **Полный контроль (используется в примере):** Ручная передача всех параметров. Это единственный способ указать margin и spacing для тайлсета, загруженного отдельно.
const tiles = map.addTilesetImage(tilesetName, textureKey, tileWidth, tileHeight, tileMargin, tileSpacing);
Важно: если вы передаёте больше двух аргументов, движок ожидает именно такую полную сигнатуру. Параметры tileMargin и tileSpacing по умолчанию равны 0.
Отладка и практические советы
Как определить, что проблема именно в отступах? Если тайлы отображаются со смещением, «захватывают» края соседних тайлов или графический слой выглядит «рваным», в первую очередь проверьте эти параметры.
1. **Где взять значения?** Откройте файл тайлсета в редакторе Tiled. В свойствах тайлсета (Tile Set) будут поля "Spacing" и "Margin". Именно эти числа нужно передавать в Phaser.
2. **Проверка загрузки.** Вместо this.load.image для тайлсетов с отступами можно использовать this.load.spritesheet, указав параметры сразу при загрузке, как в закомментированной строке примера. Это может быть более удобно.
3. **Пиксель-арт.** Обратите внимание на конфигурацию игры в примере:
pixelArt: true,
Этот параметр включает специальный режим рендеринга для пиксельной графики, который также влияет на текстурное наложение. Всегда включайте его для ретро-стилистики.
Что попробовать дальше
Корректная работа с тайлсетами в Phaser требует внимания к деталям, таким как margin и spacing. Главное правило: если вы загружаете карту из Tiled JSON, Phaser попытается сделать всё за вас. Если же вы создаёте карту вручную или используете отдельный файл тайлов — всегда явно передавайте параметры отступов в addTilesetImage.
Для экспериментов попробуйте:
- Изменить значения tileMargin и tileSpacing в примере на 0 и наблюдать за артефактами.
- Создать собственный тайлсет в Tiled с разными значениями отступов и экспортировать его для использования в Phaser.
- Использовать load.spritesheet с параметрами для загрузки тайлсета и затем передавать в addTilesetImage только имя и ключ, проверяя, считываются ли параметры автоматически.
