发射射线

前言

通过发射射线(和自定义形状的对象)来检测命中的物体是游戏开发中最常见的任务之一。这是 AI 等复杂行为的基础。本教程将介绍在 2D 和 3D 中的实现方法。

Godot stores all the low-level game information in servers, while the scene is only a frontend. As such, ray casting is generally a lower-level task. For simple raycasts, nodes like RayCast3D and RayCast2D will work, as they return every frame what the result of a raycast is.

但是很多时候,射线投射需要更具交互性,因此必须存在通过代码执行此操作的方法。

空间

In the physics world, Godot stores all the low-level collision and physics information in a space. The current 2d space (for 2D Physics) can be obtained by accessing CanvasItem.get_world_2d().space. For 3D, it's Node3D.get_world_3d().space.

对于 3D 和 2D,得到的空间 RID 可分别在 PhysicsServer3DPhysicsServer2D 中使用。

获取空间

Godot 物理默认与游戏逻辑在同一个线程中运行,但可以设置为在单独的线程中运行以提高效率。因此,唯一安全访问空间的时间是在 Node._physics_process() 回调期间。从该函数之外访问空间可能会产生一个错误,因为空间会被锁定

To perform queries into physics space, the PhysicsDirectSpaceState2D and PhysicsDirectSpaceState3D must be used.

在 2D 中使用以下代码:

func _physics_process(delta):
    var space_rid = get_world_2d().space
    var space_state = PhysicsServer2D.space_get_direct_state(space_rid)

或者更直接:

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state

在 3D 中:

func _physics_process(delta):
    var space_state = get_world_3d().direct_space_state

Raycast 查询

要执行 2D 射线查询,可以使用 PhysicsDirectSpaceState2D.intersect_ray() 方法。例如:

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
    # use global coordinates, not local to node
    var query = PhysicsRayQueryParameters2D.create(Vector2(0, 0), Vector2(50, 100))
    var result = space_state.intersect_ray(query)

结果是一个字典。如果射线什么都没有击中,那么字典就是空的。如果击中了,就会包含碰撞信息:

if result:
    print("Hit at point: ", result.position)

发生碰撞时,result 字典包含以下数据:

{
   position: Vector2 # point in world space for collision
   normal: Vector2 # normal in world space for collision
   collider: Object # Object collided or null (if unassociated)
   collider_id: ObjectID # Object it collided against
   rid: RID # RID it collided against
   shape: int # shape index of collider
   metadata: Variant() # metadata of collider
}

3D 空间中的数据也是类似的,只不过使用的是 Vector3 坐标。请注意,要启用与 Area3D 的碰撞,必须将布尔值参数 collide_with_areas 设置为 true

const RAY_LENGTH = 1000

func _physics_process(delta):
    var space_state = get_world_3d().direct_space_state
    var cam = $Camera3D
    var mousepos = get_viewport().get_mouse_position()

    var origin = cam.project_ray_origin(mousepos)
    var end = origin + cam.project_ray_normal(mousepos) * RAY_LENGTH
    var query = PhysicsRayQueryParameters3D.create(origin, end)
    query.collide_with_areas = true

    var result = space_state.intersect_ray(query)

碰撞例外

光线投射的常见用例是使角色能够收集有关其周围世界的数据。这种情况的一个问题是该角色上有碰撞体,因此光线只会检测到其父节点上的碰撞体,如下图所示:

../../_images/raycast_falsepositive.webp

为了避免自相交,intersect_ray() 参数对象可以通过其 exclude 属性获取一个排除数组。这是一个如何从 CharacterBody2D 或任何其他碰撞对象节点使用它的示例:

extends CharacterBody2D

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
    var query = PhysicsRayQueryParameters2D.create(global_position, player_position)
    query.exclude = [self]
    var result = space_state.intersect_ray(query)

例外数组可以包含对象或 RID。

碰撞遮罩

虽然例外方法适用于排除父体, 但如果需要大型和/或动态的例外列表, 则会变得非常不方便. 在这种情况下, 使用碰撞层/遮罩系统要高效得多.

intersect_ray() 参数对象也可以提供一个碰撞掩码。例如,要使用与父物体相同的掩码,请使用 collision_mask 成员变量。排除数组也可以作为最后一个参数提供:

extends CharacterBody2D

func _physics_process(delta):
    var space_state = get_world_2d().direct_space_state
    var query = PhysicsRayQueryParameters2D.create(global_position, target_position,
        collision_mask, [self])
    var result = space_state.intersect_ray(query)

关于如何设置碰撞掩码, 请参阅 代码示例 .

来自屏幕的 3D 光线投射

将射线从屏幕投射到 3D 物理空间对于拾取对象非常有用。没有必要这样做,因为 CollisionObject3D 有一个“input_event”信号,可以让你知道何时点击它,但如果你希望手动执行该操作,可这样。

要从屏幕投射光线,你需要一个 Camera3D 节点。Camera3D 可以有两种投影模式:透视和正交。因此,必须获取射线原点和方向。这是因为 origin 在正交模式下会发生变化,而 normal 在透视模式下会发生变化:

../../_images/raycast_projection.png

要使用相机获取它, 可以使用以下代码:

const RAY_LENGTH = 1000.0

func _input(event):
    if event is InputEventMouseButton and event.pressed and event.button_index == 1:
          var camera3d = $Camera3D
          var from = camera3d.project_ray_origin(event.position)
          var to = from + camera3d.project_ray_normal(event.position) * RAY_LENGTH

请记住,在 _input() 期间空间可能被锁定,所以实践中应该在 _physics_process() 中运行这个查询。