文章目錄
  1. 1. Introduction
  2. 2. 碰撞检测
    1. 2.1. What
    2. 2.2. Why
    3. 2.3. How
      1. 2.3.1. 功能需求
      2. 2.3.2. 理论知识
        1. 2.3.2.1. 2D形状
          1. 2.3.2.1.1.
          2. 2.3.2.1.2. 圆形
          3. 2.3.2.1.3. 矩形(轴对齐包围盒-AABB)
          4. 2.3.2.1.4. 有向包围盒(OBB)
          5. 2.3.2.1.5. 胶囊体
          6. 2.3.2.1.6. 扇形
          7. 2.3.2.1.7. 凸多边形
      3. 2.3.3. 分离轴定理
      4. 2.3.4. 形状碰撞检测
        1. 2.3.4.1. 点与圆形
        2. 2.3.4.2. 点与矩形(AABB)
        3. 2.3.4.3. 点与多边形
          1. 2.3.4.3.0.1. 角度和法
          2. 2.3.4.3.0.2. 叉积(点线)法
      5. 2.3.4.4. 圆形与圆形
      6. 2.3.4.5. 圆形与胶囊体
      7. 2.3.4.6. 圆形与矩形(AABB)
      8. 2.3.4.7. 圆形与有向包围盒(OBB)
      9. 2.3.4.8. 圆形与凸多边形
      10. 2.3.4.9. 圆形与扇形
      11. 2.3.4.10. 矩形(AABB)与矩形(AABB)
      12. 2.3.4.11. 进阶学习
    4. 2.3.5. 线段相交
  • 3. 实战
  • 4. 注意事项
  • 5. Github
  • 6. 学习总结
  • 7. Reference
  • Introduction

    游戏开发过程中,特别是战斗中避免不了要判定各种攻击和技能是否击中,而这些判定都得通过碰撞系统来实现正确的判定。本篇文章正是为了学习了解战斗系统中攻击技能判定是如何实现的而编写的。

    后续大部分理论知识和小部分文字以及截图来源:

    【Unity】图形相交检测

    感谢该博主的无私分享

    所有的代码后续会传到Github上,想查看可视化绘制的完整代码可在Github上下到。

    碰撞检测

    What

    碰撞检测 — 碰撞检测指判定物体(不论是3D还是2D物体)之间的相交。比如物理系统里碰撞检测指的是物体之间的实体碰撞以及物理效果。战斗系统里碰撞检测指的是技能和攻击的命中判定。

    Why

    那么为什么需要碰撞检测了?

    1. 物理游戏里要模拟真实的物理效果,碰撞检测是必不可少的。
    2. 战斗系统里,攻击和技能的命中判定都离不开碰撞检测。
    3. 纯数学级别的碰撞检测模拟比Unity自带的Collider准确且高效。

    Note:

    1. 虽然我们可以通过Unity现成的Collider去做碰撞检测,但Collider的碰撞判定顺序是不可控的,并且Collider组件会带来很大的性能开销,所以无论是出于准确性还是性能考虑,战斗力的技能和攻击判定我们都不能直接用Collider(特别是涉及网络同步的时候)。

    How

    功能需求

    1. 支持3D一些简单形状(点,AABB,OBB,球形)的碰撞检测判定
    2. 支持2D一些简单形状(点,矩形,圆形,扇形,胶囊体,OBB)的碰撞检测判定

    理论知识

    碰撞检测从原理上来说就是数学运算,就是图形相交检测。

    所以要实现一套自己的碰撞检测系统,我们要先学习图形相交检测相关的数学知识。

    这里我们以2D的碰撞检测为例来学习实现碰撞检测系统。复杂的3D碰撞检测很多时候可以简化到2D来实现相同的效果并且得到更高效的判定结果。后续只会实现一部分简单的3D形状的碰撞判定。

    2D形状

    点在2D里我们通过X,Y坐标来定义,所以点的定义如下:

    1
    public Vector2 Point;
    圆形

    圆形是由圆心+半径来定义,圆形的定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /// <summary>
    /// 2D圆形定义
    /// </summary>
    public struct Circle2D
    {
    /// <summary>
    /// 中心点位置
    /// </summary>
    public Vector2 Center;

    /// <summary>
    /// 半径
    /// </summary>
    public float Radius;

    public Circle2D(Vector2 center, float radius)
    {

    Center = center;
    Radius = radius;
    }
    }
    矩形(轴对齐包围盒-AABB)

    矩形(轴对齐包围盒-AABB)是由中心点+长宽来定义,长宽边分别与X/Y轴平行的矩形,矩形(轴对齐包围盒-AABB)定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /// <summary>
    /// 2D AABB定义
    /// </summary>
    public struct AABB2D
    {
    /// <summary>
    /// 中心点位置
    /// </summary>
    public Vector2 Center;

    /// <summary>
    /// 宽和高
    /// </summary>
    public Vector2 Extents;

    public AABB2D(Vector2 center, Vector2 extents)
    {

    Center = center;
    Extents = extents;
    }
    }

    Note:

    1. 矩形(轴对齐包围盒-AABB)的长宽平行于X和Y轴
    有向包围盒(OBB)

    有向包围盒(OBB)可以理解成一个带旋转角度的矩形(轴对齐包围盒-AABB),正因为矩形(轴对齐包围盒-AABB)无法表达长宽不平行与X和Y轴的形状,所以才有了有向包围盒(OBB)。

    让我们通过下图来形象的理解圆形,矩形(轴对齐包围盒-AABB)和有向包围盒(OBB)的区别:

    CIRCLE_AABB_OBB_DES

    向包围盒(OBB)由中心点+长宽+旋转角度来定义,定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /// <summary>
    /// 2D OBB定义
    /// </summary>
    public struct OBB2D
    {
    /// <summary>
    /// 中心点位置
    /// </summary>
    public Vector2 Center;

    /// <summary>
    /// 宽和高
    /// </summary>
    public Vector2 Extents;

    /// <summary>
    /// 旋转角度
    /// </summary>
    public float Angle;

    public OBB2D(Vector2 center, Vector2 extents, float angle)
    {

    Center = center;
    Extents = extents;
    Angle = angle;
    }
    }
    胶囊体

    胶囊体是由起点+线段u+胶囊体半径d定义。胶囊体实际上是与线段u的最短距离d的点的集合。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    /// <summary>
    /// 2D胶囊体定义
    /// </summary>
    public struct Capsule2D
    {
    /// <summary>
    /// 起点
    /// </summary>
    public Vector2 StartPoint;

    /// <summary>
    /// 起点线段
    /// </summary>
    public Vector2 PointLine;

    /// <summary>
    /// 胶囊体半径
    /// </summary>
    public float Radius;

    public Capsule2D(Vector2 startpoint, Vector2 pointline, float radius)
    {

    StartPoint = startpoint;
    PointLine = pointline;
    Radius = radius;
    }
    }

    SphereDefinition

    从上面引申出了点与线段的最短距离算法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /// <summary>
    /// 点与线段的最短距离
    /// </summary>
    /// <param name="startpoint">线段起点</param>
    /// <param name="line">线段向量</param>
    /// <param name="targetpoint">求解点</param>
    /// <returns></returns>
    public static float SqrDistanceBetweenSegmentAndPoint(Vector2 startpoint, Vector2 line, Vector2 targetpoint)
    {

    float t = Vector2.Dot(targetpoint - startpoint, line) / line.sqrMagnitude;
    return (targetpoint - (startpoint + Mathf.Clamp01(t) * line)).sqrMagnitude;
    }

    要理解上面的公式,需要知道向量里的一些基本概念,参考:

    向量学习

    上面的计算公式理解如下:

    float t = Vector2.Dot(targetpoint - startpoint, line) / line.sqrMagnitude; // 计算求解点映射到线段line上的比例。

    (startpoint + Mathf.Clamp01(t) * line) // 计算的是映射点P点的坐标

    (targetpoint - (startpoint + Mathf.Clamp01(t) * line) ) // 表达的是目标点到线段的那条向量d

    (targetpoint - (startpoint + Mathf.Clamp01(t) line) ) .sqrMagnitude; // 得到目标点到线段的向量的距离平方(*后续用平方比代替开方比,因为平方比在计算机上比开方比更快)

    后续计算形状相交很多都会转化成点与线的距离来比较实现碰撞检测

    Note:

    1. 判定一个点处于胶囊体内部,就是判断点与线段的距离
    2. 计算机开方计算比平方计算慢,所以我们采用平方比较来代替开方值比较
    扇形

    扇形是由起点+扇形半径+扇形朝向+扇形角度定义,扇形定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /// <summary>
    /// 2D扇形定义
    /// </summary>
    public struct Sector2D
    {
    /// <summary>
    /// 扇形起点
    /// </summary>
    public Vector2 StartPoint;

    /// <summary>
    /// 扇形半径
    /// </summary>
    public float Radius;

    /// <summary>
    /// 扇形朝向
    /// </summary>
    public Vector2 Direction;

    /// <summary>
    /// 扇形角度
    /// </summary>
    public float Angle;

    public Sector2D(Vector2 startpoint, float radius, Vector2 direction, float angle)
    {

    StartPoint = startpoint;
    Radius = radius;
    Direction = direction;
    Angle = angle;
    }
    }
    凸多边形

    多边形的定义是否多个顶点位置定义的,多边形定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /*
    * Description: Polygon2D.cs
    * Author: TONYTANG
    * Create Date: 2022/03/20
    */


    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    namespace TH.Module.Collision2D
    {
    /// <summary>
    /// Polygon2D.cs
    /// 多边形定义
    /// </summary>
    public struct Polygon2D
    {
    /// <summary>
    /// 多边形顶点(注意保持统一顺时针或者逆时针定义)
    /// </summary>
    public Vector2[] Vertexes;

    public Polygon2D(Vector2[] vertexes)
    {

    Vertexes = vertexes;
    }

    /// <summary>
    /// 是否是有效多边形
    /// </summary>
    /// <returns></returns>
    public bool IsValide()
    {

    if (Vertexes == null || Vertexes.Length < 3))
    {
    Debug.LogError("多边形顶点数不应该小于3个!");
    return false;
    }
    return true;
    }
    }
    }

    Note:

    1. 上述定义并不能保证一定是凸多边形

    分离轴定理

    了解了各种形状的定义后,在了解真正的碰撞检测判定前,为了更好的理解后面的碰撞检测计算,我们先来了解一个概念分离轴定理

    分离轴定理(separating axis theorem, SAT)分离轴定理是指,两个不相交的凸集必然存在一个分离轴,使两个凸集在该轴上的投影是分离的。

    判断两个形状是否相交,实际上是判断分离轴是否能把两个形状分离。若存在分离轴能使两个图形分离,则这两个图形是分离的。

    基于以上理论,寻找分离轴是我们要做的工作,重新考虑两个圆形的相交检测,实际上我们做的是把圆心连线的方向作为分离轴:

    SAT

    通过分离轴定理的定义可以看出,通过找到两个凸集形状的分离轴,我们可以实现两个形状相交的判定,这个会成为我们后面判定碰撞检测的重要理论知识

    接下来让我们真正进入碰撞检测实战。

    形状碰撞检测

    点与圆形

    点与圆形的相交判定实际上就是判定点与圆心的距离是否大于半径。

    这里理论很简单,就不上效果图了,直接看代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /// <summary>
    /// 判断圆形与点之间的交叉检测
    /// </summary>
    /// <param name="circle1"></param>
    /// <param name="point"></param>
    /// <returns></returns>
    public static bool CircleAndPointIntersection2D(Circle2D circle1, Vector2 point)
    {

    return (circle1.Center - point).sqrMagnitude < circle1.Radius * circle1.Radius;
    }

    点与矩形(AABB)

    点与矩形的判定也比较容易,利用AABB的轴和XZ平行,我们可以直接比较点的X和Z是否在AABB的最大最小范围内即可(也可以理解成点与AABB中心的距离是否小于等于AABB的宽/2和长/2)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /// <summary>
    /// 点是否在矩形(AABB)内
    /// </summary>
    /// <param name="aabb"></param>
    /// <param name="point"></param>
    /// <returns></returns>
    public static bool PointInAABB2D(AABB2D aabb, Vector2 point)
    {

    // 点和AABB原点的位置偏移(等价于将点移动到AABB坐标系)
    Vector2 offset = aabb.Center - point;
    offset = Vector2.Max(offset, -offset);
    // 判定偏移是否小于AABB宽/2和AABB长/2
    return offset.x <= aabb.Extents.x / 2 && offset.y <= aabb.Extents.y / 2;
    }

    PointIntersectionWithAABB2D

    代码比较简单就不详细解释了。

    点与多边形

    判定一个点是否在多边形内,本文使用的是角度和叉积(点线)判定法,更多的解题思路参考:

    详谈判断点在多边形内的七种方法(最全面)

    上文中提到射线法比较优,但考虑到理解难易度,还是角度和叉积(点线)法更易理解,本文主要以这两种解法为例。

    角度和法

    学过平面几何的同学都知道,如果一个点在凸多边形内,那么这个点和凸多边形各顶点连线构成的夹角和应该为360度

    角度和法正是利用了这一个定义来实现一个点是否在凸多边形内的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /// <summary>
    /// 点是否在多边形内(内角和法)
    /// </summary>
    /// <param name="polygon">多边形</param>
    /// <param name="point"></param>
    /// <returns></returns>
    public static bool PointInPolygon2D(Polygon2D polygon, Vector2 point)
    {

    if(!polygon.IsValide())
    {
    return false;
    }
    Vector2[] polygonVertexes = polygon.Vertexes;
    int polygonVertexsNumber = polygonVertexes.Length;
    // 点与所有多边形顶点的连线向量
    Vector2[] pointLines = new Vector2[polygonVertexsNumber];
    for(int i = 0, length = polygonVertexsNumber; i < length; i++)
    {
    pointLines[i] = polygonVertexes[i] - point;
    }
    // 计算所有多边形顶点连线之间的夹角总和
    float totalAngle = Vector2.SignedAngle(pointLines[polygonVertexsNumber - 1], pointLines[0]);
    for(int i = 0, length = polygonVertexsNumber - 1; i < length; i++)
    {
    totalAngle += Vector2.SignedAngle(pointLines[i], pointLines[i + 1]);
    }
    if (Mathf.Abs(Mathf.Abs(totalAngle) - 360f) < 0.1f)
    {
    return true;
    }
    return false;
    }

    PolygonVertexesInfo

    PointOutOfPolygon

    PointInPolygon

    从上面可以看到我们通过累加点到多边形所有顶点构成的线段的夹角总和判定除了点是否在多边形内。但上面的方法new了大量的Vector3数组以及用到了Vector2.SignedAngle()等反三角函数,所以速度和效率上并不是不太优,接下来我们会讲到针对点是否在凸多边形内的更优解法(叉乘(点线)法)。

    Note:

    1. 此方法适用于凸多边形和凹多边形
    叉积(点线)法

    叉积(点线)法的核心思想是判定点到多边形所有顶点构成的边是否都在相邻多边形边的一侧(顺时针的话需要都在左侧,逆时针的话需要都在右侧)

    这里我们利用叉乘(叉乘结果>0还是<0能区分)来实现两个向量的左右关系判定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    /// <summary>
    /// 点是否在凸边形内(叉积(点线)法)
    /// Note:
    /// 1. 确保是凸多边形
    /// 2. 确保凸多边形顶点是顺时针
    /// </summary>
    /// <param name="polygon">凸多边形</param>
    /// <param name="point"></param>
    /// <returns></returns>
    public static bool PointInConvexPolygon2D(Polygon2D polygon, Vector2 point)
    {

    if (!polygon.IsValide())
    {
    return false;
    }
    Vector2[] polygonVertexes = polygon.Vertexes;
    int polygonVertexsNumber = polygonVertexes.Length;
    Vector3 pointLine;
    Vector3 polygonEdge;
    int index;
    // 这里采用顶点顺时针判定
    for (int i = 0, length = polygonVertexsNumber; i < length; i++)
    {
    pointLine = new Vector3(point.x - polygonVertexes[i].x, 0, point.y - polygonVertexes[i].y);
    index = (i + 1) % polygonVertexsNumber;
    polygonEdge = new Vector3(polygonVertexes[index].x - polygonVertexes[i].x, 0, polygonVertexes[index].y - polygonVertexes[i].y);
    // 计算多边形顶点的连线向量和边的左右关系
    // Vector3.Cross(A, B).y > 0 表示A在B的左侧
    // Vector3.Cross(A, B).y < 0 表示A在B的右侧
    // Vector3.Cross(A, B).y == 0 表示A和B平行
    if (Vector3.Cross(pointLine, polygonEdge).y >= 0)
    {
    return false;
    }
    }
    return true;
    }

    PolygonVertexesInfo2

    PointOutOfPolygon2

    PointInPolygon2

    从上面的效果图可以看出我们通过叉积法也成功判定出了点是否在凸多边形内。

    但上面的方式需要创建大量的Vector3来构建边以及叉乘判定,虽然Vector3是结构体类型不会造成GC问题,但核心只是为了实现两个向量的方向判定,依然有优化的空间。

    我们可以把2D的向量方向当做就在XZ平面的3D向量来计算(即把Y轴值当做0),通过叉乘最原始的公式直接计算叉乘后Y轴值的大小来判定两个向量的方向关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    /// <summary>
    /// 点是否在凸边形内(叉积(点线)法--不构建Vector3纯数学运算)
    /// Note:
    /// 1. 确保是凸多边形
    /// 2. 确保凸多边形顶点是顺时针
    /// </summary>
    /// <param name="polygon">凸多边形</param>
    /// <param name="point"></param>
    /// <returns></returns>
    public static bool BetterPointInConvexPolygon2D(Polygon2D polygon, Vector2 point)
    {

    if (!polygon.IsValide())
    {
    return false;
    }
    Vector2[] polygonVertexes = polygon.Vertexes;
    int polygonVertexsNumber = polygonVertexes.Length;
    int index;
    float lineX;
    float lineZ;
    float edgeX;
    float edgeZ;
    // 把2D向量当做XZ平面的3D向量来处理
    // 不构建Vector3的边,直接利用叉乘公式计算叉乘后的Y值
    // 然后通过Y值来判定两个2D向量的方向
    // 这里采用顶点顺时针判定
    for (int i = 0, length = polygonVertexsNumber; i < length; i++)
    {
    lineX = point.x - polygonVertexes[i].x;
    lineZ = point.y - polygonVertexes[i].y;
    index = (i + 1) % polygonVertexsNumber;
    edgeX = polygonVertexes[index].x - polygonVertexes[i].x;
    edgeZ = polygonVertexes[index].y - polygonVertexes[i].y;
    // 利用叉乘原始公式进行计算判定两个向量的方向
    // Vector3.Cross(A, B).y > 0 表示A在B的左侧
    // Vector3.Cross(A, B).y < 0 表示A在B的右侧
    // Vector3.Cross(A, B).y == 0 表示A和B平行
    if((lineZ * edgeX - lineX * edgeZ) > 0)
    {
    return false;
    }
    }
    return true;
    }

    PolygonVertexesInfo3

    PointOutOfPolygon3

    PointInPolygon3

    可以看到不通过构建Vector3,我们直接利用叉乘的公式把2D当3D计算出叉乘的Y值,依然可以有效的判定出点是否在凸多边形内。

    在3D维度两个向量不再是在单纯的XZ平面而是在任意平面,这里不再能直接使用叉乘的y值进行判定,除非两个向量在XZ平面。

    Note:

    1. 此方法适用于凸多边形
    2. 注意叉积判定对于多边形顶点的顺序要求
    3. 2维向量叉乘AXB = (0, 0, x1 y2 - x2 y1),x1 y2 - x2 y1大于0表示A在B的右侧,小于0表示A在B的左侧,等于0表示A和B平行(大前提是左手坐标系,右手坐标系左右侧是反过来的)
    4. 叉乘y大于0表示A在B的左侧还是小于0表示A在B的左侧主要看我们用的左手坐标系还是右手坐标系,因为Unity是左手坐标系,所欲叉乘y大于0表示A在B的左侧
    5. 利用叉乘结果y大于0还是y小于0区分方向的同时,我们还能用于推断向量A在向量B的左侧还是右侧(大前提是两个向量在XZ平面)

    圆形与圆形

    圆形和圆形相交判定起始就是2个圆心的距离是否大于两个圆的半径之和。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /// <summary>
    /// 判断圆形与圆形之间的交叉检测
    /// </summary>
    /// <param name="circle1"></param>
    /// <param name="circle2"></param>
    /// <returns></returns>
    public static bool CircleAndCircleIntersection2D(Circle2D circle1, Circle2D circle2)
    {

    return (circle1.Center - circle2.Center).sqrMagnitude < (circle1.Radius + circle2.Radius) * (circle1.Radius + circle2.Radius);
    }

    这个比较简单没什么好说的。

    圆形与胶囊体

    结合前面分离轴定理,我们要想判定圆形和胶囊体是否相交,首先找到分离轴,而圆形和胶囊体的分离轴是胶囊体线段上距离圆形最近的点P与圆心所在的方向。

    所以判定圆心到胶囊体线段的距离是否大于胶囊体半径+圆形半径即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /// <summary>
    /// 判断圆形与胶囊体相交
    /// </summary>
    /// <param name="circle"></param>
    /// <param name="capsule"></param>
    /// <returns></returns>
    public static bool CircleAndCapsuleIntersection2D(Circle2D circle, Capsule2D capsule)
    {

    float sqrdistance = SqrDistanceBetweenSegmentAndPoint(capsule.StartPoint, capsule.PointLine, circle.Center);
    return sqrdistance < (circle.Radius + capsule.Radius) * (circle.Radius + capsule.Radius);
    }

    Circle2DIntersectionWithCapsule2D

    Capsule

    Circle2DIntersectionWithRotateCapsule2D

    Capsule2

    圆形与矩形(AABB)

    利用AABB的对称性,我们以AABB中心为原点,两边为坐标轴的坐标系。通过将圆形移动AABB的大小,然后看圆形所在位置与中心点距离是否还大于圆形半径来判定圆形和AABB是否相交。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /// <summary>
    /// 判断圆形与AABB相交
    /// </summary>
    /// <param name="circle"></param>
    /// <param name="aabb"></param>
    /// <returns></returns>
    public static bool CircleAndAABBIntersection2D(Circle2D circle, AABB2D aabb)
    {

    // 以AABB中心为圆心
    Vector2 v = Vector2.Max(circle.Center - aabb.Center, -(circle.Center - aabb.Center));
    // 把圆心坐标偏移AABB大小
    Vector2 u = Vector2.Max(v - aabb.Extents / 2, Vector2.zero);
    // 判定圆心距离是否还大于圆形半径判定相交
    return u.sqrMagnitude < circle.Radius * circle.Radius;
    }

    Circle2DIntersectionWithAABB

    AABB2DInfo

    圆形与有向包围盒(OBB)

    结合前面OBB的定义:

    有向包围盒(OBB)可以理解成一个带旋转角度的矩形(轴对齐包围盒-AABB)

    可以看出OBB和AABB的主要区别是支持了选择的AABB,那么我们判定OBB和圆形的相交检测也就等价于把圆形旋转到OBB所在坐标系后,然后当做圆形和AABB相交判定即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /// <summary>
    /// Vector3临时变量(用于优化new Vector3)
    /// </summary>
    private static Vector3 Vector3Temp;

    /// <summary>
    /// 判定圆形和OBB相交检测
    /// </summary>
    /// <param name="circle"></param>
    /// <param name="obb"></param>
    /// <returns></returns>
    public static bool CircleAndOBBIntersection2D(Circle2D circle, OBB2D obb)
    {

    // 以OBB中心为坐标原点
    Vector2 point = circle.Center - obb.Center;
    Vector3Temp.x = point.x;
    Vector3Temp.y = 0;
    Vector3Temp.z = point.y;
    // 将圆心通过反向旋转转到OBB坐标系
    Vector3 point2 = Quaternion.AngleAxis(-obb.Angle, Vector3.up) * Vector3Temp;
    point.x = point2.x;
    point.y = point2.z;
    // 将旋转后的圆心位置和OBB做AABB方式的相交判定
    Vector2 v = Vector2.Max(point, -point);
    // 把圆心坐标偏移OBB大小
    Vector2 u = Vector2.Max(v - obb.Extents / 2, Vector2.zero);
    // 判定圆心距离是否还大于圆形半径判定相交
    return u.sqrMagnitude < circle.Radius * circle.Radius;
    }

    Circle2DIntersectionWithOBB2D

    OBB2DInfo

    Note:

    1. Quaternion.AngleAxis()如果绕Y轴旋转直接乘以Vector2会得到错误的结果,所以我们需要当做Vector3来计算,最后再转换回Vector2
    2. 虽然Vector3是结构体不会造成GC,但不想每次判定都创建Vector3所以定义了一个临时Vector3用于计算重用

    圆形与凸多边形

    圆形与凸多边形相交判定只需要在点和凸多边形相交判定上增加圆心不在凸多边形内时,判定圆心到多边形每条边的距离是否有小于圆形半径即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /// <summary>
    /// 判断圆形与多边形相交
    /// </summary>
    /// <param name="circle"></param>
    /// <param name="polygon"></param>
    /// <returns></returns>
    public static bool CircleAndPolygonIntersection2D(Circle2D circle, Polygon2D polygon)
    {

    // 判定圆心是否在多边形内
    if(BetterPointInConvexPolygon2D(polygon, circle.Center))
    {
    return true;
    }
    // 圆心不在多边形内,则判定圆心与每条边的距离是否小于半径
    Vector2 circleCenter = circle.Center;
    float sqrR = circle.Radius * circle.Radius;
    var vertexes = polygon.Vertexes;
    int polygonVertexsNumber = polygon.Vertexes.Length;
    Vector2 edge;
    int index;
    for (int i = 0, length = polygonVertexsNumber; i < length; i++)
    {
    index = (i + 1) % polygonVertexsNumber;
    edge.x = vertexes[index].x - vertexes[i].x;
    edge.y = vertexes[index].y - vertexes[i].y;
    // 判定圆心到单条边的距离是否小于半径
    if(SqrDistanceBetweenSegmentAndPoint(vertexes[i], edge, circleCenter) < sqrR)
    {
    return true;
    }
    }
    return false;
    }

    Circle2DIntersectionPolygon2D

    注释写的很详细了,就不详细解释说明了。

    圆形与扇形

    当扇形角度大于180度时,就不再是凸多边形了,不能适用于分离轴理论。

    我们把相交判定分成两个部分判定:

    1. 圆形在扇形角度内,直接判定扇形原点和圆心距离是否大于扇形半径+圆形半径
    2. 圆形在扇形角度外,利用扇形对称性,将原因映射到扇形一侧,通过映射后的原因与扇形一条边的距离是否小于圆形半径判定相交
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /// <summary>
    /// 判断圆形与扇形相交
    /// </summary>
    /// <param name="circle"></param>
    /// <param name="sector"></param>
    /// <returns></returns>
    public static bool CircleAndSectorIntersection2D(Circle2D circle, Sector2D sector)
    {

    Vector2 tempDistance = circle.Center - sector.StartPoint;
    float halfAngle = Mathf.Deg2Rad * sector.Angle / 2;
    // 判定扇形起点和圆心距离是否小于扇形半径+圆形半径
    if (tempDistance.sqrMagnitude < (sector.Radius + circle.Radius) * (sector.Radius + circle.Radius))
    {
    // 判定圆形是否在扇形角度内
    if (Vector3.Angle(tempDistance, sector.Direction) < sector.Angle / 2)
    {
    return true;
    }
    else
    {
    // 利用扇形的对称性,将圆心位置映射到一侧
    Vector2 targetInsectorAxis = new Vector2(Vector2.Dot(tempDistance, sector.Direction), Mathf.Abs(Vector2.Dot(tempDistance, new Vector2(-sector.Direction.y, sector.Direction.x))));
    // 扇形的一条边
    Vector2 directionInSectorAxis = sector.Radius * new Vector2(Mathf.Cos(halfAngle), Mathf.Sin(halfAngle));
    // 通过判定映射后的原因与扇形一条边的距离是否小于圆形半径判定相交
    return SqrDistanceBetweenSegmentAndPoint(Vector2.zero, directionInSectorAxis, targetInsectorAxis) <= circle.Radius * circle.Radius;
    }
    }
    return false;
    }

    Circle2DIntersectionWithSector2D

    Sector2DInfo

    Note:

    1. 当扇形角度大于180度时,就不再是凸多边形了,不能适用于分离轴理论。

    矩形(AABB)与矩形(AABB)

    AABB与AABB的相交判定,考虑到AABB的轴都相同且都为矩形的特殊性,相交判定只需要判定两个AABB原点的距离是否有任何一个小于宽/2之和或长/2之和即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /// <summary>
    /// 判定AABB和AABB相交检测
    /// </summary>
    /// <param name="aabb1"></param>
    /// <param name="aabb2"></param>
    /// <returns></returns>
    public static bool AABBAndAABBIntersection2D(AABB2D aabb1, AABB2D aabb2)
    {

    // 两个AABB原点的位置偏移
    Vector2 offset = aabb2.Center - aabb1.Center;
    offset = Vector2.Max(offset, -offset);
    // 判定偏移是否小于两者宽/2之和和两者长/2之和
    return offset.x <= (aabb1.Extents.x / 2 + aabb2.Extents.x / 2) && offset.y <= (aabb1.Extents.y / 2 + aabb2.Extents.y / 2);
    }

    AABB2DIntersectionWithAABB2D

    AABB2DInfo1

    AABB2DInfo2

    AABB和AABB的判定比较简单,就不详细解释了。

    进阶学习

    关于OBB等凸多边形相关的交叉判定,我们需要用到分离轴定理,这里暂时不深入实现了(TODO),详情参考:

    UNITY实战进阶-OBB包围盒详解-6

    关于3D的相交检测,这里我们用Unity Bounds提供的关于OBB的实现类。

    更多的3D相交检测自定义实现,可以参考二维的实现扩展到3维,这里暂时不做进一步的学习记录。(大部分时候我们可以简化相交检测,比如讲3D简化到2D,扇形椭圆简化到圆形或点来实现相交碰撞检测)

    线段相交

    线段相交理论知识参考:

    Unity3D C#数学系列之判断两条线段是否相交并求交点

    LineIntersectionPoint

    相交大前提:

    1. AB和CD必须共面。

    如何判定AB和CD共面了?

    通过计算ACD平面法线是否与AB垂直即可(2D向量可以省去这一步)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /// <summary>
    /// 4个点是否共面
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="c"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static bool IsCoplanarVector(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
    {

    // 通过叉乘计算CA和CD的法线向量
    var ab = b - a;
    var ca = a - c;
    var cd = d - c;
    var normal = Vector3.Cross(ca, cd);
    // 通过ab向量和CA和CD法线向量的点乘是否为0来判定是否垂直
    // a,b,c,d从而判定出是否共面
    if(Mathf.Approximately(Vector3.Dot(normal, ab), Mathf.Epsilon))
    {
    return true;
    }
    return false;
    }

    如何判定相交:

    1. 快速排斥和跨立实现判定是否相交

    这里我采用跨立法来判定,跨立法是指两个线段相交必须满足两个线段的两个点分别在另一个线段的两侧

    这里我们利用叉乘来判定两个向量的方向从而判定是否线段在另一个线段两侧。

    2D向量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    /// <summary>
    /// 2D向量叉乘
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <returns></returns>
    public static float Cross(Vector2 a, Vector2 b)
    {

    return a.x * b.y - b.x * a.y;
    }

    /// <summary>
    /// 4个点是否ab和cd线段是否相交
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="c"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static bool IsCross(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
    {

    // 通过叉乘计算CA和CD的法线向量
    var ab = b - a;
    var ac = c - a;
    var ad = d - a;
    // 叉乘判定两个向量的方向
    // 判定ab是否在cd两侧
    // 以ab为基准,如果ab叉乘ac或ad大于0表示逆时针反之顺时针,0表示两者方向相同
    // 那么c和d要在ab同一侧,则ab叉乘ac的结果和ab叉乘ad的结果相乘需要>0
    if (Vector2Utilities.Cross(ab, ac) * Vector2Utilities.Cross(ab, ad) >= 0)
    {
    return false;
    }
    // 同理以cd为基准,如果ca和cb叉乘结果相乘>0则表示a和b在cd同一侧
    var ca = a - c;
    var cb = b - c;
    var cd = d - c;
    if (Vector2Utilities.Cross(cd, ca) * Vector2Utilities.Cross(cd, cb) >= 0)
    {
    return false;
    }
    return true;
    }

    3D向量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    /// <summary>
    /// 4个点是否ab和cd线段是否相交
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="c"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static bool IsCross(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
    {

    // 通过叉乘计算CA和CD的法线向量
    var ab = b - a;
    var ac = c - a;
    var ad = d - a;
    // 叉乘判定两个向量的方向
    // 判定ab是否在cd两侧
    // 以ab为基准,如果ac和ad叉乘结果再点乘>0则表示c和d在ab同一侧
    // 3D向量叉乘是向量,通过计算两个叉乘向量的点乘判定是否同方向从而判定是否在一侧
    if (Vector3.Dot(Vector3.Cross(ab, ac), Vector3.Cross(ab, ad)) >= 0)
    {
    return false;
    }
    // 同理以cb为基准,如果ca和cb叉乘结果再点乘>0表示a和b在cd同一侧
    var ca = a - c;
    var cb = b - c;
    var cd = d - c;
    if (Vector3.Dot(Vector3.Cross(cd, ca), Vector3.Cross(cd, cb)) >= 0)
    {
    return false;
    }
    return true;
    }

    如何计算交点:

    1. 几何法分析出交点

    让我们直接看下结论:

    LineIntersectionFormula

    这个结论主要是通过2D平面几何的相似三角形以及向量叉乘在的集合解释是平行四边形面积(两个三角形面积之和)推到而来的。

    LineIntersectionPicture

    详情推到参考:

    Unity3D C#数学系列之判断两条线段是否相交并求交点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /// <summary>
    /// 计算AB与CD两条线段的交点.
    /// </summary>
    /// <param name="a">A点</param>
    /// <param name="b">B点</param>
    /// <param name="c">C点</param>
    /// <param name="d">D点</param>
    /// <param name="intersectPos">AB与CD的交点</param>
    /// <returns>是否相交 true:相交 false:未相交</returns>
    public static bool GetIntersectPoint(Vector2 a, Vector2 b, Vector2 c, Vector2 d, out Vector2 intersectPos)
    {

    intersectPos = Vector2.zero;
    // Note:
    // 1. 2D向量不需要判定共面
    if (!IsCross(a, b, c, d))
    {
    return false;
    }
    // 根据推到结论
    // AO / AB = Cross(CA, CD) / Cross(CD, AB)
    // O = A + Cross(CA, CD) / Cross(CD, AB) * AB
    var ab = b - a;
    var ca = a - c;
    var cd = d - c;
    Vector3 v1 = Vector3.Cross(ca, cd);
    Vector3 v2 = Vector3.Cross(cd, ab);
    float ratio = Vector3.Dot(v1, v2) / v2.sqrMagnitude;
    intersectPos = a + ab * ratio;
    return true;
    }

    LineIntersectionPointCapture

    可以看到我们成功可视化算出了两条线段相交的点。

    那么如果我们不考虑两条线段相交,单纯想求得两个线段(包含延长线)上的交点时,应该怎么计算了?

    实际上相似三角形的推论即使两个线段不相交也是成立的,所以我们唯一要确保的就是两个线段不平行,只有两个线段不平行才有交点。所以我们不需要判定线段两个点是否在两侧只需判定两个线段是否平行,剩下的步骤和之前一致即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    /// <summary>
    /// 4个点是否ab和cd线段是否平行
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="c"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static bool IsParallel(Vector2 a, Vector2 b, Vector2 c, Vector2 d)
    {

    // 通过叉乘计算AB和CD是否为0判定是否平行
    var ab = b - a;
    var cd = d - c;
    return Mathf.Approximately(Vector2Utilities.Cross(ab, cd), Mathf.Epsilon);
    }

    /// <summary>
    /// 计算AB与CD两条线段的交点(包含衍生直线)
    /// </summary>
    /// <param name="a">A点</param>
    /// <param name="b">B点</param>
    /// <param name="c">C点</param>
    /// <param name="d">D点</param>
    /// <param name="intersectPos">AB与CD的交点</param>
    /// <returns>是否相交 true:相交 false:未相交</returns>
    public static bool GetLineIntersectPoint(Vector2 a, Vector2 b, Vector2 c, Vector2 d, out Vector2 intersectPos)
    {

    intersectPos = Vector2.zero;
    // Note:
    // 1. 2D向量不需要判定共面
    if (IsParallel(a, b, c, d))
    {
    return false;
    }
    // 根据推到结论
    // AO / AB = Cross(CA, CD) / Cross(CD, AB)
    // O = A + Cross(CA, CD) / Cross(CD, AB) * AB
    var ab = b - a;
    var ca = a - c;
    var cd = d - c;
    Vector3 v1 = Vector3.Cross(ca, cd);
    Vector3 v2 = Vector3.Cross(cd, ab);
    float ratio = Vector3.Dot(v1, v2) / v2.sqrMagnitude;
    intersectPos = a + ab * ratio;
    return true;
    }

    LineIntersectionPoint2Capture

    可以看到我们通过不判定线段点是否在两侧,只判定平时依然计算出了两个线段在延长线上的交点。

    实战

    待添加……

    注意事项

    • 复杂的3D碰撞检测我们可以映射到2D来简化运算量实现相同的效果

    • 能够用简单的形状实现效果尽量用简单的形状来实现避免过于复杂的碰撞检测判定

    • 在坐标平移旋转转换时注意先旋转后平移,不然先平移后旋转会导致坐标转换错误,因为先旋转后平移!=先平移后旋转

    Github

    CollisionDetectionStudy

    学习总结

    1. 2D图形相交检测更多的是平面几何知识,通过将图形转换成纯数学定义来求解
    2. 向量里的点乘叉乘对于求解向量的角度和旋转方向以及两个向量的相对位置很有用

    Reference

    【Unity】图形相交检测

    3D 碰撞检测

    Unity3D-游戏中的技能碰撞检测

    [包围盒]球,AABB,OBB

    UNITY实战进阶-OBB包围盒详解-6

    unity3d:两条线段相交并求交点坐标

    Unity3D C#数学系列之判断两条线段是否相交并求交点

    文章目錄
    1. 1. Introduction
    2. 2. 碰撞检测
      1. 2.1. What
      2. 2.2. Why
      3. 2.3. How
        1. 2.3.1. 功能需求
        2. 2.3.2. 理论知识
          1. 2.3.2.1. 2D形状
            1. 2.3.2.1.1.
            2. 2.3.2.1.2. 圆形
            3. 2.3.2.1.3. 矩形(轴对齐包围盒-AABB)
            4. 2.3.2.1.4. 有向包围盒(OBB)
            5. 2.3.2.1.5. 胶囊体
            6. 2.3.2.1.6. 扇形
            7. 2.3.2.1.7. 凸多边形
        3. 2.3.3. 分离轴定理
        4. 2.3.4. 形状碰撞检测
          1. 2.3.4.1. 点与圆形
          2. 2.3.4.2. 点与矩形(AABB)
          3. 2.3.4.3. 点与多边形
            1. 2.3.4.3.0.1. 角度和法
            2. 2.3.4.3.0.2. 叉积(点线)法
        5. 2.3.4.4. 圆形与圆形
        6. 2.3.4.5. 圆形与胶囊体
        7. 2.3.4.6. 圆形与矩形(AABB)
        8. 2.3.4.7. 圆形与有向包围盒(OBB)
        9. 2.3.4.8. 圆形与凸多边形
        10. 2.3.4.9. 圆形与扇形
        11. 2.3.4.10. 矩形(AABB)与矩形(AABB)
        12. 2.3.4.11. 进阶学习
      4. 2.3.5. 线段相交
  • 3. 实战
  • 4. 注意事项
  • 5. Github
  • 6. 学习总结
  • 7. Reference