繫結訊號

在本課中,我們將介紹訊號。它們是節點在發生特定事件時發出的消息,例如按下按鈕。其他節點可以連接到該訊號,並在事件發生時呼叫函式。

訊號是 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 場景。

到功能表 Scene > New Scene 建立新場景。

../../_images/signals_01_new_scene.webp

在 Scene 面板點擊 2D Scene 按鈕,即會新增一個 Node2D 作為根節點。

../../_images/signals_02_2d_scene.webp

在檔案系統面板中,按一下之前保存的 sprite_2d.tscn 檔並將其拖動到 Node2D 上,對其進行產生實體。

../../_images/signals_03_dragging_scene.png

我們要新增另一個節點作為 Sprite2D 的同層級節點。請在 Node2D 上按右鍵並選擇 Add Child Node

../../_images/signals_04_add_child_node.webp

搜尋並新增 Button 節點。

../../_images/signals_05_add_button.webp

該節點預設比較小。在視口中,點擊並拖拽該按鈕右下角的手柄來調整大小。

../../_images/signals_06_drag_button.png

如果看不到手柄,請確保工具列中的選擇工具處於活動狀態。

../../_images/signals_07_select_tool.webp

點擊並拖拽按鈕使其更接近精靈。

你也可以在 Inspector 中編輯 Button 的 Text 屬性來顯示文字。請輸入 Toggle motion

../../_images/signals_08_toggle_motion_text.webp

你的視口跟場景樹應該會長這樣。

../../_images/signals_09_scene_setup.png

如果你還沒保存場景的話,保存新建的場景為 node_2d.tscn 。然後你就可以使用 F6`(macOS 則為 :kbd:`Cmd + R )來運作。此時,你可以看到按鈕,但是按下之後不會有任何反應。

使用程式碼來連接訊號

然後,我們希望將按鈕的“pressed”訊號連接到我們的 Sprite2D,並且我們想要呼叫一個新函式來打開和關閉其運動。我們需要像我們在上一課中所做的操作一樣,將一個腳本附加到 Sprite2D 節點。

你可以在 Node 面板連接訊號。選取 Button 節點,並在編輯器右側點選 Inspector 旁的 Node 分頁。

../../_images/signals_10_node_dock.webp

停靠欄顯示所選節點上可用的訊號列表。

../../_images/signals_11_pressed_signals.webp

按兩下“pressed”訊號,打開節點連接視窗。

../../_images/signals_12_node_connection.webp

然後,您可以將訊號連接到 Sprite2D 節點。該節點需要一個用於接收按鈕訊號的函式,當按鈕發出訊號時,Godot 將呼叫該函式。編輯器會為您生成一個。按照規範,我們將這些回呼函式方法命名為"_on_node_name_signal_name"。在這裡,它被命名為"_on_button_pressed"。

備註

通過編輯器的節點面板連接訊號時,可以使用兩種模式。簡單的一個隻允許您連接到附加了腳本的節點,並在它們上面建立一個新的回呼函式。

../../_images/signals_advanced_connection_window.png

進階視圖可讓你連接到任意節點與內建函式、為回呼新增參數並設定選項。你可以點擊視窗右下的 Advanced 來切換模式。

備註

如果您使用外部編輯器(例如 VS Code),這個自動產生程式碼的功能可能無法運作。在這種情況下,您需要透過程式碼連接訊號,如同下一節所說明。

按下 Connect 以完成訊號連接,並跳轉到 Script 工作區。你應會看到左側邊緣帶有連線圖示的新方法。

../../_images/signals_13_signals_connection_icon.webp

如果按一下該圖示,將彈出一個視窗並顯示有關連接的資訊。此功能僅在編輯器中連接節點時可用。

../../_images/signals_14_signals_connection_info.webp

讓我們用程式碼替換帶有 pass 關鍵字的一行,以切換節點的運動。

我們的 Sprite2D 由於 _process() 函式中的程式碼而移動。Godot 提供了一種打開和關閉處理的方法:Node.set_process() 。Node 的另一個方法 is_processing() ,如果空閒處理處於活動狀態,則返回 true 。我們可以使用 not 關鍵字來反轉該值。

func _on_button_pressed():
    set_process(not is_processing())

此函式將切換處理,進而切換按下按鈕時圖示的移動。

在嘗試遊戲之前,我們需要簡化 _process() 函式,以自動移動節點,而不是等待使用者輸入。將其替換為以下程式碼,這是我們在兩課前看到的程式碼:

func _process(delta):
    rotation += angular_speed * delta
    var velocity = Vector2.UP.rotated(rotation) * speed
    position += velocity * 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())

F6 (macOS 為 Cmd + R )執行目前場景,並點擊按鈕以觀察精靈開始與停止。

使用程式碼來連接訊號

您可以通過程式碼連接訊號,而不是使用編輯器。這在腳本中建立節點或產生實體場景時是必需的。

讓我們在這裡使用一個不同的節點。Godot 有一個 Timer 節點,可用於實作技能冷卻時間、武器重裝等。

回到 2D 工作區。你可以點擊視窗頂端的「2D」文字,或是按下鍵盤快速鍵 Ctrl + F1 (macOS 上是 Ctrl + Cmd + 1)。

在“場景”面板中,右鍵點擊 Sprite2D 節點並新增新的子節點。搜索 Timer 並新增對應節點。你的場景現在應該類似這樣。

../../_images/signals_15_scene_tree.webp

選取 Timer 節點,前往 Inspector 啟用 Autostart 屬性。

../../_images/signals_18_timer_autostart.webp

點擊 Sprite2D 旁的腳本圖示,返回腳本工作區。

../../_images/signals_16_click_script.webp

我們需要執行兩個操作來通過程式碼連接節點:

  1. 從 Sprite2D 獲得 Timer 的引用。

  2. 通過 Timer 的“timeout”訊號呼叫 connect() 方法。

備註

要使用程式碼來連接訊號,你需要呼叫所需監聽節點訊號的 connect() 方法。這裡我們要監聽的是 Timer 的“timeout”訊號。

我們想要在場景實例化時連接訊號,而我們可以使用 Node._ready() 這個內建函式來做到這一點。當節點完全實例化後,引擎會自動呼叫這個函式。

為了獲取相對於目前節點的引用,我們使用方法 Node.get_node() 。我們可以將引用儲存在變數中。

func _ready():
    var timer = get_node("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)

該行讀起來是這樣的:我們將計時器的“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

visible 屬性是一個布林值,用於控制節點的可見性。 visible = not visible 行切換該值。如果 visibletrue ,它就會變成 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

自定訊號

備註

本節介紹的是如何定義並使用你自己的訊號,不依賴之前課程所建立的專案。

您可以在腳本中定義自訂訊號。例如,假設您希望在玩家的生命值為零時通過螢幕顯示遊戲結束。為此,當他們的生命值達到 0 時,您可以定義一個名為“died”或“health_depleted”的訊號。

extends Node2D

signal health_depleted

var health = 10

備註

由於訊號表示剛剛發生的事件,我們通常在其名稱中使用過去時態的動作動詞。

自訂訊號與內建訊號運作方式相同:它們會出現在 Node 分頁中,你可以像其他訊號一樣連接它們。

../../_images/signals_17_custom_signal.webp

要在腳本中發出信號,需呼叫信號的 emit() 方法。

func take_damage(amount):
    health -= amount
    if health <= 0:
        health_depleted.emit()

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

extends Node2D

signal health_changed(old_value, new_value)

var health = 10

備註

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

若需要傳遞數值,可將數值放在 emit_signal 函式的第二個參數內:

func take_damage(amount):
    var old_health = health
    health -= amount
    health_changed.emit(old_health, health)

總結

Godot 中的任何節點都會在發生特定事件時發出訊號,例如按下按鈕。其他節點可以連接到單個訊號並對所選事件做出反應。

訊號有很多用途。有了它們,你可以對進入或退出遊戲世界的節點、碰撞、角色進入或離開某個區域、介面元素的大小變化等等做出反應。

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

在下一章 您的第一個 2D 遊戲 中,你將會製作一款完整的 2D 遊戲,把目前學到的知識全部實際應用。