Godot中的向量
在 Godot 游戏引擎中,向量是表示位置、方向和速度等概念的基础。理解向量对于开发任何类型的游戏都至关重要。Godot 主要使用 Vector2
(二维向量)和 Vector3
(三维向量)这两种类型。本教程将使用 C# 语言来演示向量的常见操作。
什么是向量?
简单来说,向量是一个有大小(长度)和方向的量。它通常被表示为从原点 (0,0) 或 (0,0,0) 开始的一个箭头,指向特定的坐标。
Vector2
: 用于二维空间,例如 UI 布局、2D 游戏中的移动等。它有两个分量:X
和Y
。Vector3
: 用于三维空间,例如 3D 游戏中的角色位置、摄像机方向等。它有三个分量:X
、Y
和Z
。
声明和初始化向量
在 C# 中声明和初始化 Godot 向量非常简单:
C#
using Godot;
public partial class MyNode : Node
{
public override void _Ready()
{
// 声明并初始化一个 Vector2
Vector2 position2D = new Vector2(100, 50);
GD.Print($"2D 位置: {position2D}"); // 输出: 2D 位置: (100, 50)
// 声明并初始化一个 Vector3
Vector3 position3D = new Vector3(10, 20, 30);
GD.Print($"3D 位置: {position3D}"); // 输出: 3D 位置: (10, 20, 30)
// 使用静态属性初始化零向量
Vector2 zeroVector2 = Vector2.Zero; // 等同于 new Vector2(0, 0)
Vector3 zeroVector3 = Vector3.Zero; // 等同于 new Vector3(0, 0, 0)
// 使用静态属性初始化方向向量 (仅 Vector2)
Vector2 up = Vector2.Up; // (0, -1) 在 Godot 中,Y轴负方向是“上”
Vector2 down = Vector2.Down; // (0, 1)
Vector2 left = Vector2.Left; // (-1, 0)
Vector2 right = Vector2.Right; // (1, 0)
}
}
向量的常见操作
1. 向量加法和减法
向量加法常用于移动物体或计算两个位置之间的相对位移。
C#
public override void _Ready()
{
Vector2 posA = new Vector2(10, 5);
Vector2 posB = new Vector2(3, 2);
// 向量加法:将 posB 的位移加到 posA 上
Vector2 newPos = posA + posB; // (10+3, 5+2) = (13, 7)
GD.Print($"加法结果: {newPos}");
// 向量减法:计算从 posA 到 posB 的向量(方向和距离)
Vector2 direction = posB - posA; // (3-10, 2-5) = (-7, -3)
GD.Print($"减法结果 (方向): {direction}");
}
2. 向量乘法(标量乘法)
向量与一个标量(单个数字)相乘会改变向量的长度,但方向不变。常用于加速、减速或缩放。
C#
public override void _Ready()
{
Vector2 direction = new Vector2(1, 0); // 右方向
float speed = 5.0f;
// 乘以标量:使向量的长度变为原来的 speed 倍
Vector2 velocity = direction * speed; // (1*5, 0*5) = (5, 0)
GD.Print($"速度向量: {velocity}");
// 除以标量:常用于归一化后的向量乘以速度
Vector2 scaledVector = new Vector2(10, 20) / 2.0f; // (5, 10)
GD.Print($"缩放向量: {scaledVector}");
}
3. 向量的长度(Magnitude)
向量的长度表示它的大小或距离。Length()
方法返回向量的欧几里得长度。
C#
public override void _Ready()
{
Vector2 vec = new Vector2(3, 4);
float length = vec.Length(); // sqrt(3*3 + 4*4) = sqrt(9 + 16) = sqrt(25) = 5
GD.Print($"向量长度: {length}");
// 如果只需要比较长度,使用 LengthSquared() 更高效,因为它避免了开方运算
float lengthSq = vec.LengthSquared(); // 3*3 + 4*4 = 25
GD.Print($"向量长度平方: {lengthSq}");
}
4. 向量归一化(Normalization)
归一化是将向量的长度变为 1,但保持其方向不变的过程。归一化后的向量称为单位向量。它在表示方向时非常有用,因为它只包含方向信息,而没有大小信息。
C#
public override void _Ready()
{
Vector2 direction = new Vector2(10, 0);
Vector2 normalizedDirection = direction.Normalized(); // (1, 0)
GD.Print($"归一化向量: {normalizedDirection}");
GD.Print($"归一化向量的长度: {normalizedDirection.Length()}"); // 应该接近 1.0f
// 示例:将一个方向向量乘以速度来获得实际速度
Vector2 movementDirection = new Vector2(1, -1); // 向右上方
Vector2 unitDirection = movementDirection.Normalized(); // 转换为单位向量
float playerSpeed = 100.0f;
Vector2 playerVelocity = unitDirection * playerSpeed;
GD.Print($"玩家速度向量: {playerVelocity}");
}
5. 点积(Dot Product)
点积是两个向量的乘积,结果是一个标量。它可以用来判断两个向量的相似程度或夹角。
如果点积为正,两向量方向大致相同(夹角小于 90 度)。
如果点积为零,两向量垂直(夹角 90 度)。
如果点积为负,两向量方向大致相反(夹角大于 90 度)。
C#
public override void _Ready()
{
Vector2 vecA = new Vector2(1, 0); // 右
Vector2 vecB = new Vector2(0, 1); // 上
Vector2 vecC = new Vector2(-1, 0); // 左
float dotAB = vecA.Dot(vecB); // 1*0 + 0*1 = 0 (垂直)
float dotAC = vecA.Dot(vecC); // 1*(-1) + 0*0 = -1 (完全相反)
float dotAA = vecA.Dot(vecA); // 1*1 + 0*0 = 1 (完全相同,如果是单位向量)
GD.Print($"vecA . vecB (垂直): {dotAB}");
GD.Print($"vecA . vecC (相反): {dotAC}");
GD.Print($"vecA . vecA (相同): {dotAA}");
// 应用场景:检测敌人是否在玩家前方
Vector2 playerForward = Vector2.Up; // 假设玩家面向上方
Vector2 enemyRelativePosition = new Vector2(0.5f, -0.2f); // 敌人在玩家的右下方
// 如果方向向量是单位向量,点积就是它们夹角的余弦值
// 这里需要先归一化,才能准确判断角度
float dotProduct = playerForward.Dot(enemyRelativePosition.Normalized());
if (dotProduct > 0)
{
GD.Print("敌人大致在玩家前方");
}
else
{
GD.Print("敌人大致在玩家后方或侧面");
}
}
点积(Dot Product)在实际游戏开发中用途非常广泛,尤其在判断方向、光照计算和AI行为等方面发挥着关键作用。
点积在实际开发中的应用
点积是两个向量相乘得到一个标量(一个数字)的运算。它的数学公式是:
A⋅B=∣A∣∗∣B∣∗cos(θ)
其中 ∣A∣ 和 ∣B∣ 是向量A和B的长度,θ 是它们之间的夹角。
如果两个向量都是单位向量(长度为1的向量,即经过归一化处理),那么公式就简化为:
A⋅B=cos(θ)
这意味着两个单位向量的点积直接等于它们夹角的余弦值。这个特性在实际应用中极为重要。
以下是点积在游戏开发中的几个主要应用场景:
1. 判断方向和视野 (Field of View - FOV)
这是点积最常见和最直观的应用之一。通过计算角色前方向量与目标方向向量的点积,我们可以判断目标是位于角色的前方、后方还是侧面。
实现方式:
获取角色(或观察者)的“前方”方向向量(通常是角色的局部 Z 轴或 Y 轴,并归一化)。
计算从角色位置指向目标位置的向量,并归一化。
计算这两个单位向量的点积。
点积结果的含义:
点积 >0: 目标大致在角色前方(夹角小于 90 度)。值越接近 1,表示方向越一致。
点积 =0: 目标与角色方向垂直(夹角 90 度)。
点积 <0: 目标大致在角色后方(夹角大于 90 度)。值越接近 -1,表示方向越相反。
示例应用:
敌人视野检测: 判断一个敌人是否进入了玩家的视野锥形区域。你可以设置一个视野角度(例如 60 度),然后计算
cos(30度)
作为阈值。如果敌人方向向量与敌人前方向量的点积大于这个阈值,则玩家在敌人的视野内。玩家朝向检测: 判断一个可交互物体是否在玩家正前方,只有在正前方时才允许交互。
AI 行为: 优先攻击视野内的敌人,或者根据敌人方位选择不同的战术。
2. 光照计算
在 3D 渲染中,点积是计算漫反射光照(Diffuse Lighting)的核心。漫反射光照的亮度取决于光源方向与物体表面法线(垂直于表面的单位向量)之间的夹角。
实现方式:
获取物体表面的法线向量(已归一化)。
获取从物体表面指向光源的向量(已归一化)。
计算这两个单位向量的点积。
点积结果的含义:
当光源方向与法线方向一致(夹角为 0 度,点积为 1)时,物体表面受到最强的光照。
当光源方向与法线方向垂直(夹角为 90 度,点积为 0)时,物体表面没有漫反射光照(通常被称为掠射角)。
当光源在物体背面时(夹角大于 90 度,点积为负),理论上不应有漫反射光照,所以通常会钳制到 0。
示例应用:
卡通渲染或 PBR 渲染: 用于着色器中计算每个像素接收到的光照强度,从而模拟物体的明暗。
动态光照效果: 根据光源和物体之间的相对位置实时调整光照效果。
3. 投影 (Projection)
点积可以用来计算一个向量在另一个向量上的投影长度。这在物理模拟和游戏逻辑中很有用。
实现方式:
假设要将向量
A
投影到向量B
上。先将
B
归一化得到B_normalized
。计算
A
与B_normalized
的点积,结果就是A
在B
方向上的有向长度。
示例应用:
碰撞响应: 计算一个物体的速度向量在碰撞法线上的分量,从而决定反弹的方向和力度。
角色在斜坡上的滑动: 计算重力向量在斜坡法线方向上的分量,以模拟角色在斜坡上的滑动效果。
自定义物理: 例如,计算一个力在某个特定方向上的有效分量。
4. 判断点在直线哪一侧 (仅限 2D)
在 2D 游戏中,结合点积和叉积(2D 叉积返回一个标量),可以判断一个点在一条直线的哪一侧。单独使用点积时,通常是判断点到直线上一个参考点的相对方向。
示例应用:
导航网格(NavMesh)的边界判断: 确定角色是否越过了某个可通行区域的边界。
简单的 AI 路径跟随: 判断 AI 当前是否偏离了预设路径的一侧。
6. 叉积(Cross Product - 仅限 Vector3)
叉积是两个 Vector3
的乘积,结果是一个新的 Vector3
向量。这个新向量垂直于原始的两个向量。叉积的长度等于原始向量长度与它们夹角正弦的乘积。在 Godot 的 Vector2
中,Cross()
方法返回一个 float
,表示一个 2D 向量到另一个 2D 向量的“旋转方向”或“哪个向量在另一个向量的顺时针/逆时针方向”。
Vector3
的叉积:
C#
public override void _Ready()
{
Vector3 vecA = Vector3.Right; // (1, 0, 0)
Vector3 vecB = Vector3.Up; // (0, 1, 0)
// 叉积结果是垂直于 vecA 和 vecB 的向量
Vector3 crossProduct = vecA.Cross(vecB); // (0, 0, 1) 即 Vector3.Forward
GD.Print($"Vector3 叉积: {crossProduct}"); // 结果应是世界坐标系的 Z 轴正方向 (0,0,1)
}
Vector2
的叉积:
Godot 的 Vector2
的 Cross()
方法有两个重载:
vecA.Cross(vecB)
: 返回vecA.X * vecB.Y - vecA.Y * vecB.X
。如果结果为正,vecB
在vecA
的逆时针方向;如果为负,在顺时针方向。vecA.Cross(scalar)
: 返回一个新的Vector2
,相当于vecA
旋转 90 度并乘以标量。
C#
public override void _Ready()
{
Vector2 vecA = Vector2.Right; // (1, 0)
Vector2 vecB = Vector2.Up; // (0, -1) (Godot的Y轴负方向是上)
// Vector2 的 Cross 产品:判断旋转方向
// 1 * (-1) - 0 * 0 = -1
float cross2D = vecA.Cross(vecB); // 负值表示 vecB 在 vecA 的顺时针方向
GD.Print($"Vector2 叉积 (旋转方向): {cross2D}");
// Vector2 的 Cross(float) 方法:旋转向量
Vector2 rotatedVec = vecA.Cross(1.0f); // 绕原点逆时针旋转 90 度
GD.Print($"Vector2 旋转 90 度: {rotatedVec}"); // (0, 1)
}
7. 距离(Distance)
计算两个点之间的距离。
C#
public override void _Ready()
{
Vector2 point1 = new Vector2(0, 0);
Vector2 point2 = new Vector2(3, 4);
float distance = point1.DistanceTo(point2); // 等同于 (point2 - point1).Length()
GD.Print($"两点间距离: {distance}");
}
8. 线性插值(Lerp)
Lerp (Linear Interpolation) 是一种平滑地从一个值过渡到另一个值的方法。在向量中,它用于平滑地从一个位置移动到另一个位置或平滑地改变方向。
C#
public override void _Ready()
{
Vector2 startPos = new Vector2(0, 0);
Vector2 endPos = new Vector2(100, 100);
float t = 0.5f; // t 介于 0 到 1 之间
// 计算位于 startPos 和 endPos 中间的点
Vector2 interpolatedPos = startPos.Lerp(endPos, t); // (50, 50)
GD.Print($"插值位置 (t=0.5): {interpolatedPos}");
// 在 _Process 或 _PhysicsProcess 中使用 Lerp 进行平滑移动
// public override void _Process(double delta)
// {
// Vector2 targetPosition = new Vector2(200, 100);
// Position = Position.Lerp(targetPosition, (float)delta * 5.0f); // 平滑移动到目标位置
// }
}
实际应用举例
移动角色
在一个 2D 平台游戏中,你可以这样控制角色的移动:
C#
// 假设这是一个 CharacterBody2D 节点
public partial class Player : CharacterBody2D
{
public float Speed = 200.0f;
public override void _PhysicsProcess(double delta)
{
Vector2 inputDirection = Vector2.Zero;
if (Input.IsActionPressed("ui_right"))
{
inputDirection.X += 1;
}
if (Input.IsActionPressed("ui_left"))
{
inputDirection.X -= 1;
}
if (Input.IsActionPressed("ui_down"))
{
inputDirection.Y += 1;
}
if (Input.IsActionPressed("ui_up"))
{
inputDirection.Y -= 1;
}
// 归一化输入方向,确保斜向移动速度不会比水平/垂直移动快
// 只有当有输入时才归一化,避免 Length() 或 Normalized() 在零向量上出错
if (inputDirection.Length() > 0)
{
inputDirection = inputDirection.Normalized();
}
Velocity = inputDirection * Speed;
MoveAndSlide(); // 使用 CharacterBody2D 的内置移动方法
}
}
朝向鼠标方向
让一个 2D 精灵朝向鼠标的方向:
C#
// 假设这是一个 Sprite2D 节点
public partial class RotatingSprite : Sprite2D
{
public override void _Process(double delta)
{
// 获取鼠标在世界坐标系中的位置
Vector2 mouseGlobalPos = GetGlobalMousePosition();
// 计算从精灵到鼠标的向量
Vector2 directionToMouse = mouseGlobalPos - GlobalPosition;
// 设置精灵的旋转角度
// Atan2 返回向量与 X 轴正方向的夹角(弧度)
Rotation = directionToMouse.Angle();
}
}
总结
向量是 Godot 游戏开发中不可或缺的工具。熟练掌握 Vector2
和 Vector3
的基本操作(加减乘除、长度、归一化、点积、叉积、Lerp)将大大提高你处理游戏逻辑的能力,无论是移动、碰撞检测、方向判断还是物理模拟,都离不开向量的运用。