发射射线

简介

游戏开发中最常见的任务之一是发射射线(或自定义形状的对象)并检查其击中的内容. 这可以产生复杂的行为,如AI等. 本教程将介绍如何在2D和3D中执行此操作.

Godot将所有低级游戏信息存储在服务器中,而场景只是一个前端. 因此,发射射线通常是较低级别的任务. 对于简单的射线发射,使用 RayCastRayCast2D 节点就可以了,因为它们将每一帧都返回射线投射的结果.

但是,很多时候,射线投射应该是一个更具交互性的过程,因此必须存在通过代码执行此操作的方法.

空间

在物理世界中,Godot将所有低级的碰撞和物理信息存储在一个 空间 中.当前的2D空间,对于2D物理,可以通过访问 CanvasItem.get_world_2d().space 获得.对于3D,则为 Spatial.get_world().space .

结果空间 RID 可在3D的 PhysicsServer 和2D的 Physics2DServer 中.

获取空间

Godot物理默认与游戏逻辑运行在同一个线程中,但可以设置为在一个单独的线程上运行,以便更高效地工作.由于这一点,只有在 Node._physics_process() 回调期间访问空间才是安全的.从这个函数之外访问它可能会因为空间被 锁定 而导致错误.

要对物理空间执行查询,必须使用 Physics2DDirectSpaceStatePhysicsDirectSpaceState .

在2D中使用以下代码:

GDscript

C#

  1. func _physics_process(delta):
  2. var space_rid = get_world_2d().space
  3. var space_state = Physics2DServer.space_get_direct_state(space_rid)
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. var spaceRid = GetWorld2d().Space;
  4. var spaceState = Physics2DServer.SpaceGetDirectState(spaceRid);
  5. }

或者更直接:

GDScript

C#

  1. func _physics_process(delta):
  2. var space_state = get_world_2d().direct_space_state
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. var spaceState = GetWorld2d().DirectSpaceState;
  4. }

在3D中:

GDScript

C#

  1. func _physics_process(delta):
  2. var space_state = get_world().direct_space_state
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. var spaceState = GetWorld().DirectSpaceState;
  4. }

Raycast查询

为了执行二维 raycast射线查询,可以使用方法 Physics2DDirectSpaceState.intersect_ray() .例如:

GDScript

C#

  1. func _physics_process(delta):
  2. var space_state = get_world_2d().direct_space_state
  3. # use global coordinates, not local to node
  4. var result = space_state.intersect_ray(Vector2(0, 0), Vector2(50, 100))
  1. public override void _PhysicsProcess(float delta)
  2. {
  3. var spaceState = GetWorld2d().DirectSpaceState;
  4. // use global coordinates, not local to node
  5. var result = spaceState.IntersectRay(new Vector2(), new Vector2(50, 100));
  6. }

结果是一个字典.如果射线没有击中任何东西,字典将是空的.如果它确实碰撞到了物体,将包含碰撞信息碰撞:

GDScript

C#

  1. if result:
  2. print("Hit at point: ", result.position)
  1. if (result.Count > 0)
  2. GD.Print("Hit at point: ", result["position"]);

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

  1. {
  2. position: Vector2 # point in world space for collision
  3. normal: Vector2 # normal in world space for collision
  4. collider: Object # Object collided or null (if unassociated)
  5. collider_id: ObjectID # Object it collided against
  6. rid: RID # RID it collided against
  7. shape: int # shape index of collider
  8. metadata: Variant() # metadata of collider
  9. }

使用Vector3坐标,数据在3D空间中类似.

碰撞异常

光线投射的常见用例是使角色能够收集有关其周围世界的数据. 这个问题的一个问题是同一个角色有一个对撞机,因此光线只会检测其父对手,如下图所示:

../../_images/raycast_falsepositive.png

为了避免自相交, intersect_ray() 函数可以采用可选的第三个参数,这是一个异常数组. 这是如何从KinematicBody2D或任何其他碰撞对象节点使用它的示例:

GDScript

C#

  1. extends KinematicBody2D
  2. func _physics_process(delta):
  3. var space_state = get_world_2d().direct_space_state
  4. var result = space_state.intersect_ray(global_position, enemy_position, [self])
  1. class Body : KinematicBody2D
  2. {
  3. public override void _PhysicsProcess(float delta)
  4. {
  5. var spaceState = GetWorld2d().DirectSpaceState;
  6. var result = spaceState.IntersectRay(globalPosition, enemyPosition, new Godot.Collections.Array { this });
  7. }
  8. }

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

碰撞遮罩

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

intersect_ray() 的第四个可选参数是一个碰撞掩码.例如,要使用与父级相同的掩码,请使用 collision_mask 成员变量:

GDScript

C#

  1. extends KinematicBody2D
  2. func _physics_process(delta):
  3. var space_state = get_world().direct_space_state
  4. var result = space_state.intersect_ray(global_position, enemy_position,
  5. [self], collision_mask)
  1. class Body : KinematicBody2D
  2. {
  3. public override void _PhysicsProcess(float delta)
  4. {
  5. var spaceState = GetWorld2d().DirectSpaceState;
  6. var result = spaceState.IntersectRay(globalPosition, enemyPosition,
  7. new Godot.Collections.Array { this }, CollisionMask);
  8. }
  9. }

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

来自屏幕的3D射线投射

将一条射线从屏幕上投射到3D物理空间,对于对象的选取是很有用,但没有太多必要这样做,因为 CollisionObject 有一个 “input_event” 信号,会让你知道它是什么时候被点击的,但是如果有想要手动操作需要,可这样.

要从屏幕投射光线,您需要 Camera 节点. 相机``可以是两种投影模式:透视和正交. 因此,必须获得射线原点和方向. 这是因为 ``origin 在正交模式下改变,而 normal 在透视模式下改变:

../../_images/raycast_projection.png

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

GDScript

C#

  1. const ray_length = 1000
  2. func _input(event):
  3. if event is InputEventMouseButton and event.pressed and event.button_index == 1:
  4. var camera = $Camera
  5. var from = camera.project_ray_origin(event.position)
  6. var to = from + camera.project_ray_normal(event.position) * ray_length
  1. private const float rayLength = 1000;
  2. public override void _Input(InputEvent @event)
  3. {
  4. if (@event is InputEventMouseButton eventMouseButton && eventMouseButton.Pressed && eventMouseButton.ButtonIndex == 1)
  5. {
  6. var camera = (Camera)GetNode("Camera");
  7. var from = camera.ProjectRayOrigin(eventMouseButton.Position);
  8. var to = from + camera.ProjectRayNormal(eventMouseButton.Position) * rayLength;
  9. }
  10. }

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