Сповіщення Godot

Кожен об'єкт у Godot реалізує метод _notification. Його мета - дозволити Об'єкту реагувати на різноманітні зворотні виклики на рівні движка, які можуть стосуватися його. Наприклад, якщо движок каже CanvasItem "малювати" ("draw"), він викликає _notification(NOTIFICATION_DRAW).

Деякі з цих сповіщень, наприклад, малювання (draw), корисно замінити у скриптах. Настільки, що Godot поставляє багатьох із них зі спеціальними функціями:

  • _ready() : NOTIFICATION_READY

  • _enter_tree() : NOTIFICATION_ENTER_TREE

  • _exit_tree() : NOTIFICATION_EXIT_TREE

  • _process(delta) : NOTIFICATION_PROCESS

  • _physics_process(delta) : NOTIFICATION_PHYSICS_PROCESS

  • _input() : NOTIFICATION_INPUT

  • _unhandled_input() : NOTIFICATION_UNHANDLED_INPUT

  • _draw() : NOTIFICATION_DRAW

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

  • Object::NOTIFICATION_POSTINITIALIZE: зворотний виклик, який спрацьовує під час ініціалізації об’єкта. Не доступний для скриптів.

  • Object::NOTIFICATION_PREDELETE: зворотний виклик, який спрацьовує до того, як движок видалить Об’єкт, тобто 'деструктор'.

  • MainLoop::NOTIFICATION_WM_MOUSE_ENTER: зворотний виклик, який спрацьовує, коли мишка потрапляє у вікно операційної системи, що відображає вміст гри.

І багато зворотних викликів, які існують у вузлах, не мають спеціальних методів, але все ще є досить корисними.

  • Node::NOTIFICATION_PARENTED: зворотний виклик, який запускається щоразу, коли один додає дочірній вузол до іншого вузла.

  • Node::NOTIFICATION_UNPARENTED: зворотний виклик, який запускається в будь-який час, коли один видаляє дочірній вузол з іншого вузла.

  • Popup::NOTIFICATION_POST_POPUP: зворотний виклик, який запускається після того, як вузол спливаючого вікна завершує будь-який метод popup*. Зверніть увагу на відмінність від сигнала about_to_show, який спрацьовує до появи.

Доступ до всіх цих спеціальних сповіщень можна отримати за допомогою універсального метода _notification.

Примітка

Методи в документації, позначені як "virtual" (віртуальні), також призначені для перевизначення у скриптах.

Класичним прикладом є метод _init в Об'єкті. Хоча він не має еквівалента NOTIFICATION_* движок все одно викликає метод. Більшість мов (крім C#) покладаються на нього як на конструктор.

Отже, в якій ситуації слід використовувати кожне з цих сповіщень або віртуальні функції?

_process та _physics_process vs. *_input

Використовуйте _process коли потрібна різниця між кадрами, залежна від частоти кадрів. Якщо коду, який оновлює дані об’єктів, потрібно оновлюватися якомога частіше, це правильне місце. Тут часто виконуються повторювані логічні перевірки та кешування даних, але це зводиться до частоти оновлення оцінок. Якщо йому не потрібно виконуватися кожен кадр, то реалізація циклу Timer-yield-timeout - кращий варіант.

# Infinitely loop, but only execute whenever the Timer fires.
# Allows for recurring operations that don't trigger script logic
# every frame (or even every fixed frame).
while true:
    my_method()
    $Timer.start()
    yield($Timer, "timeout")

Використовуйте _physics_process коли потрібен часовий проміжок, незалежний від частоти кадрів, між кадрами. Якщо код потребує постійного оновлення з часом, незалежно від того, наскільки швидко чи повільно просувається час, це правильне місце. Тут слід виконувати повторювані операції кінематичного та об'єктного перетворення.

Хоча можливо, щоб досягти найкращої продуктивності, слід уникати перевірки вводу під час цих зворотних викликів. _process і _physics_process будуть спрацьовувати при будь-якій нагоді (вони не "відпочивають" за замовчуванням). На відміну від них зворотні виклики *_input будуть спрацьовувати лише на кадрах, в яких двигун фактично виявив ввід.

Можна перевірити наявність введення у зворотних викликах ввода. Якщо ви хочете використовувати дельта-час (проміжок часу), можете за необхідності отримати його із відповідних методів.

# Called every frame, even when the engine detects no input.
func _process(delta):
    if Input.is_action_just_pressed("ui_select"):
        print(delta)

# Called during every input event.
func _unhandled_input(event):
    match event.get_class():
        "InputEventKey":
            if Input.is_action_just_pressed("ui_accept"):
                print(get_process_delta_time())
public class MyNode : Node
{

    // Called every frame, even when the engine detects no input.
    public void _Process(float delta)
    {
        if (Input.IsActionJustPressed("ui_select"))
            GD.Print(delta);
    }

    // Called during every input event. Equally true for _input().
    public void _UnhandledInput(InputEvent event)
    {
        switch (event)
        {
            case InputEventKey keyEvent:
                if (Input.IsActionJustPressed("ui_accept"))
                    GD.Print(GetProcessDeltaTime());
                break;
            default:
                break;
        }
    }

}

_init, initialization та export

Якщо скрипт ініціалізує власне піддерево вузла без сцени, цей код повинен виконуватися тут. Інші властивості або незалежні від Дерева Сцен ініціалізації також повинні працювати тут. Це спрацьовує до _ready або _enter_tree, проте після того, як скрипт створює та ініціалізує свої властивості.

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

# "one" is an "initialized value". These DO NOT trigger the setter.
# If someone set the value as "two" from the Inspector, this would be an
# "exported value". These DO trigger the setter.
export(String) var test = "one" setget set_test

func _init():
    # "three" is an "init assignment value".
    # These DO NOT trigger the setter, but...
    test = "three"
    # These DO trigger the setter. Note the `self` prefix.
    self.test = "three"

func set_test(value):
    test = value
    print("Setting: ", test)
public class MyNode : Node
{
    private string _test = "one";

    // Changing the value from the inspector does trigger the setter in C#.
    [Export]
    public string Test
    {
        get { return _test; }
        set
        {
            _test = value;
            GD.Print("Setting: " + _test);
        }
    }

    public MyNode()
    {
        // Triggers the setter as well
        Test = "three";
    }
}

При створенні екземпляра сцени значення властивостей встановлюються відповідно до наступної послідовності:

  1. Присвоєння початкового значення: примірник присвоює або значення ініціалізації, або значення присвоєння init. Призначення Init мають пріоритет над значеннями ініціалізації.

  2. Присвоєння експортованого значення: якщо створити екземпляр зі сцени, а не зі скрипта, Godot призначить експортоване значення замінюючи початкове значення, визначене у скрипті.

Як результат, створення екземпляра скрипта замість сцени вплине як на ініціалізацію, так і на кількість разів, коли механізм викликає сеттер.

_ready, _enter_tree та NOTIFICATION_PARENTED

При створенні екземпляра сцени, підключеної до першої виконаної сцени, Godot створює екземпляри вузлів по дереву (здійснюючи виклики _init`) і будує дерево, що йде вниз від кореня. Це спричиняє виклики ``_enter_tree каскадом вниз дерева. Після того, як дерево завершено, викликаються вузли листів _ready. Вузол викликає цей метод, як тільки всі дочірні вузли закінчать виклик свого. Потім це призводить до зворотного каскаду, що повертається до кореня дерева.

Під час створення скрипта або автономної сцени вузли не додаються до Дерева Сцен при створенні, тому жоден зворотний виклик _enter_tree не запускається. Натомість трапляються лише виклики _init та пізніше виклики _ready.

Якщо потрібно ініціювати поведінку, яка виникає як поведінка батьківського вузла до іншого, незалежно від того, відбувається вона як частина основної/активної сцени чи ні, можна використовувати сповіщення PARENTED. Наприклад, ось фрагмент, який підключає метод вузла до користувацького сигналу на батьківському вузлі без збоїв. Корисно для вузлів, орієнтованих на дані, які можна створити під час виконання.

extends Node

var parent_cache

func connection_check():
    return parent.has_user_signal("interacted_with")

func _notification(what):
    match what:
        NOTIFICATION_PARENTED:
            parent_cache = get_parent()
            if connection_check():
                parent_cache.connect("interacted_with", self, "_on_parent_interacted_with")
        NOTIFICATION_UNPARENTED:
            if connection_check():
                parent_cache.disconnect("interacted_with", self, "_on_parent_interacted_with")

func _on_parent_interacted_with():
    print("I'm reacting to my parent's interaction!")
public class MyNode : Node
{
    public Node ParentCache = null;

    public void ConnectionCheck()
    {
        return ParentCache.HasUserSignal("InteractedWith");
    }

    public void _Notification(int what)
    {
        switch (what)
        {
            case NOTIFICATION_PARENTED:
                ParentCache = GetParent();
                if (ConnectionCheck())
                    ParentCache.Connect("InteractedWith", this, "OnParentInteractedWith");
                break;
            case NOTIFICATION_UNPARENTED:
                if (ConnectionCheck())
                    ParentCache.Disconnect("InteractedWith", this, "OnParentInteractedWith");
                break;
        }
    }

    public void OnParentInteractedWith()
    {
        GD.Print("I'm reacting to my parent's interaction!");
    }
}