Table of Contents

射线检测

中级 程序员

射线检测(Raycast) 是最常用的物理查询类型。它沿某个方向投射一条射线,并返回它碰撞到的内容。更通俗地说,就像发射一束看不见的激光,看看它击中了什么。

射线检测可用于武器发射或 NPC AI。

射线检测查询

要执行射线检测,请使用 Simulation.RayCast。如果成功命中某些内容,该方法返回 true。在这种情况下,关于命中的所有信息都将包含在 HitInfo 中。

public void Shoot()
{
    var simulation = Entity.GetSimulation();
    
    // 射线开始的原点
    var origin = Entity.Transform.WorldMatrix.TranslationVector;
    
    // 射线的方向
    // 在本例中,它将沿实体的前方方向前进
    var direction = Entity.Transform.WorldMatrix.Forward;
    
    // 射线的最大距离
    var maxDistance = 16f;
    
    if (simulation.RayCast(origin, direction, maxDistance, out HitInfo result))
    {
        // 处理成功命中
    }
}

穿透式射线检测查询

这种查询与普通查询的区别在于,当射线到达某个对象时,它不会停止,而是继续前进,直到达到最大长度。

要执行穿透式射线检测,请使用 Simulation.RayCastPenetrating。

public void Shoot()
{
    var hits = new List<HitInfo>();
    simulation.RayCastPenetrating(origin, direction, maxDistance, hits);
    
    // 遍历所有结果
    foreach (var hitInfo in hits)
    {
        // 处理成功命中
    }
}
Note

命中结果的返回顺序不做任何保证。如果你想让它们按距离排序,必须自己对结果进行排序。

穿透式射线检测查询(使用 stackalloc)

在重复执行穿透式射线检测时,它必须不断为结果在堆上分配内存,这会给垃圾回收器(GC)带来更大压力(意味着更多的内存占用)。更优的解决方案是使用 stackalloc。

public void Shoot()
{
    // 分配一个最多可容纳 16 个元素的缓冲区
    Span<HitInfoStack> buffer = stackalloc HitInfoStack[16];
    
    // 遍历所有结果
    foreach (var hitInfo in simulation.RayCastPenetrating(origin, maxDistance, buffer))
    {
        // 处理成功命中
    }
}
Note

span 能容纳的元素数量是有限的。如果命中数量超过缓冲区大小,则只会返回最近的那些命中。

处理命中

每个命中都由 HitInfo 表示,它包含关于命中的所有信息。

获取实体

因为 Collidable 是一个组件,所以你可以从它获取实体。

var entity = hit.Collidable.Entity;

获取与命中的距离

与命中的距离可以使用射线原点和 Point 通过 Vector3.Distance 计算得出。

var actualDistance = Vector3.Distance(origin, hit.Point);

使用碰撞层遮罩

每个查询都有一个可选参数,用于指定应在哪些碰撞层上执行该查询。

有关如何使用碰撞层遮罩的更多信息,请访问物理查询主页。

示例

以下是游戏中使用射线检测的几个示例。

造成伤害

当按下鼠标左键时,如果发射射线的实体前方有一个带有 'HostileUnit' 脚本的实体,就对它造成伤害。碰撞层 0 和 1 将被忽略。

public override void Update()
{
    var simulation = Entity.GetSimulation();
    var rayDir = Entity.Transform.WorldMatrix.Forward;
    var maxDistance = 10f;
    
    // 除 0 和 1 之外的所有层
    var layers = CollisionMask.Everything & ~(CollisionMask.Layer0 | CollisionMask.Layer1); 

    if (Input.IsMouseButtonPressed(MouseButton.Left) &&
        simulation.RayCast(rayStart, rayDir, maxDistance, out HitInfo hitResult, layers))
    {
        var hostile = hitResult.Collidable.Entity.Get<HostileUnit>();
        
        // 如果命中的是敌对单位,施加伤害
        if (hostile != null)
            hostile.DealDamage();
    }
}

从穿透式射线检测获取信息

该方法将在实体所面对的方向上执行穿透式射线检测,范围为 16 米。

该方法只会记录前 16 次命中的信息。

public void Shoot()
{
    var simulation = Entity.GetSimulation();
    var origin = Entity.Transform.WorldMatrix.TranslationVector;
    var direction = Entity.Transform.WorldMatrix.Forward;
    
    Span<HitInfoStack> buffer = stackalloc HitInfoStack[16];
    
    foreach (var hitInfo in simulation.RayCastPenetrating(origin, direction, 16f, buffer))
    {
        Log.Warning($"T: {hitInfo.Distance}\nNormal: {hitInfo.Normal}\nEntity : {hitInfo.Collidable.Entity}");
    }
}

使用鼠标进行射线检测

该方法将从鼠标在屏幕上的位置执行射线检测。

public static bool ScreenPositionToWorldPositionRaycast(Vector2 screenPos, CameraComponent camera)
{
    Matrix invViewProj = Matrix.Invert(camera.ViewProjectionMatrix);

    // 重构 (-1, +1) 范围内的投影空间位置。
    // 不要忘记在屏幕坐标中 Y 向下,但在投影空间中 Y 向上
    Vector3 sPos;
    sPos.X = screenPos.X * 2f - 1f;
    sPos.Y = 1f - screenPos.Y * 2f;

    // 计算射线检测的近端(起点)
    // 假设它具有相同的投影空间 (x,y) 坐标且 z = 0(位于近平面上)
    // 我们需要将它反投影到世界空间
    sPos.Z = 0f;
    var vectorNear = Vector3.Transform(sPos, invViewProj);
    vectorNear /= vectorNear.W;

    // 计算射线检测的远端(终点)
    // 假设它具有相同的投影空间 (x,y) 坐标且 z = 1(位于远平面上)
    // 我们需要将它反投影到世界空间
    sPos.Z = 1f;
    var vectorFar = Vector3.Transform(sPos, invViewProj);
    vectorFar /= vectorFar.W;

    var delta = (vectorFar - vectorNear).XYZ();
    var maxDistance = delta.Length();
    var dir = delta / maxDistance; // 归一化 delta

    // 从近平面上的点向远平面上的点执行射线检测并获取碰撞结果
    return camera.Entity.GetSimulation().RayCast(vectorNear.XYZ(), dir, maxDistance, out HitInfo hit);
}