Паттерны сигналов в Godot 4 — Полное руководство

1. Введение — что изменилось в Godot 4

Godot 4 полностью переработал работу сигналов. Если вы переходите с Godot 3, старый строковый синтаксис object.connect("signal_name", target, "method_name") больше не работает. Вместо него в Godot 4 используется API на основе Callable, который типобезопасен, удобен для рефакторинга и отлавливает ошибки на этапе компиляции, а не во время выполнения.

Это руководство охватывает всё, что нужно знать о сигналах в Godot 4.4+: объявление, подключение, отключение, испускание, ожидание и наиболее полезные практические паттерны.

Главное

В Godot 4 сигналы являются объектами первого класса. Вы обращаетесь к ним как к свойствам (например, button.pressed) и вызываете у них методы (.connect(), .emit(), .disconnect()). Никакого строкового API.

2. Объявление пользовательских сигналов

Объявляйте сигналы в начале скрипта с помощью ключевого слова 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)
Совет

Всегда добавляйте подсказки типов к параметрам сигналов. Это включает автодополнение в редакторе и делает код самодокументируемым. Сигналы с подсказками типов также показывают информацию о параметрах во вкладке Signals в доке узла (Node dock).

3. Подключение сигналов (новый способ)

Самое большое изменение в Godot 4 — способ подключения сигналов. Вместо передачи строк вы используете сигнал как свойство и вызываете .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(), чтобы передавать дополнительные данные вместе с сигналом. Это удобно, когда несколько сигналов подключаются к одному и тому же методу.

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)

Лямбда-подключения (встроенные)

Для коротких обработчиков можно использовать лямбда-функцию напрямую. Это позволяет держать простую логику рядом с местом подключения.

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!")
)
Предупреждение

Лямбда-подключения сложно отключить, потому что у вас нет ссылки на анонимную функцию. Если позже понадобится отключение, сохраните Callable в переменную или используйте именованный метод.

Подключение в редакторе (Node dock)

Сигналы по-прежнему можно подключать через интерфейс редактора Godot. Выберите узел, откройте Node dock > вкладку Signals, дважды щёлкните по сигналу и выберите целевой узел и метод. Редактор автоматически сгенерирует метод вроде _on_button_pressed() в вашем скрипте. Это идентично вызову .connect() в коде — просто экономит время на набор текста.

4. Отключение сигналов

Отключайте сигнал, когда больше не хотите его получать. Это важно для предотвращения ошибок при освобождении узлов или переключении игровых состояний.

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)
Лучшая практика

Для межсценовых подключений сигналов (например, к автозагрузке / 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. Испускание сигналов

В Godot 4 используйте .emit() вместо старого emit_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. Ожидание сигналов через await

Godot 4 заменил yield() на await. Это позволяет приостановить функцию до срабатывания сигнала, что идеально подходит для катсцен, обучающих сцен, последовательных анимаций и событий по таймеру.

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

Получение значений из ожидаемых сигналов

Если сигнал испускает аргументы, 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. Флаги соединения сигналов

Godot предоставляет флаги соединения в качестве второго аргумента .connect(), которые изменяют поведение соединения.

CONNECT_ONE_SHOT

Соединение автоматически удаляется после первого срабатывания сигнала. Идеально для одноразовых событий, таких как анимации смерти или разблокировка достижений.

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

Подключённый метод вызывается в конце текущего кадра (во время простоя), а не немедленно. Полезно, когда обработчик сигнала изменяет дерево сцены, что небезопасно делать во время обработки физики или сигналов.

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

Комбинирование флагов

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

8. Частые паттерны

EventBus (глобальный хаб сигналов)

Паттерн EventBus использует синглтон-автозагрузку (Autoload) для развязки узлов. Любой узел может испускать или прослушивать глобальные события без прямой ссылки на другой узел. Это один из самых мощных паттернов в 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. Он автоматически станет глобально доступным синглтоном.

Ретрансляция сигналов (связь родитель-потомок)

Родительский узел прослушивает сигналы своих потомков и ретранслирует или агрегирует информацию. Потомкам никогда не нужно знать друг о друге.

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

Динамические подключения для порождённых узлов

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)
Ожидание сигнала yield(obj, "sig") await obj.sig
Привязка доп. аргументов connect("sig", obj, "method", [data]) sig.connect(method.bind(data))
Одноразовое 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. Подключение к освобождённому узлу

Если вы подключаете сигнал к методу узла, а этот узел освобождается через queue_free(), следующее испускание сигнала приведёт к сбою. Решения:

  • Отключайтесь в _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. Забытое отключение межсценовых сигналов

Подключения к сигналам автозагрузки (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. Двойное подключение одного и того же сигнала

Если _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)

Хотите, чтобы ИИ управлял архитектурой сигналов?

Godot MCP Pro подключает ИИ-ассистентов вроде Claude напрямую к вашему редактору Godot. Он может подключать, отключать, проверять и визуализировать потоки сигналов по всему проекту — автоматически.

analyze_signal_flow find_signal_connections connect_signal disconnect_signal get_signals
Получить Godot MCP Pro — $15