14.数学模块
14.1 知识点
数学模块的作用
在游戏开发中,我们经常需要进行各种通用的数学计算和几何判断,例如:
- 角度和弧度转换:在不同计算场景中需要进行单位转换
- 距离计算:判断两个对象之间的距离,常用于攻击范围、移动距离等
- 位置判断:判断对象是否在屏幕外、是否在扇形攻击范围内
- 射线检测:用于鼠标点击选择、子弹碰撞检测等
- 范围检测:检测指定范围内的所有对象,用于技能范围、爆炸范围等
常见场景:
- 计算玩家与敌人的XZ平面距离(忽略高度差)
- 判断技能释放时目标是否在前方扇形范围内
- 鼠标点击屏幕,通过射线检测选中3D对象
- 释放范围技能时,检测范围内的所有敌人
- 判断UI提示是否超出屏幕范围需要隐藏
传统方式的问题:
- 重复编写代码:每次都需要重新编写距离计算、角度判断等逻辑
- 代码分散:数学计算逻辑分散在各个模块中,难以统一管理
- 容易出错:手动计算距离、角度等容易出现错误
- 缺乏封装:Unity的API需要多次调用和组合,使用不够便捷
解决方案:
通过统一的数学工具类,封装常用的数学计算和几何判断逻辑,提供一致的API接口,简化开发流程。
主要功能:
- 角度和弧度转换:封装Unity的Deg2Rad和Rad2Deg常量
- 平面距离计算:支持XZ平面和XY平面的距离计算
- 屏幕外判断:判断世界坐标点是否在屏幕可见范围外
- 扇形范围判断:判断目标是否在指定扇形攻击范围内
- 射线检测封装:简化Unity射线检测的使用,支持回调
- 范围检测封装:封装盒装检测和球体检测,支持泛型回调
数学模块的基本原理
设计理念:
- 工具类设计:使用静态类提供工具方法,无需实例化
- 委托回调:检测方法使用委托回调,便于灵活处理结果
- 泛型支持:支持获取不同类型的对象(Collider、GameObject、组件)
- 平面计算:针对2D和3D游戏提供不同平面的距离计算
- 性能优化:直接使用Unity的高效API,减少额外开销
核心功能:
- 角度转换:基于Mathf.Deg2Rad和Mathf.Rad2Deg封装
- 距离计算:通过将Vector3的某个分量置0实现平面距离计算
- 屏幕判断:使用Camera.WorldToScreenPoint转换坐标后判断
- 扇形判断:结合距离和角度双重判断(距离<=半径 && 角度<=扇形角/2)
- 射线检测:封装Physics.Raycast和Physics.RaycastAll
- 范围检测:封装Physics.OverlapBox和Physics.OverlapSphere
数学模块实现
角度和弧度转换
Unity中的Mathf类虽然提供了Deg2Rad和Rad2Deg转换系数,但直接使用容易出错,例如:
- 容易混淆乘法和除法
- 需要记住什么时候用哪个系数
- 代码可读性较差
封装的优势:
- 避免错误:无需手动进行乘法运算,避免使用错误的系数
- 提高可读性:方法名清晰表达意图,代码更易理解
- 统一接口:外部只需传入角度或弧度,直接得到结果
实现代码:
/// <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弧度
距离计算相关
在游戏开发中,我们经常需要计算对象之间的平面距离,而不考虑高度差:
应用场景:
- 2D游戏:只需要计算XY平面上的距离,Z轴变化不影响距离
- 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虽然功能强大,但使用相对繁琐:
传统使用方式的问题:
- 需要手动处理RaycastHit:需要声明变量和out参数
- 返回值处理复杂:需要判断是否命中,再获取对象
- 获取组件需要额外步骤:从GameObject获取组件需要额外调用职责
封装的优势:
- 委托回调:通过委托直接处理结果,代码更简洁
- 多种重载:支持获取RaycastHit、GameObject或指定组件
- 批量检测:封装RaycastAll,支持一次检测多个对象
- 参数统一:统一处理距离、层级等参数
实现代码:
/// <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感知范围内的目标
封装的优势:
- 盒装检测:支持3D盒状区域的检测
- 球体检测:支持球状范围的检测
- 泛型支持:支持获取Collider、GameObject或组件类型
- 委托回调:每个检测到的对象都会触发回调
实现代码:
/// <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