Unity基础-范围检测

Unity基础-范围检测

七、范围检测

概述

范围检测(Overlap Detection)是Unity物理系统中的一种常用技术,用于在不产生实际物理碰撞或不依赖刚体的情况下,判断指定区域内是否存在或有哪些碰撞器。它常用于游戏中瞬时的攻击范围判断、技能效果判定、区域触发等场景,例如:

  • 玩家在前方5米处释放一个范围,检测此范围内的对象是否受到伤害。
  • 玩家攻击,在前方1米圆形范围内检测对象是否受到伤害。
  • 类似这种并没有实体物体,只是想检测在指定某一范围是否让敌方受到伤害时便可以使用。

简而言之,在指定位置进行范围判断,我们可以得到处于指定范围内的对象,目的是对这些对象进行处理(比如受伤、减血等)。

必备条件与注意点

  • 必备条件:想要被范围检测到的对象,必须具备碰撞器(Collider)!
  • 注意点
    1. 范围检测相关API是瞬时的,只有当执行该代码时才进行一次范围检测。
    2. 范围检测相关API并不会真正产生一个碰撞器,它仅仅是进行碰撞判断计算。

层级 (LayerMask)

在进行范围检测时,我们通常需要检测指定层级(Layer)上的物体,以避免检测到不相关的对象,提高效率和精确性。Unity使用位运算来表示层级信息。

  • 通过名字得到层级编号:LayerMask.NameToLayer("LayerName")
  • 层级编号是0~31,刚好32位,是一个int数。
  • 每一个编号都代表二进制的一位。通过将1左移对应层级编号,可以得到该层级的二进制表示。
    • 0 层:1 << 00000 0000 0000 0000 0000 0000 0000 0001 (= 1)
    • 1 层:1 << 10000 0000 0000 0000 0000 0000 0000 0010 (= 2)
    • 2 层:1 << 20000 0000 0000 0000 0000 0000 0000 0100 (= 4)
    • 3 层:1 << 30000 0000 0000 0000 0000 0000 0000 1000 (= 8)
    • 4 层:1 << 40000 0000 0000 0000 0000 0000 0001 0000 (= 16)
  • 好处:一个int就可以表示所有想要检测的层级信息。我们可以通过位运算来选择想要检测的层级。
    • 检测单层:1 << LayerMask.NameToLayer("Default")
    • 检测多层:(1 << LayerMask.NameToLayer("Layer1")) | (1 << LayerMask.NameToLayer("Layer2"))
    • 检测所有层(默认):不传入层级参数,或传入-1

范围检测API

Unity Physics 类提供了一系列范围检测的API。它们通常分为两类:

  • 直接返回Collider[]数组的方法(有GC开销):例如 Physics.OverlapBox,每次调用会创建一个新的数组。
  • 非分配式方法(NonAlloc,无GC开销):例如 Physics.OverlapBoxNonAlloc,需要传入一个预先分配好的Collider[]数组来存储结果,推荐在频繁调用时使用。
1. 盒形范围检测 (Box Overlap)

在指定的一个立方体范围内检测所有碰撞体。

  • Physics.OverlapBox(center, halfExtents, orientation, layerMask)

    • center:立方体的中心点(世界坐标)。
    • halfExtents:立方体各边一半的长度(Vector3)。
    • orientation:立方体的旋转(Quaternion)。
    • layerMask:可选参数,要检测的层级(int)。
    • 返回值Collider[]数组,包含在该范围内的所有碰撞器。
  • Physics.OverlapBoxNonAlloc(center, halfExtents, results, orientation, layerMask)

    • results:预先分配好的Collider[]数组,用于存储检测结果。
    • 返回值int,表示实际检测到的碰撞器数量。

示例代码:

// 声明一个Collider数组用于NonAlloc方法
Collider[] boxColliders = new Collider[10]; 

// 使用OverlapBox(会产生GC)
Collider[] detectedColliders = Physics.OverlapBox(transform.position + Vector3.forward * 1, Vector3.one / 2, Quaternion.identity, 1 << LayerMask.NameToLayer("Default"));
if (detectedColliders.Length > 0)
{
    Debug.Log($"OverlapBox检测到 {detectedColliders.Length} 个碰撞体。");
}

// 使用OverlapBoxNonAlloc(无GC)
int numColliders = Physics.OverlapBoxNonAlloc(transform.position + Vector3.forward * 1, Vector3.one / 2, boxColliders, Quaternion.identity, 1 << LayerMask.NameToLayer("Default"));
if (numColliders > 0)
{
    Debug.Log($"OverlapBoxNonAlloc检测到 {numColliders} 个碰撞体。");
    for (int i = 0; i < numColliders; i++)
    {
        Debug.Log($"  - {boxColliders[i].name}");
    }
}
2. 球形范围检测 (Sphere Overlap)

在指定的一个球形范围内检测所有碰撞体。

  • Physics.OverlapSphere(position, radius, layerMask)

    • position:球体的中心点(世界坐标)。
    • radius:球体的半径。
    • layerMask:可选参数,要检测的层级(int)。
    • 返回值Collider[]数组,包含在该范围内的所有碰撞器。
  • Physics.OverlapSphereNonAlloc(position, radius, results, layerMask)

    • results:预先分配好的Collider[]数组。
    • 返回值int,表示实际检测到的碰撞器数量。

示例代码:

// 声明一个Collider数组用于NonAlloc方法
Collider[] sphereColliders = new Collider[10];

// 使用OverlapSphere(会产生GC)
Collider[] detectedSphereColliders = Physics.OverlapSphere(transform.position + Vector3.down * 1, 10f, 1 << LayerMask.NameToLayer("Default"));
if (detectedSphereColliders.Length > 0)
{
    Debug.Log($"OverlapSphere检测到 {detectedSphereColliders.Length} 个碰撞体。");
}

// 使用OverlapSphereNonAlloc(无GC)
int numSphereColliders = Physics.OverlapSphereNonAlloc(transform.position + Vector3.down * 1, 10f, sphereColliders, 1 << LayerMask.NameToLayer("Default"));
if (numSphereColliders > 0)
{
    Debug.Log($"OverlapSphereNonAlloc检测到 {numSphereColliders} 个碰撞体。");
    for (int i = 0; i < numSphereColliders; i++)
    {
        Debug.Log($"  - {sphereColliders[i].name}");
    }
}
3. 胶囊范围检测 (Capsule Overlap)

在指定的一个胶囊体范围内检测所有碰撞体。

  • Physics.OverlapCapsule(point1, point2, radius, layerMask)

    • point1:胶囊体底部半球的中心点(世界坐标)。
    • point2:胶囊体顶部半球的中心点(世界坐标)。
    • radius:胶囊体的半径。
    • layerMask:可选参数,要检测的层级(int)。
    • 返回值Collider[]数组,包含在该范围内的所有碰撞器。
  • Physics.OverlapCapsuleNonAlloc(point1, point2, radius, results, layerMask)

    • results:预先分配好的Collider[]数组。
    • 返回值int,表示实际检测到的碰撞器数量。

示例代码:

// 声明一个Collider数组用于NonAlloc方法
Collider[] capsuleColliders = new Collider[10];

// 使用OverlapCapsule(会产生GC)
Collider[] detectedCapsuleColliders = Physics.OverlapCapsule(transform.position, transform.position + Vector3.forward * 5, 0.5f, 1 << LayerMask.NameToLayer("Default"));
if (detectedCapsuleColliders.Length > 0)
{
    Debug.Log($"OverlapCapsule检测到 {detectedCapsuleColliders.Length} 个碰撞体。");
}

// 使用OverlapCapsuleNonAlloc(无GC)
int numCapsuleColliders = Physics.OverlapCapsuleNonAlloc(transform.position, transform.position + Vector3.forward * 5, 0.5f, capsuleColliders, 1 << LayerMask.NameToLayer("Default"));
if (numCapsuleColliders > 0)
{
    Debug.Log($"OverlapCapsuleNonAlloc检测到 {numCapsuleColliders} 个碰撞体。");
    for (int i = 0; i < numCapsuleColliders; i++)
    {
        Debug.Log($"  - {capsuleColliders[i].name}");
    }
}

实际应用示例:WASD移动与多类型范围检测

这个示例提供一个PlayController脚本,用于控制一个游戏对象的移动,并实现三种不同形状的碰撞范围检测:盒形、胶囊体和球形。移动通过Transform.Translate实现,碰撞检测则利用Unity的Physics.Overlap系列方法。

核心思想:

  • WASD 移动控制:通过获取水平和垂直输入,使用Transform.Translate方法实现游戏对象的移动。
  • 多种碰撞检测
    • Physics.OverlapBox:用于检测一个指定盒子范围内的所有碰撞体。
    • Physics.OverlapCapsule:用于检测一个指定胶囊体范围内的所有碰撞体。
    • Physics.OverlapSphere:用于检测一个指定球形范围内的所有碰撞体。
  • LayerMask (层掩码):用于过滤碰撞检测,只检测指定层上的对象,提高效率和精度。

代码实现 (PlayController.cs)

点击展开/折叠 PlayController.cs 代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayController : MonoBehaviour
{
    // 用于存储移动向量
    private Vector3 movement;
    // 移动速度,可在Inspector面板中调整
    public float moveSpeed = 2f; 
    // 用于存储碰撞检测结果的Collider数组,预设大小为10个,避免频繁GC
    private Collider[] colliders = new Collider[10];

    // Update是MonoBehaviour的生命周期函数,每帧调用一次
    void Update()
    {
        // 处理对象的移动逻辑
        HandleMovement();
        // 处理对象的碰撞检测逻辑
        HandleOverLap();
    }

    /// 
    /// 处理碰撞检测逻辑。
    /// 根据按键(J、K、L)执行不同类型的范围检测。
    /// 
    void HandleOverLap()
    {
        // J键按下时执行盒形(Box)范围检测
        if(Input.GetKeyDown(KeyCode.J))
        {
            // Physics.OverlapBox:检测一个立方体范围内的所有碰撞体
            // transform.position + transform.forward * 1:检测中心点在物体前方1个单位
            // Vector3.one:检测盒子的尺寸为1x1x1
            // transform.rotation:检测盒子的旋转与物体当前旋转一致
            // 1 << LayerMask.NameToLayer("Default"):只检测"Default"层上的物体
            int numDetected = Physics.OverlapBoxNonAlloc(transform.position + transform.forward * 1, 
                                                           Vector3.one / 2, // 半尺寸,所以实际尺寸是Vector3.one
                                                           colliders, 
                                                           transform.rotation, 
                                                           1 << LayerMask.NameToLayer("Default"));
            // 清空数组中未检测到的部分
            for (int i = numDetected; i < colliders.Length; i++)
            {
                colliders[i] = null;
            }
            
            if (numDetected > 0)
            {
                Debug.Log($"J键:盒形检测到 {numDetected} 个碰撞体!");
            }
            else
            {
                Debug.Log("J键:盒形检测未发现碰撞体。");
            }
        }
        
        // K键按下时执行胶囊体(Capsule)范围检测
        if(Input.GetKeyDown(KeyCode.K))
        {
            // Physics.OverlapCapsule:检测一个胶囊体范围内的所有碰撞体
            // transform.position:胶囊体的底部中心点
            // transform.position + transform.forward * 5:胶囊体的顶部中心点,在物体前方5个单位
            // 0.5f:胶囊体的半径
            // 1 << LayerMask.NameToLayer("Default"):只检测"Default"层上的物体
            int numDetected = Physics.OverlapCapsuleNonAlloc(transform.position, 
                                                               transform.position + transform.forward * 5, 
                                                               0.5f, 
                                                               colliders, 
                                                               1 << LayerMask.NameToLayer("Default"));
            // 清空数组中未检测到的部分
            for (int i = numDetected; i < colliders.Length; i++)
            {
                colliders[i] = null;
            }

            if (numDetected > 0)
            {
                Debug.Log($"K键:胶囊形检测到 {numDetected} 个碰撞体!");
            }
            else
            {
                Debug.Log("K键:胶囊形检测未发现碰撞体。");
            }
        }
        
        // L键按下时执行球形(Sphere)范围检测
        if(Input.GetKeyDown(KeyCode.L))
        {
            // Physics.OverlapSphere:检测一个球形范围内的所有碰撞体
            // transform.position + Vector3.down * 1:球体的中心点在物体下方1个单位
            // 10f:球体的半径为10个单位
            // 1 << LayerMask.NameToLayer("Default"):只检测"Default"层上的物体
            int numDetected = Physics.OverlapSphereNonAlloc(transform.position + Vector3.down * 1, 
                                                              10f, 
                                                              colliders, 
                                                              1 << LayerMask.NameToLayer("Default"));
            // 清空数组中未检测到的部分
            for (int i = numDetected; i < colliders.Length; i++)
            {
                colliders[i] = null;
            }

            if (numDetected > 0)
            {
                Debug.Log($"L键:球形检测到 {numDetected} 个碰撞体!");
            }
            else
            {
                Debug.Log("L键:球形检测未发现碰撞体。");
            }
        }

        // 如果检测到碰撞体,则遍历并打印所有检测到的碰撞体名称
        // 这里使用循环来确保只处理实际检测到的数量
        for(int i = 0; i < colliders.Length; i++)
        {
            // 确保当前元素不为空才打印
            if (colliders[i] != null) 
            {
                Debug.Log($"检测到碰撞体: {colliders[i].name}"); 
            }
        }
    }

    /// 
    /// 处理移动逻辑。
    /// 根据WASD键的输入控制物体的移动和旋转。
    /// 
    void HandleMovement()
    {
        // 获取水平轴(A/D键或左右箭头)的输入
        float horizontal = Input.GetAxis("Horizontal");
        // 获取垂直轴(W/S键或上下箭头)的输入
        float vertical = Input.GetAxis("Vertical");
        
        // 基于垂直输入前后移动
        // Time.deltaTime 用于使移动速度与帧率无关
        transform.Translate(Vector3.forward * vertical * moveSpeed * Time.deltaTime);

        // 基于水平输入左右旋转
        // 旋转速度可以根据moveSpeed调整,或者单独设置一个rotateSpeed
        transform.Rotate(Vector3.up * horizontal * moveSpeed * 50 * Time.deltaTime); // 乘以一个系数使旋转更明显
    }
}

使用建议

  1. 性能优化:在频繁进行范围检测的场景,优先使用NonAlloc系列方法(如OverlapBoxNonAlloc),以避免每次调用都产生新的Collider[]数组,从而减少GC(垃圾回收)开销。
  2. 精确控制:善用LayerMask参数来精确控制只检测特定层级的对象,提高检测效率和准确性。
  3. 调试可视化:在开发过程中,可以使用Debug.DrawBoxDebug.DrawLine等方法来可视化检测范围,帮助理解和调试。
  4. 触发器设置:范围检测通常用于获取范围内的对象信息,而不是模拟物理碰撞。确保被检测对象上的碰撞器如果设置为Is Trigger,则它们不会产生物理交互。

你可能感兴趣的:(Unity基础,unity,游戏引擎,c#,游戏策划,开发语言)