射線投射

前言

遊戲開發中最常見的任務之一就是發射射線(或自訂形狀的物件)並檢查撞擊目標。這能實現複雜的行為、AI 等等。本教學將說明如何在 2D 與 3D 中執行這項操作。

Godot 將所有底層遊戲資訊儲存在伺服器中,場景僅作為前端。因此,射線投射通常屬於較底層的操作。對於簡單的射線投射,可以使用 RayCast3DRayCast2D 節點,這些節點會在每個畫面更新時回傳射線投射的結果。

但許多情況下,射線投射需要更具互動性,因此必須能以程式碼進行操作。

空間

在物理系統中,Godot 將所有底層的碰撞與物理資訊儲存在 空間 中。目前的 2D 空間(用於 2D 物理)可透過 CanvasItem.get_world_2d().space 取得。3D 空間則可透過 Node3D.get_world_3d().space 取得。

取得的空間 RID 可以分別用於 3D 的 PhysicsServer3D 及 2D 的 PhysicsServer2D

存取空間

Godot 的物理系統預設和遊戲邏輯運作於同一執行緒,但也可以設定於不同執行緒中以提升效能。因此,僅在 Node._physics_process() 回呼中存取空間是安全的。若在此函式之外存取,可能因空間被 鎖定 而導致錯誤。

要對物理空間進行查詢,必須使用 PhysicsDirectSpaceState2DPhysicsDirectSpaceState3D

在 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

射線查詢

要執行 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() 內進行查詢。