[Godot] 通过AABB包围盒和射线法检测碰撞

前言

最近需要做鼠标选择单位的功能,所以给大家分享一下我是如何实现的,简单来说,是在鼠标点击后,通过我写的获取单位点集的方法,先通过AABB包围盒进行粗筛,然后再通过射线法进行精确判定,最后根据Y轴的大小得到选中的单位,下面我给大家分享一下我的实现逻辑

效果展示


代码实现

鼠标点击

这里本来没有什么好说的,但是给大家分享一个普遍可能会遇到的问题,就是鼠标点击UI,因为比如我直接用的Input.IsActionJustPressed()方法做点击,这回导致当我们点击UI的时候也会去做点击单位,解决办法如下:

cs 复制代码
//当点击UI时直接返回
if (gameManager.GetViewport().GuiGetHoveredControl() != null)
{
    return;
}

这样,我们只需要注意把不会遮挡的UI,比如占满全屏的Control节点的的Mouse-Filter 设置为Ignore就好

AABB包围盒

介绍

AABB(Axis-Aligned Bounding Box)的全称是轴对齐包围盒。简单来说,就是找一个最小的矩形,能把你的多边形完全框住,我们先用他进行粗筛,否则如果每次都要对所有单位做精确的多边形判定,CPU会直接原地爆炸

包围盒计算

首先注意,我们要提前设置好单位的点集,因为之后的射线法判断,所以需要按顺序设置,这里我用的是List存的点集,用Rect2存的AABB包围盒,接下来我们需要计算包围盒,注意计算好的包围盒需要跟着单位作为常量

cs 复制代码
public Rect2 CalulateAABB(List pointSet)      
{
    float minX = pointSet[0].X;
    float maxX = pointSet[0].X;
    float minY = pointSet[0].Y;
    float maxY = pointSet[0].Y;

    for (int i = 0; i < pointSet.Count; i++)
    {
        Vector2 p = pointSet[i];
        if (p.X < minX) minX = p.X;
        else if (p.X > maxX) maxX = p.X;
        if (p.Y < minY) minY = p.Y;
        else if (p.Y > maxY) maxY = p.Y;
    }

    return new Rect2(new Vector2(minX, minY), new Vector2(maxX - minX, maxY - minY));
}

这里的逻辑就是:不断更新四个边界值,最后用左下角坐标和宽高构造出矩形,因为我们是用的Rect2,所以注意用的两个点分别是positionsize

使用AABB进行粗筛

cs 复制代码
// 先通过AABB包围盒粗筛
foreach (var unit in unitPointSet)
{
    if (unit.AABB.HasPoint(mousePos - unit.nowPos))
    {
        // 通过射线法判断是否在多边形内
        if (IsPointInPolygon(mousePos - unit.nowPos, unit.pointSet))
            tmpUnit.Add(unit);
    }
}

这里有个细节:mousePos - unit.nowPos是把鼠标坐标转换到单位的局部坐标系。因为我们存储的点集都是相对于单位中心(0,0)的,所以需要先做这个转换

射线法精确判定

通过了AABB粗筛之后,就要进入真正的重头戏了:判断点是否在多边形内部

射线法的核心思想

想象你站在某个位置,往右边射出一根无限长的射线。这根射线会和多边形的边界相交。如果交点数量是奇数,说明你在多边形内部;如果是偶数(包括0),说明你在外部

我们可以把它想象成"翻墙"的过程:

  • 你在外面,翻过1道墙,就进去了(奇数)
  • 再翻1道墙,就出来了(偶数)
  • 再翻1道墙,又进去了(奇数)

所以,统计撞击次数的奇偶性,就能判断最终位置是在内还是外

代码实现
cs 复制代码
//射线法,连接点(顺序),判断碰撞为奇数在多边形内
private bool IsPointInPolygon(Vector2 mouse, List pointSet)
{
    bool inside = false;
    for (int i = 0, j = pointSet.Count - 1; i < pointSet.Count; j = i++)
    {
        if ((pointSet[i].Y > mouse.Y) != (pointSet[j].Y > mouse.Y) &&
        mouse.X < (pointSet[j].X - pointSet[i].X) * (mouse.Y - pointSet[i].Y) / (pointSet[j].Y - pointSet[i].Y) + pointSet[i].X)
            inside = !inside;
    }
    return inside;
}

这段代码非常精简,接下来我给大家拆开讲解一下

1) 双指针

首先看循环头部:for (int i = 0, j = pointSet.Count - 1; i < pointSet.Count; j = i++)

这里为什么要用两个指针,而不是直接用(i + 1) % pointSet.Count?

原因如下:

  1. 性能考虑: 模除运算在底层涉及除法,比简单的赋值和自增要慢。在需要对成百上千个单位进行实时判定的游戏中,这种细微差别会累积成可观的性能开销

  2. 逻辑清晰 : j = i++这种"指针尾随"的写法是图形学算法的标准写法,一开始就处理封口边,循环体内部非常纯净

2) 坐标计算
  1. 高度判断
cs 复制代码
(pointSet[i].Y > mouse.Y) != (pointSet[j].Y > mouse.Y)

这一行是在判断:这条边的两个端点,一个在鼠标上方,一个在鼠标下方

如果两个点都在鼠标上方,或者都在下方,那这条边根本不会和水平射线相交,直接跳过就行

  1. 计算交点X坐标

这是整个算法最核心的数学部分:

cs 复制代码
mouse.X < (pointSet[j].X - pointSet[i].X) * (mouse.Y - pointSet[i].Y) / (pointSet[j].Y - pointSet[i].Y) + pointSet[i].X

我们把它看作**"相似三角形"或者"线性插值比例"**,就很容易理解了 :

  • 先算垂直进度比例:(mouse.Y - p1.Y) / (p2.Y - p1.Y)。这就是算出当前鼠标的高度,占这条线段总高度的百分之几 。

  • 应用到水平方向:因为点在同一条斜线上,你垂直方向爬了 50%,水平方向也一定移动了总宽度的 50% 。所以用线段总宽度 (p2.X - p1.X) 乘以这个比例,算出 X 轴上的偏移量。

  • 坐标还原:最后加上起点的 p1.X,就是地图上的真实绝对坐标了 。

对应的数学公式表现为:

hitX=ΔX×mouseOffsetΔY+XstarthitX = \Delta X \times \frac{\text{mouseOffset}}{\Delta Y} + X_{\text{start}}hitX=ΔX×ΔYmouseOffset+Xstart

完整判断代码

cs 复制代码
//注意我这里返回的是我单位的唯一Id,-1就是空,大家根据自己的需要修改
public long CalculateChooseUnit(Vector2 mousePos, List<(long id, List<Vector2> pointSet, Rect2 AABB, Vector2 nowPos)> unitPointSet)
{
    List <(pointSet, Rect2 AABB, Vector2 nowPos)> tmpUnit = new();

    // 先通过AABB包围盒粗筛
    foreach (var unit in unitPointSet)
    {
        if (unit.AABB.HasPoint(mousePos - unit.nowPos))
        {
            // 通过射线法判断是否在多边形内
            if (IsPointInPolygon(mousePos - unit.nowPos, unit.pointSet))
                tmpUnit.Add(unit);
        }
    }

    if (tmpUnit.Count == 0)
        return (-1);

    // 如果有多个单位重叠,选择Y坐标最大的(最下面的)
    var maxY = tmpUnit[0];
    for (int i = 1; i < tmpUnit.Count; i++)
    {
        if (tmpUnit[i].nowPos.Y > maxY.nowPos.Y)
            maxY = tmpUnit[i];
    }
    return (maxY.id);
}

结尾

以上就是我实现鼠标选择,检测碰撞的思路。AABB粗筛保证性能,射线法保证准确性,两者结合能处理各种复杂形状,感谢大家观看

相关推荐
大黄说说1 小时前
解锁 .NET 性能极限:深入解析 Span 与零拷贝内存艺术
java·数据结构·算法
风痕天际1 小时前
Godot扫雷游戏制作记录4——计算周围地雷数并显示
游戏·游戏引擎·godot
知识即是力量ol1 小时前
深入理解 Snowflake 雪花算法:原理、本质、趋势递增问题与分布式顺序困境全解析
java·分布式·算法·雪花算法·snowflake·全局唯一id·分布式id生成器
啊阿狸不会拉杆1 小时前
《计算机视觉:模型、学习和推理》第 11 章-链式模型和树模型
人工智能·学习·算法·机器学习·计算机视觉·hmm·链式模型
二年级程序员2 小时前
一篇文章掌握“树”(上)
c语言·数据结构·算法
薛定e的猫咪2 小时前
【Bayesian Analysis 2023】大数据背景下的分布式贝叶斯模型选择
大数据·分布式·算法·数学建模
菜鸡儿齐2 小时前
leetcode-搜索二维矩阵
算法·leetcode·矩阵
hans汉斯2 小时前
《数据挖掘》期刊推介&征稿指南
图像处理·人工智能·算法·yolo·数据挖掘·超分辨率重建·汉斯出版社
炽烈小老头2 小时前
【每天学习一点算法 2026/02/24】矩阵置零
学习·算法·矩阵