Notificações em Godot

Cada objeto no Godot implementa um método _notification. Sua finalidade é permitir que o Objeto responda a uma variedade de chamadas de retorno em nível de motor que possam estar relacionadas a ele. Por exemplo, se o motor diz a um CanvasItem para "desenhar", ele irá chamar _notification(NOTIFICATION_DRAW).

Algumas destas notificações, como desenhar, são úteis para sobrepor em scripts. Tanto que o Godot expões muitas delas com funções dedicadas:

  • _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

O que os usuários podem não perceber é que existem notificações para outros tipos além do Node sozinho:

E muitas das chamadas de retorno que existem em Nós não têm métodos dedicados mas ainda são bastante úteis.

  • Node::NOTIFICATION_PARENTED: uma chamada de retorno que aciona sempre que se adiciona um nó filho a outro nó.

  • Node::NOTIFICATION_UNPARENTED: uma chamada de retorno que aciona sempre que se remove um nó filho de outro nó.

  • Popup::NOTIFICATION_POST_POPUP: uma chamada de retorno que aciona após um nó Popup completar qualquer método Popup*. Observe a diferença do seu sinal about_to_show que aciona antes de sua aparência.

Pode-se acessar todas essas notificações personalizadas a partir do método universal _notification.

Nota

Os métodos na documentação rotulados como "virtual" também são destinados a serem substituídos por scripts.

Um exemplo clássico é o método _init em Object. Embora ele não tenha uma NOTIFICAÇÃO_* equivalente, o motor ainda chama o método. A maioria das linguagens (exceto C#) dependem dele como um construtor.

Então, em que situação se deve usar cada uma destas notificações ou funções virtuais?

_process vs. _physics_process vs. *_input

Use _process` quando precisar de um deltatime dependente de framerate entre frames. Se o código que atualiza os dados do objeto precisa ser atualizado o mais frequentemente possível, este é o lugar certo. As verificações lógicas recorrentes e o armazenamento em cache de dados costumam ser executados aqui, mas se resume à frequência na qual se precisa das avaliações para atualizar. Se eles não precisarem executar cada frame, então implementar um loop Timer-yield-timeout é outra opção.

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

Use _physics_process quando precisar de um deltatime entre frames independente de framerate. Se o código precisa de atualizações consistentes ao longo do tempo, independentemente de quão rápido ou lento o tempo avança, este é o lugar certo. As operações recorrentes de cinemática e transformação de objetos devem ser executadas aqui.

Embora seja possível, para obter o melhor desempenho, deve-se evitar fazer verificações de entrada durante estas chamadas de retorno. _process e _physics_process acionarão em todas as oportunidades (eles não "descançam" por padrão). Em contraste, as chamadas de retorno *_input acionarão apenas nos quadros nos quais o motor realmente detectou a entrada.

É possível verificar se há ações de entrada dentro das chamadas de retorno de entrada da mesma forma. Se alguém quiser usar o tempo delta, pode-se buscá-lo a partir dos métodos deltatime relacionados conforme necessário.

# 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 vs. initialization vs. export

Se o script inicializa sua própria sub-árvore de nó, sem uma cena, esse código deve ser executado aqui. Outras propriedades ou inicializações independentes da SceneTree-independent também devem ser executadas aqui. Isto aciona antes de _ready ou _enter_tree, mas depois que um script cria e inicializa suas propriedades.

Scripts têm três tipos de atribuições de propriedade que podem ocorrer durante a instanciação:

# "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";
    }
}

Ao instanciar uma cena, os valores das propriedades serão configurados de acordo com a seguinte sequência:

  1. Atribuição de valor inicial: a instanciação atribuirá o valor de inicialização ou o valor de atribuição inicial. As atribuições de inicialização têm prioridade sobre os valores de inicialização.

  2. Atribuição de valor exportado: Se criar uma instância de uma cena em vez de um script, o Godot atribuirá o valor exportado para substituir o valor inicial definido no script.

Como resultado, instanciar um script versus uma cena afetará tanto a inicialização como o número de vezes que o mecanismo chama o setter.

_ready vs. _enter_tree vs. NOTIFICATION_PARENTED

Ao instanciar uma cena conectada à primeira cena executada, o Godot irá instanciar nós abaixo da árvore (fazendo chamadas _init) e construir a árvore descendo a partir da raiz. Isto faz com que as chamadas _enter_tree desçam em cascata pela árvore. Quando a árvore estiver completa, os nós da folha chamam _ready. Um nó chamará este método assim que todos os nós filhos tenham terminado de chamar o seu. Isto então causa uma cascata inversa que volta à raiz da árvore.

When instantiating a script or a standalone scene, nodes are not added to the SceneTree upon creation, so no _enter_tree callbacks trigger. Instead, only the _init call occurs. When the scene is added to the SceneTree, the _enter_tree and _ready calls occur.

Se for necessário disparar o comportamento que ocorre como nós pais de outro, independentemente de ocorrer como parte da cena principal/ativa ou não, pode-se usar a notificação PARENTED. Por exemplo, aqui está um trecho que conecta o método de um nó a um sinal personalizado no nó pai sem falhar. Útil em nós centrados em dados que podem ser criados durante a execução.

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