在 Godot 游戏引擎中,向量是表示位置、方向和速度等概念的基础。理解向量对于开发任何类型的游戏都至关重要。Godot 主要使用 Vector2(二维向量)和 Vector3(三维向量)这两种类型。本教程将使用 C# 语言来演示向量的常见操作。

什么是向量?

简单来说,向量是一个有大小(长度)和方向的量。它通常被表示为从原点 (0,0) 或 (0,0,0) 开始的一个箭头,指向特定的坐标。

  • Vector2: 用于二维空间,例如 UI 布局、2D 游戏中的移动等。它有两个分量:XY

  • Vector3: 用于三维空间,例如 3D 游戏中的角色位置、摄像机方向等。它有三个分量:XYZ

声明和初始化向量

在 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)

这是点积最常见和最直观的应用之一。通过计算角色前方向量与目标方向向量的点积,我们可以判断目标是位于角色的前方、后方还是侧面。

  • 实现方式:

    1. 获取角色(或观察者)的“前方”方向向量(通常是角色的局部 Z 轴或 Y 轴,并归一化)。

    2. 计算从角色位置指向目标位置的向量,并归一化。

    3. 计算这两个单位向量的点积

  • 点积结果的含义:

    • 点积 >0: 目标大致在角色前方(夹角小于 90 度)。值越接近 1,表示方向越一致。

    • 点积 =0: 目标与角色方向垂直(夹角 90 度)。

    • 点积 <0: 目标大致在角色后方(夹角大于 90 度)。值越接近 -1,表示方向越相反。

  • 示例应用:

    • 敌人视野检测: 判断一个敌人是否进入了玩家的视野锥形区域。你可以设置一个视野角度(例如 60 度),然后计算 cos(30度) 作为阈值。如果敌人方向向量与敌人前方向量的点积大于这个阈值,则玩家在敌人的视野内。

    • 玩家朝向检测: 判断一个可交互物体是否在玩家正前方,只有在正前方时才允许交互。

    • AI 行为: 优先攻击视野内的敌人,或者根据敌人方位选择不同的战术。

2. 光照计算

在 3D 渲染中,点积是计算漫反射光照(Diffuse Lighting)的核心。漫反射光照的亮度取决于光源方向与物体表面法线(垂直于表面的单位向量)之间的夹角。

  • 实现方式:

    1. 获取物体表面的法线向量(已归一化)。

    2. 获取从物体表面指向光源的向量(已归一化)。

    3. 计算这两个单位向量的点积

  • 点积结果的含义:

    • 当光源方向与法线方向一致(夹角为 0 度,点积为 1)时,物体表面受到最强的光照。

    • 当光源方向与法线方向垂直(夹角为 90 度,点积为 0)时,物体表面没有漫反射光照(通常被称为掠射角)。

    • 当光源在物体背面时(夹角大于 90 度,点积为负),理论上不应有漫反射光照,所以通常会钳制到 0。

  • 示例应用:

    • 卡通渲染或 PBR 渲染: 用于着色器中计算每个像素接收到的光照强度,从而模拟物体的明暗。

    • 动态光照效果: 根据光源和物体之间的相对位置实时调整光照效果。

3. 投影 (Projection)

点积可以用来计算一个向量在另一个向量上的投影长度。这在物理模拟和游戏逻辑中很有用。

  • 实现方式:

    1. 假设要将向量 A 投影到向量 B 上。

    2. 先将 B 归一化得到 B_normalized

    3. 计算 AB_normalized点积,结果就是 AB 方向上的有向长度

  • 示例应用:

    • 碰撞响应: 计算一个物体的速度向量在碰撞法线上的分量,从而决定反弹的方向和力度。

    • 角色在斜坡上的滑动: 计算重力向量在斜坡法线方向上的分量,以模拟角色在斜坡上的滑动效果。

    • 自定义物理: 例如,计算一个力在某个特定方向上的有效分量。

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 的 Vector2Cross() 方法有两个重载:

  • vecA.Cross(vecB): 返回 vecA.X * vecB.Y - vecA.Y * vecB.X。如果结果为正,vecBvecA 的逆时针方向;如果为负,在顺时针方向。

  • 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 游戏开发中不可或缺的工具。熟练掌握 Vector2Vector3 的基本操作(加减乘除、长度、归一化、点积、叉积、Lerp)将大大提高你处理游戏逻辑的能力,无论是移动、碰撞检测、方向判断还是物理模拟,都离不开向量的运用。