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

При разработке игр с большими тайловыми картами критически важно, чтобы движок отсекал (culled) невидимые тайлы, повышая производительность. Однако, когда вы начинаете применять смещения слоёв, факторы прокрутки, масштабирование или работать с картами из тайлов разного размера, логика отсечения может дать сбой, и на экране появятся артефакты. Эта статья разбирает официальный тестовый пример Phaser, который наглядно проверяет корректность отсечения тайлов в различных сложных сценариях. Вы научитесь создавать аналогичные тесты для своих проектов, чтобы быть уверенными в стабильности графики и эффективности рендеринга.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


var config = {
    type: Phaser.WEBGL,
    width: 1000,
    height: 800,
    backgroundColor: '#2d2d88',
    parent: 'phaser-example',
    pixelArt: true,
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};

var totalTests = 0;
var testsPassed = 0;
var assert = (message, condition) => {
    totalTests++;
    if (condition) testsPassed++;
    console.assert(condition, message)
};

var game = new Phaser.Game(config);

function preload() {
    
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.tilemapTiledJSON('desert', 'assets/tilemaps/maps/desert.json');
    this.load.image('desert-tiles', 'assets/tilemaps/tiles/tmw_desert_spacing.png');

    this.load.tilemapTiledJSON('mario', 'assets/tilemaps/maps/super-mario.json');
    this.load.image('SuperMarioBros-World1-1', 'assets/tilemaps/tiles/super-mario.png');

    this.load.tilemapTiledJSON('features-test', 'assets/tilemaps/maps/features-test.json');
    this.load.image('ground_1x1', 'assets/tilemaps/tiles/ground_1x1.png');
    this.load.image('dangerous-kiss', 'assets/tilemaps/tiles/dangerous-kiss.png');
    this.load.image('walls_1x2', 'assets/tilemaps/tiles/walls_1x2.png');
    this.load.image('tiles2', 'assets/tilemaps/tiles/tiles2.png');
}

function create() {
    // Visual test to make sure tiles are culled properly when factoring in:
    // - Layer position
    // - Scroll factor
    // - Layer scale
    // - Maps that have multiple tilesizes

    // Static map with offset, scroll factor & scale
    var map = this.make.tilemap({ key: 'desert' });
    var tiles = map.addTilesetImage('Desert', 'desert-tiles', 32, 32, 1, 1);
    var layer = map.createLayer(0, tiles, -300, -400);
    layer.setScrollFactor(0.25);

    // Dynamic map with offset, scroll factor & scale
    var map = this.make.tilemap({ key: 'mario' });
    var tiles = map.addTilesetImage('SuperMarioBros-World1-1');
    var layer = map.createLayer(0, tiles, 50, -25);
    layer.setScrollFactor(1);
    layer.setScale(2, 0.5);

    // Map with multiple tileset sizes
    var map = this.make.tilemap({ key: 'features-test' });
    var ground_1x1 = map.addTilesetImage('ground_1x1');
    var tiles2 = map.addTilesetImage('tiles2');
    var dangerousTiles = map.addTilesetImage('dangerous-kiss');
    var layer = map.createLayer('Tile Layer 1', ground_1x1, 0, 300);
    var layer2 = map.createLayer('Offset Tile Layer', tiles2, 0, 300);
    var layer3 = map.createLayer('Small Tile Layer', dangerousTiles, 300, 300).setScrollFactor(0.5);

    var cursors = this.input.keyboard.createCursorKeys();
    var controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        speed: 0.5
    };
    controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
}

function update (time, delta)
{
    controls.update(delta);
}

Суть примера: зачем нужен визуальный тест

Исходный код представляет собой не игру, а инструмент для проверки (visual test). Его цель — убедиться, что система рендеринга тайловых карт Phaser корректно определяет, какие тайлы находятся в области видимости камеры, а какие можно не отрисовывать (отсечь). Это становится нетривиальной задачей, когда к слоям применяются трансформации.

В коде создаётся несколько карт с разными параметрами, а затем с помощью клавиатуры можно перемещать камеру, наблюдая, не появляются ли графические артефакты (например, внезапное исчезновение или появление тайлов не там, где нужно). Для автоматизации проверки в коде есть простая функция assert, подсчитывающая пройденные тесты, хотя в данном примере она не вызывается — проверка проводится вручную, "на глаз".

Ключевые сложные случаи, которые проверяет этот тест: * Позиция слоя (offset) * Коэффициент прокрутки слоя (scrollFactor) * Масштаб слоя (scale) * Карты, составленные из тайловых наборов (tilesets) с разным размером тайлов.

Настройка сцены и загрузка ресурсов

Конфигурация игры стандартна, но важно использовать Phaser.WEBGL для доступа к полному функционалу рендерера. Также включен режим pixelArt: true, что влияет на текстурирование.

В функции preload загружаются три разные карты в формате Tiled JSON и соответствующие им изображения тайловых наборов. Обратите внимание на разные пути и имена. Phaser позволяет загружать несколько карт асинхронно перед началом сцены.

var config = {
    type: Phaser.WEBGL,
    width: 1000,
    height: 800,
    backgroundColor: '#2d2d88',
    parent: 'phaser-example',
    pixelArt: true,
    scene: {
        preload: preload,
        create: create,
        update: update
    }
};
function preload() {
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.tilemapTiledJSON('desert', 'assets/tilemaps/maps/desert.json');
    this.load.image('desert-tiles', 'assets/tilemaps/tiles/tmw_desert_spacing.png');
    // ... загрузка других карт и изображений
}

Создание слоёв с трансформациями (функция create)

В функции create создаются три тайловые карты, демонстрирующие разные аспекты, которые могут сломать отсечение.

**1. Статичная карта со смещением и фактором прокрутки:** Создаётся карта 'desert'. Слой смещается на (-300, -400), а scrollFactor устанавливается в 0.25. Это значит, что слой будет прокручиваться в 4 раза медленнее камеры, создавая эффект параллакса. Движок должен корректно вычислять видимую область с учётом этого коэффициента.

var map = this.make.tilemap({ key: 'desert' });
var tiles = map.addTilesetImage('Desert', 'desert-tiles', 32, 32, 1, 1);
var layer = map.createLayer(0, tiles, -300, -400);
layer.setScrollFactor(0.25);

**2. Динамическая карта с масштабированием:** Создаётся карта 'mario'. Её слой имеет начальное смещение (50, -25), полный scrollFactor (1) и неоднородный масштаб: setScale(2, 0.5) (растяжение по X в 2 раза, сжатие по Y в 2 раза). Неравномерное масштабирование — особенно сложный случай для расчёта границ.

var map = this.make.tilemap({ key: 'mario' });
var tiles = map.addTilesetImage('SuperMarioBros-World1-1');
var layer = map.createLayer(0, tiles, 50, -25);
layer.setScrollFactor(1);
layer.setScale(2, 0.5);

**3. Карта с тайлами разного размера:** Карта 'features-test' использует несколько тайлсетов. Обратите внимание, что для ground_1x1 не указаны размеры тайла в addTilesetImage, так как они берутся из данных JSON. Слои создаются на разных горизонтальных позициях (X: 0 и 300), а у одного из них снова задан scrollFactor (0.5). Движок должен корректно обрабатывать смешивание тайлов разного размера в одном слое.

var map = this.make.tilemap({ key: 'features-test' });
var ground_1x1 = map.addTilesetImage('ground_1x1');
var tiles2 = map.addTilesetImage('tiles2');
var dangerousTiles = map.addTilesetImage('dangerous-kiss');
var layer = map.createLayer('Tile Layer 1', ground_1x1, 0, 300);
var layer2 = map.createLayer('Offset Tile Layer', tiles2, 0, 300);
var layer3 = map.createLayer('Small Tile Layer', dangerousTiles, 300, 300).setScrollFactor(0.5);

Управление камерой для интерактивной проверки

Чтобы проверить отсечение в динамике, нужно иметь возможность перемещать камеру. В примере для этого используется система управления Phaser.Cameras.Controls.FixedKeyControl.

Сначала создаётся объект курсоров из клавиатуры. Затем конфигурационный объект controlConfig связывает эти клавиши с основной камерой (this.cameras.main) и задаёт скорость перемещения. Созданный экземпляр controls обновляется каждый кадр в функции update, что позволяет плавно двигать камеру стрелками.

var cursors = this.input.keyboard.createCursorKeys();
var controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    speed: 0.5
};
controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
function update (time, delta) {
    controls.update(delta);
}

Теперь, запустив пример и двигая камеру, вы можете визуально оценить, нет ли рывков, мигания тайлов или других аномалий на стыках слоёв с разными параметрами. Корректная работа — это плавное и предсказуемое появление тайлов из-за краёв экрана.

Что попробовать дальше

Данный тестовый пример — отличный шаблон для проверки рендеринга ваших собственных сложных тайловых сцен в Phaser. Он наглядно показывает, какие комбинации трансформаций требуют пристального внимания. Для экспериментов попробуйте: изменить scrollFactor на отрицательные значения, применить вращение к слою (через setRotation), или добавить в одну карту тайлсеты с сильно отличающимися размерами. Также вы можете расширить встроенную функцию assert, добавляя проверки на конкретные координаты тайлов, чтобы автоматизировать тестирование и интегрировать его в pipeline разработки.