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

Работа с интерактивными объектами (спрайтами, кнопками) — базовая задача в любой игре. Phaser предоставляет удобный API для обработки событий ввода, например, `pointerover` и `pointerdown`. Однако, если вам потребуется динамически отключить реакцию объекта на ввод, простого вызова `this.input.disable()` может оказаться недостаточно. В примере из коллекции, который мы разберем, показан важный нюанс: после отключения интерактивности спрайта необходимо также сбросить состояние курсора с помощью `this.input.manager.resetCursor()`. Иначе объект может "застрять" в состоянии наведения (hover), сохраняя визуальный эффект (например, tint), даже когда он уже не должен реагировать. Эта статья поможет вам избежать подобных багов, правильно управляя интерактивностью в своих проектах.

Версия 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 ()
    {
        const sprite = this.add.sprite(400, 300, 'eye').setInteractive({ cursor: 'pointer' });

        sprite.on('pointerdown', function (pointer)
        {
            this.input.disable(sprite);
            this.input.manager.resetCursor(sprite.input);
            sprite.clearTint();
        }, this);

        sprite.on('pointerover', function (pointer)
        {
            this.setTint(0xff0000);
        });

        sprite.on('pointerout', function (pointer)
        {
            this.clearTint();
        });

        sprite.on('pointerup', function (pointer)
        {
            this.clearTint();
        });
    }
}

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

const game = new Phaser.Game(config);

Анализ кода: создание и обработка событий спрайта

В примере создается спрайт с изображением глаза в центре экрана. С помощью метода setInteractive() объекту добавляется интерактивность. Обратите внимание на опцию { cursor: 'pointer' } — она меняет вид курсора мыши при наведении на спрайт, что является хорошей визуальной подсказкой для игрока.

Затем для спрайта назначаются обработчики основных событий указателя (pointer events): - pointerover и pointerout — меняют и сбрасывают tint (оттенок) спрайта при наведении и уходе курсора. - pointerdown и pointerup — обрабатывают клик. Именно в pointerdown происходит ключевое действие: отключение интерактивности.

const sprite = this.add.sprite(400, 300, 'eye').setInteractive({ cursor: 'pointer' });

sprite.on('pointerover', function (pointer) {
    this.setTint(0xff0000);
});

sprite.on('pointerout', function (pointer) {
    this.clearTint();
});

sprite.on('pointerup', function (pointer) {
    this.clearTint();
});

Проблема: почему объект "залипает" после отключения?

В обработчике pointerdown происходит вызов this.input.disable(sprite). Этот метод немедленно отключает все события ввода для указанного игрового объекта. Казалось бы, после этого спрайт должен перестать реагировать на любые действия мыши.

Однако возникает проблема: если курсор находился над спрайтом в момент клика (что почти всегда верно), то событие pointerout никогда не сработает. Внутренний менеджер ввода Phaser (this.input.manager) продолжает считать, что курсор всё ещё наведён на этот объект. Визуально это проявляется в том, что tint (красный оттенок), применённый при pointerover, не сбрасывается, хотя объект уже неинтерактивен.

sprite.on('pointerdown', function (pointer) {
    // Отключаем объект. События больше не будут генерироваться.
    this.input.disable(sprite);
    // Tint не сбросится, так как pointerout не вызовется.
    sprite.clearTint();
}, this);

Решение: сброс состояния курсора в менеджере ввода

Для корректного отключения интерактивности в середине взаимодействия необходимо явно сообщить менеджеру ввода, что состояние курсора для этого объекта нужно обнулить. Для этого используется метод this.input.manager.resetCursor(), который принимает в качестве аргумента свойство input отключаемого объекта.

Ключевая строка this.input.manager.resetCursor(sprite.input) делает следующее: 1. Сбрасывает внутренний флаг, указывающий, что курсор находится над данным объектом. 2. Это позволяет избежать "залипания" визуальных эффектов, связанных с наведением. 3. Возвращает курсор мыши к дефолтному виду (так как опция cursor: 'pointer' больше не активна).

Исправленный обработчик события pointerdown выглядит так:

sprite.on('pointerdown', function (pointer) {
    // 1. Отключаем объект
    this.input.disable(sprite);
    // 2. КРИТИЧЕСКИ ВАЖНО: Сбрасываем состояние курсора для этого объекта
    this.input.manager.resetCursor(sprite.input);
    // 3. Теперь можно безопасно сбросить tint
    sprite.clearTint();
}, this);

Порядок важен: сначала disable(), затем resetCursor(). Вызов resetCursor() после отключения объекта безопасен и корректен.

Когда это может понадобиться на практике?

Динамическое отключение интерактивности — частый сценарий в играх: - **Кнопки и переключатели**: Сделали одну кнопку неактивной после нажатия, чтобы предотвратить повторное срабатывание (например, кнопка "Отправить" в форме). - **Состояния игрового объекта**: Персонаж получил статус "оглушён" и временно не должен реагировать на клики. - **Управление UI**: Отключение всей панели интерфейса во время кат-сцены или диалога.

Во всех этих случаях, если отключение может произойти в момент, когда курсор находится над объектом, используйте связку disable() + resetCursor(). Это обеспечит предсказуемое поведение и чистоту UI.

Альтернативный подход — не использовать визуальные эффекты, зависящие от pointerover/pointerout (например, tint), а управлять внешним видом через отдельные флаги состояния. Но даже в этом случае сброс курсора остаётся хорошей практикой.

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

Метод this.input.disable() — мощный инструмент для управления интерактивностью, но при его использовании в середине цикла событий указателя необходимо вручную сбрасывать состояние курсора через this.input.manager.resetCursor(). Это предотвращает артефакты вроде "залипшего" визуального состояния. **Идеи для экспериментов:** 1. Попробуйте отключать объект не по pointerdown, а по pointerup, и посмотрите, как изменится поведение. 2. Создайте цепочку из нескольких интерактивных спрайтов. Отключите один из них и перетащите курсор на другой — убедитесь, что состояние курсора сбрасывается корректно. 3. Изучите метод this.input.enable() и попробуйте реализовать кнопку, которая после нажатия временно отключается, а через 2 секунды снова становится активной, корректно сбрасывая все состояния.