射线检测
中级 程序员
射线检测(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 表示,它包含关于命中的所有信息。
Point— 碰撞体在世界中被命中的位置。Normal— 被命中表面的法向量。Distance— 当Direction为单位长度时,原点与Point之间的距离。当它不是单位长度时,你可以预期actualDistance = Direction.Length * DistanceCollidable— 被命中的可碰撞体组件。ChildIndex— 如果是复合碰撞体,则为可碰撞体的碰撞体中所用形状的索引。
获取实体
因为 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);
}