О чем этот пример
Понимание векторной математики — ключ к созданию сложной игровой механики, от определения поля зрения персонажа до расчета отскоков. Скалярное произведение (dot product) — один из фундаментальных инструментов, позволяющий определить угол между векторами и их взаимное направление. Эта статья на практическом примере покажет, как использовать `Phaser.Math.Vector2` для визуализации и применения скалярного произведения в реальном времени, реагируя на движение курсора.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
create ()
{
this.angle = 0;
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 ()
{
const graphics = this.graphics;
graphics.clear();
this.angle += 0.005;
// Vector starting at 0/0
this.point.set(Math.cos(this.angle) * 250, Math.sin(this.angle) * 250);
// drawn from the center (as if center was 0/0)
graphics.lineBetween(400, 300, 400 + this.point.x, 300 + this.point.y);
graphics.lineStyle(2, 0x00aa00);
graphics.lineBetween(400, 300, 400 + this.point2.x, 300 + this.point2.y);
const dotProduct = this.point.dot(this.point2);
const area = this.point.length() * this.point2.length();
const angleBetween = Math.acos(dotProduct / area);
// only used to determine arc direction
const cross = this.point.cross(this.point2);
graphics.lineStyle(2, 0xaa0000);
graphics.beginPath();
graphics.arc(400, 300, 100, this.angle, this.angle + (cross < 0 ? -angleBetween : angleBetween));
graphics.strokePath();
this.text.setText([
'Dot product: ' + dotProduct,
'Normalized dot product: ' + dotProduct / area,
'Angle between vectors: ' + Phaser.Math.RadToDeg(angleBetween),
'Pointer is ' + (dotProduct > 0 ? 'in front of' : 'behind') + ' the blue vector direction'
]);
}
}
const config = {
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и векторов
В методе create() инициализируются основные объекты для визуализации и хранения данных. Создаются два вектора: один будет вращаться по окружности, а второй — следовать за указателем мыши. Текстовый объект будет выводить рассчитанные значения.
create ()
{
this.angle = 0;
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;
});
}
Ключевой момент — нормализация координат указателя. Поскольку центр сцены условно принят за точку (400, 300), мы вычитаем эти значения из координат курсора, чтобы получить вектор point2, исходящий из центра. Метод .copy() объекта Vector2 удобно использовать для переноса координат.
Анимация и расчет скалярного произведения
В методе update() происходит анимация первого вектора, его отрисовка и вычисление dot product. Скалярное произведение рассчитывается с помощью встроенного метода вектора .dot().
update ()
{
this.graphics.clear();
this.angle += 0.005;
this.point.set(Math.cos(this.angle) * 250, Math.sin(this.angle) * 250);
graphics.lineBetween(400, 300, 400 + this.point.x, 300 + this.point.y);
graphics.lineStyle(2, 0x00aa00);
graphics.lineBetween(400, 300, 400 + this.point2.x, 300 + this.point2.y);
const dotProduct = this.point.dot(this.point2);
}
Вектор point задается через тригонометрические функции, что заставляет его равномерно вращаться. Отрисовка линий (lineBetween) смещается на центр сцены (400, 300). Результат dotProduct — это число, характеризующее проекцию одного вектора на направление другого.
От произведения к углу и направлению
Зная длины векторов и их скалярное произведение, можно вычислить угол между ними. Также определяется, находится ли второй вектор (курсор) впереди или позади направления первого.
const area = this.point.length() * this.point2.length();
const angleBetween = Math.acos(dotProduct / area);
const cross = this.point.cross(this.point2);
this.text.setText([
'Dot product: ' + dotProduct,
'Normalized dot product: ' + dotProduct / area,
'Angle between vectors: ' + Phaser.Math.RadToDeg(angleBetween),
'Pointer is ' + (dotProduct > 0 ? 'in front of' : 'behind') + ' the blue vector direction'
]);
Здесь area — произведение длин векторов. Разделив dotProduct на area, мы получаем косинус угла между векторами. Функция Math.acos() дает сам угол в радианах, который затем конвертируется в градусы через Phaser.Math.RadToDeg. Векторное произведение (cross) используется только для определения направления дуги: его знак показывает, в какую сторону от первого вектора ко второму нужно рисовать угол.
Визуализация угла между векторами
Для наглядности угол между векторами отрисовывается в виде красной дуги. Направление рисования дуги (по или против часовой стрелки) зависит от знака векторного произведения.
graphics.lineStyle(2, 0xaa0000);
graphics.beginPath();
graphics.arc(400, 300, 100, this.angle, this.angle + (cross < 0 ? -angleBetween : angleBetween));
graphics.strokePath();
Метод arc() рисует дугу с центром в (400, 300). Стартовый угол равен текущему углу вращения синего вектора (this.angle). Конечный угол вычисляется как стартовый угол плюс или минус вычисленный angleBetween в зависимости от знака cross. Это создает эффект «подсвечивания» сектора между векторами.
Что попробовать дальше
Скалярное произведение в Phaser — это не абстрактная математика, а готовый инструмент для игровой логики. С его помощью можно легко определять, находится ли объект в поле зрения NPC (dot > 0), рассчитывать силу удара или отражения, управлять камерой. Для экспериментов попробуйте изменить логику проверки: например, реагировать только когда косинус угла больше 0.5 (угол меньше 60 градусов), что создаст узкую зону обнаружения. Или привяжите длину зеленого вектора к силе «толчка» от синего, если тот представляет собой направление движения.
