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

Работа с геометрией и коллизиями — основа для создания сложной игровой механики. В этом примере мы исследуем метод `GetRaysFromPointToPolygon`, который позволяет вычислять все лучи от заданной точки до границ набора полигонов. Этот подход может быть полезен для реализации видимости персонажа, системы освещения с препятствиями или проверки траектории снаряда на наличие помех. Мы разберемся, как работает этот метод, и узнаем, как визуализировать его результаты для отладки.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    create ()
    {
        const border = new Phaser.Geom.Polygon([ 0, 0, 800, 0, 800, 600, 0, 600, 0, 0 ]);

        const shape1 = new Phaser.Geom.Polygon([ 100, 150, 120, 50, 200, 80, 140, 210, 100, 150 ]);
        const shape2 = new Phaser.Geom.Polygon([ 100, 200, 120, 250, 60, 300, 100, 200 ]);
        const shape3 = new Phaser.Geom.Polygon([ 200, 260, 220, 150, 300, 200, 350, 320, 200, 260 ]);
        const shape4 = new Phaser.Geom.Polygon([ 340, 60, 360, 40, 370, 70, 340, 60 ]);
        const shape5 = new Phaser.Geom.Polygon([ 450, 190, 560, 170, 540, 270, 430, 290, 450, 190 ]);
        const shape6 = new Phaser.Geom.Polygon([ 400, 95, 580, 50, 480, 150, 400, 95 ]);
        const shape7 = new Phaser.Geom.Polygon([ 100, 150, 120, 50, 200, 80, 140, 210, 100, 150 ]);
        const shape8 = new Phaser.Geom.Polygon([ 100, 200, 120, 250, 60, 300, 100, 200 ]);
        const shape9 = new Phaser.Geom.Polygon([ 200, 260, 220, 150, 300, 200, 350, 320, 200, 260 ]);
        const shape10 = new Phaser.Geom.Polygon([ 340, 60, 360, 40, 370, 70, 340, 60 ]);
        const shape11 = new Phaser.Geom.Polygon([ 450, 190, 560, 170, 540, 270, 430, 290, 450, 190 ]);
        const shape12 = new Phaser.Geom.Polygon([ 400, 95, 580, 50, 480, 150, 400, 95 ]);

        Phaser.Geom.Polygon.Translate(shape2, 0, 150);
        Phaser.Geom.Polygon.Translate(shape5, 50, 0);
        Phaser.Geom.Polygon.Translate(shape7, 550, 200);
        Phaser.Geom.Polygon.Translate(shape8, 300, 200);
        Phaser.Geom.Polygon.Translate(shape9, 280, 170);
        Phaser.Geom.Polygon.Translate(shape10, 140, 480);
        Phaser.Geom.Polygon.Translate(shape11, -300, 270);
        Phaser.Geom.Polygon.Translate(shape12, 200, -30);

        // const shapes = [ border, shape1, shape2, shape3, shape4, shape5, shape6, shape7, shape8, shape9, shape10, shape11, shape12 ];

        const shapes = [ shape1 ];

        const ray = new Phaser.Geom.Line(400, 300, 400, 300);

        const debug = this.add.graphics();

        const draw = (pointer) => {

            const intersects = Phaser.Geom.Intersects.GetRaysFromPointToPolygon(pointer.worldX, pointer.worldY, shapes);

            // console.log(intersects);

            debug.clear();

            //  Draw the intersections

            debug.lineStyle(1, 0xff0000);
            debug.fillStyle(0xff0000);

            intersects.forEach(line => {

                ray.setTo(pointer.worldX, pointer.worldY, line.x, line.y);

                debug.strokeLineShape(ray);
                debug.fillCircle(line.x, line.y, 4);

            });

            //  Draw all shapes

            debug.lineStyle(1, 0x00ff00);

            shapes.forEach(shape => {

                debug.strokePoints(shape.points);

            });

        };

        this.input.on('pointerdown', (pointer) => {

            const intersects = Phaser.Geom.Intersects.GetRaysFromPointToPolygon(pointer.worldX, pointer.worldY, shapes);

            console.log(pointer.worldX, pointer.worldY, intersects);

        });

        draw({ worldX: 140, worldY: 96 });
        // draw({ worldX: 450, worldY: 200 });
        // draw({ worldX: 400, worldY: 200 });

        this.input.on('pointermove', pointer => draw(pointer));

        /*
        const line1 = new Phaser.Geom.Line(140, 128, 140, 129);

        debug.lineStyle(4, 0xff0000);
        debug.strokeLineShape(line1);

        const line2 = new Phaser.Geom.Line(140, 210, 100, 150);

        debug.lineStyle(1, 0x00ff00);
        debug.strokeLineShape(line2);

        const c = Phaser.Geom.Intersects.GetLineToLine(line1, line2, true);

        console.log('direct test');
        console.log(c);
        */

    }
}

const config = {
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

let game = new Phaser.Game(config);

Подготовка сцены и полигонов

В первую очередь в методе create создается граница игрового поля и набор полигонов различных форм. Все они являются экземплярами Phaser.Geom.Polygon. Координаты в массиве определяют вершины каждой фигуры.

Для смещения некоторых полигонов используется статический метод Phaser.Geom.Polygon.Translate, который сдвигает все вершины фигуры на заданные значения по осям X и Y. Это позволяет расположить фигуры в разных частях сцены, не пересчитывая координаты вручную.

const border = new Phaser.Geom.Polygon([ 0, 0, 800, 0, 800, 600, 0, 600, 0, 0 ]);
const shape1 = new Phaser.Geom.Polygon([ 100, 150, 120, 50, 200, 80, 140, 210, 100, 150 ]);
Phaser.Geom.Polygon.Translate(shape2, 0, 150);

В демонстрационных целях в массиве shapes оставлен только shape1, чтобы упростить визуализацию и анализ.

Создание луча и графики для отладки

Для визуализации процесса создается луч Phaser.Geom.Line и графический объект debug. Луч изначально является точкой (его начальная и конечная точки совпадают), но в дальнейшем он будет переопределяться.

const ray = new Phaser.Geom.Line(400, 300, 400, 300);
const debug = this.add.graphics();

Графический объект debug будет использоваться для рисования всех элементов: лучей-пересечений, точек пересечения и контуров самих полигонов. Это ключевой инструмент для отладки геометрических вычислений.

Функция отрисовки и метод GetRaysFromPointToPolygon

Основная логика заключена в функции draw. Она принимает объект, похожий на pointer, с координатами worldX и worldY. С помощью метода Phaser.Geom.Intersects.GetRaysFromPointToPolygon вычисляются все точки на границах полигонов из массива shapes, до которых можно провести луч из заданной точки, не пересекая внутреннюю область полигонов.

const intersects = Phaser.Geom.Intersects.GetRaysFromPointToPolygon(pointer.worldX, pointer.worldY, shapes);

Метод возвращает массив объектов Phaser.Geom.Point. Каждая такая точка — это место на границе полигона, куда упирается луч.

Функция draw очищает предыдущую графику, затем для каждой найденной точки пересечения рисует луч от исходной точки до точки на полигоне и закрашивает саму точку пересечения.

debug.clear();
debug.lineStyle(1, 0xff0000);
debug.fillStyle(0xff0000);
intersects.forEach(line => {
    ray.setTo(pointer.worldX, pointer.worldY, line.x, line.y);
    debug.strokeLineShape(ray);
    debug.fillCircle(line.x, line.y, 4);
});

После этого функция отрисовывает зеленые контуры всех полигонов для наглядности.

Обработка ввода и начальная отрисовка

Для интерактивности к событию движения указателя pointermove привязывается функция draw. Это позволяет в реальном времени наблюдать, как меняются лучи при перемещении курсора.

this.input.on('pointermove', pointer => draw(pointer));

Также добавлен обработчик pointerdown, который выводит в консоль координаты клика и массив найденных пересечений — полезно для детального анализа.

Перед началом интерактивного режима функция draw вызывается один раз с фиксированными координатами, чтобы сразу показать пример работы системы.

draw({ worldX: 140, worldY: 96 });

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

Метод GetRaysFromPointToPolygon — мощный инструмент для анализа геометрической видимости. Его можно использовать для создания Field of View (FOV) у стражников в стелс-играх, моделирования лучей света, преломляющихся в препятствиях, или для расчёта зоны поражения кругового заклинания, учитывающего стены. Поэкспериментируйте: добавьте больше полигонов, измените форму луча на сектор (конус) для ограничения угла обзора или попробуйте динамически менять набор shapes в процессе игры.