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

При создании игр часто нужно понять взаимное положение объектов: поворачивает ли враг к вам левым боком, в какую сторону повернуть руль или находится ли точка по нужную сторону от линии. Ручные расчёты углов и синусов — путь сложный и неоптимальный. Векторное произведение (cross product) даёт простой и элегантный ответ на эти вопросы одним вызовом метода. Эта статья покажет, как использовать встроенный в Phaser метод `cross()` для определения ориентации одного вектора относительно другого и вычисления синуса угла между ними. Вы научитесь применять этот мощный математический инструмент для игровой логики, экономя время и повышая читаемость кода.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    angle = 0;
    text;
    point2;
    point;
    graphics;

    create ()
    {
        this.graphics = this.add.graphics({ lineStyle: { width: 2, color: 0x2266aa } });

        this.point = new Phaser.Math.Vector2(250, 0);
        this.point2 = new Phaser.Math.Vector2(250, 0);

        this.text = this.add.text(30, 30, '');

        this.input.on('pointermove', pointer =>
        {
            this.point2.copy(pointer);

            this.point2.x -= 400;
            this.point2.y -= 300;
        });
    }

    update ()
    {
        this.graphics.clear();

        this.angle += 0.005;

        // vector starting at 0/0
        this.point.setTo(Math.cos(this.angle) * 250, Math.sin(this.angle) * 250);

        // drawn from the center (as if center was 0/0)
        this.graphics.lineBetween(400, 300, 400 + this.point.x, 300 + this.point.y);

        this.graphics.lineStyle(2, 0x00aa00);
        this.graphics.lineBetween(400, 300, 400 + this.point2.x, 300 + this.point2.y);

        const cross = this.point.cross(this.point2);

        const area = this.point.length() * this.point2.length();

        const angleBetween = Math.asin(cross / area);

        this.text.setText([
            `Cross product: ${cross}`,
            `Normalized cross product: ${cross / area}`,
            `Sinus of the angle between vectors: ${Phaser.Math.RadToDeg(angleBetween)}`,
            `Green vector is on the ${cross > 0 ? 'right' : 'left'}`
        ].join('\n'));
    }
}

const config = {
    width: 800,
    height: 600,
    type: Phaser.AUTO,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Что такое векторное произведение?

В контексте 2D-графики векторное произведение (cross product) — это операция над двумя векторами, результатом которой является скаляр (число). Его знак и абсолютное значение несут важную информацию:

* **Знак (+/-)** указывает, находится ли второй вектор справа (+) или слева (-) от первого, если смотреть из начала координат. * **Абсолютное значение** равно произведению длин векторов на синус угла между ними: |a x b| = |a| * |b| * sin(θ).

В Phaser эта операция реализована как метод .cross() у объекта класса Phaser.Math.Vector2.

const vectorA = new Phaser.Math.Vector2(10, 0);
const vectorB = new Phaser.Math.Vector2(0, 5);
const crossResult = vectorA.cross(vectorB); // Результат: 50

Разбираем пример: визуализация и логика

Рассмотренный пример создаёт интерактивную демонстрацию. Синий вектор вращается по окружности, а зелёный вектор следует за курсором мыши. Вся математика происходит в методе update().

Ключевые моменты инициализации:
*   `this.point` и `this.point2` — это наши векторы, исходящие из центра (точки [0,0] в своей системе координат).
*   Для отрисовки их начало смещается в центр экрана (координаты [400, 300]) с помощью `graphics.lineBetween`.
*   Положение `this.point2` обновляется по движению указателя, с поправкой на смещение центра экрана.
this.input.on('pointermove', pointer =>
{
    this.point2.copy(pointer);
    // Приводим координаты к системе с центром в (0,0)
    this.point2.x -= 400;
    this.point2.y -= 300;
});

Вычисление и интерпретация результата

Ядро логики находится в update(). После расчёта векторного произведения мы получаем три полезные величины.

const cross = this.point.cross(this.point2); // Скалярное значение
const area = this.point.length() * this.point2.length(); // Произведение длин (максимально возможный |cross|)
const angleBetween = Math.asin(cross / area); // Угол между векторами

1. **cross**: "сырое" векторное произведение. Его знак сразу говорит, справа (> 0) или слева (< 0) находится зелёный вектор (point2) относительно синего (point). 2. **cross / area**: это нормированное произведение, равное sin(θ). Его значение лежит в диапазоне [-1, 1]. 3. **angleBetween**: чтобы получить угол в радианах, берём арксинус от синуса (Math.asin). Для отображения в градусах используем утилиту Phaser Phaser.Math.RadToDeg().

Практическое применение в играх

Как использовать эту математику в реальном проекте? Вот несколько идей:

* **Определение стороны поворота:** Нужно понять, с какой стороны от линии движения NPC находится игрок, чтобы выбрать анимацию поворота. Знак cross даст мгновенный ответ. * **Проверка ориентации многоугольника:** В графике и физике важно знать, является ли многоугольник выпуклым и в каком порядке заданы вершины (по или против часовой стрелки). Последовательное вычисление cross для сторон даст ответ. * **Упрощение расчёта углов:** Часто синус угла (нормированное произведение) полезнее самого угла, особенно для плавного вращения или смещения.

// Пример: определяем, с какой стороны от линии AB находится точка C
function isPointLeft(A, B, C) {
    const vectorAB = new Phaser.Math.Vector2(B.x - A.x, B.y - A.y);
    const vectorAC = new Phaser.Math.Vector2(C.x - A.x, C.y - A.y);
    return vectorAB.cross(vectorAC) > 0; // true = слева, false = справа
}

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

Векторное произведение — это компактный и мощный инструмент для анализа взаимного положения объектов в 2D-пространстве. Вместо громоздких расчётов с углами достаточно одного вызова vector.cross(), чтобы получить информацию о направлении и синусе угла. Поэкспериментируйте: попробуйте модифицировать пример, чтобы зелёный вектор не следовал за курсором, а был, например, направлен к случайной точке на экране. Используйте знак произведения, чтобы менять цвет текста или запускать частицы с определённой стороны от синего вектора. Это отличный способ наглядно закрепить понимание работы этого метода.