О чем этот пример
В разработке игр часто возникает задача проецирования одного вектора на другой — например, для определения тени объекта, отражения снаряда или вычисления ближайшей точки на линии движения. Встроенный метод `projectUnit()` класса `Vector2` в Phaser решает эту задачу элегантно и эффективно. Эта статья на практическом примере покажет, как использовать проекцию векторов для создания визуализации, где точка «преследует» вращающуюся линию, что может стать основой для расчёта траекторий, прицеливания или векторных эффектов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
angle = 0;
projectedPoint;
point2;
point;
graphics;
create ()
{
this.graphics = this.add.graphics({ lineStyle: { width: 2, color: 0x2266aa }, fillStyle: { color: 0xaa0000 } });
// ProjectUnit assumes normalized point
// i.e. it has magnitude of 1
this.point = new Phaser.Math.Vector2(1, 0);
this.point2 = new Phaser.Math.Vector2(250, 0);
this.projectedPoint = this.point2.projectUnit(this.point);
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;
// unit vector
this.point.setTo(Math.cos(this.angle), Math.sin(this.angle));
// project a point on point2 on point
this.point2.projectUnit(this.point, this.projectedPoint);
// set magnitude to 250, because it's unit point, we can simply multiply
this.point.x *= 250;
this.point.y *= 250;
// drawn from the center (as if center was 0/0)
this.graphics.lineBetween(400, 300, 400 + this.point.x, 300 + this.point.y);
// draw projecting point
this.graphics.lineStyle(2, 0x00aa00);
this.graphics.lineBetween(400, 300, 400 + this.point2.x, 300 + this.point2.y);
// move relative to center
this.projectedPoint.x += 400;
this.projectedPoint.y += 300;
this.graphics.fillPointShape(this.projectedPoint, 15);
this.graphics.lineStyle(1, 0xaa0000);
this.graphics.lineBetween(this.point2.x + 400, this.point2.y + 300, this.projectedPoint.x, this.projectedPoint.y);
}
}
const config = {
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Суть метода projectUnit()
Метод projectUnit() вычисляет проекцию одного вектора на другой, при условии, что вектор, на который проецируют, является единичным (его длина равна 1). Это ключевое ограничение, о котором важно помнить. Проекция — это, по сути, «тень» одного вектора на направление другого.
this.projectedPoint = this.point2.projectUnit(this.point);
В данном вызове this.point2 проецируется на единичный вектор this.point. Результат — новый вектор this.projectedPoint, который лежит на той же линии, что и this.point, и показывает, насколько далеко в этом направлении «вытянулся» this.point2. Метод может работать как с созданием нового вектора (как в строке выше), так и с записью результата в существующий объект, что полезно для оптимизации.
this.point2.projectUnit(this.point, this.projectedPoint);
В этой форме результат вычисления запишется в уже созданный вектор this.projectedPoint, избегая лишнего создания объектов в цикле update.
Подготовка сцены и векторов
В методе create() инициализируются основные объекты. Обрати внимание, что начальный вектор this.point создаётся как (1, 0) — это единичный вектор, направленный вдоль оси X. Вектор this.point2 изначально задан статически, но его значение сразу перезаписывается проекцией.
this.graphics = this.add.graphics({ lineStyle: { width: 2, color: 0x2266aa }, fillStyle: { color: 0xaa0000 } });
this.point = new Phaser.Math.Vector2(1, 0);
this.point2 = new Phaser.Math.Vector2(250, 0);
this.projectedPoint = this.point2.projectUnit(this.point);
Слушатель события pointermove привязывает вектор this.point2 к положению курсора мыши, предварительно смещая координаты так, чтобы центр сцены (400, 300) стал началом координат для математических операций. Это стандартный приём для упрощения векторных вычислений.
this.input.on('pointermove', pointer => {
this.point2.copy(pointer);
this.point2.x -= 400;
this.point2.y -= 300;
});
Анимация и визуализация в update()
Каждый кадр в update() происходит перерасчёт и отрисовка. Сначала this.point становится вращающимся единичным вектором.
this.angle += 0.005;
this.point.setTo(Math.cos(this.angle), Math.sin(this.angle));
Затем вычисляется проекция текущего положения курсора (this.point2) на это вращающееся направление. Результат сохраняется в this.projectedPoint.
this.point2.projectUnit(this.point, this.projectedPoint);
Чтобы нарисовать линию направления (this.point) подлине, её умножают на скаляр (250). Важно: умножение происходит после проекции, так как для её корректности вектор this.point должен оставаться единичным.
this.point.x *= 250;
this.point.y *= 250;
Далее идёт отрисовка с помощью this.graphics. Все векторы, которые хранятся относительно центра (0,0), при рисовании сдвигаются на координаты центра экрана (400, 300).
// Ось проекции (синяя линия)
this.graphics.lineBetween(400, 300, 400 + this.point.x, 300 + this.point.y);
// Вектор от курсора (зелёная линия)
this.graphics.lineBetween(400, 300, 400 + this.point2.x, 300 + this.point2.y);
// Спроецированная точка (красный круг)
this.projectedPoint.x += 400;
this.projectedPoint.y += 300;
this.graphics.fillPointShape(this.projectedPoint, 15);
// Линия от курсора до его проекции (красная пунктирная)
this.graphics.lineBetween(this.point2.x + 400, this.point2.y + 300, this.projectedPoint.x, this.projectedPoint.y);
Зачем нужен отдельный единичный вектор?
Метод projectUnit() требует единичный вектор в качестве основы для проекции по причине производительности и математической чистоты. Если бы вектор не был нормализован, внутри метода пришлось бы делить на его длину (норму). Заставляя разработчика предоставить уже готовый единичный вектор, Phaser избегает лишних вычислений квадратного корня и деления в потенциально горячем цикле.
Если у тебя есть вектор произвольной длины dir, и ты хочешь спроецировать на него вектор `v`, шаги будут такими:
let dir = new Phaser.Math.Vector2(150, 50); // Произвольный вектор
let v = new Phaser.Math.Vector2(300, 200); // Вектор для проекции
// 1. Создаём копию и нормализуем её (приводим к длине 1)
let unitDir = dir.clone().normalize();
// 2. Проецируем
let projection = v.projectUnit(unitDir);
Такой подход даёт полный контроль: ты можешь кэшировать единичный вектор, если направление не меняется, и избежать нормализации каждый кадр.
Что попробовать дальше
Метод projectUnit() — это мощный и производительный инструмент для работы с векторной математикой в Phaser. Он идеально подходит для задач, где направление известно и нормализовано заранее: расчёт силы вдоль поверхности, определение компоненты скорости, создание векторных индикаторов или, как в нашем примере, интерактивная визуализация.
Попробуй поэкспериментировать: используй проекцию для создания прицела, который всегда показывает ближайшую точку на линии полёта снаряда, или для симуляции отражения шара от наклонной поверхности, рассчитав компоненту скорости, параллельную плоскости.
