射线投射
使用射线投射在空间中追踪一条线,并查询与场景中实体的碰撞。
射线投射是游戏开发中的基础工具。借助射线投射,你可以在空间中追踪一条虚拟直线,并查询是否有任何实体与该直线相交。这对于计算视线、子弹轨迹、寻路算法以及许多其他应用都很有用。
当玩家按下指针按钮,或主按钮或次按钮时,会从玩家的位置沿其所看的方向发射一条射线,见 按钮事件 了解更多详情。本文档介绍如何从任意位置和方向追踪一条不可见的射线,且不依赖玩家操作,你可以在许多其他场景中使用它。
请注意,射线投射只能命中带有碰撞器的对象。所以如果你想检测射线与 3D 模型的碰撞,请采取以下任一方式:
该模型必须包含 碰撞网格.
该
GLTFContainer必须配置为使用 带碰撞遮罩的可见几何体.添加一个 MeshCollider 组件.
此外,最好给 3D 模型分配自定义 碰撞层 ,这样射线就只需计算与相关实体的碰撞,而不是与所有带有碰撞器的对象计算。
创建一条射线
所有射线都有一个起点和一个方向。起点基于某个实体的位置,并采用该实体 Transform 组件中的值。射线的方向可以通过 4 种不同方式定义:
局部:相对于实体朝前方向的方向,也会受到任何父实体变换的影响。这对于检测遵循航向的车辆前方障碍物很有用。
global:忽略实体的旋转,并像实体旋转为 0 一样朝向某个方向。例如,这对于始终向下非常有用。
全局目标:在实体位置与场景中的目标全局位置之间追踪一条线。它会忽略实体的旋转。例如,这对于制作塔防游戏很有用,每座塔的炮塔都可以指向空间中的一个精确坐标。
目标实体:在实体位置与第二个目标实体的位置之间追踪一条线。它会忽略任一实体的旋转。
以下代码使用局部方向创建一条射线检测:
const myEntity = engine.addEntity()
Transform.create(myEntity, {
position: Vector3.create(4, 1, 4),
})
raycastSystem.registerLocalDirectionRaycast(
{
entity: myEntity,
opts: { direction: Vector3.Forward() },
},
function (raycastResult) {
// 回调函数
}
)使用以下函数,以不同方式提供方向来创建射线检测:
raycastSystem.registerLocalDirectionRaycast():创建一条带有 局部 方向的射线检测。方向字段期望一个Vector3,它描述了相对于实体及其旋转的向量(例如Vector3.Forward()最终会使用实体的变换前向向量)raycastSystem.registerGlobalDirectionRaycast():创建一条带有 global 方向的射线检测。方向字段期望一个Vector3,用于描述全局方向。raycastSystem.registerGlobalTargetRaycast():创建一条方向由 全局目标 位置目标字段期望一个Vector3定义的射线检测。raycastSystem.registerTargetEntityRaycast():创建一条方向指向 目标实体 位置targetEntity字段期望一个实体引用,该实体的位置将被用作射线的目标。
在使用以上任一方法创建射线时,可使用以下可选字段:
maxDistance: 数值 用于设置这条射线的追踪长度。如果未设置,默认值为 16 米。queryType: RaycastQueryType 枚举值,用于定义射线是返回所有命中的实体还是只返回第一个。可用选项如下:RaycastQueryType.RQT_HIT_FIRST: (默认) 只返回第一个命中的实体,从起点开始计算。RaycastQueryType.RQT_QUERY_ALL:返回所有命中的实体,从起点一直到射线的最大距离。
originOffset:不从实体的起点位置开始射线检测,而是添加一个偏移量,从相对位置开始查询。例如,你可以使用一个小偏移,防止射线与实体自身的碰撞器发生碰撞。如果未设置,默认值为Vector3.Zero().collisionMask:仅检测某些碰撞层中的碰撞。可将其与自定义碰撞层一起使用,或仅检测物理层或指针事件层。参见 碰撞层。如果未设置,默认使用的层是ColliderLayer.CL_PHYSICS.continuous:如果为 true,则会在每一帧持续运行一次射线检测查询。如果为 false,则该射线只会在当前帧使用。如果未设置,默认值为 false。当使用局部或全局方向设置方向时,
方向字段默认值为Vector3.Forward().当使用全局目标设置方向时,
目标字段默认值为Vector3.Zero().当使用实体目标设置方向时,
targetEntity字段默认值为场景的根实体,位于Vector3.Zero().
下面是使用这 4 种方法中的每一种来确定射线方向的示例:
import { raycastSystem, RaycastQueryType, ColliderLayer } from "@dcl/sdk/ecs"
射线检测结果
处理射线检测的回调函数会接收一个对象,其中包含关于射线本身以及任何被命中的实体的数据。
globalOrigin:射线发射的起始位置,相对于场景。方向:射线所指向的全局方向,作为一个Vector3.hits:一个数组,每个被命中的实体对应一个对象。如果没有命中的实体,这个数组为空。如果射线检测使用了RaycastQueryType.RQT_HIT_FIRST,这个数组只会包含一个对象。
中的每个对象都包含: hits 数组
entityId:被射线命中的实体的 ID 数字。meshName: 字符串 ,包含被命中的 3D 模型中具体网格的内部名称。当一个 3D 模型由多个网格组成时,这很有用。位置: Vector3 用于表示射线与命中实体相交的位置(相对于场景)length:射线从起点到命中实体发生位置的长度。normalHit: Vector3 用于表示世界空间中命中表面的法线。globalOrigin: Vector3 用于表示射线起点的位置(相对于场景)方向:射线所指向的全局方向,作为一个Vector3.
以下示例会遍历被命中的实体:
处理命中的实体
当你获得一个命中实体的射线检测结果时,你可以使用 entityId 与该实体及其组件进行交互。实体不过是一个 数字,因此该 entityId 值本身可以被解释为一个 Entity 类型。
碰撞层
最好只检查与相关实体的碰撞,以提升场景性能。 collisionMask 字段允许你只列出特定的碰撞层——物理层(场景墙壁和地板)、指针层(指针事件)、玩家层(头像),或者你可以自由分配的 8 个自定义层。参见 碰撞层.
默认情况下, collisionMask 字段设置为 ColliderLayer.CL_PHYSICS。你可以将此值更改为列出其他层,或者使用 | 分隔符将多个层组合起来。
周期性射线检测
使用 raycastSystem的函数时,默认行为是创建一条只查询一次碰撞的单条射线。作为替代方案,你可以将 continuous 字段设置为 true ,以便在游戏循环的每个 tick 中都运行一次查询和回调函数。
下面的示例会从此时起持续运行射线检测查询
当不再需要时,请移除任何周期性射线检测。为此,你必须使用 raycastSystem.removeRaycasterEntity.
在可能的情况下,请使用一个系统(或 interval 函数,位于 Utils 库中)以较稀疏的固定间隔运行射线检测查询,例如每秒一次,或者每五分之一秒一次。
上面的示例每 0.1 秒运行一次周期性射线检测。它使用一个计时器组件和一个系统的 dt 属性来均匀计时。它还包含一个上下振荡的立方体,由另一个系统控制,使其进出射线路径。
通过系统执行射线检测
执行周期性射线检测的另一种方法,是在系统的循环函数内部运行它们。这使你能够对它们何时以及如何工作有更多控制。你可以不注册回调函数,而是通过 raycastSystem.registerRaycast 执行射线检测查询,然后在系统函数中检查该操作返回的数据。
请注意,由于射线检测是在系统中执行的,结果要到下一个 tick 才可用,因此需要运行两次系统:一次用于为下一帧注册射线检测,下一帧再处理其结果。
与玩家碰撞
你可以通过在掩码中包含任意一个头像碰撞层,直接使用射线检测来检测头像:
ColliderLayer.CL_PLAYER:匹配任意头像——本地玩家以及场景中渲染的任何其他玩家。ColliderLayer.CL_MAIN_PLAYER:仅匹配本地(主)玩家。
这两个层可以组合使用,也可以单独使用。射线检测的默认 collisionMask 为 CL_PHYSICS,这不会命中头像——你必须显式启用。
当射线检测命中本地玩家时, hit.entityId 为 engine.PlayerEntity。命中远程头像时,不会携带场景本地实体 ID(该字段为 0),因为远程玩家并不是你场景世界中的实体——但仍会报告其 位置, length, normalHit以及其他几何数据。
从玩家发射射线
若要从玩家位置沿着摄像机朝向追踪一条射线,你可以使用摄像机或头像来追踪一条射线 保留实体.
以下示例使用 engine.CameraEntity 实体,从玩家摄像机位置向前追踪一条射线。
从光标位置发射射线
你也可以从玩家的光标位置向 3D 世界中追踪一条射线。这可用于拖动物体、射击游戏等。
在这个示例中,我们检测玩家何时按下 E 键,然后从光标位置向 3D 世界中追踪一条射线。接着我们检查射线是否命中任何实体,如果命中了,就对其执行某些操作。
高级语法
创建射线检测组件
Raycast 组件描述了用于查询相交实体的不可见射线。射线从实体的位置开始追踪,该位置由 Transform 组件定义,并受到任何父实体的影响。方向可以通过多种方式定义,
射线使用以下数据定义:
方向:一个对象,包含一个$case字段用于选择方向类型,以及一个取决于该类型的附加字段,用于确定这个方向。以下是可接受的$case:“localDirection”:相对于实体朝前方向的方向,也会受到任何父实体变换的影响。这对于检测遵循航向的车辆前方障碍物很有用。旋转由localDirection字段定义,为一个Vector3,用于描述旋转。“globalDirection”:忽略实体的旋转,并像实体旋转为 0 一样朝向某个方向。这对于例如始终向下很有用。旋转由globalDirection字段定义,为一个Vector3,用于描述旋转。“globalTarget”:在实体位置与场景中的目标全局位置之间追踪一条线。它会忽略实体的旋转。对于创建塔防游戏很有用,每座塔的炮塔都可以指向空间中的一个精确坐标。目标由globalTarget字段定义,为一个Vector3定义,它描述全局位置。“targetEntity”:在实体位置与第二个目标实体的位置之间追踪一条线。它会忽略任一实体的旋转。目标由targetEntity字段定义,该字段持有实体的引用。
maxDistance: 数值 用于设置这条射线的追踪长度。queryType: RaycastQueryType 枚举值,用于定义射线是返回所有命中的实体还是只返回第一个。可用选项如下:RaycastQueryType.RQT_HIT_FIRST:只返回第一个命中的实体,从起点开始计算。RaycastQueryType.RQT_QUERY_ALL:返回所有命中的实体,从起点一直到射线的最大距离。
collisionMask:仅检测某些碰撞层中的碰撞。可将其与自定义碰撞层一起使用,或仅检测物理层或指针事件层。参见 碰撞层。默认值为ColliderLayer.CL_POINTER | ColliderLayer.CL_PHYSICS.originOffset:不从实体的起点位置开始射线检测,而是添加一个偏移量,从相对位置开始查询。例如,你可以使用一个小偏移,防止射线与实体自身的 3D 模型发生碰撞。continuous:如果为 true,则会在每一帧持续运行一次射线检测查询。如果为 false,则该射线只会在当前帧使用。默认情况下该值为 false。
下面的示例使用全局旋转来确定方向,并且只返回射线发出所在帧中被命中的第一个实体。
下面的示例会沿实体的前向方向发射一条射线,只返回第一个命中的物体。它会持续执行。它还包含 0.5 的轻微偏移,以防止射线击中实体自身的碰撞体。
这个示例在两个实体之间追踪一条射线。它会返回中间所有被击中的实体。
Raycast 结果组件
创建 Raycast 组件后,添加了该组件的实体将拥有一个 RaycastResult 组件。该组件包含有关射线任何命中的信息。请设置一个系统来检查这些数据。
该 RaycastResult 组件包含以下数据:
globalOrigin:射线发射的起始位置,相对于场景。方向:射线所指向的全局方向,作为一个Vector3.hits:一个数组,每个被命中的实体对应一个对象。如果没有命中的实体,这个数组为空。如果射线检测使用了RaycastQueryType.RQT_HIT_FIRST,这个数组只会包含一个对象。
中的每个对象都包含: hits 数组
entityId:被射线命中的实体的 ID 数字。meshName: 字符串 ,包含被命中的 3D 模型中具体网格的内部名称。当一个 3D 模型由多个网格组成时,这很有用。位置: Vector3 用于表示射线与命中实体相交的位置(相对于场景)length:射线从起点到命中实体发生位置的长度。normalHit: Vector3 用于表示世界空间中命中表面的法线。globalOrigin: Vector3 用于表示射线起点的位置(相对于场景)方向:射线所指向的全局方向,作为一个Vector3.
下面的示例展示了如何使用一个系统从单个实体访问结果:
下一个示例展示了如何访问 RaycastResult 使用一个从场景中所有实体获取 组件查询.
在一个场景中,如果你出于不同目的使用多种射线(例如路径查找、视线检查、投射物追踪等),你可能希望使用不同的 碰撞层,以避免计算无关的碰撞。
最后更新于