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

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

Версия 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('block', 'assets/sprites/block.png');
    }

    create ()
    {
        this.input.on(Phaser.Input.Events.POINTER_UP_OUTSIDE, () => console.info('phaser pointer up outside'));
        this.input.on(Phaser.Input.Events.POINTER_UP, () => console.info('phaser pointer up inside'));

        this.add.text(this.scale.width / 2, this.scale.height / 2, 'This is a test Phaser game', { align: 'center', color: '#ffffff', fontSize: '32px', fontFamily: 'Arial' }).setOrigin(0.5);

        this.game.canvas.addEventListener('mousedown', () => console.log('mouse down on canvas'));

        const block = this.physics.add.image(400, 100, 'block')
            .setVelocity(100, 200)
            .setBounce(1, 1)
            .setCollideWorldBounds(true);

        this.input.setDraggable(block.setInteractive());

        this.input.on('dragstart', (pointer, obj) =>
        {
            obj.body.moves = false;
        });

        this.input.on('drag', (pointer, obj, dragX, dragY) =>
        {
            obj.setPosition(dragX, dragY);
        });

        this.input.on('dragend', (pointer, obj) =>
        {
            obj.body.moves = true;
        });
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            gravity: { y: 200 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);
window.addEventListener('mouseup', () => console.info('mouse up on window'));

События Phaser и нативные события DOM

Phaser предоставляет собственную систему событий для ввода, которая абстрагирует разницу между мышью и касаниями, используя общее понятие pointer. Однако иногда требуется знать, где именно произошло событие: внутри игрового холста или снаружи.

В примере кода наглядно показано это различие. События Phaser, такие как POINTER_UP и POINTER_UP_OUTSIDE, отслеживаются системой ввода движка. Параллельно с этим код добавляет нативное слушатель событий mouseup на объект window. Это позволяет сравнить поведение.

this.input.on(Phaser.Input.Events.POINTER_UP_OUTSIDE, () => console.info('phaser pointer up outside'));
this.input.on(Phaser.Input.Events.POINTER_UP, () => console.info('phaser pointer up inside'));
window.addEventListener('mouseup', () => console.info('mouse up on window'));

При отпускании кнопки мыши внутри холста сработают POINTER_UP и глобальный mouseup. Если отпустить кнопку за пределами холста, сработают POINTER_UP_OUTSIDE и тот же глобальный mouseup. Это ключевая особенность для обработки drag&drop.

Практический пример: перетаскивание физического тела

Рассмотрим, как событие POINTER_UP_OUTSIDE интегрируется в логику перетаскивания спрайта с физикой. В примере создается физический блок (block), который делается перетаскиваемым.

const block = this.physics.add.image(400, 100, 'block')
    .setVelocity(100, 200)
    .setBounce(1, 1)
    .setCollideWorldBounds(true);
this.input.setDraggable(block.setInteractive());

Для управления физикой во время перетаскивания используются события dragstart, drag и dragend.

this.input.on('dragstart', (pointer, obj) =>
{
    obj.body.moves = false;
});

this.input.on('drag', (pointer, obj, dragX, dragY) =>
{
    obj.setPosition(dragX, dragY);
});

this.input.on('dragend', (pointer, obj) =>
{
    obj.body.moves = true;
});

Здесь dragstart отключает движение физического тела (body.moves = false), чтобы его не увлекали силы гравитации или столкновения во время перетаскивания. Событие dragend включает движение обратно. Важно, что dragend в Phaser срабатывает как при отпускании внутри холста, так и при срабатывании POINTER_UP_OUTSIDE. Это гарантирует, что физика тела всегда будет восстановлена, даже если пользователь "выбросил" мышку за пределы игры.

Отладка и анализ потока событий

Чтобы понять последовательность событий, в исходном примере добавлены console.log. Это отличный способ для отладки.

this.game.canvas.addEventListener('mousedown', () => console.log('mouse down on canvas'));

Нативное событие mousedown на холсте поможет точно зафиксировать начало взаимодействия. Сравнивая вывод в консоль, можно построить точную картину: 1. mouse down on canvas – начало перетаскивания. 2. Событие dragstart в Phaser (без лога, но оно отключает физику). 3. Множество событий drag при движении мыши. 4. В зависимости от места отпускания кнопки: * Внутри холста: phaser pointer up inside, затем dragend, и mouse up on window. * Снаружи холста: phaser pointer up outside, затем dragend, и mouse up on window.

Такой подход позволяет убедиться, что логика завершения перетаскивания (dragend) отрабатывает в любом случае, предотвращая ситуацию, когда тело навсегда останется в "замороженном" состоянии (body.moves = false).

Конфигурация сцены и физики

Для работы примера необходима правильная настройка игры. Ключевые моменты конфигурации:

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    physics: {
        default: 'arcade',
        arcade: {
            debug: true, // Визуализация физических тел для отладки
            gravity: { y: 200 }
        }
    },
    scene: Example
};

Установка debug: true в настройках Arcade Physics включает отображение контуров тел и векторов скорости, что невероятно полезно при настройке взаимодействия. Гравитация { y: 200 } заставляет блок падать вниз, наглядно демонстрируя, как свойство body.moves временно отменяет это воздействие во время перетаскивания.

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

Событие POINTER_UP_OUTSIDE — это важный инструмент в арсенале разработчика Phaser для создания отзывчивого и надежного пользовательского ввода. Оно гарантирует, что интерактивные элементы, особенно перетаскиваемые, корректно завершают свои действия независимо от действий пользователя. Для экспериментов попробуйте

  1. Создать UI-кнопку, которая должна "отжиматься" только если указатель отпущен над ней (используйте POINTER_UP), а не снаружи
  2. Реализовать механику "броска" объекта, где вектор скорости в момент dragend рассчитывается и применяется к телу, и убедиться, что это работает даже при отпускании за пределами окна