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

При работе со сложными сценами в Phaser разработчики часто используют контейнеры для группировки игровых объектов. Однако при добавлении Spine-персонажей в контейнеры может возникнуть неочевидное поведение с управлением видимостью. В этой статье мы разберем конкретный пример и объясним, почему простое свойство `visible` не всегда работает ожидаемо с контейнеризованными объектами. Понимание этой механики поможет избежать багов в отображении анимаций и даст более точный контроль над визуальной частью вашей игры.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super({
            pack: {
                files: [
                    { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/3.8.95/SpinePluginDebug.js', sceneKey: 'spine' }
                ]
            }
        });
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('logo', 'assets/sprites/phaser.png');

        this.load.setPath('assets/spine/3.8/demos/');

        this.load.spine('set1', 'demos.json', [ 'atlas1.atlas' ], true);
    }

    create ()
    {
        let boy1 = this.add.spine(120, 400, 'set1.spineboy', 'idle', true).setScale(0.5);
        boy1.drawDebug = true;

        let boy2 = this.add.spine(400, 400, 'set1.spineboy', 'idle', true).setScale(0.5);

        let image1 = this.add.image(200, 450, 'logo').setOrigin(0);

        let container = this.add.container();

        container.add([ boy1, boy2, image1 ]);

        boy2.visible = false;
    }
}

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

const game = new Phaser.Game(config);

Что делает пример

В данном примере создается сцена с двумя Spine-персонажами (Spineboy), одним изображением и контейнером. Все три визуальных объекта добавляются в контейнер, после чего у второго персонажа свойство visible устанавливается в false.

На первый взгляд кажется, что второй Spineboy должен стать невидимым. Однако при запуске примера мы видим, что невидимым становится только первый персонаж, хотя свойство visible менялось у второго. Это демонстрирует важную особенность работы контейнеров и Spine-объектов.

boy2.visible = false;

Анализ кода инициализации

Пример начинается с настройки плагина Spine и загрузки ресурсов. Обратите внимание на параметры метода load.spine.

this.load.spine('set1', 'demos.json', [ 'atlas1.atlas' ], true);

Здесь: - 'set1' — ключ для доступа к данным Spine. - 'demos.json' — файл с данными скелетной анимации. - [ 'atlas1.atlas' ] — атлас текстур. - true — указывает, что загрузка должна быть выполнена с использованием XHR, что необходимо для кросс-доменной загрузки в данном примере.

Создание объектов происходит в методе create. Spine-объекты создаются через фабрику this.add.spine.

let boy1 = this.add.spine(120, 400, 'set1.spineboy', 'idle', true).setScale(0.5);
let boy2 = this.add.spine(400, 400, 'set1.spineboy', 'idle', true).setScale(0.5);

Параметры: 1. X-координата. 2. Y-координата. 3. Ключ ресурса с префиксом ('set1.spineboy'). 4. Название анимации по умолчанию ('idle'). 5. Флаг looptrue для зацикливания анимации.

У первого объекта включена отладка: boy1.drawDebug = true, что визуализирует его кости и полигоны.

Почему видимость работает не так, как ожидается

Ключевой момент — добавление объектов в контейнер.

let container = this.add.container();
container.add([ boy1, boy2, image1 ]);

Контейнер в Phaser — это объект, который сам управляет трансформациями (позиция, масштаб, поворот) и видимостью своих дочерних элементов. Когда вы устанавливаете свойство container.visible = false, все его дети также становятся невидимыми.

Однако в этом примере мы меняем свойство у дочернего элемента (boy2). Проблема возникает из-за внутренней реализации отрисовки Spine-объектов, особенно когда включен режим drawDebug. В некоторых версиях плагина или при определенных условиях (как в данном примере с багом) изменение свойства visible у отдельного Spine-ребенка внутри контейнера может некорректно взаимодействовать с системой рендеринга контейнера.

Фактически, контейнер может «перехватывать» или сбрасывать состояние видимости своих детей на каждом кадре, основываясь на своем внутреннем состоянии, что приводит к неожиданному результату.

Практическое решение и обходные пути

Как же правильно управлять видимостью объектов внутри контейнера?

1. **Управление через контейнер (не подходит для выборочного скрытия):** Сделать невидимым весь контейнер.

container.visible = false;

2. **Управление через свойство alpha:** Часто более надежный способ для плавного или выборочного скрытия.

boy2.alpha = 0;
    // Или для полного управления:
    boy2.setVisible(false); // Метод может быть надежнее прямого присваивания

Однако в контексте данного конкретного бага с контейнером, alpha также может вести себя неочевидно.

3. **Извлечение объекта из контейнера:** Если объект должен управляться независимо, его не следует добавлять в контейнер.

// Не добавляем boy2 в контейнер
    container.add([ boy1, image1 ]);
    // Теперь boy2 существует отдельно, и его видимостью можно управлять напрямую
    boy2.visible = false;

4. **Использование групп (Phaser.GameObjects.Group) для логической группировки** без влияния на свойства видимости.

let myGroup = this.add.group();
    myGroup.addMultiple([boy1, boy2, image1]);
    // Видимостью каждого объекта в группе можно управлять независимо
    boy2.visible = false;

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

Данный пример наглядно демонстрирует, что взаимодействие контейнеров Phaser со сложными объектами, такими как Spine-анимации, может иметь тонкости. Прямое присваивание свойства visible дочернему объекту внутри контейнера не всегда гарантирует ожидаемый результат из-за внутреннего механизма обновления контейнера. **Идеи для экспериментов:** 1. Проверьте, как ведет себя свойство visible у других типов объектов (спрайты, текст) внутри этого же контейнера. 2. Попробуйте отключить режим drawDebug у первого персонажа и посмотрите, изменится ли поведение. 3. Создайте иерархию из нескольких вложенных контейнеров и попробуйте управлять видимостью на разных уровнях. 4. Изучите, влияет ли на поведение порядок добавления объектов в массив container.add().