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 循环是另一种选择。

GDScript

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

当一个帧之间需要独立于帧速率的 deltatime 时,请使用 _physics_process。如果不管时间是快还是慢,代码需要随着时间的推移进行一致的更新,那么这是正确的放置这些代码的地方。重复的运动学和对象变换操作,应在此处执行。

为了获得最佳性能,应尽可能避免在这些回调期间,进行输入检查。_process_physics_process 将在每个机会触发(默认情况下它们不会 休息)。相反,*_input 回调仅在,引擎实际检测到输入的帧上触发。

可以同样检查输入回调中的输入动作。如果要使用增量时间,则可以根据需要从相关的增量时间方法中获取它。

GDScript

C#

  1. # Called every frame, even when the engine detects no input.
  2. func _process(delta):
  3. if Input.is_action_just_pressed("ui_select"):
  4. print(delta)
  5. # Called during every input event.
  6. func _unhandled_input(event):
  7. match event.get_class():
  8. "InputEventKey":
  9. if Input.is_action_just_pressed("ui_accept"):
  10. print(get_process_delta_time())
  1. public class MyNode : Node
  2. {
  3. // Called every frame, even when the engine detects no input.
  4. public void _Process(float delta)
  5. {
  6. if (Input.IsActionJustPressed("ui_select"))
  7. GD.Print(delta);
  8. }
  9. // Called during every input event. Equally true for _input().
  10. public void _UnhandledInput(InputEvent event)
  11. {
  12. switch (event)
  13. {
  14. case InputEventKey keyEvent:
  15. if (Input.IsActionJustPressed("ui_accept"))
  16. GD.Print(GetProcessDeltaTime());
  17. break;
  18. default:
  19. break;
  20. }
  21. }
  22. }

_init VS initialization VS export

如果脚本初始化它自己的节点子树,没有场景,代码应该在这里执行。其他属性或独立于 SceneTreeinitialization 也应在此处运行。这会在 _ready_enter_tree 之前触发,但是会在脚本创建并初始化其属性之后触发。

脚本具有实例化期间,可能发生的三种类型的属性分配:

GDScript

C#

  1. # "one" is an "initialized value". These DO NOT trigger the setter.
  2. # If someone set the value as "two" from the Inspector, this would be an
  3. # "exported value". These DO trigger the setter.
  4. export(String) var test = "one" setget set_test
  5. func _init():
  6. # "three" is an "init assignment value".
  7. # These DO NOT trigger the setter, but...
  8. test = "three"
  9. # These DO trigger the setter. Note the `self` prefix.
  10. self.test = "three"
  11. func set_test(value):
  12. test = value
  13. print("Setting: ", test)
  1. public class MyNode : Node
  2. {
  3. private string _test = "one";
  4. // Changing the value from the inspector does trigger the setter in C#.
  5. [Export]
  6. public string Test
  7. {
  8. get { return _test; }
  9. set
  10. {
  11. _test = value;
  12. GD.Print("Setting: " + _test);
  13. }
  14. }
  15. public MyNode()
  16. {
  17. // Triggers the setter as well
  18. Test = "three";
  19. }
  20. }

当实例化一个场景时,将根据以下顺序设置属性值:

  1. 初始值赋值instantiation 将分配 instantiation 值或 init 分配值。init 分配的优先级高于 initialization 值。
  2. 导出值赋值:如果从一个场景而不是脚本中实例化,Godot将分配导出的值,来替换脚本中定义的初始值。

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

_ready VS _enter_tree VS NOTIFICATION_PARENTED

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

在实例化一个脚本或一个独立场景时,节点不会在创建时添加到场景树,所以没有 _enter_tree 的回调触发器。相反,只发生 _init 和之后的 _ready 调用。

如果需要触发作为节点设置父级到另一个节点而发生的行为,无论它是否作为在主要/活动场景中的部分发生,都可以使用 PARENTED 通知。例如,这有一个将节点的方法,连接到父节点上的自定义信号,而不会失败的代码段。在运行时可能创建的,以数据为中心的节点上有用。

GDScript

C#

  1. extends Node
  2. var parent_cache
  3. func connection_check():
  4. return parent.has_user_signal("interacted_with")
  5. func _notification(what):
  6. match what:
  7. NOTIFICATION_PARENTED:
  8. parent_cache = get_parent()
  9. if connection_check():
  10. parent_cache.connect("interacted_with", self, "_on_parent_interacted_with")
  11. NOTIFICATION_UNPARENTED:
  12. if connection_check():
  13. parent_cache.disconnect("interacted_with", self, "_on_parent_interacted_with")
  14. func _on_parent_interacted_with():
  15. print("I'm reacting to my parent's interaction!")
  1. public class MyNode : Node
  2. {
  3. public Node ParentCache = null;
  4. public void ConnectionCheck()
  5. {
  6. return ParentCache.HasUserSignal("InteractedWith");
  7. }
  8. public void _Notification(int what)
  9. {
  10. switch (what)
  11. {
  12. case NOTIFICATION_PARENTED:
  13. ParentCache = GetParent();
  14. if (ConnectionCheck())
  15. ParentCache.Connect("InteractedWith", this, "OnParentInteractedWith");
  16. break;
  17. case NOTIFICATION_UNPARENTED:
  18. if (ConnectionCheck())
  19. ParentCache.Disconnect("InteractedWith", this, "OnParentInteractedWith");
  20. break;
  21. }
  22. }
  23. public void OnParentInteractedWith()
  24. {
  25. GD.Print("I'm reacting to my parent's interaction!");
  26. }
  27. }