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

В процессе разработки игр часто возникает необходимость перетаскивать объекты и динамически удалять их со сцены. Однако если уничтожить объект (`destroy()`) во время его перетаскивания, может возникнуть ошибка, так как система ввода продолжит обрабатывать события для уже несуществующего объекта. Эта статья разбирает реальный пример из баг-трекера Phaser (Issue #4337) и показывает, как безопасно управлять жизненным циклом перетаскиваемых элементов. Понимание этой механики поможет вам создавать более стабильные интерфейсы и игровые механики.

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

Живой запуск

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

Исходный код


var config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: {
        create: create
    }
};

var game = new Phaser.Game(config);

function create()
{
    this.input.addPointer();

    var circ = this.add.circle(300, 200, 96, 0xffff00);

    circ.setInteractive();

    this.input.setDraggable(circ);

    circ.on('drag', function (p, x, y) {

        circ.x = x;
        circ.y = y;

    });

    var testRect = this.add.rectangle(400, 0, 128, 128, 0x00ffff);

    this.tweens.add({
        targets: testRect,
        angle: 360,
        repeat: -1,
        duration: 2000
    });

    var killRect = this.add.rectangle(0, 0, 128, 128, 0xff0000).setOrigin(0, 0);

    killRect.setInteractive();

    killRect.once('pointerdown', function () {

        circ.destroy();

    });

    this.input.keyboard.once('keydown_A', function () {

        circ.destroy();

    });

    this.input.keyboard.once('keydown-A', function () {

        circ.destroy();

    });
}

Суть проблемы: события ввода и уничтожение объектов

В Phaser, когда вы делаете объект перетаскиваемым с помощью this.input.setDraggable(), система ввода начинает отслеживать его специальным образом. Она создаёт внутреннюю привязку между указателем мыши (или касанием) и этим объектом.

Если во время активного перетаскивания (когда кнопка мыши зажата и объект движется) вызвать метод destroy() для этого объекта, система ввода попытается продолжить обработку события drag в следующем кадре. Но так как объект уже удалён, это приводит к ошибке.

circ.on('drag', function (p, x, y) {
    circ.x = x;
    circ.y = y;
});

killRect.once('pointerdown', function () {
    circ.destroy(); // Опасность! Если удалить во время drag, будет ошибка.
});

Анализ примера кода и триггеры уничтожения

В предоставленном примере создаётся жёлтый круг (circ), который можно перетаскивать. Есть три способа его уничтожить: 1. Клик по красному прямоугольнику (killRect) в левом верхнем углу. 2. Нажатие клавиши `A(обработчик черезkeydown_A`). 3. Нажатие клавиши `A(обработчик черезkeydown-A`).

Последние два обработчика демонстрируют разные форматы названия событий клавиатуры в Phaser. Оба сработают, но лучше придерживаться одного формата (обычно используют keydown-A).

// Уничтожение по клику на красный прямоугольник
killRect.once('pointerdown', function () {
    circ.destroy();
});

// Уничтожение по нажатию клавиши A (два варианта)
this.input.keyboard.once('keydown_A', function () {
    circ.destroy();
});

this.input.keyboard.once('keydown-A', function () {
    circ.destroy();
});

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

Практическое решение: безопасное уничтожение перетаскиваемых объектов

Чтобы избежать ошибки, необходимо перед уничтожением объекта отвязать его от системы перетаскивания или убедиться, что событие drag не активно. Самый надёжный способ — использовать метод input.setDraggable() повторно, но с параметром false, чтобы отключить перетаскивание.

function safeDestroy() {
    // 1. Отключаем объект от системы перетаскивания
    this.input.setDraggable(circ, false);
    // 2. Удаляем все слушатели событий с объекта (опционально)
    circ.removeAllListeners();
    // 3. Уничтожаем объект
    circ.destroy();
}

// Используем безопасный метод
killRect.once('pointerdown', safeDestroy, this);

Важно отметить, что простое удаление слушателей через circ.removeAllListeners() не решает проблему полностью, так как основная привязка остаётся внутри менеджера ввода Phaser. Поэтому вызов setDraggable(circ, false) является ключевым.

Дополнительные меры и проверка состояния

В более сложных сценариях можно добавить проверку на то, перетаскивается ли объект в данный момент. Это можно сделать, отслеживая флаги или события dragstart и dragend.

var isBeingDragged = false;

circ.on('dragstart', function () {
    isBeingDragged = true;
});

circ.on('dragend', function () {
    isBeingDragged = false;
});

function conditionalDestroy() {
    if (isBeingDragged) {
        // Если объект тянут, отключаем drag и уничтожаем
        this.input.setDraggable(circ, false);
        circ.destroy();
        console.warn('Объект уничтожен во время перетаскивания!');
    } else {
        // Если не тянут, можно уничтожать сразу
        circ.destroy();
    }
}

Такой подход даёт больше контроля и позволяет, например, показывать пользователю предупреждение или выполнять дополнительную логику.

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

Уничтожение перетаскиваемых объектов в Phaser требует осторожности. Всегда отключайте объект от системы drag с помощью this.input.setDraggable(object, false) перед вызовом destroy(). Это гарантирует стабильность вашей игры. Для экспериментов попробуйте: 1. Создать систему, где несколько объектов можно перетаскивать и удалять. 2. Реализовать "корзину" или "зону уничтожения", при попадании в которую объекты безопасно удаляются. 3. Добавить анимацию или эффект частиц в момент безопасного уничтожения объекта.