Сигналы C#

Подробное объяснение сигналов в целом см. в разделе Использование сигналов в пошаговом руководстве.

Сигналы реализуются с помощью событий C#, идиоматического способа представления the observer pattern в C#. Это рекомендуемый способ использования сигналов в C#, которому и посвящена эта страница.

В некоторых случаях необходимо использовать старые API Connect() и Disconnect(). Подробнее см. в разделе Использование Connect и Disconnect.

Если при обработке сигнала вы столкнулись с исключением System.ObjectDisposedException, возможно, вы пропустили отключение сигнала. Подробнее см. в разделе Автоматическое отключение при освобождении приемника.

Сигналы как events C#

Для обеспечения большей типобезопасности все сигналы Godot также доступны через events. Вы можете обрабатывать эти события, как и любые другие, с помощью операторов += и -=.

Timer myTimer = GetNode<Timer>("Timer");
myTimer.Timeout += () => GD.Print("Timeout!");

Кроме того, вы всегда можете получить доступ к именам сигналов, связанным с типом узла, через его вложенный класс SignalName. Это полезно, например, когда требуется ожидать сигнал (см. Ключевое слово await).

await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);

Пользовательские сигналы как события C#

Чтобы объявить пользовательское событие в скрипте C#, используйте атрибут [Signal] для открытого типа делегата. Обратите внимание, что имя этого делегата должно заканчиваться на EventHandler.

[Signal]
public delegate void MySignalEventHandler();

[Signal]
public delegate void MySignalWithArgumentEventHandler(string myString);

После этого, Godot автоматически создаст соответствующие события в фоновом режиме. Вы сможете использовать их так же, как и любые другие сигналы Godot. Обратите внимание, что имена событий формируются с использованием имени вашего делегата без завершающей части EventHandler.

public override void _Ready()
{
    MySignal += () => GD.Print("Hello!");
    MySignalWithArgument += SayHelloTo;
}

private void SayHelloTo(string name)
{
    GD.Print($"Hello {name}!");
}

Предупреждение

Если вы хотите подключиться к этим сигналам в редакторе, вам придется (пере)собрать проект, чтобы они появились.

Для этого вы можете нажать кнопку Build в правом верхнем углу редактора.

Излучение сигнала

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

public void MyMethodEmittingSignals()
{
    EmitSignal(SignalName.MySignal);
    EmitSignal(SignalName.MySignalWithArgument, "World");
}

В отличие от других событий C#, вы не можете использовать Invoke для вызова событий, связанных с сигналами Godot.

Сигналы поддерживают аргументы любого Variant-compatible type.

Следовательно, любой Node или RefCounted будет автоматически совместим, но пользовательские объекты данных должны будут наследовать от GodotObject или одного из его подклассов.

using Godot;

public partial class DataObject : GodotObject
{
    public string MyFirstString { get; set; }
    public string MySecondString { get; set; }
}

Связанные значения

Иногда требуется привязать значения к сигналу при установлении соединения, а не при отправке сигнала (или в дополнение к этому). Для этого можно использовать анонимную функцию, как в следующем примере.

Здесь сигнал Button.Pressed не принимает аргументов. Но мы хотим использовать один и то же ModifyValue для кнопок "plus" и "minus". Поэтому мы привязываем значение модификатора во время подключения сигналов.

public int Value { get; private set; } = 1;

public override void _Ready()
{
    Button plusButton = GetNode<Button>("PlusButton");
    plusButton.Pressed += () => ModifyValue(1);

    Button minusButton = GetNode<Button>("MinusButton");
    minusButton.Pressed += () => ModifyValue(-1);
}

private void ModifyValue(int modifier)
{
    Value += modifier;
}

Создание сигнала во время выполнения

Наконец, вы можете создавать собственные сигналы прямо во время игры. Для этого используйте метод AddUserSignal. Имейте в виду, что его следует выполнить до любого использования этих сигналов (как подключения к ним, так и их отправки). Также учтите, что созданные таким образом сигналы не будут видны через вложенный класс SignalName.

public override void _Ready()
{
    AddUserSignal("MyCustomSignal");
    EmitSignal("MyCustomSignal");
}

Использование Connect и Disconnect

В целом не рекомендуется использовать Connect() и Disconnect(). Эти API не обеспечивают такой же уровень безопасности типа, как события. Однако они необходимы для connecting to signals defined by GDScript и прохождения ConnectFlags.

В следующем примере первое нажатие кнопки выводит сообщение Greetings!. OneShot отключает сигнал, поэтому повторное нажатие кнопки ничего не даст.

public override void _Ready()
{
    Button button = GetNode<Button>("GreetButton");
    button.Connect(Button.SignalName.Pressed, Callable.From(OnButtonPressed), (uint)GodotObject.ConnectFlags.OneShot);
}

public void OnButtonPressed()
{
    GD.Print("Greetings!");
}

Автоматическое отключение при освобождении приемника

Обычно при освобождении любого объекта GodotObject (например, любого Node) Godot автоматически разрывает все соединения, связанные с этим объектом. Это происходит как для источников, так и для приёмников сигналов.

Например, узел с этим кодом выведет "Привет!" при нажатии кнопки, а затем освободится. Освобождение узла отключает сигнал, поэтому повторное нажатие кнопки ничего не даст:

public override void _Ready()
{
    Button myButton = GetNode<Button>("../MyButton");
    myButton.Pressed += SayHello;
}

private void SayHello()
{
    GD.Print("Hello!");
    Free();
}

Если приемник сигнала освобождается, а передатчик сигнала еще активен, в некоторых случаях автоматического отключения не происходит:

  • Сигнал подключен к лямбда-выражению, которое захватывает переменную.

  • Сигнал является пользовательским сигналом.

В следующих разделах эти случаи объясняются более подробно и содержатся рекомендации по ручному отключению.

Примечание

Автоматическое отключение абсолютно надёжно, если источник сигнала освобождается до освобождения любого из его приёмников. Если проект ориентирован на такую схему, вышеуказанные ограничения могут не иметь значения.

Нет автоматического отключения: лямбда-выражение (lambda expression), которое захватывает переменную

Если вы подключаетесь к лямбда-выражению, которое захватывает переменные, Godot не может определить, что лямбда-выражение связано с экземпляром, который его создал. Это приводит к потенциально неожиданному поведению в этом примере:

Timer myTimer = GetNode<Timer>("../Timer");
int x = 0;
myTimer.Timeout += () =>
{
    x++; // This lambda expression captures x.
    GD.Print($"Tick {x} my name is {Name}");
    if (x == 3)
    {
        GD.Print("Time's up!");
        Free();
    }
};
Tick 1, my name is ExampleNode
Tick 2, my name is ExampleNode
Tick 3, my name is ExampleNode
Time's up!
[...] System.ObjectDisposedException: Cannot access a disposed object.

На такте 4, лямбда-выражение пытается обратиться к свойству Name узла, но узел уже освобождён. Это приводит к исключению.

Чтобы отписаться, сохраните ссылку на делегат, созданный лямбда-выражением, и передайте его в -=. Например, этот узел подписывается и отписывается, используя методы жизненного цикла _EnterTree и _ExitTree:

[Export]
public Timer MyTimer { get; set; }

private Action _tick;

public override void _EnterTree()
{
    int x = 0;
    _tick = () =>
    {
        x++;
        GD.Print($"Tick {x} my name is {Name}");
        if (x == 3)
        {
            GD.Print("Time's up!");
            Free();
        }
    };
    MyTimer.Timeout += _tick;
}

public override void _ExitTree()
{
    MyTimer.Timeout -= _tick;
}

В этом примере Free заставляет узел покинуть дерево, что вызывает _ExitTree. _ExitTree отключает сигнал, поэтому _tick больше никогда не вызывается.

Используемые методы жизненного цикла зависят от того, что делает узел. Другой вариант — подключаться к сигналам в _Ready и отключаться в Dispose.

Примечание

Godot использует Delegate.Target, чтобы определить, с каким примером связан делегат. Когда выражение лямбды не захватывает переменную, генерируемый делегат Target является примером, который создал делегата. Когда переменная захватывается, Target вместо этого указывает на сгенерированный тип, который сохраняет за собой захватываемую переменную. Это разрывает связь. Если вы хотите увидеть, будет ли делегат автоматически очищен, попробуйте проверить его Target.

Callable.From не влияет на Delegate.Target, поэтому подключение лямбда-функции, которая захватывает переменные с помощью Connect, работает не лучше, чем +=.

Нет автоматического отключения: пользовательский сигнал (custom signal)

Подключение к пользовательскому сигналу с помощью += не отключается автоматически после освобождения принимающего узла.

Чтобы отключиться, используйте -= в подходящее время. Например:

[Export]
public MyClass Target { get; set; }

public override void _EnterTree()
{
    Target.MySignal += OnMySignal;
}

public override void _ExitTree()
{
    Target.MySignal -= OnMySignal;
}

Другое решение — использовать Connect, которое автоматически отключается с помощью пользовательских сигналов:

[Export]
public MyClass Target { get; set; }

public override void _EnterTree()
{
    Target.Connect(MyClass.SignalName.MySignal, Callable.From(OnMySignal));
}