訊號

簡介

訊號是 Godot 版本的 觀察者 (Observer) 設計模式。使用訊號可以讓一個節點送出訊息,然後讓其他節點能監聽該訊息並進行回應。舉例來說,有了訊號就不需要一直重複檢查同一個按鈕來確認按鈕有沒有被按下,而可以通過讓按鈕送出訊號來通知其他人按鈕被按下了。

備註

欲瞭解更多有關觀察者模式的資訊,請參考:https://gameprogrammingpatterns.com/observer.html

訊號是一種 解耦 遊戲物件的方法,可以更好地組織與管理程式碼。不需要強制讓某個遊戲物件去配合數個物件,而是讓物件發出訊號,所有需要反應的物件都可以訂閱訊號並進行回應。

下方的例子說明了如何能在專案內使用訊號。

計時器範例

為了瞭解訊號怎麼運作的,來試試看 Timer (計時器) 節點。先建立場景,並設定一個 Node2D 與兩個子節點:Timer 與 Sprite 。接著在場景 Dock 內把 Node2D 重命名為 TimerExample (計時器範例)。

可以用 Godot 的圖示或其他喜歡的圖片來設定 Sprite 紋理貼圖。要設定紋理貼圖,請在 Sprite 的 Texture (紋理貼圖) 下拉選單內選擇 [載入] 。之後在根節點上附加腳本,但在這裡先不要新增任何程式碼。

現在,場景樹應該會長這樣:

../../_images/signals_node_setup.png

來看看 Timer 節點的屬性,先把 Autostart (自動開始) 旁邊的 [開啟] 勾選框打勾。這樣會讓計時器在場景執行的時候自動開始。而 Wait Time (等待時間) 可以保持預設的 1 秒。

在屬性面板分頁旁邊有一個叫做 [節點] 的分頁。點開這個分頁,會看到目前選擇節點可送出的所有訊號。在本例中,我們需要的是「timeout」訊號。這個訊號會在 Timer 倒數到 0 的時候送出。

../../_images/signals_node_tab_timer.png

點擊「timeout()」訊號,然後選擇訊號面板下方的 [連接...]。接下來會看到下面這個視窗,可以設定要如何連接訊號:

../../_images/signals_connect_dialog_timer.png

視窗左邊是場景中的節點,可以選擇一個節點來「監聽」訊號。請注意,Timer 節點顯示為藍色的,這代表 Timer 是送出訊號的節點。這裡先選擇根節點。

警告

目標節點 必須有 附加腳本,不然會顯示錯誤訊息。

如果打開「進階」選單,就可以看到右邊能選擇要綁定的引數數量以及引數 (可用) 的型別。這個功能適用於同一個方法連接多個不同訊號的情況,因為每個訊號在傳遞時都會依據這裡設定的額外呼叫引數來代入不同的值。

視窗底下有一個 [Receiver 方法] 欄位,這個欄位表示目標節點腳本中的接收函式名稱。預設 Godot 會使用 _on_<節點名稱>_<訊號名稱> 這樣的命名格式,但也可以自己修改 Receiver 方法名稱。

點擊 [連接] 後,就可以看到腳本中建立了函式:

extends Node2D


func _on_Timer_timeout():
    pass # Replace with function body.
public class TimerExample : Node2D
{
    public void _on_Timer_timeout()
    {
        // Replace with function body.
    }
}

現在我們可以把預設代入的程式碼改成收到訊號後要執行的程式。來讓 Sprite 閃爍一下:

extends Node2D


func _on_Timer_timeout():
    # Note: the `$` operator is a shorthand for `get_node()`,
    # so `$Sprite` is equivalent to `get_node("Sprite")`.
    $Sprite.visible = !$Sprite.visible
public class TimerExample : Node2D
{
    public void _on_Timer_timeout()
    {
        var sprite = GetNode<Sprite>("Sprite");
        sprite.Visible = !sprite.Visible;
    }
}

執行場景後就看到 Sprite 一秒閃爍一次。可以修改 Timer 的 Wait Time (等待時間) 屬性來更改頻率。

使用程式碼來連接訊號

除了使用編輯器界面,也可以使用程式碼來建立訊號連接。如果用程式碼來實體化節點的話,通常也必須使用程式碼來建立訊號連接,因為這種情況下沒辦法用編輯器界面。

首先,要先把 Timer 的訊號斷開。選擇 Timer 節點後,打開 [節點] 分頁,選擇連接並點擊 [中斷]。

../../_images/signals_disconnect_timer.png

要使用程式碼建立連接,可以使用 connect 函式。我們寫在 _ready() 函式內,這樣一來連接就會在執行的時候建立。這個函式的語法是 <來源節點>.connect(<訊號名稱>, <目標節點>, <目標函式名稱>) 。下列程式碼會建立本例中的 Timer 連接:

extends Node2D


func _ready():
    $Timer.connect("timeout", self, "_on_Timer_timeout")


func _on_Timer_timeout():
    $Sprite.visible = !$Sprite.visible
public class TimerExample : Node2D
{
    public override void _Ready()
    {
        GetNode("Timer").Connect("timeout", this, nameof(_on_Timer_timeout));
    }

    public void _on_Timer_timeout()
    {
        var sprite = GetNode<Sprite>("Sprite");
        sprite.Visible = !sprite.Visible;
    }
}

自定訊號

Godot 中可以宣告自定訊號:

extends Node2D


signal my_signal
public class Main : Node2D
{
    [Signal]
    public delegate void MySignal();
}

宣告後,自定訊號會出現在屬性面板上,並可以用與內建訊號相同的方式建立連接。

使用 emit_signal 函式來通過程式碼送出訊號:

extends Node2D


signal my_signal


func _ready():
    emit_signal("my_signal")
public class Main : Node2D
{
    [Signal]
    public delegate void MySignal();

    public override void _Ready()
    {
        EmitSignal(nameof(MySignal));
    }
}

訊號也可以宣告一個或多個引數。在括號中指定引數的名稱:

extends Node


signal my_signal(value, other_value)
public class Main : Node
{
    [Signal]
    public delegate void MySignal(bool value, int other_value);
}

備註

訊號的引數會顯示在編輯器的節點 Dock 中。Godot 會使用訊號引數來產生回呼函式。但送出訊號時一樣可以送出任意數量的引數,可自行決定是否要送出正確的數量。

To pass values, add them as subsequent arguments to the emit_signal function:

extends Node


signal my_signal(value, other_value)


func _ready():
    emit_signal("my_signal", true, 42)
public class Main : Node
{
    [Signal]
    public delegate void MySignal(bool value, int other_value);

    public override void _Ready()
    {
        EmitSignal(nameof(MySignal), true, 42);
    }
}

結論

許多 Godot 內建的型別都提供各種訊號可用來偵測事件。例如,當金幣 Area2D 物件送出 body_entered 訊號,就表示玩家物理形體進入了金幣的碰撞區域,代表玩家蒐集到金幣。

在下一個章節 第一個遊戲 中,我們將製作一個完整的遊戲,其中會用到訊號來連接各個不同的遊戲元件。