Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

物理系統簡介

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

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

在本指南中,你將學到:

  • Godot 的四種碰撞物件型態

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

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

備註

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

警告

Physics in Godot, regardless of physics engine, is not deterministic, the nature of physics engine determinism is very complex and has to do with many factors, this means physics is not guaranteed to run the same way for seemingly identical situations.

碰撞物件

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

  • Area2D

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

  • StaticBody2D

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

  • RigidBody2D

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

  • CharacterBody2D

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

物理材質

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

碰撞形狀

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

備註

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

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

重要

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

../../_images/player_coll_shape.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),內含詳細程式碼的範例專案。