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

При разработке игр часто возникает необходимость динамически управлять интерактивностью объектов. Например, сделать перетаскиваемый предмет неактивным после определённого действия. Пример `disableInteractive.js` демонстрирует распространённую ошибку и правильный подход к использованию метода `disableInteractive`. Эта статья поможет вам избежать багов и научит корректно управлять состоянием интерактивности спрайтов и других игровых объектов.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
    }

    create ()
    {
        this.input.topOnly = false;
        
        for (var i = 0; i < 2; i++)
        {
            const sprite = this.add.sprite(400, 300, 'eye')
                .setInteractive({ draggable: true })
                .on('dragend', function ()
                {
                    console.log('sprite', sprite.id);
                    this.disableInteractive();
                });
            sprite.id = i;
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Разбор исходного кода

В данном примере создаётся сцена, которая загружает одно изображение и в методе create создаёт два интерактивных спрайта.

class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
    }
Ключевые моменты настройки:
1.  `this.input.topOnly = false;` — это свойство указывает, что события ввода (например, перетаскивания) будут обрабатываться всеми объектами под курсором, а не только верхним. Это важно для понимания контекста.
2.  В цикле создаются два спрайта с одинаковыми координатами. Каждому назначается интерактивность с возможностью перетаскивания (`draggable: true`).
3.  Каждому спрайту вручную присваивается свойство `id` для идентификации.
4.  На каждый спрайт вешается обработчик события `'dragend'`, который логирует `id` и пытается отключить интерактивность этого спрайта с помощью `this.disableInteractive()`.

Проблема: контекст выполнения `this`

Основная ошибка в исходном коде кроется внутри функции-обработчика события 'dragend'.

.on('dragend', function ()
{
    console.log('sprite', sprite.id);
    this.disableInteractive();
});

Здесь используется обычная function declaration. В JavaScript контекст this внутри такой функции зависит от того, как она была вызвана. При вызове из системы событий Phaser, this внутри обработчика будет указывать не на экземпляр спрайта (sprite), как ожидается, а на сам объект InputPlugin, который это событие инициировал.

Следовательно, вызов this.disableInteractive() не сработает, так как у объекта InputPlugin нет такого метода. Это типичная ошибка, которая может привести к неожиданному поведению или молчаливому сбою.

Решение: использование стрелочных функций или bind

Чтобы исправить ошибку, нужно гарантировать, что внутри обработчика this ссылается на спрайт. Есть два основных способа.

**Способ 1: Стрелочная функция** Стрелочные функции не имеют своего this и захватывают его из окружающего лексического контекста. В данном случае это контекст метода create, где переменная sprite является нужным нам объектом.

.on('dragend', () =>
{
    console.log('sprite', sprite.id);
    sprite.disableInteractive();
});

**Способ 2: Явное указание контекста с помощью .bind()** Можно использовать обычную функцию, но привязать к ней нужный контекст с помощью метода .bind().

.on('dragend', function ()
{
    console.log('sprite', this.id); // Теперь this - это sprite
    this.disableInteractive();
}.bind(sprite));

Оба способа работоспособны, но использование стрелочной функции в данном сценарии является более современным и лаконичным решением.

Что делает метод `disableInteractive`?

Метод disableInteractive() — часть API любого игрового объекта Phaser, который был сделан интерактивным через setInteractive. Его вызов приводит к следующим последствиям: 1. Объект перестаёт получать и обрабатывать любые события ввода: клики, наведения, перетаскивания и т.д. 2. Он удаляется из списка интерактивных объектов менеджера ввода. 3. Если для объекта было настроено перетаскивание, оно останавливается.

Важно понимать, что это «одностороннее» действие. Чтобы снова включить взаимодействие, необходимо заново вызвать setInteractive с нужными параметрами.

// Отключение взаимодействия
sprite.disableInteractive();

// Повторное включение взаимодействия (например, по другому событию)
sprite.setInteractive({ draggable: true });

Итоговый исправленный код

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

create ()
{
    this.input.topOnly = false;
    
    for (let i = 0; i < 2; i++)
    {
        const sprite = this.add.sprite(400, 300, 'eye')
            .setInteractive({ draggable: true })
            .on('dragend', () =>
            {
                console.log('sprite', sprite.id);
                sprite.disableInteractive();
            });
        sprite.id = i;
    }
}

Обратите внимание также на замену var на let в цикле. Это важно для корректной работы замыканий внутри стрелочной функции, чтобы каждая итерация цикла имела свою собственную константу sprite.

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

Корректное управление контекстом this — фундаментальный аспект работы с событиями в Phaser и JavaScript в целом. Использование стрелочных функций для обработчиков событий объектов — самый простой и надёжный способ избежать ошибок. Для экспериментов попробуйте

  1. Включить свойство topOnly обратно в true и посмотреть, как изменится поведение при перетаскивании
  2. Создать кнопку или другое событие, которое будет снова включать интерактивность спрайта через setInteractive
  3. Реализовать логику, где разные спрайты отключают друг друга