Các mẫu Signal trong Godot 4 — Hướng dẫn đầy đủ

1. Giới thiệu — Điều gì đã thay đổi trong Godot 4

Godot 4 đã đại tu hoàn toàn cách hoạt động của signal. Nếu bạn đến từ Godot 3, cú pháp cũ dựa trên chuỗi object.connect("signal_name", target, "method_name") đã biến mất. Thay vào đó, Godot 4 sử dụng một API dựa trên Callable, an toàn về kiểu, thân thiện với việc tái cấu trúc, và bắt lỗi tại thời điểm biên dịch thay vì thời điểm chạy.

Hướng dẫn này bao quát mọi thứ bạn cần biết về signal trong Godot 4.4+: khai báo, kết nối, ngắt kết nối, phát, await, và các mẫu thực tế hữu ích nhất.

Điểm mấu chốt

Trong Godot 4, signal là các đối tượng hạng nhất. Bạn truy cập chúng như các thuộc tính (ví dụ button.pressed) và gọi các phương thức trên chúng (.connect(), .emit(), .disconnect()). Không còn API dựa trên chuỗi nữa.

2. Khai báo signal tùy chỉnh

Khai báo signal ở đầu script bằng từ khóa signal. Bạn có thể tùy chọn chỉ định tên tham số và kiểu để phục vụ tài liệu và tính năng tự động hoàn thành của trình chỉnh sửa.

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)
Mẹo

Luôn thêm gợi ý kiểu cho các tham số signal. Điều này bật tính năng tự động hoàn thành trong trình chỉnh sửa và làm cho mã của bạn tự mô tả. Signal có gợi ý kiểu cũng hiển thị thông tin tham số trong tab Signals của Node dock.

3. Kết nối signal (cách mới)

Thay đổi lớn nhất trong Godot 4 là cách bạn kết nối signal. Thay vì truyền chuỗi, bạn sử dụng signal như một thuộc tính và gọi .connect() với một Callable (một tham chiếu đến một hàm).

Kết nối cơ bản

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!")

Kết nối với đối số bổ sung qua bind()

Dùng .bind() để truyền dữ liệu bổ sung cùng với signal. Điều này hữu ích khi kết nối nhiều signal đến cùng một phương thức.

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)

Kết nối bằng Lambda (nội tuyến)

Với các handler ngắn, bạn có thể dùng trực tiếp một hàm lambda. Điều này giữ logic đơn giản gần với điểm kết nối.

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!")
)
Cảnh báo

Kết nối bằng lambda không thể dễ dàng ngắt kết nối vì bạn không có tham chiếu đến hàm ẩn danh. Nếu bạn cần ngắt kết nối sau này, hãy lưu Callable vào một biến hoặc dùng một phương thức có tên thay thế.

Kết nối trong trình chỉnh sửa (Node Dock)

Bạn vẫn có thể kết nối signal thông qua giao diện trình chỉnh sửa Godot. Chọn một node, mở Node dock > tab Signals, nhấp đúp vào một signal, và chọn node đích cùng phương thức. Trình chỉnh sửa sẽ tự động tạo một phương thức như _on_button_pressed() trong script của bạn. Điều này giống hệt việc gọi .connect() trong mã — nó chỉ tiết kiệm thao tác gõ.

4. Ngắt kết nối signal

Ngắt kết nối một signal khi bạn không còn muốn nhận nó nữa. Điều này quan trọng để tránh lỗi khi các node bị giải phóng hoặc khi chuyển đổi trạng thái trò chơi.

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)
Thực hành tốt nhất

Với các kết nối signal xuyên scene (ví dụ kết nối đến một Autoload), luôn ngắt kết nối trong _exit_tree() để ngăn các tham chiếu treo lơ lửng khi node bị giải phóng:

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. Phát signal

Trong Godot 4, dùng .emit() thay cho emit_signal() cũ. Signal là một đối tượng, nên bạn gọi phương thức trực tiếp trên nó.

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
Ghi chú

emit_signal("signal_name") cũ về mặt kỹ thuật vẫn hoạt động trong Godot 4 nhưng đã không còn được khuyến nghị. Luôn ưu tiên signal_name.emit() cho mã mới. Phiên bản dựa trên chuỗi có thể bị loại bỏ trong một bản phát hành Godot tương lai.

6. Await signal

Godot 4 đã thay thế yield() bằng await. Điều này cho phép bạn tạm dừng một hàm cho đến khi một signal được phát, hoàn hảo cho các đoạn cắt cảnh, hướng dẫn, hoạt ảnh tuần tự và các sự kiện định thời.

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!")

Lấy giá trị từ các signal đã await

Nếu signal phát ra đối số, await sẽ trả về chúng. Với một đối số duy nhất, bạn nhận trực tiếp giá trị. Với nhiều đối số, bạn nhận một mảng.

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. Cờ kết nối signal

Godot cung cấp các cờ kết nối làm đối số thứ hai cho .connect() nhằm thay đổi hành vi của kết nối.

CONNECT_ONE_SHOT

Kết nối tự động bị loại bỏ sau khi signal được phát một lần. Hoàn hảo cho các sự kiện một lần như hoạt ảnh cái chết hoặc mở khóa thành tựu.

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

Phương thức được kết nối sẽ được gọi vào cuối khung hình hiện tại (trong thời gian nhàn rỗi) thay vì ngay lập tức. Hữu ích khi handler của signal sửa đổi cây scene, điều không an toàn để thực hiện trong quá trình xử lý vật lý hoặc xử lý 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()

Kết hợp các cờ

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

8. Các mẫu thường dùng

EventBus (trung tâm signal toàn cục)

Mẫu EventBus sử dụng một singleton Autoload để tách rời các node. Bất kỳ node nào cũng có thể phát hoặc lắng nghe các sự kiện toàn cục mà không cần tham chiếu trực tiếp đến node khác. Đây là một trong những mẫu mạnh mẽ nhất trong 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()
Thiết lập Autoload

Vào Project > Project Settings > Autoload, thêm script event_bus.gd của bạn, và đặt tên là EventBus. Nó sẽ tự động trở thành một singleton có thể truy cập toàn cục.

Chuyển tiếp signal (giao tiếp cha-con)

Một node cha lắng nghe signal của các node con và chuyển tiếp hoặc tổng hợp thông tin. Các node con không bao giờ cần biết về nhau.

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 cho luồng trò chơi tuần tự

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()

Kết nối động cho các node được sinh ra

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. Bảng tra cứu chuyển đổi (Godot 3 → 4)

Hãy đánh dấu bảng này để tra cứu nhanh khi chuyển dự án Godot 3 của bạn.

Thao tác Godot 3 Godot 4
Kết nối connect("sig", obj, "method") sig.connect(method)
Ngắt kết nối disconnect("sig", obj, "method") sig.disconnect(method)
Phát emit_signal("sig", args) sig.emit(args)
Kiểm tra kết nối is_connected("sig", obj, "method") sig.is_connected(method)
Chờ signal yield(obj, "sig") await obj.sig
Gắn đối số bổ sung connect("sig", obj, "method", [data]) sig.connect(method.bind(data))
Một lần connect("sig", obj, "method", [], CONNECT_ONESHOT) sig.connect(method, CONNECT_ONE_SHOT)

10. Các lỗi thường gặp

1. Dùng connect dựa trên chuỗi (kiểu 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. Dùng emit_signal() thay vì .emit()

Mặc dù emit_signal() vẫn hoạt động, nó bỏ qua các kiểm tra tại thời điểm biên dịch và đã chính thức không còn được khuyến nghị. Hãy dùng .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. Kết nối đến một node đã bị giải phóng

Nếu bạn kết nối một signal đến một phương thức trên một node, và node đó bị queue_free(), lần phát signal tiếp theo sẽ gây sập. Các giải pháp:

  • Ngắt kết nối trong _exit_tree()
  • Dùng CONNECT_ONE_SHOT cho các sự kiện một lần
  • Kiểm tra is_instance_valid(target) trước khi phát
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. Quên ngắt kết nối các signal xuyên scene

Các kết nối đến signal Autoload vẫn tồn tại qua các lần thay đổi scene. Nếu bạn kết nối trong _ready() nhưng không bao giờ ngắt kết nối, bạn sẽ gặp lỗi hoặc hành vi ngoài ý muốn khi scene được tải lại.

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. Kết nối cùng một signal hai lần

Nếu _ready() được gọi nhiều lần (ví dụ khi gán lại node cha), bạn có thể vô tình kết nối cùng một phương thức hai lần. Handler khi đó sẽ được phát hai lần cho mỗi lần emit.

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)

Muốn AI quản lý kiến trúc signal của bạn?

Godot MCP Pro kết nối các trợ lý AI như Claude trực tiếp với trình chỉnh sửa Godot của bạn. Nó có thể kết nối, ngắt kết nối, kiểm tra và trực quan hóa các luồng signal trên toàn bộ dự án của bạn — một cách tự động.

analyze_signal_flow find_signal_connections connect_signal disconnect_signal get_signals
Nhận Godot MCP Pro — $15