unity射线与几何检测 - “与世界的交互”

第一部分:为什么需要射线与几何检测?

1. 游戏世界的"触觉"

在现实世界中,我们通过触觉感知物体。在游戏世界中,射线和几何检测就是我们的"虚拟触觉",让代码能够感知和响应游戏世界的几何结构。

2. 三个核心问题

射线和几何检测主要解决:

  1. "前面有什么?"(视线检测、射击)

  2. "我站在什么上面?"(地面检测)

  3. "周围有什么?"(区域检测)


第二部分:射线基础 ------ 虚拟的激光笔

1. 射线的定义

射线是一条从起点向特定方向无限延伸的数学直线:

  • 起点:射线开始的位置

  • 方向:射线延伸的方向(通常已归一化)

csharp

复制代码
// Unity中的Ray结构体
public struct Ray {
    public Vector3 origin;    // 起点
    public Vector3 direction; // 方向(已归一化)
}

2. 创建射线的四种方法

方法1:手动创建

csharp

复制代码
// 创建一个从(0,0,0)向上发射的射线
Ray ray = new Ray(Vector3.zero, Vector3.up);

// 或者使用构造函数
Ray ray2;
ray2.origin = transform.position;
ray2.direction = transform.forward;
方法2:从相机发射(最常用)

csharp

复制代码
// 从屏幕坐标发射射线(用于鼠标点击)
Vector3 mousePosition = Input.mousePosition;
Ray mouseRay = Camera.main.ScreenPointToRay(mousePosition);

// 从视口坐标发射(范围0-1)
Ray viewportRay = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); // 屏幕中心

// 实际应用:3D物体选择
void Update() {
    if (Input.GetMouseButtonDown(0)) {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit)) {
            Debug.Log($"点击了: {hit.collider.gameObject.name}");
        }
    }
}
方法3:从物体发射

csharp

复制代码
// 从物体位置向前发射
Ray forwardRay = new Ray(transform.position, transform.forward);

// 从特定偏移位置发射
Vector3 offset = new Vector3(0, 1, 0); // 头顶上方1单位
Ray offsetRay = new Ray(transform.position + offset, transform.forward);
方法4:两点确定一条射线

csharp

复制代码
// 虽然Unity没有直接的方法,但我们可以计算
Vector3 start = transform.position;
Vector3 end = target.position;
Vector3 direction = (end - start).normalized;
Ray twoPointRay = new Ray(start, direction);

3. 可视化射线(调试关键)

csharp

复制代码
// 方法1:Debug.DrawRay(只显示一帧)
Debug.DrawRay(transform.position, transform.forward * 10, Color.red);

// 方法2:Debug.DrawLine(显示线段)
Debug.DrawLine(startPoint, endPoint, Color.green, 2.0f); // 显示2秒

// 方法3:在编辑器中可视化
void OnDrawGizmos() {
    Gizmos.color = Color.cyan;
    // 绘制射线
    Gizmos.DrawRay(transform.position, transform.forward * 5);
    
    // 绘制球体表示起点
    Gizmos.DrawSphere(transform.position, 0.1f);
}

// 实际应用:绘制武器射线
public class WeaponDebug : MonoBehaviour {
    public float range = 100f;
    
    void Update() {
        // 绘制武器瞄准线
        Debug.DrawRay(transform.position, transform.forward * range, Color.red);
        
        // 如果按下开火键,显示命中点
        if (Input.GetButton("Fire1")) {
            RaycastHit hit;
            if (Physics.Raycast(transform.position, transform.forward, out hit, range)) {
                Debug.DrawLine(transform.position, hit.point, Color.yellow, 0.1f);
                Debug.DrawSphere(hit.point, 0.1f, Color.yellow, 0.1f);
            }
        }
    }
}

第三部分:射线检测 ------ Physics.Raycast 详解

1. 基础射线检测

csharp

复制代码
// 最简单的形式:检测前方是否有物体
RaycastHit hitInfo; // 用于存储击中信息
float maxDistance = 100f;

if (Physics.Raycast(transform.position, transform.forward, out hitInfo, maxDistance)) {
    // 检测到了碰撞体
    Debug.Log($"击中了: {hitInfo.collider.gameObject.name}");
    Debug.Log($"击中点: {hitInfo.point}");
    Debug.Log($"距离: {hitInfo.distance}");
}

// 使用Ray结构体的形式
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out hitInfo, maxDistance)) {
    // 同样有效
}

2. RaycastHit:击中信息详解

csharp

复制代码
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
    // 1. 基本碰撞信息
    GameObject hitObject = hit.collider.gameObject;  // 击中的游戏对象
    Transform hitTransform = hit.transform;          // 击中的变换组件
    Vector3 hitPoint = hit.point;                    // 世界空间中的击中点
    Vector3 hitNormal = hit.normal;                  // 击中表面的法线方向
    float distance = hit.distance;                   // 从射线起点到击中点的距离
    
    // 2. 物理材质信息
    PhysicMaterial physicMaterial = hit.collider.sharedMaterial;
    
    // 3. 纹理坐标(UV)
    Vector2 textureCoord = hit.textureCoord;         // 主纹理UV坐标
    Vector2 textureCoord2 = hit.textureCoord2;       // 第二个纹理UV坐标
    
    // 4. 三角形信息(仅MeshCollider)
    int triangleIndex = hit.triangleIndex;           // 击中的三角形索引
    
    // 5. 刚体信息
    Rigidbody hitRigidbody = hit.rigidbody;          // 击中的刚体(如果有)
    
    // 6. 碰撞器信息
    Collider hitCollider = hit.collider;             // 击中的碰撞器
}

3. LayerMask:选择性检测

csharp

复制代码
// 创建LayerMask的几种方法

// 方法1:在Inspector中设置
public LayerMask targetLayers;

// 方法2:通过层名
int layerMask = LayerMask.GetMask("Enemy", "Environment");

// 方法3:通过层号
int enemyLayer = 8;
int environmentLayer = 9;
int layerMask2 = (1 << enemyLayer) | (1 << environmentLayer);

// 方法4:排除特定层
int allExceptLayer10 = ~(1 << 10); // 排除第10层
int allExceptUI = ~LayerMask.GetMask("UI");

// 使用LayerMask进行检测
if (Physics.Raycast(ray, out hit, maxDistance, layerMask)) {
    // 只检测指定层的物体
}

// 实际应用:只检测可射击的物体
public class ShootingSystem : MonoBehaviour {
    public LayerMask shootableLayers;
    
    void Update() {
        if (Input.GetButtonDown("Fire1")) {
            Ray ray = new Ray(transform.position, transform.forward);
            RaycastHit hit;
            
            if (Physics.Raycast(ray, out hit, 100f, shootableLayers)) {
                // 只击中shootableLayers中的物体
                if (hit.collider.CompareTag("Enemy")) {
                    hit.collider.GetComponent<EnemyHealth>().TakeDamage(25);
                }
            }
        }
    }
}

4. 查询触发器

csharp

复制代码
// QueryTriggerInteraction 枚举
// - Ignore:忽略所有触发器
// - Collide:与触发器碰撞
// - UseGlobal:使用Physics设置

// 检测触发器
if (Physics.Raycast(ray, out hit, maxDistance, layerMask, QueryTriggerInteraction.Collide)) {
    // 会检测到触发器
}

// 忽略触发器
if (Physics.Raycast(ray, out hit, maxDistance, layerMask, QueryTriggerInteraction.Ignore)) {
    // 不会检测到触发器
}

第四部分:高级射线检测方法

1. RaycastAll:检测所有击中点

csharp

复制代码
// 检测射线路径上的所有物体
RaycastHit[] allHits = Physics.RaycastAll(ray, maxDistance);

// 按距离排序(最近的先处理)
Array.Sort(allHits, (a, b) => a.distance.CompareTo(b.distance));

// 处理每个击中点
foreach (RaycastHit hit in allHits) {
    Debug.Log($"击中: {hit.collider.name} 在距离 {hit.distance} 处");
    
    // 实际应用:穿透射击
    if (hit.collider.CompareTag("Enemy")) {
        hit.collider.GetComponent<EnemyHealth>().TakeDamage(10);
    }
}

// 实际应用:激光武器穿透多个目标
public class LaserWeapon : MonoBehaviour {
    public LineRenderer laserLine;
    public float range = 50f;
    
    void Update() {
        if (Input.GetButton("Fire1")) {
            RaycastHit[] hits = Physics.RaycastAll(transform.position, transform.forward, range);
            
            // 按距离排序
            hits = hits.OrderBy(h => h.distance).ToArray();
            
            // 创建激光点
            Vector3[] linePoints = new Vector3[hits.Length + 1];
            linePoints[0] = transform.position;
            
            for (int i = 0; i < hits.Length; i++) {
                linePoints[i + 1] = hits[i].point;
                
                // 对每个击中的敌人造成伤害
                if (hits[i].collider.CompareTag("Enemy")) {
                    hits[i].collider.GetComponent<EnemyHealth>().TakeDamage(25);
                }
            }
            
            laserLine.positionCount = linePoints.Length;
            laserLine.SetPositions(linePoints);
        } else {
            laserLine.positionCount = 0;
        }
    }
}

2. RaycastNonAlloc:避免GC分配

csharp

复制代码
// 预分配数组,避免垃圾回收
private RaycastHit[] hitResults = new RaycastHit[10]; // 最多检测10个物体

void Update() {
    // 返回实际击中的数量
    int hitCount = Physics.RaycastNonAlloc(ray, hitResults, maxDistance);
    
    for (int i = 0; i < hitCount; i++) {
        Debug.Log($"击中: {hitResults[i].collider.name}");
    }
}

// 实际应用:高性能射击检测
public class PerformanceShooting : MonoBehaviour {
    private RaycastHit[] hitBuffer = new RaycastHit[32]; // 32个缓冲区
    private int frameCount = 0;
    
    void Update() {
        frameCount++;
        
        // 每10帧检测一次(减少检测频率)
        if (frameCount % 10 == 0) {
            int hits = Physics.RaycastNonAlloc(transform.position, transform.forward, 
                                              hitBuffer, 100f);
            
            for (int i = 0; i < hits; i++) {
                ProcessHit(hitBuffer[i]);
            }
        }
    }
    
    void ProcessHit(RaycastHit hit) {
        // 处理击中逻辑
    }
}

3. SphereCast:球形射线检测

csharp

复制代码
// 沿射线方向检测一个移动的球体
float radius = 0.5f; // 球体半径
if (Physics.SphereCast(ray, radius, out hit, maxDistance)) {
    // 检测到碰撞
}

// 另一种形式:指定起点
if (Physics.SphereCast(transform.position, radius, transform.forward, out hit, maxDistance)) {
    // 检测到碰撞
}

// 实际应用:角色移动碰撞检测
public class CharacterCollision : MonoBehaviour {
    public float characterRadius = 0.5f;
    public float stepHeight = 0.3f;
    
    void CheckMovement(Vector3 moveDirection, float moveDistance) {
        RaycastHit hit;
        
        // 使用SphereCast检测前方是否有障碍
        if (Physics.SphereCast(transform.position, characterRadius, 
                              moveDirection, out hit, moveDistance)) {
            // 检查是否是可跨越的台阶
            if (hit.distance < moveDistance && CanStepOver(hit.point)) {
                // 跨越台阶
                transform.position += Vector3.up * stepHeight;
            } else {
                // 被障碍物阻挡
                float slideDistance = hit.distance - 0.1f; // 留一点空隙
                transform.position += moveDirection.normalized * slideDistance;
            }
        } else {
            // 无障碍物,自由移动
            transform.position += moveDirection.normalized * moveDistance;
        }
    }
    
    bool CanStepOver(Vector3 obstaclePoint) {
        // 检测上方是否有空间
        Vector3 checkPoint = obstaclePoint + Vector3.up * stepHeight;
        return !Physics.CheckSphere(checkPoint, characterRadius);
    }
}

4. CapsuleCast:胶囊体检测

csharp

复制代码
// 定义胶囊体的两个半球中心
Vector3 point1 = transform.position + Vector3.up * 1f; // 顶部
Vector3 point2 = transform.position - Vector3.up * 1f; // 底部
float radius = 0.5f;

if (Physics.CapsuleCast(point1, point2, radius, transform.forward, out hit, maxDistance)) {
    // 胶囊体在移动方向上检测到碰撞
}

// 实际应用:第三人称角色控制器
public class ThirdPersonController : MonoBehaviour {
    public float height = 2f;
    public float radius = 0.3f;
    
    void CheckCapsuleCollision(Vector3 movement) {
        Vector3 top = transform.position + Vector3.up * (height - radius);
        Vector3 bottom = transform.position + Vector3.up * radius;
        
        RaycastHit hit;
        if (Physics.CapsuleCast(bottom, top, radius, movement.normalized, out hit, movement.magnitude)) {
            // 尝试从侧面滑动
            Vector3 slideDirection = Vector3.ProjectOnPlane(movement, hit.normal);
            movement = slideDirection.normalized * movement.magnitude;
        }
        
        transform.position += movement;
    }
}

5. BoxCast:盒形检测

csharp

复制代码
// 定义盒子的参数
Vector3 center = transform.position;
Vector3 halfExtents = new Vector3(0.5f, 1f, 0.5f); // 盒子的一半尺寸
Quaternion orientation = transform.rotation; // 盒子的朝向

if (Physics.BoxCast(center, halfExtents, transform.forward, out hit, orientation, maxDistance)) {
    // 盒子在移动方向上检测到碰撞
}

// 实际应用:车辆碰撞检测
public class VehicleCollision : MonoBehaviour {
    public Vector3 boxSize = new Vector3(1f, 0.5f, 2f);
    
    void FixedUpdate() {
        // 检测前方
        if (Physics.BoxCast(transform.position, boxSize * 0.5f, transform.forward, 
                           out RaycastHit hit, transform.rotation, 5f)) {
            // 根据距离调整速度
            float speedFactor = Mathf.Clamp01(hit.distance / 5f);
            ApplyBrakes(speedFactor);
        }
    }
    
    void OnDrawGizmos() {
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
        Gizmos.color = Color.red;
        Gizmos.DrawWireCube(Vector3.zero, boxSize);
    }
}

第五部分:重叠检测 ------ 区域检测

1. OverlapSphere:球形区域检测

csharp

复制代码
// 检测球形区域内的所有碰撞体
Vector3 center = transform.position;
float radius = 5f;
Collider[] colliders = Physics.OverlapSphere(center, radius);

foreach (Collider collider in colliders) {
    Debug.Log($"在范围内的物体: {collider.name}");
}

// 使用LayerMask过滤
Collider[] enemies = Physics.OverlapSphere(center, radius, LayerMask.GetMask("Enemy"));

// 实际应用:爆炸范围伤害
public class Explosion : MonoBehaviour {
    public float explosionRadius = 10f;
    public float explosionForce = 1000f;
    public int damage = 50;
    
    void Explode() {
        Collider[] affectedObjects = Physics.OverlapSphere(transform.position, explosionRadius);
        
        foreach (Collider col in affectedObjects) {
            // 计算距离衰减
            float distance = Vector3.Distance(transform.position, col.transform.position);
            float damageMultiplier = 1f - Mathf.Clamp01(distance / explosionRadius);
            
            // 应用伤害
            Health health = col.GetComponent<Health>();
            if (health != null) {
                health.TakeDamage((int)(damage * damageMultiplier));
            }
            
            // 应用物理力
            Rigidbody rb = col.GetComponent<Rigidbody>();
            if (rb != null) {
                Vector3 direction = (col.transform.position - transform.position).normalized;
                rb.AddForce(direction * explosionForce * damageMultiplier);
            }
        }
        
        // 可视化爆炸范围
        Debug.DrawWireSphere(transform.position, explosionRadius, Color.red, 2f);
    }
}

2. OverlapBox:盒形区域检测

csharp

复制代码
// 检测盒形区域内的碰撞体
Vector3 center = transform.position;
Vector3 halfExtents = new Vector3(2f, 1f, 3f); // 盒子的一半尺寸
Quaternion orientation = transform.rotation;
Collider[] boxColliders = Physics.OverlapBox(center, halfExtents, orientation);

// 实际应用:门开关检测
public class DoorTrigger : MonoBehaviour {
    public Vector3 detectionSize = new Vector3(1f, 2f, 0.5f);
    
    void Update() {
        Collider[] colliders = Physics.OverlapBox(transform.position, detectionSize * 0.5f, transform.rotation);
        
        bool playerNearby = false;
        foreach (Collider col in colliders) {
            if (col.CompareTag("Player")) {
                playerNearby = true;
                break;
            }
        }
        
        // 控制门开关
        GetComponent<Door>().SetOpen(playerNearby);
    }
    
    void OnDrawGizmosSelected() {
        Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
        Gizmos.color = new Color(0, 1, 0, 0.3f);
        Gizmos.DrawCube(Vector3.zero, detectionSize);
    }
}

3. OverlapCapsule:胶囊体区域检测

csharp

复制代码
// 检测胶囊体区域内的碰撞体
Vector3 point0 = transform.position + Vector3.up * 1f;
Vector3 point1 = transform.position - Vector3.up * 1f;
float radius = 0.5f;
Collider[] capsuleColliders = Physics.OverlapCapsule(point0, point1, radius);

// 实际应用:近战攻击范围检测
public class MeleeAttack : MonoBehaviour {
    public float attackRange = 1.5f;
    public float attackHeight = 2f;
    
    void Attack() {
        Vector3 top = transform.position + transform.forward * attackRange + Vector3.up * attackHeight;
        Vector3 bottom = transform.position + transform.forward * attackRange;
        
        Collider[] hitColliders = Physics.OverlapCapsule(bottom, top, 0.5f);
        
        foreach (Collider col in hitColliders) {
            if (col.CompareTag("Enemy")) {
                col.GetComponent<EnemyHealth>().TakeDamage(20);
            }
        }
    }
}

4. NonAlloc版本的重叠检测

csharp

复制代码
// 避免GC分配
private Collider[] overlapResults = new Collider[20];

void Update() {
    int numColliders = Physics.OverlapSphereNonAlloc(transform.position, 5f, overlapResults);
    
    for (int i = 0; i < numColliders; i++) {
        // 处理每个碰撞体
    }
}

第六部分:实际应用场景

场景1:第一人称射击系统

csharp

复制代码
public class FPSWeapon : MonoBehaviour {
    public float range = 100f;
    public int damage = 25;
    public float fireRate = 0.1f;
    public ParticleSystem muzzleFlash;
    public GameObject impactEffect;
    
    private float nextFireTime = 0f;
    
    void Update() {
        if (Input.GetButton("Fire1") && Time.time >= nextFireTime) {
            nextFireTime = Time.time + fireRate;
            Shoot();
        }
    }
    
    void Shoot() {
        // 播放枪口效果
        muzzleFlash.Play();
        
        // 计算射线
        Ray ray = new Ray(transform.position, transform.forward);
        RaycastHit hit;
        
        // 射击检测
        if (Physics.Raycast(ray, out hit, range)) {
            // 命中效果
            GameObject impact = Instantiate(impactEffect, hit.point, Quaternion.LookRotation(hit.normal));
            Destroy(impact, 2f);
            
            // 伤害处理
            EnemyHealth enemy = hit.collider.GetComponent<EnemyHealth>();
            if (enemy != null) {
                // 计算伤害衰减(距离越远伤害越低)
                float distance = hit.distance;
                float damageMultiplier = Mathf.Clamp01(1f - distance / range);
                int actualDamage = Mathf.RoundToInt(damage * damageMultiplier);
                
                enemy.TakeDamage(actualDamage, hit.point);
                
                // 如果是爆头
                if (hit.collider.CompareTag("Head")) {
                    enemy.TakeCriticalDamage(actualDamage * 2);
                }
            }
            
            // 处理可破坏物体
            DestructibleObject destructible = hit.collider.GetComponent<DestructibleObject>();
            if (destructible != null) {
                destructible.TakeDamage(damage);
            }
        }
        
        // 显示弹道(仅调试)
        Debug.DrawRay(ray.origin, ray.direction * range, Color.red, 0.1f);
    }
}

场景2:智能敌人AI视野系统

csharp

复制代码
public class EnemyVision : MonoBehaviour {
    public float sightRange = 20f;
    public float fieldOfView = 90f;
    public LayerMask targetMask;
    public LayerMask obstacleMask;
    
    private Transform player;
    
    void Start() {
        player = GameObject.FindGameObjectWithTag("Player").transform;
    }
    
    void Update() {
        if (CanSeePlayer()) {
            Debug.Log("发现玩家!");
            // 进入警戒状态
        }
    }
    
    bool CanSeePlayer() {
        if (player == null) return false;
        
        Vector3 directionToPlayer = (player.position - transform.position);
        float distanceToPlayer = directionToPlayer.magnitude;
        
        // 距离检查
        if (distanceToPlayer > sightRange) return false;
        
        // 视野角度检查
        float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
        if (angleToPlayer > fieldOfView / 2) return false;
        
        // 视线遮挡检查
        RaycastHit hit;
        if (Physics.Raycast(transform.position, directionToPlayer.normalized, 
                           out hit, distanceToPlayer, obstacleMask)) {
            // 如果有障碍物遮挡
            return false;
        }
        
        // 最终确认:玩家是否在目标层
        if (Physics.Raycast(transform.position, directionToPlayer.normalized, 
                           out hit, distanceToPlayer, targetMask)) {
            if (hit.collider.CompareTag("Player")) {
                return true;
            }
        }
        
        return false;
    }
    
    void OnDrawGizmosSelected() {
        // 绘制视野范围
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, sightRange);
        
        // 绘制视野锥
        Vector3 leftBoundary = Quaternion.Euler(0, -fieldOfView / 2, 0) * transform.forward * sightRange;
        Vector3 rightBoundary = Quaternion.Euler(0, fieldOfView / 2, 0) * transform.forward * sightRange;
        
        Gizmos.color = Color.green;
        Gizmos.DrawRay(transform.position, leftBoundary);
        Gizmos.DrawRay(transform.position, rightBoundary);
        Gizmos.DrawLine(transform.position + leftBoundary, transform.position + rightBoundary);
    }
}

场景3:高级地面检测与斜坡处理

csharp

复制代码
public class AdvancedGroundCheck : MonoBehaviour {
    [Header("检测设置")]
    public float groundCheckDistance = 0.2f;
    public float groundCheckRadius = 0.3f;
    public LayerMask groundLayer;
    public float maxSlopeAngle = 45f;
    
    [Header("状态")]
    public bool isGrounded;
    public bool isOnSlope;
    public Vector3 groundNormal;
    public float slopeAngle;
    
    private CapsuleCollider capsule;
    
    void Start() {
        capsule = GetComponent<CapsuleCollider>();
    }
    
    void FixedUpdate() {
        CheckGround();
    }
    
    void CheckGround() {
        // 使用SphereCast进行更准确的地面检测
        Vector3 rayStart = transform.position + Vector3.up * (capsule.height * 0.5f);
        float castDistance = capsule.height * 0.5f + groundCheckDistance;
        
        RaycastHit sphereHit;
        bool sphereCastHit = Physics.SphereCast(rayStart, groundCheckRadius, 
                                               Vector3.down, out sphereHit, 
                                               castDistance, groundLayer);
        // 使用Raycast作为备用
        RaycastHit rayHit;
        bool rayCastHit = Physics.Raycast(rayStart, Vector3.down, 
                                         out rayHit, castDistance, groundLayer);
        
        if (sphereCastHit || rayCastHit) {
            RaycastHit hit = sphereCastHit ? sphereHit : rayHit;
            
            // 计算地面角度
            groundNormal = hit.normal;
            slopeAngle = Vector3.Angle(groundNormal, Vector3.up);
            
            // 判断是否在地面上
            isGrounded = slopeAngle <= maxSlopeAngle;
            isOnSlope = slopeAngle > 5f && slopeAngle <= maxSlopeAngle;
            
            if (isGrounded) {
                // 调整角色位置,使其贴紧地面
                float heightDifference = hit.distance - (capsule.height * 0.5f);
                if (heightDifference < 0) {
                    transform.position += Vector3.up * -heightDifference;
                }
                
                // 如果是斜坡,计算斜坡方向
                if (isOnSlope) {
                    Vector3 slopeDirection = Vector3.ProjectOnPlane(Vector3.forward, groundNormal).normalized;
                    // 使用slopeDirection进行斜坡移动
                }
            }
        } else {
            isGrounded = false;
            isOnSlope = false;
            groundNormal = Vector3.up;
            slopeAngle = 0;
        }
    }
    
    // 获取斜坡上的移动方向
    public Vector3 GetSlopeMoveDirection(Vector3 inputDirection) {
        if (isOnSlope) {
            return Vector3.ProjectOnPlane(inputDirection, groundNormal).normalized;
        }
        return inputDirection.normalized;
    }
    
    void OnDrawGizmosSelected() {
        if (capsule == null) return;
        
        Vector3 rayStart = transform.position + Vector3.up * (capsule.height * 0.5f);
        float castDistance = capsule.height * 0.5f + groundCheckDistance;
        
        // 绘制检测范围
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawWireSphere(rayStart, groundCheckRadius);
        Gizmos.DrawLine(rayStart, rayStart + Vector3.down * castDistance);
        
        // 绘制地面法线
        if (isGrounded) {
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(transform.position, groundNormal * 2);
        }
    }
}

场景4:3D对象选择与拖拽系统

csharp

复制代码
public class ObjectSelector3D : MonoBehaviour {
    public Material highlightMaterial;
    public Material selectedMaterial;
    public LayerMask selectableLayers;
    
    private GameObject selectedObject;
    private Material originalMaterial;
    private Renderer objectRenderer;
    private bool isDragging = false;
    private Vector3 offset;
    
    void Update() {
        HandleSelection();
        HandleDragging();
    }
    
    void HandleSelection() {
        if (Input.GetMouseButtonDown(0)) {
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            
            if (Physics.Raycast(ray, out hit, Mathf.Infinity, selectableLayers)) {
                // 清除之前的选择
                ClearSelection();
                
                // 选择新对象
                selectedObject = hit.collider.gameObject;
                objectRenderer = selectedObject.GetComponent<Renderer>();
                
                if (objectRenderer != null) {
                    originalMaterial = objectRenderer.material;
                    objectRenderer.material = highlightMaterial;
                }
                
                // 计算拖拽偏移
                Vector3 mouseWorldPos = GetMouseWorldPosition();
                offset = selectedObject.transform.position - mouseWorldPos;
                
                isDragging = true;
            } else {
                // 点击空白处,清除选择
                ClearSelection();
            }
        }
    }
    
    void HandleDragging() {
        if (isDragging && selectedObject != null && Input.GetMouseButton(0)) {
            // 应用高亮材质
            if (objectRenderer != null) {
                objectRenderer.material = selectedMaterial;
            }
            
            // 获取鼠标在世界空间的位置
            Vector3 mouseWorldPos = GetMouseWorldPosition();
            
            // 使用射线投射确定拖拽平面
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            Plane dragPlane = new Plane(Vector3.up, selectedObject.transform.position);
            
            if (dragPlane.Raycast(ray, out float distance)) {
                Vector3 targetPos = ray.GetPoint(distance);
                selectedObject.transform.position = targetPos + offset;
            }
        }
        
        // 释放鼠标时清除拖拽状态
        if (Input.GetMouseButtonUp(0)) {
            if (isDragging && objectRenderer != null) {
                objectRenderer.material = highlightMaterial;
            }
            isDragging = false;
        }
    }
    
    Vector3 GetMouseWorldPosition() {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
        
        if (groundPlane.Raycast(ray, out float distance)) {
            return ray.GetPoint(distance);
        }
        
        return Vector3.zero;
    }
    
    void ClearSelection() {
        if (selectedObject != null && objectRenderer != null) {
            objectRenderer.material = originalMaterial;
        }
        
        selectedObject = null;
        objectRenderer = null;
        isDragging = false;
    }
}

第七部分:性能优化与最佳实践

1. 分层检测策略

csharp

复制代码
public class OptimizedDetection : MonoBehaviour {
    [Header("检测层次")]
    public bool useSimpleRaycast = true;      // 使用简单射线(性能最好)
    public bool useSphereCast = false;        // 使用球体射线(更准确)
    public bool useMultiRaycast = false;      // 使用多重射线(最准确)
    
    [Header("性能控制")]
    public int maxChecksPerFrame = 10;        // 每帧最大检测次数
    public float checkInterval = 0.1f;        // 检测间隔(秒)
    
    private float lastCheckTime;
    
    void Update() {
        // 控制检测频率
        if (Time.time - lastCheckTime < checkInterval) return;
        lastCheckTime = Time.time;
        
        if (useSimpleRaycast) {
            PerformSimpleRaycast();
        } else if (useSphereCast) {
            PerformSphereCast();
        } else if (useMultiRaycast) {
            PerformMultiRaycast();
        }
    }
    
    void PerformSimpleRaycast() {
        // 最基本的射线检测
        RaycastHit hit;
        if (Physics.Raycast(transform.position, transform.forward, out hit, 10f)) {
            ProcessDetection(hit);
        }
    }
    
    void PerformSphereCast() {
        // 使用SphereCast进行更准确的检测
        RaycastHit hit;
        if (Physics.SphereCast(transform.position, 0.5f, transform.forward, out hit, 10f)) {
            ProcessDetection(hit);
        }
    }
    
    void PerformMultiRaycast() {
        // 使用多个射线进行最准确的检测
        int checksThisFrame = 0;
        float[] rayAngles = { -15f, 0f, 15f };
        
        foreach (float angle in rayAngles) {
            if (checksThisFrame >= maxChecksPerFrame) break;
            
            Vector3 direction = Quaternion.Euler(0, angle, 0) * transform.forward;
            RaycastHit hit;
            
            if (Physics.Raycast(transform.position, direction, out hit, 10f)) {
                ProcessDetection(hit);
            }
            
            checksThisFrame++;
        }
    }
    
    void ProcessDetection(RaycastHit hit) {
        // 处理检测结果
    }
}

2. 对象池与缓存

csharp

复制代码
public class RaycastCacheSystem : MonoBehaviour {
    private Dictionary<GameObject, float> detectionCache = new Dictionary<GameObject, float>();
    private float cacheDuration = 2f; // 缓存2秒
    
    void Update() {
        CheckForObjects();
        CleanupCache();
    }
    
    void CheckForObjects() {
        RaycastHit[] hits = new RaycastHit[20];
        int hitCount = Physics.SphereCastNonAlloc(transform.position, 5f, Vector3.forward, hits);
        
        for (int i = 0; i < hitCount; i++) {
            GameObject obj = hits[i].collider.gameObject;
            
            // 检查缓存
            if (!detectionCache.ContainsKey(obj) || 
                Time.time - detectionCache[obj] > cacheDuration) {
                
                // 执行昂贵的检测逻辑
                PerformExpensiveDetection(obj);
                
                // 更新缓存
                detectionCache[obj] = Time.time;
            }
        }
    }
    
    void PerformExpensiveDetection(GameObject obj) {
        // 这里执行昂贵的检测逻辑
        // 例如:视线检测、路径计算等
    }
    
    void CleanupCache() {
        // 定期清理过期的缓存
        List<GameObject> toRemove = new List<GameObject>();
        
        foreach (var kvp in detectionCache) {
            if (Time.time - kvp.Value > cacheDuration * 2) {
                toRemove.Add(kvp.Key);
            }
        }
        
        foreach (var obj in toRemove) {
            detectionCache.Remove(obj);
        }
    }
}

3. 异步检测

csharp

复制代码
public class AsyncRaycast : MonoBehaviour {
    private bool isProcessing = false;
    private RaycastHit[] asyncResults = new RaycastHit[10];
    
    void Update() {
        if (Input.GetKeyDown(KeyCode.Space) && !isProcessing) {
            StartCoroutine(PerformAsyncRaycast());
        }
    }
    
    IEnumerator PerformAsyncRaycast() {
        isProcessing = true;
        
        // 在主线程准备数据
        Ray ray = new Ray(transform.position, transform.forward);
        
        // 在后台线程执行(使用Job System或Task)
        yield return StartCoroutine(RaycastAsync(ray, 100f));
        
        // 在主线程处理结果
        ProcessResults();
        
        isProcessing = false;
    }
    
    IEnumerator RaycastAsync(Ray ray, float maxDistance) {
        // 模拟耗时操作
        yield return new WaitForSeconds(0.1f);
        
        // 在实际项目中,这里可以使用Unity的Job System
        // 或System.Threading.Tasks进行真正的并行计算
        Physics.RaycastNonAlloc(ray, asyncResults, maxDistance);
    }
    
    void ProcessResults() {
        foreach (var hit in asyncResults) {
            if (hit.collider != null) {
                Debug.Log($"异步检测到: {hit.collider.name}");
            }
        }
    }
}

第八部分:常见问题与解决方案

问题1:射线检测不稳定(抖动)

csharp

复制代码
public class StableRaycast : MonoBehaviour {
    private RaycastHit lastHit;
    private bool hasLastHit = false;
    public float smoothingFactor = 0.5f;
    
    void Update() {
        RaycastHit currentHit;
        if (Physics.Raycast(transform.position, transform.forward, out currentHit, 10f)) {
            if (hasLastHit) {
                // 使用插值平滑过渡
                Vector3 smoothedPoint = Vector3.Lerp(lastHit.point, currentHit.point, smoothingFactor);
                Vector3 smoothedNormal = Vector3.Lerp(lastHit.normal, currentHit.normal, smoothingFactor);
                
                // 使用平滑后的结果
                ProcessHit(smoothedPoint, smoothedNormal);
            } else {
                ProcessHit(currentHit.point, currentHit.normal);
            }
            
            lastHit = currentHit;
            hasLastHit = true;
        } else {
            hasLastHit = false;
        }
    }
}

问题2:处理快速移动物体的检测

csharp

复制代码
public class FastObjectDetection : MonoBehaviour {
    public float speed = 20f;
    private Vector3 lastPosition;
    
    void Start() {
        lastPosition = transform.position;
    }
    
    void FixedUpdate() {
        Vector3 movement = transform.position - lastPosition;
        
        // 使用CapsuleCast检测移动路径上的碰撞
        RaycastHit hit;
        if (Physics.CapsuleCast(lastPosition, transform.position, 0.5f, 
                               movement.normalized, out hit, movement.magnitude)) {
            // 处理碰撞
            Debug.Log($"在移动过程中击中了: {hit.collider.name}");
            
            // 反弹或停止
            Vector3 reflection = Vector3.Reflect(movement, hit.normal);
            transform.position = hit.point + reflection.normalized * 0.1f;
        }
        
        lastPosition = transform.position;
    }
}

问题3:检测精度问题(缝隙)

csharp

复制代码
public class PreciseDetection : MonoBehaviour {
    public float skinWidth = 0.01f; // 皮肤宽度
    
    bool CheckCollision(Vector3 position, Vector3 direction, float distance) {
        // 多次检测提高精度
        int numChecks = 3;
        float checkDistance = distance / numChecks;
        
        for (int i = 0; i < numChecks; i++) {
            Vector3 checkPoint = position + direction * (checkDistance * i + skinWidth);
            
            // 使用OverlapSphere检查微小重叠
            if (Physics.OverlapSphere(checkPoint, skinWidth).Length > 0) {
                return true;
            }
        }
        
        return false;
    }
}

总结:射线与几何检测的核心思维

1. 选择合适的检测方法

  • 简单检测Physics.Raycast(视线、射击)

  • 体积检测Physics.SphereCastPhysics.CapsuleCast(角色移动)

  • 区域检测Physics.OverlapSpherePhysics.OverlapBox(爆炸、触发器)

  • 精确检测:多重射线或组合使用

2. 性能优化要点

  1. 使用LayerMask:只检测需要的层

  2. 使用NonAlloc版本:避免GC分配

  3. 控制检测频率:不要每帧检测

  4. 缓存检测结果:重复利用结果

  5. 简化检测形状:使用最简单的形状

3. 调试技巧

  1. 可视化一切 :使用Debug.DrawRayDebug.DrawLine

  2. 使用Gizmos:在Scene视图中绘制检测范围

  3. 记录日志:输出检测结果进行分析

  4. 性能分析:使用Profiler查看检测开销

4. 最终练习

创建一个完整的交互系统,包含:

  1. 鼠标点击选择物体

  2. 角色移动碰撞检测

  3. 敌人AI视野系统

  4. 武器射击系统

  5. 爆炸范围伤害

记住:射线和几何检测是游戏世界的"神经系统"。它们让游戏对象能够感知环境、做出反应,创造出沉浸式的交互体验。从简单的鼠标点击开始,逐步构建复杂的检测系统,你会逐渐掌握这门"与世界的交互"的艺术!

相关推荐
MobotStone2 小时前
三步高效拆解顶刊论文
算法
leiming62 小时前
C++ 类模板对象做函数参数
开发语言·c++·算法
王老师青少年编程2 小时前
csp信奥赛C++标准模板库STL案例应用1
c++·算法·stl·标准模板库·csp·信奥赛·binary_search
CreasyChan2 小时前
unity矩阵与变换 - “空间转换的魔术”
unity·矩阵·c#·游戏引擎
fcm192 小时前
pico之调试unity项目
unity·vr·pico
NAGNIP2 小时前
Kimi Linear——有望替代全注意力的全新注意力架构
算法·面试
WarPigs2 小时前
Unity生命周期函数笔记
unity·游戏引擎
Leoysq2 小时前
3Dmax 导入Unity 的设置
unity
智驱力人工智能2 小时前
无人机河道漂浮物检测 从项目构建到价值闭环的系统工程 无人机河道垃圾识别 农村河道漂浮物智能清理方案 无人机辅助河道清洁预警
opencv·算法·安全·yolo·目标检测·无人机·边缘计算