О чем этот пример

При создании тайлсетов из кадров анимации, упакованных в атлас, разработчики часто сталкиваются с неожиданным поведением: между тайлами появляются артефакты в виде лишних пикселей. Это происходит из-за автоматической обрезки прозрачных областей спрайтов при создании атласа. В этой статье мы разберем, как 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) кадры атласа. Для экспериментов попробуйте

  1. создать атлас с двумя версиями одного спрайтшита — обрезанной и необрезанной
  2. вручную скорректировать параметры frameWidth и frameHeight в addSpriteSheetFromAtlas, если обрезку нельзя отключить
  3. использовать отдельную текстуру для тайлсетов, не упаковывая ее в атлас с другими спрайтами