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

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

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

Живой запуск

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

Исходный код


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

var controls;
var layer;
var game = new Phaser.Game(config);

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('tiles', 'assets/tilemaps/iso/iso-64x64-outside.png');
}


function fillLayer() {
    for (let i = 0; i < 4; i++) {
      for (let j = 0; j < 4; j++) {
        layer.putTileAt(0, i, j);
      }
    }
  }

function create ()
{
    var map = this.make.tilemap({ tileWidth: 64, tileHeight: 64  });
    map.orientation = 1;

    var tileset1 = map.addTilesetImage('iso-64x64-outside', 'tiles');

    layer = map.createBlankLayer('ground', tileset1);

    // layer.setSkipCull(true);

    console.log(layer);

    var cursors = this.input.keyboard.createCursorKeys();

    // this.cameras.main.setScroll(-340, -100);

    fillLayer();

    var controlConfig = {
        camera: this.cameras.main,
        left: cursors.left,
        right: cursors.right,
        up: cursors.up,
        down: cursors.down,
        acceleration: 0.04,
        drag: 0.0005,
        maxSpeed: 0.7
    };

    controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}

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

Суть проблемы: кадрирование изометрических слоёв

Phaser активно использует кадрирование — технику оптимизации, при которой отрисовываются только объекты, попадающие в область видимости камеры. Для ортогональных карт это работает превосходно. Однако для изометрических (orientation: 1) вычисление видимой области усложняется из-за перспективы.

В некоторых версиях движка алгоритм кадрирования для изометрии может работать некорректно, "обрезая" тайлы, которые должны быть видны. Это приводит к их внезапному исчезновению при движении камеры. В предоставленном примере эта проблема моделируется, даже для маленькой карты 4x4 тайла.

Анализ ключевых строк кода

Давайте посмотрим, как создаётся изометрическая сцена в примере. Ключевой момент — конфигурация карты и слоя.

var map = this.make.tilemap({ tileWidth: 64, tileHeight: 64 });
map.orientation = 1;

Установка orientation в `1` переводит карту в изометрический режим.

layer = map.createBlankLayer('ground', tileset1);
// layer.setSkipCull(true);

Здесь создаётся слой. Закомментированная строка layer.setSkipCull(true) — это и есть решение проблемы. По умолчанию этот метод не вызван, и кадрирование активно.

Функция fillLayer просто заполняет слой тайлами с индексом 0 (первый тайл из tileset).

var controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    acceleration: 0.04,
    drag: 0.0005,
    maxSpeed: 0.7
};
controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

SmoothedKeyControl добавляет плавное управление камерой с помощью стрелок клавиатуры, что позволяет легко продемонстрировать проблему при движении.

Решение: метод setSkipCull

Phaser предоставляет прямой способ отключить кадрирование для конкретного тайлового слоя. Это метод setSkipCull.

layer.setSkipCull(true);

Вызов этого метода с аргументом true указывает движку не применять оптимизацию кадрирования к данному слою. Все тайлы на слое будут отрисовываться всегда, независимо от положения камеры. Это полностью решает проблему с исчезающими тайлами в изометрической проекции.

Важное замечание: используйте этот метод осознанно. Для очень больших карт отключение кадрирования может привести к падению производительности, так как движок будет пытаться отрисовать сотни или тысячи невидимых тайлов. Однако для карт умеренного размера или в качестве временного решения для бага — это идеальный вариант.

Где и когда вызывать setSkipCull

Метод следует вызывать сразу после создания слоя, до его заполнения данными. Это гарантирует, что корректное состояние будет установлено до любой отрисовки.

В контексте нашего примера, нужно раскомментировать одну строку в функции create:

function create ()
{
    // ... создание map и tileset ...
    layer = map.createBlankLayer('ground', tileset1);
    layer.setSkipCull(true); // <-- Вот эта строка должна быть активна
    // ... остальной код ...
}

После этого можно запустить пример и убедиться, что при плавной прокрутке камеры с помощью стрелок все 16 тайлов остаются на экране и не исчезают.

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

Метод layer.setSkipCull(true) — это простое и эффективное решение для бага с кадрированием в изометрических слоях Phaser. Оно позволяет сосредоточиться на разработке геймплея, не отвлекаясь на визуальные артефакты. **Идеи для экспериментов:** 1. Создайте большую изометрическую карту и сравните производительность со включённым и выключенным кадрированием. 2. Попробуйте применить setSkipCull только к определённым слоям (например, к фону), оставив кадрирование для слоёв с объектами. 3. Исследуйте другие свойства слоя, такие как layer.cullPaddingX и layer.cullPaddingY, которые могут помочь настроить область кадрирования более точно.