物理系統簡介

在遊戲開發時,經常需要得知遊戲中兩個物件是否相交或接觸。這稱為 碰撞偵測。偵測到碰撞後,通常會希望產生某些反應,這稱為 碰撞回應

Godot 在 2D 與 3D 中提供多種碰撞物件,能同時進行碰撞偵測與回應。專案開發時要選用哪一種常令人感到困惑。只要瞭解各種碰撞物件的運作方式與優缺點,即可避免問題並讓開發過程更順利。

在本指南中,你將學到:

  • Godot 的四種碰撞物件型態

  • 各種碰撞物件的運作方式

  • 何時、為何選用特定碰撞物件

備註

本文範例皆以 2D 物件為主。每種 2D 物理物件與碰撞形狀,在 3D 中都有相對應的版本,並且大多數情況下運作方式一致。

碰撞物件

Godot 提供四種碰撞物件,皆繼承自 CollisionObject2D。下列其中三種同時為物理主體,進一步繼承自 PhysicsBody2D

  • Area2D

    Area2D 節點提供**偵測**與**影響**功能。它可偵測物件重疊,並在有物體進入或離開時發出訊號。Area2D 也能用來覆寫特定區域內的物理屬性,如重力或阻尼。

  • StaticBody2D

    靜態主體是不會被物理引擎移動的物件。它會參與碰撞偵測,但不會因碰撞而產生移動。通常用於場景環境或不需動態行為的物件。

  • RigidBody2D

    這個節點用來進行 2D 物理模擬。你無法直接控制 RigidBody2D,而是透過施加各種力(例如重力、脈衝等),由物理引擎自動計算運動結果。深入了解剛體的使用方法

  • CharacterBody2D

    這種主體提供碰撞偵測,但不會有物理模擬。所有移動及碰撞回應都必須自行以程式碼實作。

物理材質

靜態主體與剛體可設定為使用 PhysicsMaterial。這讓你能調整物件的摩擦力與彈性,並可設為具有吸收性或粗糙效果。

碰撞形狀

一個物理主體可以有多個 Shape2D 子物件。這些形狀用來定義物件的碰撞範圍,並偵測與其他物件的接觸。

備註

為了進行碰撞偵測,至少需要給物件指派一個 Shape2D

最常見的指派形狀方式,是新增 CollisionShape2DCollisionPolygon2D 為子節點。這些節點可讓你直接在編輯器工作區繪製碰撞形狀。

重要

請注意,千萬不要在編輯器中縮放碰撞形狀。檢查器(Inspector)中的「Scale」屬性應維持 (1, 1)。要改變碰撞形狀的大小,請務必使用尺寸控制點,不要Node2D 的縮放工具。縮放碰撞形狀會導致碰撞行為異常。

../../_images/player_coll_shape1.webp

物理處理回呼

物理引擎以固定速率執行(預設每秒 60 次)。這個速率通常不同於畫面更新率(frame rate),後者會根據渲染內容與資源而變動。

所有與物理相關的程式碼都應在這個固定速率下執行。因此 Godot 區分了 物理處理與空閒處理。每幀執行的程式碼稱為空閒處理(idle processing),每次物理步進執行的程式碼則稱為物理處理。Godot 提供兩種不同的回呼函式,分別對應這兩種處理速率。

物理回呼 Node._physics_process() 會在每個物理步驟前呼叫。任何需要存取物理主體屬性的程式碼都應在這裡執行。這個方法會傳入一個 delta 參數,表示自上次步進後經過的秒數,為浮點數。預設 60Hz 更新率時,通常為 0.01666... (但不一定,詳見下述)。

備註

建議在需要時於物理運算中一定要使用 delta 參數,這樣無論更改物理更新率或玩家裝置效能不足,遊戲都能正確運作。

碰撞層與遮罩

碰撞層系統是 Godot 物理系統中既強大又常被誤解的功能之一。這個系統可用來建立各種物件間的複雜互動。其核心概念是**層(Layer)**與**遮罩(Mask)**。每個 CollisionObject2D 最多可設定 32 層物理互動。

我們來依序看看每個屬性的作用:

  • collision_layer

    這個屬性表示物件**所在**的層。預設情況下,所有主體都位於第 1 層。

  • collision_mask

    這個屬性表示主體會**偵測**哪些層的碰撞。如果某物件不在遮罩指定的層內,就會被忽略。預設所有主體都偵測第 1 層。

這些屬性可透過程式碼設定,或直接在屬性檢視器(Inspector)中設定。

管理每個層的用途有時會變得混亂,因此建議幫常用的層命名。可至 專案設定 > 層名稱 > 2D 物理 指定各層名稱。

../../_images/physics_layer_names.webp

介面範例

假設遊戲中有四種節點:牆(Wall)、玩家(Player)、敵人(Enemy)、金幣(Coin)。玩家與敵人都會與牆碰撞。玩家會偵測敵人和金幣的碰撞,但敵人與金幣互不理會。

請先將 1~4 層分別命名為「walls」、「player」、「enemies」和「coins」,並用「Layer」屬性將各節點指定到對應層。再用「Mask」屬性選擇該節點要偵測哪些層。以玩家為例,設定如下:

../../_images/player_collision_layers.webp ../../_images/player_collision_mask.webp

程式碼範例

在呼叫函式時,層是用位元遮罩(bitmask)指定的。預設啟用所有層時,遮罩值為 0xffffffff。你可用二進位、十六進位或十進位來寫遮罩值,依個人習慣調整。

若要啟用第 1、3、4 層,對應的程式碼如下:

# Example: Setting mask value for enabling layers 1, 3 and 4

# Binary - set the bit corresponding to the layers you want to enable (1, 3, and 4) to 1, set all other bits to 0.
# Note: Layer 32 is the first bit, layer 1 is the last. The mask for layers 4, 3 and 1 is therefore:
0b00000000_00000000_00000000_00001101
# (This can be shortened to 0b1101)

# Hexadecimal equivalent (1101 binary converted to hexadecimal).
0x000d
# (This value can be shortened to 0xd.)

# Decimal - Add the results of 2 to the power of (layer to be enabled - 1).
# (2^(1-1)) + (2^(3-1)) + (2^(4-1)) = 1 + 4 + 8 = 13
#
# We can use the `<<` operator to shift the bit to the left by the layer number we want to enable.
# This is a faster way to multiply by powers of 2 than `pow()`.
# Additionally, we use the `|` (binary OR) operator to combine the results of each layer.
# This ensures we don't add the same layer multiple times, which would behave incorrectly.
(1 << 1 - 1) | (1 << 3 - 1) | (1 << 4 - 1)

# The above can alternatively be written as:
# pow(2, 1 - 1) + pow(2, 3 - 1) + pow(2, 4 - 1)

也可以在任一 CollisionObject2D 上呼叫 set_collision_layer_value(layer_number, value)set_collision_mask_value(layer_number, value) 來獨立設定各位元,範例如下:

# Example: Setting mask value to enable layers 1, 3, and 4.

var collider: CollisionObject2D = $CollisionObject2D  # Any given collider.
collider.set_collision_mask_value(1, true)
collider.set_collision_mask_value(3, true)
collider.set_collision_mask_value(4, true)

也可以利用匯出註解在編輯器中以友善的圖形介面匯出位元遮罩:

@export_flags_2d_physics var layers_2d_physics

在 2D 與 3D 中,渲染層與導覽層也有額外的 export 註解可用。詳見 匯出位元旗標

Area2D

Area 節點提供**偵測**與**影響**功能。它能偵測物件重疊,並在有主體進入或離開時發出訊號。Area 也可用來覆寫特定區域的物理屬性,如重力或阻尼。

Area2D 主要有三種用途:

  • 在指定區域內覆寫物理參數(如重力)。

  • 偵測其他主體進入、離開區域,或查詢目前區域內有哪些主體。

  • 檢查與其他 Area 是否有重疊。

預設情況下,Area 也會接收滑鼠與觸控輸入。

StaticBody2D

靜態主體不會被物理引擎移動。它會參與碰撞偵測,但遇到碰撞時不會移動。不過,你可以用 constant_linear_velocityconstant_angular_velocity 屬性,讓它像在移動一樣對碰撞到的物件產生推動或旋轉效果。

StaticBody2D 節點多半用於場景環境用物件,或是不需有動態行為的物體上。

StaticBody2D 常見用途:

  • 平臺(包含移動平臺)

  • 輸送帶

  • 牆壁與其他障礙物

RigidBody2D

這個節點可以進行 2D 物理模擬。你不能直接控制 RigidBody2D,而是對它施加各種力,由物理引擎計算運動與碰撞反應(如彈跳、旋轉等)。

你可以在屬性檢視器設定剛體的「Mass」(質量)、「Friction」(摩擦)或「Bounce」(彈性)等屬性來調整剛體行為。

主體的行為也會受到世界屬性的影響,這些屬性可在 專案設定 > 物理 中設定,或是進入一個有覆寫全域物理屬性的 Area2D 也會影響。

剛體靜止一段時間後會進入睡眠狀態。進入睡眠的剛體會像靜態主體一樣,其力運算不再執行。當受到外力(碰撞或程式碼)時,會再次喚醒。

使用 RigidBody2D

使用剛體的好處之一,是許多物理行為可自動產生而無需寫程式。例如要做一個「憤怒鳥」遊戲,有掉落方塊時,只需建立多個 RigidBody2D 並調整屬性,堆疊、掉落、彈跳等都會自動運算。

但如果你想要手動控制剛體,請特別注意——直接修改剛體的 positionlinear_velocity 等屬性,可能會導致怪異行為。若需修改物理相關屬性,應該在 _integrate_forces() 回呼中處理,而不是在 _physics_process()。在這個回呼中,你可以安全地存取 PhysicsDirectBodyState2D,同步屬性變更與物理引擎。

例如,這是「太空侵略者」風格太空船的程式碼:

extends RigidBody2D

var thrust = Vector2(0, -250)
var torque = 20000

func _integrate_forces(state):
    if Input.is_action_pressed("ui_up"):
        state.apply_force(thrust.rotated(rotation))
    else:
        state.apply_force(Vector2())
    var rotation_direction = 0
    if Input.is_action_pressed("ui_right"):
        rotation_direction += 1
    if Input.is_action_pressed("ui_left"):
        rotation_direction -= 1
    state.apply_torque(rotation_direction * torque)

請注意,我們並未直接設定 linear_velocityangular_velocity 屬性,而是對剛體施加推力(thrust)與扭力(torque),讓物理引擎自動計算運動結果。

備註

剛體進入睡眠狀態時,_integrate_forces() 不會被呼叫。若需強制保持喚醒,可以讓它持續碰撞、持續施加外力,或關閉 can_sleep 屬性。注意這可能會影響效能。

碰撞接觸回報

預設情況下,剛體不會記錄碰撞接觸點,因為場景物件數量多時會佔用大量記憶體。如需開啟碰撞回報,請將 max_contacts_reported 設為非零,之後可用 PhysicsDirectBodyState2D.get_contact_count() 等方法取得接觸資訊。

如需用訊號(signal)監控碰撞,可開啟 contact_monitor 屬性。可參考 RigidBody2D 文件了解可用訊號清單。

CharacterBody2D

CharacterBody2D 會偵測與其他主體的碰撞,但不會受到重力、摩擦等物理屬性影響。必須由使用者以程式碼控制其移動,物理引擎不會自動推動角色主體。

移動角色主體時,不應直接設定其 position,而應使用 move_and_collide()move_and_slide() 方法。這些方法會沿指定向量移動主體,若偵測到碰撞則會立即停止。碰撞後的行為需自行以程式碼處理。

角色主體碰撞回應

碰撞後,你可能希望物件反彈、沿牆滑動,或改變被撞擊物件屬性。你要如何處理碰撞回應,取決於你用哪種方法來移動 CharacterBody2D。

move_and_collide

使用 move_and_collide() 時,該方法會回傳一個 KinematicCollision2D 物件,內含碰撞與撞擊對象的詳細資訊。你可以利用這些資訊決定後續的碰撞回應。

例如,若要取得碰撞發生的空間座標:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        var collision_point = collision_info.get_position()

或讓角色主體從碰撞物件彈開:

extends PhysicsBody2D

var velocity = Vector2(250, 250)

func _physics_process(delta):
    var collision_info = move_and_collide(velocity * delta)
    if collision_info:
        velocity = velocity.bounce(collision_info.get_normal())

move_and_slide

滑動是一種很常見的碰撞回應,例如玩家在俯視遊戲中沿牆移動,或在橫向卷軸遊戲中上下坡。雖然用 move_and_collide() 後可以手動寫出滑動邏輯,但 move_and_slide() 提供了更方便的作法,讓你無需大量程式碼即可實現滑動移動。

警告

move_and_slide() 會自動處理時間步進(timestep),所以你**不需要**將速度向量乘上 delta。但 gravity 屬於加速度,與時間有關,仍需乘上 delta

例如,下面的程式碼可以讓角色主體沿地面(包含坡道)行走,並在站穩地面時跳躍:

extends CharacterBody2D

var run_speed = 350
var jump_speed = -1000
var gravity = 2500

func get_input():
    velocity.x = 0
    var right = Input.is_action_pressed('ui_right')
    var left = Input.is_action_pressed('ui_left')
    var jump = Input.is_action_just_pressed('ui_select')

    if is_on_floor() and jump:
        velocity.y = jump_speed
    if right:
        velocity.x += run_speed
    if left:
        velocity.x -= run_speed

func _physics_process(delta):
    velocity.y += gravity * delta
    get_input()
    move_and_slide()

更多 move_and_slide() 的使用細節,請參考 運動學角色(2D),內含詳細程式碼的範例專案。