繫結訊號
在本課中,我們將介紹訊號。它們是節點在發生特定事件時發出的消息,例如按下按鈕。其他節點可以連接到該訊號,並在事件發生時呼叫函式。
訊號是 Godot 內建的委派機制,允許一個遊戲物件對另一個遊戲物件的變化做出反應,而無需相互引用。使用訊號可以限制 耦合 ,並保持程式碼的靈活性。
例如,您可能在螢幕上有一個代表玩家生命值的生命條。當玩家受到傷害或使用治療藥水時,您希望生命條反映變化。要做到這一點,在 Godot 中,你會使用到訊號。
如同方法(參閱 Callable),自 Godot 4.0 起,訊號也是第一級型別。這表示您可以直接將它們作為方法引數傳遞,而無需將它們作為字串傳遞,這樣可以提供更好的自動完成功能,並且更不容易出錯。請參閱 Signal 類別參考以了解您可以直接對 Signal 型別執行的操作列表。
也參考
如同簡介中提到的,訊號是 Godot 實作的觀察者模式。您可以在 遊戲程式設計模式 這份資料中找到更多相關資訊。
現在,我們將使用訊號來使上一節課( 監聽玩家的輸入 )中的 Godot 圖示移動,並通過按下按鈕來停止。
備註
在這個專案中,我們會遵守 Godot 的命名慣例。
GDScript :類別 (節點) 使用大駝峰法 (PascalCase),變數與函式名稱使用蛇形法 (snake_case),常數則使用全大寫 (ALL_CAPS) (詳細請參考 GDScript 風格指南)。
C# :類別與匯出的變數與方法使用大駝峰法 (PascalCase),Private 欄位使用底線加小駝峰 (_camelCase),區域變數與參數使用小駝峰法 (camelCase) (請參考 C# 風格指南 )。在連接訊號的時候請特別注意不要打錯方法名稱。
場景設定
為了替我們的遊戲新增一個按鈕,我們將建立一個新場景,其中會包含一個 Button 和我們在 建立腳本 教學課程中建立的 sprite_2d.tscn 場景。
通過轉到功能表“場景 -> 新建場景”來建立新場景。
在場景面板中,按一下“2D 場景”按鈕。這樣就會新增一個 Node2D 作為我們的根節點。
在檔案系統面板中,按一下之前保存的 sprite_2d.tscn 檔並將其拖動到 Node2D 上,對其進行產生實體。
我們想要新增另一個節點作為 Sprite2D 的同級節點。為此,請按右鍵 Node2D,然後選擇“新增子節點”。
搜尋並新增 Button 節點。
該節點預設比較小。在視口中,點擊並拖拽該按鈕右下角的手柄來調整大小。
如果看不到手柄,請確保工具列中的選擇工具處於活動狀態。
點擊並拖拽按鈕使其更接近精靈。
你可以通過修改屬性檢視器中的 Text 屬性來給 Button 上寫一個標籤。請輸入 Toggle motion 。
你的視口跟場景樹應該會長這樣。
如果你還沒保存場景的話,保存新建的場景為 node_2d.tscn 。然後你就可以使用 F6`(macOS 則為 :kbd:`Cmd + R )來運作。此時,你可以看到按鈕,但是按下之後不會有任何反應。
使用程式碼來連接訊號
然後,我們希望將按鈕的“pressed”訊號連接到我們的 Sprite2D,並且我們想要呼叫一個新函式來打開和關閉其運動。我們需要像我們在上一課中所做的操作一樣,將一個腳本附加到 Sprite2D 節點。
您可以在“節點”面板中連接訊號。選擇 Button 節點,然後在編輯器的右側,按一下屬性檢視器旁邊名為“節點”的分頁。
停靠欄顯示所選節點上可用的訊號列表。
按兩下“pressed”訊號,打開節點連接視窗。
然後,您可以將訊號連接到 Sprite2D 節點。該節點需要一個用於接收按鈕訊號的函式,當按鈕發出訊號時,Godot 將呼叫該函式。編輯器會為您生成一個。按照規範,我們將這些回呼函式方法命名為"_on_node_name_signal_name"。在這裡,它被命名為"_on_button_pressed"。
備註
通過編輯器的節點面板連接訊號時,可以使用兩種模式。簡單的一個隻允許您連接到附加了腳本的節點,並在它們上面建立一個新的回呼函式。
您可以在高級視圖中連接到任何節點和任何內建函式、向回呼函式新增參數、設定選項。您可以按一下視窗右下角的“高級”按鈕來切換模式。
備註
如果您使用外部編輯器(例如 VS Code),這個自動產生程式碼的功能可能無法運作。在這種情況下,您需要透過程式碼連接訊號,如同下一節所說明。
按一下“連接”按鈕以完成訊號連接並跳轉到腳本工作區。您應該會看到新方法,並在左邊距中帶有連接圖示。
如果按一下該圖示,將彈出一個視窗並顯示有關連接的資訊。此功能僅在編輯器中連接節點時可用。
讓我們用程式碼替換帶有 pass 關鍵字的一行,以切換節點的運動。
我們的 Sprite2D 由於 _process() 函式中的程式碼而移動。Godot 提供了一種打開和關閉處理的方法:Node.set_process() 。Node 的另一個方法 is_processing() ,如果空閒處理處於活動狀態,則返回 true 。我們可以使用 not 關鍵字來反轉該值。
func _on_button_pressed():
set_process(not is_processing())
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
此函式將切換處理,進而切換按下按鈕時圖示的移動。
在嘗試遊戲之前,我們需要簡化 _process() 函式,以自動移動節點,而不是等待使用者輸入。將其替換為以下程式碼,這是我們在兩課前看到的程式碼:
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
}
完整的 ''Sprite_2d.gd'' 程式碼應該是類似下面這樣的。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
func _on_button_pressed():
set_process(not is_processing())
using Godot;
public partial class MySprite2D : Sprite2D
{
private float _speed = 400;
private float _angularSpeed = Mathf.Pi;
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
}
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
}
Run the current scene by pressing F6 (Cmd + R on macOS), and click the button to see the sprite start and stop.
使用程式碼來連接訊號
您可以通過程式碼連接訊號,而不是使用編輯器。這在腳本中建立節點或產生實體場景時是必需的。
讓我們在這裡使用一個不同的節點。Godot 有一個 Timer 節點,可用於實作技能冷卻時間、武器重裝等。
回到 2D 工作區。你可以點擊視窗頂端的「2D」文字,或是按下鍵盤快速鍵 Ctrl + F1 (macOS 上是 Ctrl + Cmd + 1)。
在“場景”面板中,右鍵點擊 Sprite2D 節點並新增新的子節點。搜索 Timer 並新增對應節點。你的場景現在應該類似這樣。
選中 Timer 節點,在“屬性檢視器”中勾選 Autostart 屬性。
點擊 Sprite2D 旁的腳本圖示,返回腳本工作區。
我們需要執行兩個操作來通過程式碼連接節點:
從 Sprite2D 獲得 Timer 的引用。
通過 Timer 的“timeout”訊號呼叫
connect()方法。
備註
要使用程式碼來連接訊號,你需要呼叫所需監聽節點訊號的 connect() 方法。這裡我們要監聽的是 Timer 的“timeout”訊號。
我們想要在場景實例化時連接訊號,而我們可以使用 Node._ready() 這個內建函式來做到這一點。當節點完全實例化後,引擎會自動呼叫這個函式。
為了獲取相對於目前節點的引用,我們使用方法 Node.get_node() 。我們可以將引用儲存在變數中。
func _ready():
var timer = get_node("Timer")
public override void _Ready()
{
var timer = GetNode<Timer>("Timer");
}
get_node() 函式會查看 Sprite2D 的子節點,並按節點的名稱獲取節點。例如,如果在編輯器中將 Timer 節點重命名為“BlinkingTimer”,則必須將呼叫更改為 get_node("BlinkingTimer") 。
現在,我們可以在 _ready() 函式中將Timer連接到Sprite2D。
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
public override void _Ready()
{
var timer = GetNode<Timer>("Timer");
timer.Timeout += OnTimerTimeout;
}
該行讀起來是這樣的:我們將計時器的“timeout”訊號連接到腳本附加到的節點上。當計時器發出“timeout”時,去呼叫我們需要定義的函式``_on_timer_timeout()``。讓我們將其定義新增到腳本的底部,並使用它來切換 sprite 的可見性。
備註
按照慣例,我們將這些回呼函式方法在 GDScript 中命名為“_on_node_name_signal_name”,在 C# 中命名為“OnNodeNameSignalName”。故此處的GDScript 為“_on_timer_timeout”,C# 為“OnTimerTimeout()”。
func _on_timer_timeout():
visible = not visible
private void OnTimerTimeout()
{
Visible = !Visible;
}
visible 屬性是一個布林值,用於控制節點的可見性。 visible = not visible 行切換該值。如果 visible 是 true ,它就會變成 false ,反之亦然。
如果您現在執行 Node2D 場景,您會看到這個 Sprite 以一秒的間隔閃爍。
GDScript 範例
這就是我們小小的 Godot 圖示移動閃爍演示了!這是完整的 sprite_2d.gd 檔案,僅供參考。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
func _on_button_pressed():
set_process(not is_processing())
func _on_timer_timeout():
visible = not visible
using Godot;
public partial class MySprite2D : Sprite2D
{
private float _speed = 400;
private float _angularSpeed = Mathf.Pi;
public override void _Ready()
{
var timer = GetNode<Timer>("Timer");
timer.Timeout += OnTimerTimeout;
}
public override void _Process(double delta)
{
Rotation += _angularSpeed * (float)delta;
var velocity = Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
}
// We also specified this function name in PascalCase in the editor's connection window.
private void OnButtonPressed()
{
SetProcess(!IsProcessing());
}
private void OnTimerTimeout()
{
Visible = !Visible;
}
}
自定訊號
備註
本節介紹的是如何定義並使用你自己的訊號,不依賴之前課程所建立的專案。
您可以在腳本中定義自訂訊號。例如,假設您希望在玩家的生命值為零時通過螢幕顯示遊戲結束。為此,當他們的生命值達到 0 時,您可以定義一個名為“died”或“health_depleted”的訊號。
extends Node2D
signal health_depleted
var health = 10
using Godot;
public partial class MyNode2D : Node2D
{
[Signal]
public delegate void HealthDepletedEventHandler();
private int _health = 10;
}
備註
由於訊號表示剛剛發生的事件,我們通常在其名稱中使用過去時態的動作動詞。
自訂訊號的工作方式與內建訊號相同:它們顯示在“節點”分頁中,您可以像連接其他訊號一樣連接到它們。
要在腳本中發出信號,需呼叫信號的 emit() 方法。
func take_damage(amount):
health -= amount
if health <= 0:
health_depleted.emit()
public void TakeDamage(int amount)
{
_health -= amount;
if (_health <= 0)
{
EmitSignal(SignalName.HealthDepleted);
}
}
訊號也可以宣告一個或多個參數。在括號中指定參數的名稱:
extends Node2D
signal health_changed(old_value, new_value)
var health = 10
using Godot;
public partial class MyNode : Node
{
[Signal]
public delegate void HealthChangedEventHandler(int oldValue, int newValue);
private int _health = 10;
}
備註
訊號的參數會顯示在編輯器的節點 Dock 中。Godot 會使用訊號參數來產生回呼函式。但送出訊號時一樣可以送出任意數量的參數,可自行決定是否要送出正確的數量。
若需要傳遞數值,可將數值放在 emit_signal 函式的第二個參數內:
func take_damage(amount):
var old_health = health
health -= amount
health_changed.emit(old_health, health)
public void TakeDamage(int amount)
{
int oldHealth = _health;
_health -= amount;
EmitSignal(SignalName.HealthChanged, oldHealth, _health);
}
總結
Godot 中的任何節點都會在發生特定事件時發出訊號,例如按下按鈕。其他節點可以連接到單個訊號並對所選事件做出反應。
訊號有很多用途。有了它們,你可以對進入或退出遊戲世界的節點、碰撞、角色進入或離開某個區域、介面元素的大小變化等等做出反應。
許多 Godot 內建的型別都提供各種訊號可用來偵測事件。例如,當金幣 Area2D 物件送出 body_entered 訊號,就表示玩家物理形體進入了金幣的碰撞區域,代表玩家蒐集到金幣。
在下一章 您的第一個 2D 遊戲 中,你將會製作一款完整的 2D 遊戲,把目前學到的知識全部實際應用。