รูปแบบการใช้ Signal ใน Godot 4 — คู่มือฉบับสมบูรณ์

1. บทนำ — อะไรเปลี่ยนไปใน Godot 4

Godot 4 ได้ปรับปรุงวิธีการทำงานของ signal ใหม่ทั้งหมด หากคุณย้ายมาจาก Godot 3 ไวยากรณ์แบบเดิมที่อิงกับสตริงอย่าง object.connect("signal_name", target, "method_name") ได้ถูกยกเลิกไปแล้ว แทนที่ด้วย API แบบ อิง callable ที่ปลอดภัยด้านชนิดข้อมูล เอื้อต่อการรีแฟกเตอร์ และจับข้อผิดพลาดตั้งแต่ตอนคอมไพล์แทนที่จะเป็นตอนรันไทม์

คู่มือนี้ครอบคลุมทุกอย่างที่คุณต้องรู้เกี่ยวกับ signal ใน Godot 4.4+: การประกาศ การเชื่อม การตัดการเชื่อม การส่ง การ await และรูปแบบที่ใช้งานจริงที่มีประโยชน์ที่สุด

ประเด็นสำคัญ

ใน Godot 4 signal คืออ็อบเจกต์ระดับสูงสุด (first-class object) คุณเข้าถึงมันในฐานะพร็อพเพอร์ตี (เช่น button.pressed) และเรียกเมธอดบนมัน (.connect(), .emit(), .disconnect()) ไม่ต้องใช้ API แบบสตริงอีกต่อไป

2. การประกาศ Signal แบบกำหนดเอง

ประกาศ signal ที่ส่วนบนสุดของสคริปต์ด้วยคีย์เวิร์ด signal คุณสามารถระบุชื่อและชนิดของพารามิเตอร์เพิ่มเติมได้เพื่อการทำเอกสารและการเติมคำอัตโนมัติในเอดิเตอร์

player.gd
# Simple signal with no parameters
signal game_over

# Signal with typed parameters
signal health_changed(new_health: int)

# Signal with multiple parameters
signal item_picked_up(item_name: String, quantity: int)

# Signal with no type hints (works, but typed is recommended)
signal something_happened(data)
เคล็ดลับ

ควรเพิ่มการระบุชนิด (type hint) ให้พารามิเตอร์ของ signal เสมอ สิ่งนี้จะเปิดใช้งานการเติมคำอัตโนมัติในเอดิเตอร์และทำให้โค้ดของคุณอธิบายตัวเองได้ signal ที่มีการระบุชนิดยังแสดงข้อมูลพารามิเตอร์ในแท็บ Signals ของ Node dock อีกด้วย

3. การเชื่อม Signal (วิธีใหม่)

การเปลี่ยนแปลงที่ใหญ่ที่สุดใน Godot 4 คือวิธีการเชื่อม signal แทนที่จะส่งสตริง คุณใช้ signal ในฐานะพร็อพเพอร์ตีและเรียก .connect() พร้อมกับ Callable (การอ้างอิงไปยังฟังก์ชัน)

การเชื่อมพื้นฐาน

GDScript
# Godot 3 (OLD — no longer works in Godot 4)
# button.connect("pressed", self, "_on_button_pressed")

# Godot 4 — callable-based connection
button.pressed.connect(_on_button_pressed)

func _on_button_pressed() -> void:
    print("Button was pressed!")

การเชื่อมพร้อมอาร์กิวเมนต์เพิ่มเติมด้วย bind()

ใช้ .bind() เพื่อส่งข้อมูลเพิ่มเติมไปพร้อมกับ signal สิ่งนี้มีประโยชน์เมื่อคุณเชื่อม signal หลายตัวเข้ากับเมธอดเดียวกัน

GDScript
# Pass extra data via bind()
buy_button.pressed.connect(_on_item_button.bind("sword"))
sell_button.pressed.connect(_on_item_button.bind("shield"))

func _on_item_button(item_id: String) -> void:
    print("Selected item: ", item_id)

การเชื่อมแบบ Lambda (Inline)

สำหรับตัวจัดการที่สั้น คุณสามารถใช้ฟังก์ชัน lambda ได้โดยตรง วิธีนี้ช่วยให้ตรรกะที่เรียบง่ายอยู่ใกล้กับจุดที่เชื่อม

GDScript
# Lambda — great for one-liners
button.pressed.connect(func(): print("Button pressed!"))

# Lambda with parameters
health_changed.connect(func(hp: int): health_label.text = str(hp))

# Multi-line lambda
enemy.died.connect(func():
    score += 100
    score_label.text = "Score: %d" % score
    print("Enemy defeated!")
)
คำเตือน

การเชื่อมแบบ lambda ไม่สามารถตัดการเชื่อมได้ง่าย ๆ เพราะคุณไม่มีการอ้างอิงไปยังฟังก์ชันนิรนาม หากคุณต้องการตัดการเชื่อมภายหลัง ให้เก็บ Callable ไว้ในตัวแปรหรือใช้เมธอดที่มีชื่อแทน

การเชื่อมในเอดิเตอร์ (Node Dock)

คุณยังคงเชื่อม signal ผ่าน UI ของเอดิเตอร์ Godot ได้ เลือกโหนด เปิด Node dock > แท็บ Signals ดับเบิลคลิกที่ signal แล้วเลือกโหนดและเมธอดเป้าหมาย เอดิเตอร์จะสร้างเมธอดอย่าง _on_button_pressed() ในสคริปต์ของคุณโดยอัตโนมัติ ซึ่งให้ผลเหมือนกับการเรียก .connect() ในโค้ด — เพียงแต่ช่วยประหยัดการพิมพ์เท่านั้น

4. การตัดการเชื่อม Signal

ตัดการเชื่อม signal เมื่อคุณไม่ต้องการรับมันอีกต่อไป สิ่งนี้สำคัญต่อการหลีกเลี่ยงข้อผิดพลาดเมื่อโหนดถูกปลดปล่อยหรือเมื่อสลับสถานะของเกม

GDScript
# Disconnect a signal
button.pressed.disconnect(_on_button_pressed)

# Always check before disconnecting to avoid errors
if button.pressed.is_connected(_on_button_pressed):
    button.pressed.disconnect(_on_button_pressed)
แนวปฏิบัติที่ดี

สำหรับการเชื่อม signal ข้ามฉาก (เช่น การเชื่อมกับ Autoload) ให้ตัดการเชื่อมใน _exit_tree() เสมอ เพื่อป้องกันการอ้างอิงที่ค้างเมื่อโหนดถูกปลดปล่อย:

GDScript
func _ready() -> void:
    EventBus.player_died.connect(_on_player_died)

func _exit_tree() -> void:
    if EventBus.player_died.is_connected(_on_player_died):
        EventBus.player_died.disconnect(_on_player_died)

5. การส่ง Signal

ใน Godot 4 ให้ใช้ .emit() แทน emit_signal() แบบเดิม signal เป็นอ็อบเจกต์ ดังนั้นคุณจึงเรียกเมธอดบนมันได้โดยตรง

player.gd
signal health_changed(new_health: int)
signal died

var health: int = 100

func take_damage(amount: int) -> void:
    health -= amount
    health_changed.emit(health)  # Emit with argument

    if health <= 0:
        died.emit()  # Emit with no arguments
หมายเหตุ

emit_signal("signal_name") แบบเดิมยังคงทำงานได้ในทางเทคนิคใน Godot 4 แต่ถูกยกเลิกการใช้งานแล้ว สำหรับโค้ดใหม่ให้เลือกใช้ signal_name.emit() เสมอ เวอร์ชันที่อิงสตริงอาจถูกลบออกในรีลีสของ Godot ในอนาคต

6. การรอ Signal ด้วย await

Godot 4 ได้แทนที่ yield() ด้วย await ซึ่งช่วยให้คุณหยุดฟังก์ชันชั่วคราวจนกว่า signal จะทำงาน เหมาะอย่างยิ่งสำหรับคัตซีน บทเรียนสอนเล่น แอนิเมชันตามลำดับ และเหตุการณ์ที่จับเวลา

GDScript
func play_cutscene() -> void:
    # Wait for a timer
    await get_tree().create_timer(2.0).timeout

    # Wait for an animation to finish
    animation_player.play("intro")
    await animation_player.animation_finished

    # Wait for player input (custom signal)
    dialogue_label.text = "Press any key to continue..."
    await player_pressed_continue

    # Continue execution after the signal fires
    print("Cutscene complete!")

การรับค่าจาก Signal ที่ await

หาก signal ส่งอาร์กิวเมนต์ออกมา await จะคืนค่าเหล่านั้นให้ สำหรับอาร์กิวเมนต์เดียว คุณจะได้ค่านั้นโดยตรง สำหรับหลายอาร์กิวเมนต์ คุณจะได้อาร์เรย์

GDScript
# Single parameter — returns the value directly
var final_health: int = await health_changed
print("Health is now: ", final_health)

# Multiple parameters — returns an array
var result = await item_picked_up
var item_name: String = result[0]
var quantity: int = result[1]

7. Connection Flags ของ Signal

Godot มี connection flags ให้ใช้เป็นอาร์กิวเมนต์ตัวที่สองของ .connect() ซึ่งปรับเปลี่ยนพฤติกรรมของการเชื่อม

CONNECT_ONE_SHOT

การเชื่อมจะถูกลบออกโดยอัตโนมัติหลังจาก signal ทำงานครั้งเดียว เหมาะอย่างยิ่งสำหรับเหตุการณ์ครั้งเดียว เช่น แอนิเมชันการตายหรือการปลดล็อกความสำเร็จ

GDScript
# Auto-disconnects after firing once
enemy.died.connect(_on_first_kill, CONNECT_ONE_SHOT)

func _on_first_kill() -> void:
    unlock_achievement("first_blood")

CONNECT_DEFERRED

เมธอดที่เชื่อมไว้จะถูกเรียกตอนสิ้นสุดเฟรมปัจจุบัน (ระหว่างช่วงเวลาว่าง) แทนที่จะเรียกทันที มีประโยชน์เมื่อตัวจัดการ signal ปรับเปลี่ยน scene tree ซึ่งไม่ปลอดภัยที่จะทำระหว่างการประมวลผลฟิสิกส์หรือ signal

GDScript
# Called at end of frame — safe for scene tree changes
button.pressed.connect(_on_restart, CONNECT_DEFERRED)

func _on_restart() -> void:
    get_tree().reload_current_scene()

การรวม Flags เข้าด้วยกัน

GDScript
# One-shot AND deferred
trigger.body_entered.connect(_on_trigger, CONNECT_ONE_SHOT | CONNECT_DEFERRED)

8. รูปแบบที่ใช้บ่อย

EventBus (ศูนย์กลาง Signal ระดับโกลบอล)

รูปแบบ EventBus ใช้ Autoload singleton เพื่อลดการพึ่งพากันระหว่างโหนด โหนดใด ๆ ก็สามารถส่งหรือรับฟังเหตุการณ์ระดับโกลบอลได้โดยไม่ต้องมีการอ้างอิงโดยตรงไปยังอีกโหนดหนึ่ง นี่เป็นหนึ่งในรูปแบบที่ทรงพลังที่สุดใน Godot

event_bus.gd (Autoload)
extends Node

# Define all global signals in one place
signal player_died
signal score_changed(new_score: int)
signal level_completed(level_id: int)
signal item_collected(item_name: String)
signal settings_changed
player.gd (emitter)
func die() -> void:
    # Any script can emit global signals
    EventBus.player_died.emit()
hud.gd (listener)
func _ready() -> void:
    # Any script can listen to global signals
    EventBus.score_changed.connect(_on_score_changed)
    EventBus.player_died.connect(_on_player_died)

func _on_score_changed(new_score: int) -> void:
    score_label.text = "Score: %d" % new_score

func _on_player_died() -> void:
    game_over_screen.show()
การตั้งค่า Autoload

ไปที่ Project > Project Settings > Autoload เพิ่มสคริปต์ event_bus.gd ของคุณ แล้วตั้งชื่อว่า EventBus มันจะกลายเป็น singleton ที่เข้าถึงได้ทั่วโลกโดยอัตโนมัติ

Signal Relay (การสื่อสารระหว่างพ่อแม่-ลูก)

โหนดพ่อแม่รับฟัง signal ของโหนดลูก แล้วส่งต่อหรือรวบรวมข้อมูล โหนดลูกไม่จำเป็นต้องรู้จักกันและกันเลย

inventory.gd
signal inventory_updated

func _ready() -> void:
    # Connect to all slot children
    for slot in get_children():
        if slot.has_signal("item_changed"):
            slot.item_changed.connect(_on_slot_changed)

func _on_slot_changed() -> void:
    inventory_updated.emit()

Signal + Await สำหรับลำดับการเล่นเกมแบบต่อเนื่อง

level_manager.gd
func run_level() -> void:
    spawn_enemies()
    await EventBus.all_enemies_defeated

    show_treasure_chest()
    await EventBus.chest_opened

    play_exit_animation()
    await get_tree().create_timer(1.5).timeout

    load_next_level()

การเชื่อมแบบไดนามิกสำหรับโหนดที่ Spawn ขึ้นมา

enemy_spawner.gd
func spawn_enemy(pos: Vector2) -> void:
    var enemy = enemy_scene.instantiate()
    enemy.position = pos

    # Connect signals before adding to scene tree
    enemy.died.connect(_on_enemy_died.bind(enemy))
    enemy.health_changed.connect(_on_enemy_health_changed)

    add_child(enemy)

func _on_enemy_died(enemy: Node) -> void:
    enemies_alive -= 1
    enemy.queue_free()

9. ตารางสรุปการย้าย (Godot 3 → 4)

บุ๊กมาร์กตารางนี้ไว้เป็นข้อมูลอ้างอิงด่วนเมื่อพอร์ตโปรเจกต์ Godot 3 ของคุณ

การดำเนินการ Godot 3 Godot 4
เชื่อม connect("sig", obj, "method") sig.connect(method)
ตัดการเชื่อม disconnect("sig", obj, "method") sig.disconnect(method)
ส่ง emit_signal("sig", args) sig.emit(args)
ตรวจสอบการเชื่อม is_connected("sig", obj, "method") sig.is_connected(method)
รอ signal yield(obj, "sig") await obj.sig
ผูกอาร์กิวเมนต์เพิ่มเติม connect("sig", obj, "method", [data]) sig.connect(method.bind(data))
ครั้งเดียว (One-shot) connect("sig", obj, "method", [], CONNECT_ONESHOT) sig.connect(method, CONNECT_ONE_SHOT)

10. ข้อผิดพลาดที่พบบ่อย

1. ใช้ connect แบบอิงสตริง (สไตล์ Godot 3)

GDScript
# WRONG — Godot 3 syntax, will not compile
button.connect("pressed", self, "_on_button_pressed")

# CORRECT — Godot 4 syntax
button.pressed.connect(_on_button_pressed)

2. ใช้ emit_signal() แทน .emit()

แม้ว่า emit_signal() จะยังทำงานได้ แต่มันข้ามการตรวจสอบตอนคอมไพล์และถูกยกเลิกการใช้งานอย่างเป็นทางการแล้ว ให้ใช้ .emit()

GDScript
# AVOID — deprecated, no compile-time checks
emit_signal("health_changed", health)

# PREFER — type-safe, catches typos at compile time
health_changed.emit(health)

3. เชื่อมกับโหนดที่ถูกปลดปล่อยแล้ว

หากคุณเชื่อม signal เข้ากับเมธอดบนโหนด แล้วโหนดนั้นถูก queue_free() การส่ง signal ครั้งถัดไปจะทำให้แครช วิธีแก้:

  • ตัดการเชื่อมใน _exit_tree()
  • ใช้ CONNECT_ONE_SHOT สำหรับเหตุการณ์ครั้งเดียว
  • ตรวจสอบ is_instance_valid(target) ก่อนส่ง
GDScript
# Safe emission pattern
for connection in my_signal.get_connections():
    if is_instance_valid(connection["callable"].get_object()):
        pass  # Connection is still valid
my_signal.emit()  # Godot handles invalid connections gracefully in 4.x

4. ลืมตัดการเชื่อม signal ข้ามฉาก

การเชื่อมกับ signal ของ Autoload จะคงอยู่ข้ามการเปลี่ยนฉาก หากคุณเชื่อมใน _ready() แต่ไม่เคยตัดการเชื่อม คุณจะพบข้อผิดพลาดหรือพฤติกรรมที่ไม่คาดคิดเมื่อฉากถูกโหลดใหม่

GDScript
# WRONG — never disconnects, leaks across scene changes
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

# CORRECT — clean disconnect
func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)

func _exit_tree() -> void:
    EventBus.score_changed.disconnect(_on_score_changed)

5. เชื่อม signal เดียวกันสองครั้ง

หาก _ready() ถูกเรียกหลายครั้ง (เช่น การย้ายพ่อแม่ของโหนด) คุณอาจเชื่อมเมธอดเดียวกันสองครั้งโดยไม่ตั้งใจ ตัวจัดการจะทำงานสองครั้งต่อการส่งหนึ่งครั้ง

GDScript
# Guard against double-connection
func _ready() -> void:
    if not EventBus.score_changed.is_connected(_on_score_changed):
        EventBus.score_changed.connect(_on_score_changed)

อยากให้ AI จัดการสถาปัตยกรรม Signal ของคุณไหม?

Godot MCP Pro เชื่อมต่อผู้ช่วย AI อย่าง Claude เข้ากับเอดิเตอร์ Godot ของคุณโดยตรง มันสามารถเชื่อม ตัดการเชื่อม ตรวจสอบ และแสดงผังการไหลของ signal ทั่วทั้งโปรเจกต์ของคุณได้ — โดยอัตโนมัติ

analyze_signal_flow find_signal_connections connect_signal disconnect_signal get_signals
รับ Godot MCP Pro — $15