Сигналы: объекты общаются
Сигнал — это объявление «у меня кое-что случилось», на которое могут подписаться другие узлы.
Суть: узел шлёт сигнал, не зная, кто его слушает, а слушатели подписываются и реагируют — связь остаётся слабой.
Представь, что игрок получил урон. Об этом должны узнать сразу несколько узлов: полоска здоровья (обновиться), звук (вскрикнуть), экран (мигнуть красным). Плохой путь — заставить игрока знать про каждого из них и звать их вручную: тогда стоит добавить новый элемент, и придётся править код игрока. Хороший путь — игрок просто кричит «мне нанесли урон!», а кто хочет — слушает. Это и есть сигналы, реализация классического паттерна «наблюдатель».
В GDScript ты объявляешь сигнал словом signal, шлёшь его методом emit(), а слушатель подписывается через connect(). Сигнал может нести данные — например, сколько урона.
extends Node
signal health_changed(new_health: int)
var health: int = 100
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health) # кричим всем, кто слушаетПоток сигнала (наблюдатель):
Player.take_damage()
|
v
health_changed.emit(70) ----+----> HealthBar._on_health_changed
|
+----> Screen._on_health_changed
|
+----> Sound._on_health_changedPlayer не знает, кто эти три слушателя. Он просто шлёт сигнал. Завтра ты добавишь четвёртого слушателя — код Player не изменится ни на строчку. В этом вся прелесть.
Полезно понять, когда сигнал уместен, а когда — перебор. Сигналы блистают там, где один источник события должен обслужить нескольких неизвестных заранее слушателей, или где нижний узел сообщает что-то верхнему, не имея права дёргать его напрямую. Но если два узла и так крепко связаны и всегда работают в паре, иногда честнее обычный вызов функции — он короче и понятнее. Мудрость приходит с опытом: сигналы дают гибкость, но за каждую связь, которую не видно глазами в коде, ты платишь чуть большей сложностью отладки. Используй их осознанно, а не из принципа.
Как работает под капотом
У каждого сигнала есть список подписчиков — функций, которые надо вызвать. connect() добавляет функцию в этот список. emit() пробегает по списку и вызывает каждую, передавая ей данные сигнала. Отправитель не хранит ссылок на слушателей лично — только этот список коллбэков. Поэтому связь «слабая»: стороны не знают друг о друге напрямую.
class Signal:
def __init__(self):
self.subscribers = []
def connect(self, callback):
self.subscribers.append(callback)
def emit(self, *args):
for cb in self.subscribers:
cb(*args)
health_changed = Signal()
health_changed.connect(lambda hp: print(f"Полоска HP: {hp}"))
health_changed.connect(lambda hp: print(f"Звук: ой! ({hp})"))
health = 100
health -= 30
health_changed.emit(health) # все слушатели сработают
print("Отправитель не знает, кто слушает.")Та же логика на Python ▶. Сигнал — это список коллбэков; connect добавляет, emit вызывает всех. Добавь третий connect и снова emit — отправитель не изменится. Это паттерн наблюдатель в чистом виде.
Частые ошибки
Частая ошибка — подключить сигнал и в редакторе, и в коде: обработчик вызовется дважды. Выбери одно место. Вторая — несовпадение числа аргументов: если сигнал шлёт одно число, обработчик должен принимать ровно один аргумент. Третья — пытаться эмитить чужой сигнал: emit зовут на том узле, которому сигнал принадлежит. Четвёртая — использовать сигналы там, где проще прямой вызов: если узел и так знает соседа, иногда обычный вызов функции честнее, чем сигнал.
Best practices
Сигналы — для общения снизу вверх и между равными: потомок сообщает родителю «я готов», враг сообщает «я умер». Называй сигналы как свершившийся факт: died, collected, health_changed. Передавай в сигнале только нужные данные. Подключай в одном месте — обычно в _ready или через редактор, но не в обоих. Сигналы делают код модульным: добавление слушателя не трогает отправителя.
Итоги: сигнал — это паттерн наблюдатель: отправитель шлёт emit, не зная слушателей, а те подписываются через connect. Объявляется словом signal, может нести данные. Под капотом это список коллбэков. Сигналы дают слабую связанность: новый слушатель не меняет отправителя. Подключай в одном месте и называй сигналы как факт.