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 內建的委派機制,允許一個遊戲物件對另一個遊戲物件的變化做出反應,而無需相互引用。使用訊號可以限制 耦合 ,並保持程式碼的靈活性。

例如,您可能在螢幕上有一個代表玩家生命值的生命條。當玩家受到傷害或使用治療藥水時,您希望生命條反映變化。要做到這一點,在 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.webp

我們要新增另一個節點作為 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.webp

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

使用程式碼來連接訊號

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

You can connect signals in the Signals dock. Select the Button node and, on the right side of the editor, click on the tab named Signals next to the Inspector.

../../_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"。

備註

When connecting signals via the editor's Signals dock, you can use two modes. The simple one only allows you to connect to nodes that have a script attached to them and creates a new callback function on them.

../../_images/signals_advanced_connection_window.webp

進階視圖可讓你連接到任意節點與內建函式、為回呼新增參數並設定選項。你可以點擊視窗右下的 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

備註

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

Your signals work the same way as built-in ones: they appear in the Signals tab and you can connect to them like any other.

../../_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

備註

The signal arguments show up in the editor's Signals dock, and Godot can use them to generate callback functions for you. However, you can still emit any number of arguments when you emit signals. So it's up to you to emit the correct values.

若需要傳遞數值,可將數值放在 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 遊戲,把目前學到的知識全部實際應用。