14.数学模块

14.数学模块


14.1 知识点

数学模块的作用

在游戏开发中,我们经常需要进行各种通用的数学计算和几何判断,例如:

  • 角度和弧度转换:在不同计算场景中需要进行单位转换
  • 距离计算:判断两个对象之间的距离,常用于攻击范围、移动距离等
  • 位置判断:判断对象是否在屏幕外、是否在扇形攻击范围内
  • 射线检测:用于鼠标点击选择、子弹碰撞检测等
  • 范围检测:检测指定范围内的所有对象,用于技能范围、爆炸范围等

常见场景:

  • 计算玩家与敌人的XZ平面距离(忽略高度差)
  • 判断技能释放时目标是否在前方扇形范围内
  • 鼠标点击屏幕,通过射线检测选中3D对象
  • 释放范围技能时,检测范围内的所有敌人
  • 判断UI提示是否超出屏幕范围需要隐藏

传统方式的问题:

  1. 重复编写代码:每次都需要重新编写距离计算、角度判断等逻辑
  2. 代码分散:数学计算逻辑分散在各个模块中,难以统一管理
  3. 容易出错:手动计算距离、角度等容易出现错误
  4. 缺乏封装:Unity的API需要多次调用和组合,使用不够便捷

解决方案:
通过统一的数学工具类,封装常用的数学计算和几何判断逻辑,提供一致的API接口,简化开发流程。

主要功能:

  1. 角度和弧度转换:封装Unity的Deg2Rad和Rad2Deg常量
  2. 平面距离计算:支持XZ平面和XY平面的距离计算
  3. 屏幕外判断:判断世界坐标点是否在屏幕可见范围外
  4. 扇形范围判断:判断目标是否在指定扇形攻击范围内
  5. 射线检测封装:简化Unity射线检测的使用,支持回调
  6. 范围检测封装:封装盒装检测和球体检测,支持泛型回调

数学模块的基本原理

设计理念:

  • 工具类设计:使用静态类提供工具方法,无需实例化
  • 委托回调:检测方法使用委托回调,便于灵活处理结果
  • 泛型支持:支持获取不同类型的对象(Collider、GameObject、组件)
  • 平面计算:针对2D和3D游戏提供不同平面的距离计算
  • 性能优化:直接使用Unity的高效API,减少额外开销

核心功能:

  1. 角度转换:基于Mathf.Deg2Rad和Mathf.Rad2Deg封装
  2. 距离计算:通过将Vector3的某个分量置0实现平面距离计算
  3. 屏幕判断:使用Camera.WorldToScreenPoint转换坐标后判断
  4. 扇形判断:结合距离和角度双重判断(距离<=半径 && 角度<=扇形角/2)
  5. 射线检测:封装Physics.Raycast和Physics.RaycastAll
  6. 范围检测:封装Physics.OverlapBox和Physics.OverlapSphere

数学模块实现

角度和弧度转换

Unity中的Mathf类虽然提供了Deg2RadRad2Deg转换系数,但直接使用容易出错,例如:

  • 容易混淆乘法和除法
  • 需要记住什么时候用哪个系数
  • 代码可读性较差

封装的优势:

  1. 避免错误:无需手动进行乘法运算,避免使用错误的系数
  2. 提高可读性:方法名清晰表达意图,代码更易理解
  3. 统一接口:外部只需传入角度或弧度,直接得到结果

实现代码:

/// <summary>
/// 角度转弧度的函数
/// </summary>
/// <param name="deg">角度值</param>
/// <returns>弧度值</returns>
public static float Deg2Rad(float deg)
{
    return deg * Mathf.Deg2Rad;
}

/// <summary>
/// 弧度转角度的函数
/// </summary>
/// <param name="rad">弧度值</param>
/// <returns>角度值</returns>
public static float Rad2Deg(float rad)
{
    return rad * Mathf.Rad2Deg;
}

使用示例:

// 场景1:角度转弧度
float angle = 90f;
float radian = MathUtil.Deg2Rad(angle);
Debug.Log($"90度 = {radian}弧度");  
// 输出:90度 = 1.570796弧度(约等于π/2)

// 场景2:弧度转角度
float rad = Mathf.PI;  // π弧度
float degree = MathUtil.Rad2Deg(rad);
Debug.Log($"π弧度 = {degree}度");  
// 输出:π弧度 = 180度

// 场景3:常用角度转换
float[] commonAngles = { 0f, 90f, 180f, 270f, 360f };
foreach (float deg in commonAngles)
{
    float rad = MathUtil.Deg2Rad(deg);
    Debug.Log($"{deg}度 = {rad}弧度");
}
// 输出:
// 0度 = 0弧度
// 90度 = 1.570796弧度
// 180度 = 3.141593弧度
// 270度 = 4.712389弧度
// 360度 = 6.283185弧度

距离计算相关

在游戏开发中,我们经常需要计算对象之间的平面距离,而不考虑高度差:

应用场景:

  1. 2D游戏:只需要计算XY平面上的距离,Z轴变化不影响距离
  2. 3D游戏:经常需要计算XZ平面上的距离,Y轴(高度)变化不影响距离计算
    • 例如:地面移动的角色,只关心水平距离
    • 空中飞行物虽然高度不同,但攻击范围是平面的

实现代码:

/// <summary>
/// 获取XZ平面上两点的距离
/// </summary>
/// <param name="srcPos">点1</param>
/// <param name="targetPos">点2</param>
/// <returns>距离值</returns>
public static float GetObjDistanceXZ(Vector3 srcPos, Vector3 targetPos)
{
    srcPos.y = 0;
    targetPos.y = 0;
    return Vector3.Distance(srcPos, targetPos);
}

/// <summary>
/// 判断两点之间的距离是否小于等于目标距离(XZ平面)
/// </summary>
/// <param name="srcPos">点1</param>
/// <param name="targetPos">点2</param>
/// <param name="dis">距离</param>
/// <returns>是否在范围内</returns>
public static bool CheckObjDistanceXZ(Vector3 srcPos, Vector3 targetPos, float dis)
{
    return GetObjDistanceXZ(srcPos, targetPos) <= dis;
}

/// <summary>
/// 获取XY平面上两点的距离
/// </summary>
/// <param name="srcPos">点1</param>
/// <param name="targetPos">点2</param>
/// <returns>距离值</returns>
public static float GetObjDistanceXY(Vector3 srcPos, Vector3 targetPos)
{
    srcPos.z = 0;
    targetPos.z = 0;
    return Vector3.Distance(srcPos, targetPos);
}

/// <summary>
/// 判断两点之间的距离是否小于等于目标距离(XY平面)
/// </summary>
/// <param name="srcPos">点1</param>
/// <param name="targetPos">点2</param>
/// <param name="dis">距离</param>
/// <returns>是否在范围内</returns>
public static bool CheckObjDistanceXY(Vector3 srcPos, Vector3 targetPos, float dis)
{
    return GetObjDistanceXY(srcPos, targetPos) <= dis;
}

使用示例:

// 场景1:计算XZ平面距离(3D游戏中的水平距离)
Vector3 playerPos = new Vector3(0, 2, 0);      // 玩家位置,高度2米
Vector3 enemyPos = new Vector3(5, 10, 3);      // 敌人位置,高度10米
float distance = MathUtil.GetObjDistanceXZ(playerPos, enemyPos);
Debug.Log($"玩家与敌人的XZ平面距离:{distance}米");
// 输出:玩家与敌人的XZ平面距离:5.830952米(忽略高度差)

// 场景2:判断是否在攻击范围内
Vector3 player = new Vector3(0, 0, 0);
Vector3 target = new Vector3(3, 0, 4);
float attackRange = 5f;
bool inRange = MathUtil.CheckObjDistanceXZ(player, target, attackRange);
Debug.Log($"目标是否在攻击范围内:{inRange}");
// 输出:目标是否在攻击范围内:True(距离5米,在5米范围内)

// 场景3:XY平面距离(2D游戏)
Vector3 pos1 = new Vector3(1, 2, 0);
Vector3 pos2 = new Vector3(4, 5, 100);  // Z轴变化不影响
float distXY = MathUtil.GetObjDistanceXY(pos1, pos2);
Debug.Log($"XY平面距离:{distXY}");
// 输出:XY平面距离:4.242641米(忽略Z轴)

// 场景4:实际应用 - 检测附近的敌人
Vector3 characterPos = new Vector3(0, 0, 0);
List<Vector3> enemies = new List<Vector3>
{
    new Vector3(3, 0, 4),   // 距离5米
    new Vector3(8, 0, 6),   // 距离10米
    new Vector3(2, 5, 1)    // 距离2.236米(忽略高度)
};
float detectRange = 7f;
foreach (var enemy in enemies)
{
    if (MathUtil.CheckObjDistanceXZ(characterPos, enemy, detectRange))
 syn       
        Debug.Log($"发现敌人在范围内,距离:{MathUtil.GetObjDistanceXZ(characterPos, enemy)}米");
    }
}
// 输出:
// 发现敌人在范围内,距离:5米
// 发现敌人在范围内,距离:2.236068米

屏幕外判断

在游戏开发中,经常需要判断对象是否在屏幕可见范围内:

应用场景:

  • UI提示管理:当对象超出屏幕时隐藏UI提示
  • 对象优化:屏幕外的对象可以降低更新频率
  • 特效管理:屏幕外的特效可以暂停或隐藏

实现代码:

/// <summary>
/// 判断世界坐标系下的某一坐标点是否在屏幕可见范围外
/// </summary>
/// <param name="pos">世界坐标系下的一个点的位置</param>
/// <returns>如果不在可见范围外返回true,否则返回false</returns>
public static bool IsWorldPosOutScreen(Vector3 pos)
{
    // 将世界坐标转换为屏幕坐标
    Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
    // 判断是否在屏幕范围内
    if (screenPos.x >= 0 && screenPos.x <= Screen.width &&
        screenPos.y >= 0 && screenPos.y <= Screen.height)
    {
        return false;
    }
    return true;
}

使用示例:

// 场景1:判断对象是否在屏幕外
Vector3 objectPos = new Vector3(10, 5, 20);
bool isOutOfScreen = MathUtil.IsWorldPosOutScreen(objectPos);
if (isOutOfScreen)
{
    Debug.Log("对象在屏幕外,隐藏UI提示");
    // 隐藏UI提示
    uiHint.SetActive(false);
}
else
{
    Debug.Log("对象在屏幕内,显示UI提示");
    // 显示UI提示
    uiHint.SetActive(true);
}

// 场景2:优化屏幕外的特效
void Update()
{
    Vector3 effectPos = particleEffect.transform.position;
    if (MathUtil.IsWorldPosOutScreen(effectPos))
    {
        // 屏幕外暂停特效播放
        particleEffect.Stop();
    }
    else
    {
        // 屏幕内恢复播放
        particleEffect.Play();
    }
}

// 场景3:批量检查多个对象
List<Transform> npcs = new List<Transform>();  // NPC列表
foreach (var npc in npcs)
{
    if (MathUtil.IsWorldPosOutScreen(npc.position))
    {
        // 屏幕外的NPC降低更新频率或暂停AI
        npc.GetComponent<NPCController>().SetUpdateRate(0.5f);
    }
    else
    {
        // 屏幕内的NPC正常更新
        npc.GetComponent<NPCController>().SetUpdateRate(1f);
    }
}

扇形范围判断

在游戏开发中,经常需要判断目标是否在角色的前方扇形攻击范围内:

应用场景:

  • 近战攻击范围:判断敌人是否在前方扇形范围内
  • 技能释放范围:扇形技能的范围判断建筑
  • 视野范围:NPC的视野范围判断
  • 触发条件:进入扇形范围后触发攻击行为

实现代码:

/// <summary>
/// 判断某一目标位置是否在指定扇形范围内
/// 注意:这里传入的位置和朝向都是基于同一坐标系下的
/// </summary>
/// <param name="pos">扇形中心的位置</param>
/// <param name="forward">自己的朝向</param>
/// <param name="targetPos">目标位置</param>
/// <param name="radius">半径</param>
/// <param name="angle">扇形的角度</param>
/// <returns>是否在扇形范围内</returns>
public static bool IsInSectorRangeXZ(Vector3 pos, Vector3 forward, Vector3 targetPos, float radius, float angle)
{
    pos.y = 0;
    forward.y = 0;
    targetPos.y = 0;
    // 距离 + 角度
    return Vector3.Distance(pos, targetPos) <= radius && Vector3.Angle(forward, targetPos - pos) <= angle / 2f;
}

使用示例:

// 场景1:判断敌人是否在攻击范围内
Vector3 playerPos = new Vector3(0, 0, 0);
Vector3 playerForward = Vector3.forward;  // 玩家朝向Z轴正方向
Vector3 enemyPos = new Vector3(2, 0, 3);  // 敌人位置
float attackRadius = 5f;                  // 攻击半径5米
float attackAngle = 90f;                  // 攻击角度90度

bool canAttack = MathUtil.IsInSectorRangeXZ(playerPos, playerForward, enemyPos, attackRadius, attackAngle);
if (canAttack)
{
    Debug.Log("敌人在攻击范围内,可以攻击");
    // 执行攻击逻辑
}
else
{
    Debug.Log("敌人不在攻击范围内");
}

// 场景2:扇形技能范围检测
Vector3 skillCenter = transform.position;
Vector3 skillDirection = transform.forward;  // 技能释放方向
float skillRadius = 10f;
float skillAngle = 120f;  // 120度扇形

List<GameObject> enemies = GetAllEnemies();
foreach (var enemy in enemies)
{
    Vector3 enemyPos = enemy.transform.position;
    if (MathUtil.IsInSectorRangeXZ(skillCenter, skillDirection, enemyPos, skillRadius, skillAngle))
    {
        Debug.Log($"敌人 {enemy.name} 在技能范围内,受到伤害");
        // 对敌人造成伤害
        enemy.GetComponent<EnemyHealth>().TakeDamage(skillDamage);
    }
}

// 场景3:NPC视野范围检测
Vector3 npcPos = npc.transform.position;
Vector3 npcForward = npc.transform.forward;
float viewRadius = 15f;
float viewAngle = 60f;  // 视野角度60度

Vector3 playerPosition = player.transform.position;
if (MathUtil.IsInSectorRangeXZ(npcPos, npcForward, playerPosition, viewRadius, viewAngle))
{
    Debug.Log("玩家进入NPC视野,触发警戒");
    npc.GetComponent<NPCController>().EnterAlertState();
}

// 测试用例说明:
// 传入参数:pos(0,0,0), forward(0,0,1), targetPos(3,0,5), radius=5, angle=90
// 计算结果:
// - XZ平面距离:√(3²+5²) = √34 ≈ 5.83米 > 5米(超出范围)
// - 返回结果:false(不在扇形范围内)

// 传入参数:pos(0,0,0), forward(0,0,1), targetPos(2,0,3), radius=5, angle=90
// 计算结果:
// - XZ平面距离:√(2²+3²) = √13 ≈ 3.61米 ≤ 5米(距离满足)
// - 角度:(2,0,3)与(0,0,1)的夹角 ≈ 33.69° ≤ 45°(角度满足)
// - 返回结果:true(在扇形范围内)

射线检测

Unity的射线检测API虽然功能强大,但使用相对繁琐:

传统使用方式的问题:

  1. 需要手动处理RaycastHit:需要声明变量和out参数
  2. 返回值处理复杂:需要判断是否命中,再获取对象
  3. 获取组件需要额外步骤:从GameObject获取组件需要额外调用职责

封装的优势:

  1. 委托回调:通过委托直接处理结果,代码更简洁
  2. 多种重载:支持获取RaycastHit、GameObject或指定组件
  3. 批量检测:封装RaycastAll,支持一次检测多个对象
  4. 参数统一:统一处理距离、层级等参数

实现代码:

/// <summary>
/// 射线检测,获取一个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去)</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCast(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
{
    RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
    {
        callBack.Invoke(hitInfo);
    }
}

/// <summary>
/// 射线检测,获取一个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去)</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCast(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
{
    RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
    {
        callBack.Invoke(hitInfo.collider.gameObject);
    }
}

/// <summary>
/// 射线检测,获取一个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的对象信息上挂在的指定脚本传递出去)</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCast<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
{
    RaycastHit hitInfo;
    if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
    {
        callBack.Invoke(hitInfo.collider.gameObject.GetComponent<T>());
    }
}

/// <summary>
/// 射线检测,获取到多个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去),每一个对象都会调用一次</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCastAll(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
{
    RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
    for (int i = 0; i < hitInfos.Length; i++)
    {
        callBack.Invoke(hitInfos[i]);
    }
}

/// <summary>
/// 射线检测,获取到多个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去),每一个对象都会调用一次</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCastAll(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
{
    RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
    for (int i = 0; i < hitInfos.Length; i++)
    {
        callBack.Invoke(hitInfos[i].collider.gameObject);
    }
}

/// <summary>
/// 射线检测,获取到多个对象,指定距离,指定层级
/// </summary>
/// <param name="ray">射线</param>
/// <param name="callBack">回调函数(会把碰到的对象信息上依附的脚本传递出去),每一个对象都会调用一次</param>
/// <param name="maxDistance">最大距离</param>
/// <param name="layerMask">层级筛选</param>
public static void RayCastAll<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
{
    RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
    for (int i = 0; i < hitInfos.Length; i++)
    {
        callBack.Invoke(hitInfos[i].collider.gameObject.GetComponent<T>());
    }
}

使用示例:

// 场景1:鼠标点击选中3D对象
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        int layerMask = LayerMask.GetMask("Enemy", "NPC");  // 只检测敌人和NPC层
        
        // 使用GameObject回调
        MathUtil.RayCast(ray, (GameObject hitObj) =>
        {
            Debug.Log($"点击到了:{hitObj.name}");
            // 选中对象逻辑
            hitObj.GetComponent<ISelectable>()?.OnSelected();
        }, 100f, layerMask);
    }
}

// 场景2:获取射线命中的组件
Ray ray = new Ray(transform.position, transform.forward);
int enemyLayer = LayerMask.GetMask("Enemy");

// 使用泛型直接获取组件
MathUtil.RayCast<EnemyController>(ray, (enemy) =>
{
    if (enemy != null)
    {
        Debug.Log($"检测到敌人:{enemy.EnemyName}");
        enemy.TakeDamage(10);
    }
}, 50f, enemyLayer);

// 场景3:子弹穿透多个目标
void FireBullet()
{
    Ray ray = new Ray(transform.position, transform.forward);
    int hitLayer = LayerMask.GetMask("Enemy");
    
    // 使用RayCastAll检测所有命中的对象
    MathUtil.RayCastAll(ray, (RaycastHit hit) =>
    {
        Debug.Log($"子弹命中:{hit.collider.name},距离:{hit.distance}米");
        // 对每个命中的敌人造成伤害
        hit.collider.GetComponent<EnemyHealth>()?.TakeDamage(bulletDamage);
    }, 100f, hitLayer);
}

// 场景4:获取射线命中的所有敌人组件
Ray detectionRay = Camera.main.ScreenPointToRay(Input.mousePosition);
int enemyLayerMask = 1 << LayerMask.NameToLayer("Enemy");

MathUtil.RayCastAll<EnemyController>(detectionRay, (enemy) =>
{
    if (enemy != null)
    {
        Debug.Log($"检测到敌人:{enemy.name}");
        enemy.ShowHighlight();  // 显示高亮
    }
}, 200f, enemyLayerMask);

// 测试用例说明:
// 传入参数:ray(起点(0,0,0),方向(0,0,1)),maxDistance=100,layerMask包含Enemy层
// 假设场景中有一个Enemy对象在(0,0,50)位置
// 计算结果:射线命中Enemy对象,回调函数被调用,传递Enemy的GameObject或组件

范围检测

Unity的范围检测API同样需要进行二次封装以简化使用:

应用场景:

  • 技能范围检测:检测技能范围内的所有敌人
  • 爆炸范围:检测爆炸范围内的所有对象
  • 触发区域:检测区域内的玩家或NPC
  • AI感知范围:检测AI感知范围内的目标

封装的优势:

  1. 盒装检测:支持3D盒状区域的检测
  2. 球体检测:支持球状范围的检测
  3. 泛型支持:支持获取Collider、GameObject或组件类型
  4. 委托回调:每个检测到的对象都会触发回调

实现代码:

/// <summary>
/// 进行盒装范围检测
/// </summary>
/// <typeparam name="T">想要获取的信息类型,可以填写Collider、GameObject以及对象上依附的组件类型</typeparam>
/// <param name="center">盒装中心点</param>
/// <param name="rotation">盒子的角度</param>
/// <param name="halfExtents">长宽高的一半</param>
/// <param name="layerMask">层级筛选</param>
/// <param name="callBack">回调函数</param>
public static void OverlapBox<T>(Vector3 center, Quaternion rotation, Vector3 halfExtents, int layerMask, UnityAction<T> callBack) where T : class
{
    Type type = typeof(T);
    Collider[] colliders = Physics.OverlapBox(center, halfExtents, rotation, layerMask, QueryTriggerInteraction.Collide);
    for (int i = 0; i < colliders.Length; i++)
    {
        if (type == typeof(Collider))
        {
            callBack.Invoke(colliders[i] as T);
        }
        else if (type == typeof(GameObject))
        {
            callBack.Invoke(colliders[i].gameObject as T);
        }
        else
        {
            callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
        }
    }
}

/// <summary>
/// 进行球体范围检测
/// </summary>
/// <typeparam name="T">想要获取的信息类型,可以填写Collider、GameObject以及对象上依附的组件类型</typeparam>
/// <param name="center">球体的中心点</param>
/// <param name="radius">球体的半径</param>
/// <param name="layerMask">层级筛选</param>
/// <param name="callBack">回调函数</param>
public static void OverlapSphere<T>(Vector3 center, float radius, int layerMask, UnityAction<T> callBack) where T : class
{
    Type type = typeof(T);
    Collider[] colliders = Physics.OverlapSphere(center, radius, layerMask, QueryTriggerInteraction.Collide);
    for (int i = 0; i < colliders.Length; i++)
    {
        if (type == typeof(Collider))
        {
            callBack.Invoke(colliders[i] as T);
        }
        else if (type == typeof(GameObject))
        {
            callBack.Invoke(colliders[i].gameObject as T);
        }
        else
        {
            callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
        }
    }
}

使用示例:

// 场景1:球体范围技能检测所有敌人
void CastAOE技能()
{
    Vector3 skillCenter = transform.position;
    float skillRadius = 5f;
    int enemyLayer = LayerMask.GetMask("Enemy");
    
    // 检测范围内的所有敌人
    MathUtil.OverlapSphere<EnemyController>(skillCenter, skillRadius, enemyLayer, (enemy) =>
    {
        if (enemy != null)
        {
            Debug.Log($"技能命中敌人:{enemy.name}");
            enemy.TakeDamage(skillDamage);
            // 添加击退效果
            enemy.ApplyKnockback(skillCenter, knockbackForce);
        }
    });
}

// 场景2:爆炸范围检测所有可破坏对象
void Explode()
{
    Vector3 explosionCenter = transform.position;
    float explosionRadius = 10f;
    int destructibleLayer = LayerMask.GetMask("Destructible", "Enemy");
    
    // 获取范围内的GameObject
    MathUtil.OverlapSphere<GameObject>(explosionCenter, explosionRadius, destructibleLayer, (obj) =>
    {
        Debug.Log($"爆炸波及:{obj.name}");
        // 造成伤害或破坏
        obj.GetComponent<IDestructible>()?.Destroy();
    });
}

// 场景3:盒装检测区域内的NPC
void CheckTriggerZone()
{
    Vector3 zoneCenter = transform.position;
    Quaternion zoneRotation = transform.rotation;
    Vector3 halfSize = new Vector3(5f, 2f, 5f);  // 10x4x10的盒状区域
    int npcLayer = LayerMask.GetMask("NPC");
    
    // 检测盒状区域内的NPC
    MathUtil.OverlapBox<NPCController>(zoneCenter, zoneRotation, halfSize, npcLayer, (npc) =>
    {
        if (npc != null)
        {
            Debug.Log($"NPC {npc.name} 进入触发区域");
            npc.OnEnterTriggerZone();
        }
    });
}

// 场景4:获取范围内的Collider(用于物理交互)
void CheckPhysicsObjects()
{
    Vector3 checkPos = transform.position;
    float checkRadius = 3f;
    int allLayers = -1;  // 检测所有层
    
    // 直接获取Collider
    MathUtil.OverlapSphere<Collider>(checkPos, checkRadius, allLayers, (collider) =>
    {
        Debug.Log($"检测到碰撞体:{collider.name}");
        // 可以对碰撞体进行物理操作
        Rigidbody rb = collider.attachedRigidbody;
        if (rb != null)
        {
            rb.AddExplosionForce(100f, checkPos, checkRadius);
        }
    });
}

// 测试用例说明:
// 传入参数:center(0,0,0), radius=5, layerMask包含Enemy层
// 假设场景中有3个Enemy对象:
// - Enemy1在(3,0,0),距离3米(在范围内)
// - Enemy2在(6,0,0),距离6米(超出范围)
// - Enemy3在(0,0,4),距离4米(在范围内)
// 计算结果:回调函数会被调用2次,分别传递Enemy1和Enemy3的组件

14.2 知识点代码

MathUtil.cs(数学工具类)

using System;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// 数学工具类
/// 提供常用的数学计算、距离计算、位置判断、射线检测和范围检测功能
/// </summary>
public class MathUtil
{
    #region 角度和弧度

    /// <summary>
    /// 角度转弧度的方法
    /// </summary>
    /// <param name="deg">角度值</param>
    /// <returns>弧度值</returns>
    public static float Deg2Rad(float deg)
    {
        return deg * Mathf.Deg2Rad;
    }

    /// <summary>
    /// 弧度转角度的方法
    /// </summary>
    /// <param name="rad">弧度值</param>
    /// <returns>角度值</returns>
    public static float Rad2Deg(float rad)
    {
        return rad * Mathf.Rad2Deg;
    }

    #endregion

    #region 距离计算

    /// <summary>
    /// 获取XZ平面上两点的距离
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <returns>距离值</returns>
    public static float GetObjDistanceXZ(Vector3 srcPos, Vector3 targetPos)
    {
        srcPos.y = 0;
        targetPos.y = 0;
        return Vector3.Distance(srcPos, targetPos);
    }

    /// <summary>
    /// 判断两点之间距离是否小于等于目标距离(XZ平面)
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <param name="dis">距离</param>
    /// <returns>是否在范围内</returns>
    public static bool CheckObjDistanceXZ(Vector3 srcPos, Vector3 targetPos, float dis)
    {
        return GetObjDistanceXZ(srcPos, targetPos) <= dis;
    }

    /// <summary>
    /// 获取XY平面上两点的距离
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <returns>距离值</returns>
    public static float GetObjDistanceXY(Vector3 srcPos, Vector3 targetPos)
    {
        srcPos.z = 0;
        targetPos.z = 0;
        return Vector3.Distance(srcPos, targetPos);
    }

    /// <summary>
    /// 判断两点之间距离是否小于等于目标距离(XY平面)
    /// </summary>
    /// <param name="srcPos">点1</param>
    /// <param name="targetPos">点2</param>
    /// <param name="dis">距离</param>
    /// <returns>是否在范围内</returns>
    public static bool CheckObjDistanceXY(Vector3 srcPos, Vector3 targetPos, float dis)
    {
        return GetObjDistanceXY(srcPos, targetPos) <= dis;
    }

    #endregion

    #region 位置判断

    /// <summary>
    /// 判断世界坐标系下的某一个点是否在屏幕可见范围外
    /// </summary>
    /// <param name="pos">世界坐标系下的一个点的位置</param>
    /// <returns>如果在可见范围外返回true,否则返回false</returns>
    public static bool IsWorldPosOutScreen(Vector3 pos)
    {
        // 将世界坐标转为屏幕坐标
        Vector3 screenPos = Camera.main.WorldToScreenPoint(pos);
        // 判断是否在屏幕范围内
        if (screenPos.x >= 0 && screenPos.x <= Screen.width &&
            screenPos.y >= 0 && screenPos.y <= Screen.height)
        {
            return false;
        }

        return true;
    }

    /// <summary>
    /// 判断某一个位置是否在指定扇形范围内
    /// 注意:传入的坐标向量都必须是基于同一个坐标系下的
    /// </summary>
    /// <param name="pos">扇形中心点位置</param>
    /// <param name="forward">自己的面朝向</param>
    /// <param name="targetPos">目标对象</param>
    /// <param name="radius">半径</param>
    /// <param name="angle">扇形的角度</param>
    /// <returns>是否在扇形范围内</returns>
    public static bool IsInSectorRangeXZ(Vector3 pos, Vector3 forward, Vector3 targetPos, float radius, float angle)
    {
        pos.y = 0;
        forward.y = 0;
        targetPos.y = 0;
        // 距离 + 角度
        return Vector3.Distance(pos, targetPos) <= radius && Vector3.Angle(forward, targetPos - pos) <= angle / 2f;
    }

    #endregion

    #region 射线检测

    /// <summary>
    /// 射线检测,获取一个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
        {
            callBack.Invoke(hitInfo);
        }
    }

    /// <summary>
    /// 射线检测,获取一个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
        {
            callBack.Invoke(hitInfo.collider.gameObject);
        }
    }

    /// <summary>
    /// 射线检测,获取一个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的对象信息上挂在的指定脚本传递出去)</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCast<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
    {
        RaycastHit hitInfo;
        if (Physics.Raycast(ray, out hitInfo, maxDistance, layerMask))
        {
            callBack.Invoke(hitInfo.collider.gameObject.GetComponent<T>());
        }
    }

    /// <summary>
    /// 射线检测,获取到多个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的RayCastHit信息传递出去),每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll(Ray ray, UnityAction<RaycastHit> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
        {
            callBack.Invoke(hitInfos[i]);
        }
    }

    /// <summary>
    /// 射线检测,获取到多个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的GameObject信息传递出去),每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll(Ray ray, UnityAction<GameObject> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
        {
            callBack.Invoke(hitInfos[i].collider.gameObject);
        }
    }

    /// <summary>
    /// 射线检测,获取到多个对象,指定距离,指定层级
    /// </summary>
    /// <param name="ray">射线</param>
    /// <param name="callBack">回调函数(会把碰到的对象信息上依附的脚本传递出去),每一个对象都会调用一次</param>
    /// <param name="maxDistance">最大距离</param>
    /// <param name="layerMask">层级筛选</param>
    public static void RayCastAll<T>(Ray ray, UnityAction<T> callBack, float maxDistance, int layerMask)
    {
        RaycastHit[] hitInfos = Physics.RaycastAll(ray, maxDistance, layerMask);
        for (int i = 0; i < hitInfos.Length; i++)
        {
            callBack.Invoke(hitInfos[i].collider.gameObject.GetComponent<T>());
        }
    }

    #endregion

    #region 范围检测

    /// <summary>
    /// 进行盒装范围检测
    /// </summary>
    /// <typeparam name="T">想要获取的信息类型,可以填写Collider、GameObject以及对象上依附的组件类型</typeparam>
    /// <param name="center">盒装中心点</param>
    /// <param name="rotation">盒子的角度</param>
    /// <param name="halfExtents">长宽高的一半</param>
    /// <param name="layerMask">层级筛选</param>
    /// <param name="callBack">回调函数</param>
    public static void OverlapBox<T>(Vector3 center, Quaternion rotation, Vector3 halfExtents, int layerMask,
        UnityAction<T> callBack) where T : class
    {
        Type type = typeof(T);
        Collider[] colliders =
            Physics.OverlapBox(center, halfExtents, rotation, layerMask, QueryTriggerInteraction.Collide);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (type == typeof(Collider))
            {
                callBack.Invoke(colliders[i] as T);
            }
            else if (type == typeof(GameObject))
            {
                callBack.Invoke(colliders[i].gameObject as T);
            }
            else
            {
                callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
            }
        }
    }

    /// <summary>
    /// 进行球体范围检测
    /// </summary>
    /// <typeparam name="T">想要获取的信息类型,可以填写Collider、GameObject以及对象上依附的组件类型</typeparam>
    /// <param name="center">球体的中心点</param>
    /// <param name="radius">球体的半径</param>
    /// <param name="layerMask">层级筛选</param>
    /// <param name="callBack">回调函数</param>
    public static void OverlapSphere<T>(Vector3 center, float radius, int layerMask, UnityAction<T> callBack)
        where T : class
    {
        Type type = typeof(T);
        Collider[] colliders = Physics.OverlapSphere(center, radius, layerMask, QueryTriggerInteraction.Collide);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (type == typeof(Collider))
            {
                callBack.Invoke(colliders[i] as T);
            }
            else if (type == typeof(GameObject))
            {
                callBack.Invoke(colliders[i].gameObject as T);
            }
            else
            {
                callBack.Invoke(colliders[i].gameObject.GetComponent<T>());
            }
        }
    }

    #endregion
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏