Godot 通知

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

  • _draw(): NOTIFICATION_DRAW

用户可能不会意识到 Node 之外的类型也有通知,例如:

并且,在节点中也有很多很有用的回调,但是这些回调不存在专门的方法。

你可以在通用的 _notification() 方法中访问所有这些自定义通知。

备注

文档中被标记为“virtual”的方法(即虚方法)可以被脚本覆盖重写。

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

所以说,应该在哪些情况下使用这些通知或虚函数呢?

_process vs. _physics_process vs. *_input

当需要使用“依赖于帧速率的 delta 时间增量”时,请使用 _process 。如果需要尽可能频繁地更新对象数据,也应该在这里处理。频繁执行的逻辑检查和数据缓存操作,大多数都在这里执行。但也需要注意执行频率,如果不需要每帧都执行,则可以选择用定时器循环来替代。

GDScriptC#

  1. # Allows for recurring operations that don't trigger script logic
  2. # every frame (or even every fixed frame).
  3. func _ready():
  4. var timer = Timer.new()
  5. timer.autostart = true
  6. timer.wait_time = 0.5
  7. add_child(timer)
  8. timer.timeout.connect(func():
  9. print("This block runs every 0.5 seconds")
  10. )
  1. using Godot;
  2. public partial class MyNode : Node
  3. {
  4. // Allows for recurring operations that don't trigger script logic
  5. // every frame (or even every fixed frame).
  6. public override void _Ready()
  7. {
  8. var timer = new Timer();
  9. timer.Autostart = true;
  10. timer.WaitTime = 0.5;
  11. AddChild(timer);
  12. timer.Timeout += () => GD.Print("This block runs every 0.5 seconds");
  13. }
  14. }

当需要与帧速率无关的时间增量时,请使用 _physics_process 。如果代码需要随着时间的推移进行一致的更新,不管时间推进速度是快还是慢,那么就应该在这里执行代码。频繁执行的运动学和对象变换操作,应在此处执行。

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

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

GDScriptC#

  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. using Godot;
  2. public partial class MyNode : Node
  3. {
  4. // Called every frame, even when the engine detects no input.
  5. public void _Process(double delta)
  6. {
  7. if (Input.IsActionJustPressed("ui_select"))
  8. GD.Print(delta);
  9. }
  10. // Called during every input event. Equally true for _input().
  11. public void _UnhandledInput(InputEvent @event)
  12. {
  13. switch (@event)
  14. {
  15. case InputEventKey:
  16. if (Input.IsActionJustPressed("ui_accept"))
  17. GD.Print(GetProcessDeltaTime());
  18. break;
  19. }
  20. }
  21. }

_init vs. 初始化 vs. 导出

如果脚本初始化它自己的没有场景的节点子树,则该代码将会在 _init() 中执行。其他属性或独立于 SceneTree 的初始化也应在此处运行。

备注

与 GDScript 的 _init() 方法等效的 C# 方法是构造函数。

_init()_enter_tree()_ready() 之前触发,但在脚本创建并初始化其属性之后。实例化场景时,属性值将按照以下顺序设置:

  1. 初始值赋值:一个属性被赋值为它的显式默认值,若无指定,则赋予缺省值。如果存在setter函数,它并不会被使用。

  2. ``_init()`` 赋值: 某个属性的值被任何在``_init()``中的赋值操作导致变更时,触发其setter函数。

  3. 导出值赋值: 一个导出属性的值如再次因Inspector中的任何设值操作导致变更,触发其setter函数。

GDScriptC#

  1. # test is initialized to "one", without triggering the setter.
  2. @export var test: String = "one":
  3. set(value):
  4. test = value + "!"
  5. func _init():
  6. # Triggers the setter, changing test's value from "one" to "two!".
  7. test = "two"
  8. # If someone sets test to "three" from the Inspector, it would trigger
  9. # the setter, changing test's value from "two!" to "three!".
  1. using Godot;
  2. public partial class MyNode : Node
  3. {
  4. private string _test = "one";
  5. [Export]
  6. public string Test
  7. {
  8. get { return _test; }
  9. set { _test = $"{value}!"; }
  10. }
  11. public MyNode()
  12. {
  13. // Triggers the setter, changing _test's value from "one" to "two!".
  14. Test = "two";
  15. }
  16. // If someone sets Test to "three" in the Inspector, it would trigger
  17. // the setter, changing _test's value from "two!" to "three!".
  18. }

因此,实例化一个脚本而非场景,将同时影响初始化 引擎调用setter函数的次数。

_ready、_enter_tree、NOTIFICATION_PARENTED对比

首次实例化一个场景并添加到场景树时,Godot将实例化节点(调用 _init)为树,同时自上而下地调用 _enter_tree。当树构建完成后,再自下而上地调用 _ready,最终反向回到树的顶点。

当实例化脚本或独立的场景时,节点不会在创建时被添加到 SceneTree 中,所以未触发 _enter_tree 回调。而只有 _init 调用发生。当场景被添加到 SceneTree 时,才会调用 _enter_tree_ready

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

GDScriptC#

  1. extends Node
  2. var parent_cache
  3. func connection_check():
  4. return parent_cache.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.interacted_with.connect(_on_parent_interacted_with)
  11. NOTIFICATION_UNPARENTED:
  12. if connection_check():
  13. parent_cache.interacted_with.disconnect(_on_parent_interacted_with)
  14. func _on_parent_interacted_with():
  15. print("I'm reacting to my parent's interaction!")
  1. using Godot;
  2. public partial class MyNode : Node
  3. {
  4. private Node _parentCache;
  5. public void ConnectionCheck()
  6. {
  7. return _parentCache.HasUserSignal("InteractedWith");
  8. }
  9. public void _Notification(int what)
  10. {
  11. switch (what)
  12. {
  13. case NotificationParented:
  14. _parentCache = GetParent();
  15. if (ConnectionCheck())
  16. {
  17. _parentCache.Connect("InteractedWith", Callable.From(OnParentInteractedWith));
  18. }
  19. break;
  20. case NotificationUnparented:
  21. if (ConnectionCheck())
  22. {
  23. _parentCache.Disconnect("InteractedWith", Callable.From(OnParentInteractedWith));
  24. }
  25. break;
  26. }
  27. }
  28. private void OnParentInteractedWith()
  29. {
  30. GD.Print("I'm reacting to my parent's interaction!");
  31. }
  32. }