发射射线
前言
通过发射射线(和自定义形状的对象)来检测命中的物体是游戏开发中最常见的任务之一。这是 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 可分别在 PhysicsServer3D 和 PhysicsServer2D 中使用。
获取空间
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)
public override void _PhysicsProcess(double delta)
{
var spaceRid = GetWorld2D().Space;
var spaceState = PhysicsServer2D.SpaceGetDirectState(spaceRid);
}
或者更直接:
func _physics_process(delta):
var space_state = get_world_2d().direct_space_state
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
}
在 3D 中:
func _physics_process(delta):
var space_state = get_world_3d().direct_space_state
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld3D().DirectSpaceState;
}
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)
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
// use global coordinates, not local to node
var query = PhysicsRayQueryParameters2D.Create(Vector2.Zero, new Vector2(50, 100));
var result = spaceState.IntersectRay(query);
}
结果是一个字典。如果射线什么都没有击中,那么字典就是空的。如果击中了,就会包含碰撞信息:
if result:
print("Hit at point: ", result.position)
if (result.Count > 0)
GD.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)
private const int RayLength = 1000;
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld3D().DirectSpaceState;
var cam = GetNode<Camera3D>("Camera3D");
var mousePos = GetViewport().GetMousePosition();
var origin = cam.ProjectRayOrigin(mousePos);
var end = origin + cam.ProjectRayNormal(mousePos) * RayLength;
var query = PhysicsRayQueryParameters3D.Create(origin, end);
query.CollideWithAreas = true;
var result = spaceState.IntersectRay(query);
}
碰撞例外
光线投射的常见用例是使角色能够收集有关其周围世界的数据。这种情况的一个问题是该角色上有碰撞体,因此光线只会检测到其父节点上的碰撞体,如下图所示:

为了避免自相交,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)
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
var query = PhysicsRayQueryParameters2D.Create(globalPosition, playerPosition);
query.Exclude = [GetRid()];
var result = spaceState.IntersectRay(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)
using Godot;
public partial class MyCharacterBody2D : CharacterBody2D
{
public override void _PhysicsProcess(double delta)
{
var spaceState = GetWorld2D().DirectSpaceState;
var query = PhysicsRayQueryParameters2D.Create(globalPosition, targetPosition,
CollisionMask, [GetRid()]);
var result = spaceState.IntersectRay(query);
}
}
关于如何设置碰撞掩码, 请参阅 代码示例 .
来自屏幕的 3D 光线投射
将射线从屏幕投射到 3D 物理空间对于拾取对象非常有用。没有必要这样做,因为 CollisionObject3D 有一个“input_event”信号,可以让你知道何时点击它,但如果你希望手动执行该操作,可这样。
要从屏幕投射光线,你需要一个 Camera3D 节点。Camera3D
可以有两种投影模式:透视和正交。因此,必须获取射线原点和方向。这是因为 origin
在正交模式下会发生变化,而 normal
在透视模式下会发生变化:

要使用相机获取它, 可以使用以下代码:
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
private const float RayLength = 1000.0f;
public override void _Input(InputEvent @event)
{
if (@event is InputEventMouseButton eventMouseButton && eventMouseButton.Pressed && eventMouseButton.ButtonIndex == MouseButton.Left)
{
var camera3D = GetNode<Camera3D>("Camera3D");
var from = camera3D.ProjectRayOrigin(eventMouseButton.Position);
var to = from + camera3D.ProjectRayNormal(eventMouseButton.Position) * RayLength;
}
}
请记住,在 _input()
期间空间可能被锁定,所以实践中应该在 _physics_process()
中运行这个查询。