Godot通知

Godot中的每个对象都实现 _notification 方法. 其目的是允许对象响应可能与之相关的各种引擎级回调. 例如, 如果引擎告诉一个 CanvasItem绘制, 它将调用 _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

用户可能 不会 意识到, 通知仅针对 Node 以外的类型存在:

而且, 在节点中 确实 存在许多回调, 都没有任何专用方法, 但是它们仍然非常有用.

您可以从通用的 _notification 方法, 访问所有这些自定义通知.

注解

文档中标记为 虚拟(virtual) 的方法, 也打算被脚本重写.

一个经典的例子是 Object 中的 _init 方法. 虽然它没有等效的 NOTIFICATION_*, 但是引擎仍然调用该方法. 大多数语言(C#除外)都将其用作构造函数.

那么, 在哪种情况下应该使用这些通知或虚函数呢?

_process vs. _physics_process vs. *_input

当需要帧之间依赖于帧速率的 deltatime 时, 请使用 _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")

当一个帧之间需要独立于帧速率的 deltatime 时, 请使用 _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 vs. initialization vs. export

如果脚本初始化它自己的节点子树, 没有场景, 代码应该在这里执行. 其他属性或独立于 SceneTreeinitialization 也应在此处运行. 这会在 _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. Initial value assignment: instantiation 将分配 instantiation 值或 init 分配值. init 分配的优先级高于 initialization 值.

  2. 导出值赋值 : 如果从一个场景而不是脚本中实例化,Godot将分配导出的值, 来替换脚本中定义的初始值.

因此, 实例化脚本和场景, 将影响初始化,*和* 引擎调用 setter 的次数.

_ready vs. _enter_tree vs. NOTIFICATION_PARENTED

当实例化连接到第一个执行场景的场景时,Godot将实例化树下的节点(进行 _init 调用), 并构建从根向下的树. 这导致 _enter_tree 调用, 向下级联树. 当树构建完成, 叶子节点调用 _ready. 一旦所有子节点都完成了对它们的子节点的调用, 一个节点就会调用这个方法. 然后, 这将导致反向级联回到树的根部.

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.

如果需要触发作为节点设置父级到另一个节点而发生的行为, 无论它是否作为在主要/活动场景中的部分发生, 都可以使用 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!");
    }
}