О чем этот пример
В процессе разработки игр часто возникает необходимость перетаскивать объекты и динамически удалять их со сцены. Однако если уничтожить объект (`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. Добавить анимацию или эффект частиц в момент безопасного уничтожения объекта.
