物理系統簡介
在遊戲開發時,經常需要得知遊戲中兩個物件是否相交或接觸。這稱為 碰撞偵測。偵測到碰撞後,通常會希望產生某些反應,這稱為 碰撞回應。
Godot 在 2D 與 3D 中提供多種碰撞物件,能同時進行碰撞偵測與回應。專案開發時要選用哪一種常令人感到困惑。只要瞭解各種碰撞物件的運作方式與優缺點,即可避免問題並讓開發過程更順利。
在本指南中,你將學到:
Godot 的四種碰撞物件型態
各種碰撞物件的運作方式
何時、為何選用特定碰撞物件
備註
本文範例皆以 2D 物件為主。每種 2D 物理物件與碰撞形狀,在 3D 中都有相對應的版本,並且大多數情況下運作方式一致。
碰撞物件
Godot 提供四種碰撞物件,皆繼承自 CollisionObject2D。下列其中三種同時為物理主體,進一步繼承自 PhysicsBody2D。
- Area2D
Area2D節點提供**偵測**與**影響**功能。它可偵測物件重疊,並在有物體進入或離開時發出訊號。Area2D也能用來覆寫特定區域內的物理屬性,如重力或阻尼。
- StaticBody2D
靜態主體是不會被物理引擎移動的物件。它會參與碰撞偵測,但不會因碰撞而產生移動。通常用於場景環境或不需動態行為的物件。
- RigidBody2D
這個節點用來進行 2D 物理模擬。你無法直接控制
RigidBody2D,而是透過施加各種力(例如重力、脈衝等),由物理引擎自動計算運動結果。深入了解剛體的使用方法
- CharacterBody2D
這種主體提供碰撞偵測,但不會有物理模擬。所有移動及碰撞回應都必須自行以程式碼實作。
物理材質
靜態主體與剛體可設定為使用 PhysicsMaterial。這讓你能調整物件的摩擦力與彈性,並可設為具有吸收性或粗糙效果。
碰撞形狀
一個物理主體可以有多個 Shape2D 子物件。這些形狀用來定義物件的碰撞範圍,並偵測與其他物件的接觸。
備註
為了進行碰撞偵測,至少需要給物件指派一個 Shape2D。
最常見的指派形狀方式,是新增 CollisionShape2D 或 CollisionPolygon2D 為子節點。這些節點可讓你直接在編輯器工作區繪製碰撞形狀。
重要
請注意,千萬不要在編輯器中縮放碰撞形狀。檢查器(Inspector)中的「Scale」屬性應維持 (1, 1)。要改變碰撞形狀的大小,請務必使用尺寸控制點,不要 用 Node2D 的縮放工具。縮放碰撞形狀會導致碰撞行為異常。
物理處理回呼
物理引擎以固定速率執行(預設每秒 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 物理 指定各層名稱。
介面範例
假設遊戲中有四種節點:牆(Wall)、玩家(Player)、敵人(Enemy)、金幣(Coin)。玩家與敵人都會與牆碰撞。玩家會偵測敵人和金幣的碰撞,但敵人與金幣互不理會。
請先將 1~4 層分別命名為「walls」、「player」、「enemies」和「coins」,並用「Layer」屬性將各節點指定到對應層。再用「Mask」屬性選擇該節點要偵測哪些層。以玩家為例,設定如下:
程式碼範例
在呼叫函式時,層是用位元遮罩(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_velocity 及 constant_angular_velocity 屬性,讓它像在移動一樣對碰撞到的物件產生推動或旋轉效果。
StaticBody2D 節點多半用於場景環境用物件,或是不需有動態行為的物體上。
StaticBody2D 常見用途:
平臺(包含移動平臺)
輸送帶
牆壁與其他障礙物
RigidBody2D
這個節點可以進行 2D 物理模擬。你不能直接控制 RigidBody2D,而是對它施加各種力,由物理引擎計算運動與碰撞反應(如彈跳、旋轉等)。
你可以在屬性檢視器設定剛體的「Mass」(質量)、「Friction」(摩擦)或「Bounce」(彈性)等屬性來調整剛體行為。
主體的行為也會受到世界屬性的影響,這些屬性可在 專案設定 > 物理 中設定,或是進入一個有覆寫全域物理屬性的 Area2D 也會影響。
剛體靜止一段時間後會進入睡眠狀態。進入睡眠的剛體會像靜態主體一樣,其力運算不再執行。當受到外力(碰撞或程式碼)時,會再次喚醒。
使用 RigidBody2D
使用剛體的好處之一,是許多物理行為可自動產生而無需寫程式。例如要做一個「憤怒鳥」遊戲,有掉落方塊時,只需建立多個 RigidBody2D 並調整屬性,堆疊、掉落、彈跳等都會自動運算。
但如果你想要手動控制剛體,請特別注意——直接修改剛體的 position、linear_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)
using Godot;
public partial class Spaceship : RigidBody2D
{
private Vector2 _thrust = new Vector2(0, -250);
private float _torque = 20000;
public override void _IntegrateForces(PhysicsDirectBodyState2D state)
{
if (Input.IsActionPressed("ui_up"))
{
state.ApplyForce(_thrust.Rotated(Rotation));
}
else
{
state.ApplyForce(new Vector2());
}
var rotationDir = 0;
if (Input.IsActionPressed("ui_right"))
{
rotationDir += 1;
}
if (Input.IsActionPressed("ui_left"))
{
rotationDir -= 1;
}
state.ApplyTorque(rotationDir * _torque);
}
}
請注意,我們並未直接設定 linear_velocity 或 angular_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()
using Godot;
public partial class Body : PhysicsBody2D
{
private Vector2 _velocity = new Vector2(250, 250);
public override void _PhysicsProcess(double delta)
{
var collisionInfo = MoveAndCollide(_velocity * (float)delta);
if (collisionInfo != null)
{
var collisionPoint = collisionInfo.GetPosition();
}
}
}
或讓角色主體從碰撞物件彈開:
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())
using Godot;
public partial class Body : PhysicsBody2D
{
private Vector2 _velocity = new Vector2(250, 250);
public override void _PhysicsProcess(double delta)
{
var collisionInfo = MoveAndCollide(_velocity * (float)delta);
if (collisionInfo != null)
{
_velocity = _velocity.Bounce(collisionInfo.GetNormal());
}
}
}
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()
using Godot;
public partial class Body : CharacterBody2D
{
private float _runSpeed = 350;
private float _jumpSpeed = -1000;
private float _gravity = 2500;
private void GetInput()
{
var velocity = Velocity;
velocity.X = 0;
var right = Input.IsActionPressed("ui_right");
var left = Input.IsActionPressed("ui_left");
var jump = Input.IsActionPressed("ui_select");
if (IsOnFloor() && jump)
{
velocity.Y = _jumpSpeed;
}
if (right)
{
velocity.X += _runSpeed;
}
if (left)
{
velocity.X -= _runSpeed;
}
Velocity = velocity;
}
public override void _PhysicsProcess(double delta)
{
var velocity = Velocity;
velocity.Y += _gravity * (float)delta;
Velocity = velocity;
GetInput();
MoveAndSlide();
}
}
更多 move_and_slide() 的使用細節,請參考 運動學角色(2D),內含詳細程式碼的範例專案。