For the complete documentation index, see llms.txt. This page is also available as Markdown.

射线投射

使用射线投射在空间中追踪一条线,并查询与场景中实体的碰撞。

射线投射是游戏开发中的基础工具。借助射线投射,你可以在空间中追踪一条虚拟直线,并查询是否有任何实体与该直线相交。这对于计算视线、子弹轨迹、寻路算法以及许多其他应用都很有用。

当玩家按下指针按钮,或主按钮或次按钮时,会从玩家的位置沿其所看的方向发射一条射线,见 按钮事件 了解更多详情。本文档介绍如何从任意位置和方向追踪一条不可见的射线,且不依赖玩家操作,你可以在许多其他场景中使用它。

请注意,射线投射只能命中带有碰撞器的对象。所以如果你想检测射线与 3D 模型的碰撞,请采取以下任一方式:

此外,最好给 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 种方法中的每一种来确定射线方向的示例:

射线检测结果

处理射线检测的回调函数会接收一个对象,其中包含关于射线本身以及任何被命中的实体的数据。

  • 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 属性来均匀计时。它还包含一个上下振荡的立方体,由另一个系统控制,使其进出射线路径。

💡 提示:使用 interval 函数,位于 SDK Utils 库中 ,可用更简单的方式以固定间隔运行一个函数。

通过系统执行射线检测

执行周期性射线检测的另一种方法,是在系统的循环函数内部运行它们。这使你能够对它们何时以及如何工作有更多控制。你可以不注册回调函数,而是通过 raycastSystem.registerRaycast 执行射线检测查询,然后在系统函数中检查该操作返回的数据。

请注意,由于射线检测是在系统中执行的,结果要到下一个 tick 才可用,因此需要运行两次系统:一次用于为下一帧注册射线检测,下一帧再处理其结果。

与玩家碰撞

你可以通过在掩码中包含任意一个头像碰撞层,直接使用射线检测来检测头像:

  • ColliderLayer.CL_PLAYER:匹配任意头像——本地玩家以及场景中渲染的任何其他玩家。

  • ColliderLayer.CL_MAIN_PLAYER:仅匹配本地(主)玩家。

这两个层可以组合使用,也可以单独使用。射线检测的默认 collisionMask 为 CL_PHYSICS,这不会命中头像——你必须显式启用。

当射线检测命中本地玩家时, hit.entityId 为 engine.PlayerEntity。命中远程头像时,不会携带场景本地实体 ID(该字段为 0),因为远程玩家并不是你场景世界中的实体——但仍会报告其 位置, length, normalHit以及其他几何数据。

💡 提示:若只检测 其他 玩家(排除本地玩家),请使用 CL_PLAYER 并在 hit.entityId !== engine.PlayerEntity 中进行过滤。

从玩家发射射线

若要从玩家位置沿着摄像机朝向追踪一条射线,你可以使用摄像机或头像来追踪一条射线 保留实体.

💡 提示:在大多数情况下,你可能更适合使用 指针事件 而不是射线检测。

以下示例使用 engine.CameraEntity 实体,从玩家摄像机位置向前追踪一条射线。

从光标位置发射射线

你也可以从玩家的光标位置向 3D 世界中追踪一条射线。这可用于拖动物体、射击游戏等。

在这个示例中,我们检测玩家何时按下 E 键,然后从光标位置向 3D 世界中追踪一条射线。接着我们检查射线是否命中任何实体,如果命中了,就对其执行某些操作。

💡 提示:在此示例中,我们使用主按钮(E)来触发射线检测。我们不使用指针按钮(左键单击),因为单击并拖动时默认也会改变摄像机角度。如果你想在拖动时阻止摄像机旋转,可以使用一个 虚拟摄像机 来将摄像机角度设置为固定。

高级语法

创建射线检测组件

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 使用一个从场景中所有实体获取 组件查询.

在一个场景中,如果你出于不同目的使用多种射线(例如路径查找、视线检查、投射物追踪等),你可能希望使用不同的 碰撞层,以避免计算无关的碰撞。

💡 提示:有关射线检测的可运行示例,请参见 77,-1-raycast-unit-tests 测试场景,它通过 @dcl/sdk/testing 跨以下内容的单元测试 localDirection, globalDirection, globalTarget 和 targetEntity 方向形式,包括从已变换父级发出的射线。要了解射线可命中的碰撞体层如何表现,请参见 5,5-collider-layers.

最后更新于