Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Сигнали C#

Для детального пояснення сигналів загалом перегляньте розділ Використання сигналів у покроковому посібнику.

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

У деяких випадках необхідно використовувати старіші API Connect() і Disconnect(). Перегляньте Використання Connect і Disconnect для отримання додаткової інформації.

Якщо під час обробки сигналу ви зіткнулися з System.ObjectDisposedException, можливо, ви пропускаєте відключення сигналу. Дивіться Автоматичне відключення, коли приймач звільняється для отримання додаткової інформації.

Сигнали як події C#

Щоб забезпечити більшу безпеку типу, усі сигнали Godot також доступні через events. Ви можете обробляти ці події, як і будь-які інші події, за допомогою операторів += і -=.

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

Крім того, ви завжди можете отримати доступ до імен сигналів, пов’язаних із типом вузла, через його вкладений клас SignalName. Це корисно, наприклад, коли ви хочете очікувати на сигнал (див. Ключове слово чекати).

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

Попередження

Якщо ви хочете підключитися до цих сигналів у редакторі, вам потрібно буде (пере)будувати проект, щоб вони з’явилися.

Щоб зробити це, ви можете натиснути кнопку Створити у верхньому правому куті редактора.

Випромінювання сигналу

Для випромінювання сигналів використовуйте метод EmitSignal. Зауважте, що, як і для сигналів, визначених механізмом, назви ваших власних сигналів перераховані у вкладеному класі SignalName.

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

На відміну від інших подій C#, ви не можете використовувати Invoke для виклику подій, пов’язаних із сигналами Godot.

Сигнали підтримують аргументи будь-якого типу Variant-compatible.

Отже, будь-які Node або RefCounted будуть сумісні автоматично, але спеціальні об’єкти даних повинні будуть успадковувати GodotObject або один із його підкласів.

using Godot;

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

Зв'язані значення

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

Тут сигнал Button.Pressed не приймає жодних аргументів. Але ми хочемо використовувати те саме ModifyValue для кнопок «плюс» і «мінус». Отже, ми зв’язуємо значення модифікатора під час підключення сигналів.

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 автоматично роз’єднує всі з’єднання, пов’язані з цим об’єктом. Це відбувається як для випромінювачів, так і для приймачів сигналів.

Наприклад, вузол із таким кодом надрукує "Hello!" при натисканні кнопки звільняється. Звільнення вузла відключає сигнал, тому повторне натискання кнопки нічого не робить:

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

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

Коли приймач сигналу звільняється, а випромінювач сигналу ще живий, у деяких випадках автоматичне відключення не відбудеться:

  • Сигнал пов’язаний з лямбда-виразом, який фіксує змінну.

  • Сигнал є індивідуальним.

У наступних розділах ці випадки пояснюються більш детально та містяться пропозиції щодо від’єднання вручну.

Примітка

Автоматичне відключення є цілком надійним, якщо випромінювач сигналу звільняється до того, як звільняється будь-який із його приймачів. Зі стилем проекту, який віддає перевагу цьому шаблону, наведені вище обмеження можуть не викликати занепокоєння.

Без автоматичного відключення: лямбда-вираз, який фіксує змінну

Якщо ви підключитесь до лямбда-виразу, який фіксує змінні, 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, працює не краще, ніж +=.

Без автоматичного відключення: спеціальний сигнал

Підключення до спеціального сигналу за допомогою += не розривається автоматично, коли приймальний вузол звільняється.

Щоб від’єднатися, використовуйте -= у відповідний момент. Наприклад:

[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));
}