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

Метод `overlap` в Arcade Physics позволяет обнаруживать пересечение объектов. Но что если вам нужно не просто факт пересечения, а проверка по сложной логике? Например, столкновение должно считаться только если объект попал внутрь спрайта, а не просто коснулся его прямоугольного контейнера (bounding box). В этой статье мы разберем пример, где используется четвертый, необязательный аргумент `processCallback` функции `this.physics.add.overlap`. Этот колбэк выступает в роли кастомного процессора, который решает, считать ли конкретное пересечение валидным. Мы увидим, как это позволяет реализовать точную пиксельную (или геометрическую) проверку столкновений поверх стандартной физики.

Версия 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('atari', 'assets/sprites/atari800.png');
        this.load.image('chunk', 'assets/sprites/chunk.png');
    }

    create ()
    {
        const atari = this.physics.add.image(400, 300, 'atari')
            .setAngularVelocity(30);

        atari.setBodySize(280, 280);

        const chunks = this.physics.add.group({
            key: 'chunk',
            quantity: 240,
            bounceX: 1,
            bounceY: 1,
            collideWorldBounds: true,
            velocityX: 100,
            velocityY: 100
        });

        Phaser.Actions.RandomRectangle(chunks.getChildren(), { x: 0, y: 0, width: 800, height: 100 });

        const atariRect = new Phaser.Geom.Rectangle(0, 0, atari.width, atari.height);

        this.physics.add.overlap(
            atari,
            chunks,
            null,
            function process (_atari, chunk)
            {
                const { x, y } = _atari.getLocalPoint(chunk.body.center.x, chunk.body.center.y);

                return atariRect.contains(x, y);
            }
        );
    }

    update ()
    {
        const bodies = Array.from(this.physics.world.bodies);
        for (const body of bodies)
        {
            body.debugBodyColor = body.touching.none ? 0x00ff00 : 0xff0000;
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            debugShowVelocity: false
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание объектов

В методе preload загружаются два изображения: 'atari' (большой спрайт) и 'chunk' (маленький спрайт-частица).

В create мы создаем физические объекты. Спрайт 'atari' помещается в центр и получает постоянную угловую скорость.

const atari = this.physics.add.image(400, 300, 'atari')
    .setAngularVelocity(30);

Ключевой момент: мы изменяем размер физического тела (body) объекта 'atari', делая его меньше визуального спрайта. Это нужно для базовой оптимизации проверки пересечений.

atari.setBodySize(280, 280);

Далее создается группа из 240 физических частиц 'chunk' с отскоком и случайной начальной скоростью.

const chunks = this.physics.add.group({
    key: 'chunk',
    quantity: 240,
    bounceX: 1,
    bounceY: 1,
    collideWorldBounds: true,
    velocityX: 100,
    velocityY: 100
});

Частицы случайным образом распределяются в верхней части игрового поля.

Phaser.Actions.RandomRectangle(chunks.getChildren(), { x: 0, y: 0, width: 800, height: 100 });

Создание кастомного процессора для overlap

Стандартный вызов overlap проверит пересечение прямоугольных тел 'atari' и каждого 'chunk'. Но нам нужна точность: столкновение должно засчитываться только если частица находится в пределах визуальных границ спрайта 'atari', а не его уменьшенного физического тела.

Для этого мы создаем геометрический прямоугольник, соответствующий реальным размерам спрайта 'atari'.

const atariRect = new Phaser.Geom.Rectangle(0, 0, atari.width, atari.height);

Затем настраиваем проверку пересечения с кастомной функцией-процессором process. Первые два аргумента — объекты для проверки. Третий аргумент (collideCallback) — null, так как нам не нужна реакция на столкновение в этом примере. Четвертый аргумент — наша функция process.

this.physics.add.overlap(
    atari,
    chunks,
    null,
    function process (_atari, chunk) { ... }
);

Логика работы процессора

Функция process вызывается для каждой потенциальной пары объектов, чьи тела пересекаются. Она получает два аргумента: объекты, переданные в overlap.

Задача функции — вернуть true или false. true означает, что пересечение валидно и должно быть обработано (например, вызовом collideCallback). false — пересечение игнорируется, хотя физические тела и перекрываются.

В нашем примере логика следующая: 1. Мы берем глобальные координаты центра тела частицы (chunk.body.center). 2. С помощью метода getLocalPoint преобразуем эти глобальные координаты в локальные относительно спрайта 'atari'. Это дает координаты (x, y) точки частицы внутри системы отсчета вращающегося спрайта 'atari'.

const { x, y } = _atari.getLocalPoint(chunk.body.center.x, chunk.body.center.y);

3. Мы проверяем, попадает ли эта точка в прямоугольник atariRect, который описывает визуальные границы спрайта.

return atariRect.contains(x, y);

Таким образом, даже если физическое тело частицы пересекается с уменьшенным телом 'atari', столкновение засчитается только если центр частицы находится в пределах реального изображения 'atari'. Это имитирует проверку на попадание в форму спрайта.

Визуализация в update

Метод update служит для наглядной отладки. Он проходит по всем физическим телам в мире и меняет их цвет отладки.

const bodies = Array.from(this.physics.world.bodies);
for (const body of bodies) {
    body.debugBodyColor = body.touching.none ? 0x00ff00 : 0xff0000;
}

Если тело ни с чем не контактирует в данный кадр (body.touching.none), оно подсвечивается зеленым (0x00ff00). Если есть контакт (касание или overlap) — красным (0xff0000). Благодаря нашему процессору, частицы будут красными только когда их центр внутри спрайта 'atari'.

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

Использование processCallback в this.physics.add.overlap открывает тонкий контроль над логикой столкновений. Вы можете реализовать проверку по маске, сложной геометрии, состоянию объекта или случайности. **Идеи для экспериментов:** 1. Вместо прямоугольника atariRect используйте Phaser.Geom.Circle для проверки попадания в круглую зону. 2. Добавьте в функцию-процессор вероятность срабатывания, чтобы только каждая вторая проверка возвращала true. 3. Используйте processCallback вместе с collideCallback для создания сложного поведения (например, объект наносит урон только при попадании в "уязвимую зону").