О чем этот пример
Работа с тайловыми картами — основа многих 2D-игр. Метод `copy()` объекта Tilemap в Phaser кажется простым инструментом для перемещения областей тайлов, но в его поведении есть тонкость, которая может привести к неочевидным багам. Эта статья разбирает конкретный пример из репозитория Phaser, объясняя, почему при копировании более половины карты результат может быть не таким, как вы ожидаете. Понимание этой особенности сэкономит часы отладки при реализации механик, связанных с динамическим изменением уровня.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/tiles/kenny_ground_64x64.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/copy-test.json');
}
create ()
{
const map = this.add.tilemap('map');
const tileset = map.addTilesetImage('ground', 'tiles');
const layer = map.createLayer('Tile Layer 1', tileset);
this.input.once('pointerdown', pointer => {
// When trying to copy more then half the map tilemap.copy() does not work as expected. It works in one direction but not the other.
// If the map is 5x5 and you want to move columns 2-5 to the left one it works fine. If you want to move columns 1-4 to the right one it just replicates column 1 over and over.
// This happens when you pass the half way point on a map. So on an 11x11 moving tiles 1-5 to over to column 6 works perfectly fine. Moving columns 1-6 over to column 5 is when it starts to mess up.
// Map is 12x12
// Works
// map.copy(0, 0, 6, 12, 6, 0);
map.copy(0, 0, 26, 3, 3, 4);
});
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Суть проблемы: копирование "через середину"
Исходный код демонстрирует баг (issue #6188). Комментарии разработчика четко описывают проблему: при попытке скопировать более половины ширины (или высоты) карты в определенном направлении, метод copy() ведет себя некорректно.
Например, для карты 5x5: * Копирование колонок 2-5 на одну позицию влево работает правильно. * Копирование колонок 1-4 на одну позицию вправо приводит к артефактам — первая колонка начинает реплицироваться.
Проблема возникает, когда область копирования пересекает условную "середину" карты в направлении перемещения. Давайте разберем сигнатуру метода, чтобы понять причину.
Разбираем параметры метода `tilemap.copy()`
Метод copy() используется для копирования прямоугольной области тайлов внутри одного слоя карты. Его ключевые параметры:
map.copy(sourceX, sourceY, width, height, destX, destY);
* sourceX, sourceY — координаты верхнего левого угла области-источника в тайлах.
* width, height — размеры копируемой области в тайлах.
* destX, destY — координаты верхнего левого угла, куда будет вставлена скопированная область.
В примере используется вызов:
map.copy(0, 0, 26, 3, 3, 4);
Это означает: "Скопировать область размером 26x3 тайла, начиная с координат (0,0), и вставить ее, начиная с координат (3,4)". Учитывая, что карта в примере имеет размер 12x12, ширина копируемой области (26 тайлов) явно превышает ширину самой карты. Это важная деталь.
Анализ проблемного вызова в контексте
Карта в JSON-файле имеет размер 12x12. Посмотрим на проблемную строку кода:
// map.copy(0, 0, 26, 3, 3, 4);
Здесь width установлено в 26, что больше ширины карты (12). Согласно документации Phaser, если область копирования выходит за границы слоя, она будет обрезана (clamped) до этих границ. Фактически, будет скопирована область с (0,0) до (12,3).
Далее происходит вставка этой обрезанной области в позицию (3,4). Ключевая проблема, описанная в баге, заключается во внутренней логике копирования. Когда целевая область (destX) находится левее, чем середина скопированных данных (относительно источника), алгоритм может работать с данными в неправильном порядке, приводя к дублированию.
В рабочем варианте, который закомментирован, такого не происходит:
// map.copy(0, 0, 6, 12, 6, 0);
Здесь копируется ровно половина карты (6 из 12 колонок) и вставляется со смещением 6. Это "безопасный" сценарий, не вызывающий конфликта в алгоритме.
Практическое правило и обходной путь
Основное правило: избегайте использования copy() для перемещения областей, которые больше половины размера карты *в направлении копирования*, если целевая точка (destX или destY) ведет к наложению исходной и целевой областей "через середину".
Если вам необходимо реализовать подобную логику (например, скроллинг огромной карты), рассмотрите обходной путь:
1. Копируйте данные частями, не превышающими половину размера.
2. Или используйте прямое манипулирование данными тайлового слоя через его массив data, предварительно скопировав нужные значения во временный буфер.
Пример безопасного копирования левой половины карты:
// Копируем колонки 0-5 (всего 6 колонок) в позицию 6
map.copy(0, 0, 6, map.height, 6, 0);
Перед сложными операциями всегда проверяйте границы:
const sourceWidth = 10;
const destX = 3;
if (sourceWidth > map.width / 2) {
console.warn('Копирование более половины карты может вызвать артефакты.');
}
Что попробовать дальше
Метод tilemap.copy() — мощный, но требующий аккуратности инструмент. Его внутренняя оптимизация даёт сбой при работе с большими областями, что важно учитывать при проектировании динамических уровней. Для экспериментов попробуйте
- Визуализировать границы копируемой области перед операцией
- Реализовать функцию плавного скроллинга тайловой карты, разбивая большое перемещение на несколько безопасных вызовов
copy() - Сравнить производительность
copy()с ручным циклом по массивуlayer.dataдля сложных случаев
