Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

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 之外的类型也有通知,例如:

并且 Node 中存在的许多回调没有任何专用的方法,但仍然非常有用。

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

备注

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

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

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

对比 _process、_physics_process、*_input

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

# Allows for recurring operations that don't trigger script logic
# every frame (or even every fixed frame).
func _ready():
    var timer = Timer.new()
    timer.autostart = true
    timer.wait_time = 0.5
    add_child(timer)
    timer.timeout.connect(func():
        print("This block runs every 0.5 seconds")
    )

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

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

在 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())

对比 _init、初始化、导出

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

备注

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

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

  1. 初始值赋值:为属性赋初始值,未指定初始值时赋默认值。Setter 函数即便存在也不会使用。

  2. ``_init()`` 赋值:_init() 中通过各种赋值改变属性的取值,会触发 setter 函数。

  3. 导出值赋值:如果在“检查器”中修改了导出属性的值,就会再次修改该属性的值,会触发 setter 函数。

# test is initialized to "one", without triggering the setter.
@export var test: String = "one":
    set(value):
        test = value + "!"

func _init():
    # Triggers the setter, changing test's value from "one" to "two!".
    test = "two"

# If someone sets test to "three" from the Inspector, it would trigger
# the setter, changing test's value from "two!" to "three!".

因此,选择实例化脚本还是实例化场景,对初始化和引擎调用 setter 的次数会产生影响。

对比 _ready、_enter_tree、NOTIFICATION_PARENTED

将场景实例化并首次添加到运行的场景树时,Godot 会沿着场景树从上至下实例化节点(调用 _init() 函数),再从根节点出发从上至下构建场景树。因此 _enter_tree() 是按照树的顺序从上至下一级一级调用的。场景树构建完成后,所有叶节点就会调用 _ready。一个节点的所有子节点都调用完该方法后,就会轮到该节点自己调用。此时就是逆着树的顺序从下至上一级一级调用的,最终到达根节点。

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

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

extends Node

var parent_cache

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

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

func _on_parent_interacted_with():
    print("I'm reacting to my parent's interaction!")