О чем этот пример
При создании тайлсетов из кадров анимации, упакованных в атлас, разработчики часто сталкиваются с неожиданным поведением: между тайлами появляются артефакты в виде лишних пикселей. Это происходит из-за автоматической обрезки прозрачных областей спрайтов при создании атласа. В этой статье мы разберем, как Phaser работает с обрезанными кадрами, почему это вызывает проблемы в тайлсетах и как их решить, используя правильные методы загрузки и создания спрайтшитов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene {
preload() {
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.path = 'assets/atlas/trimsheet/';
this.load.atlas('testanims', 'trimsheet.png', 'trimsheet.json');
}
create() {
const t1 = this.textures.addSpriteSheetFromAtlas('boom1', {
atlas: 'testanims',
frame: 'explosion-notrim',
frameWidth: 64,
frameHeight: 64,
endFrame: 23
});
for (let i = 0; i < 10; i++) this.add.image(32 + 65 * i, 200, 'boom1', i);
const map = this.make.tilemap({ data: [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], tileWidth: 64, tileHeight: 64 });
const tiles = map.addTilesetImage('boom1');
const layer = map.createLayer(0, tiles, 0, 0);
console.log(tiles);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Проблема: артефакты в тайлсете из-за обрезки
В исходном примере загружается атлас testanims, содержащий кадры анимации взрыва. Ключевой кадр explosion-notrim используется для создания спрайтшита.
this.load.atlas('testanims', 'trimsheet.png', 'trimsheet.json');
Проблема возникает из-за того, что многие инструменты для создания атласов (например, Texture Packer) по умолчанию обрезают прозрачные пиксели по краям каждого кадра для экономии памяти. Это приводит к тому, что физические размеры кадра в атласе (sourceSize) могут не соответствовать его визуальным размерам (spriteSourceSize).
При создании тайлсета Phaser ожидает, что все кадры имеют одинаковый и полный размер (frameWidth, frameHeight), но обрезанные кадры этого не гарантируют. В результате тайлы "съезжают", и между ними появляются фрагменты соседних кадров.
Создание спрайтшита из атласа: метод `addSpriteSheetFromAtlas`
Для создания отдельного спрайтшита из одного кадра атласа используется метод this.textures.addSpriteSheetFromAtlas. Это мощный инструмент, который "распаковывает" анимацию, хранящуюся в одном кадре атласа, в последовательность отдельных текстур.
const t1 = this.textures.addSpriteSheetFromAtlas('boom1', {
atlas: 'testanims',
frame: 'explosion-notrim',
frameWidth: 64,
frameHeight: 64,
endFrame: 23
});
Здесь:
- atlas: ключ загруженного атласа.
- frame: имя конкретного кадра внутри атласа, который содержит спрайтшит (в данном случае, это один большой изображение, где все кадры анимации расположены в ряд).
- frameWidth и frameHeight: ожидаемые размеры одного кадра в спрайтшите.
- endFrame: индекс последнего кадра для извлечения (всего будет извлечено 24 кадра, от 0 до 23).
**Важно:** Этот метод не учитывает внутреннюю обрезку (trim) кадров, если она была применена при создании атласа. Он предполагает, что переданный frame — это плотно упакованный спрайтшит, где каждый кадр имеет ровно размер frameWidth x frameHeight.
Создание тайлсета и карты тайлов
После создания текстуры boom1 ее можно использовать как тайлсет. Сначала создается объект тайловой карты (Tilemap), который определяет сетку и расстановку тайлов.
const map = this.make.tilemap({
data: [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]],
tileWidth: 64,
tileHeight: 64
});
Параметр data — это двумерный массив, где числа — это индексы тайлов в тайлсете. Далее создается сам тайлсет на основе нашей текстуры и связывается с картой.
const tiles = map.addTilesetImage('boom1');
Затем создается слой (TilemapLayer), который визуализирует карту, используя этот тайлсет.
const layer = map.createLayer(0, tiles, 0, 0);
Если кадры в исходном атласе были обрезаны, то на этом этапе и проявятся артефакты: тайлы будут иметь смещенные координаты внутри текстуры boom1.
Решение: использование необрезанных кадров и проверка данных
Чтобы избежать проблемы, необходимо убедиться, что кадр, из которого создается спрайтшит, не был обрезан при упаковке в атлас. В имени кадра в примере есть подсказка: explosion-notrim.
1. **Подготовка ассетов:** При создании атласа в настройках инструмента упаковки отключите обрезку (Trim) для того кадра, который планируется использовать как источник для спрайтшита. Или используйте отдельную, необрезанную текстуру.
2. **Проверка JSON атласа:** Откройте файл trimsheet.json и найдите описание кадра explosion-notrim. Убедитесь, что значения trimmed равно false, а spriteSourceSize имеет координаты x:0, y:0 и размер, равный sourceSize.
Пример корректной структуры кадра в JSON:
"frames": {
"explosion-notrim": {
"frame": {"x":0,"y":0,"w":1536,"h":64}, // Область в атласе
"rotated": false,
"trimmed": false, // Ключевой параметр: обрезка отключена
"spriteSourceSize": {"x":0,"y":0,"w":1536,"h":64},
"sourceSize": {"w":1536,"h":64}
}
}
Если trimmed равно true, то spriteSourceSize будет указывать на смещение, что и ломает тайлсет.
Что попробовать дальше
Проблема с артефактами в тайлсетах, созданных из атласов, возникает из-за конфликта между оптимизацией (обрезкой спрайтов) и предположением о регулярной сетке кадров. Решение — использовать для создания спрайтшитов только необрезанные (trimmed: false) кадры атласа. Для экспериментов попробуйте
- создать атлас с двумя версиями одного спрайтшита — обрезанной и необрезанной
- вручную скорректировать параметры
frameWidthиframeHeightвaddSpriteSheetFromAtlas, если обрезку нельзя отключить - использовать отдельную текстуру для тайлсетов, не упаковывая ее в атлас с другими спрайтами
