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.
Checking the stable version of the documentation...
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 之外的类型也有通知,例如:
Object::NOTIFICATION_POSTINITIALIZE:在对象初始化期间触发的回调。脚本无法访问。
Object::NOTIFICATION_PREDELETE:在引擎删除 Object 之前触发的回调,即析构函数。
并且 Node 中 确实 存在的许多回调没有任何专用的方法,但仍然非常有用。
Node::NOTIFICATION_PARENTED: 一个回调函数,每当你往某个节点里添加子节点时,它就会自动触发。
Node::NOTIFICATION_UNPARENTED: 一个回调函数,每当你从某个节点中移除子节点时,它就会自动触发。
通用的 _notification() 方法提供了对所有这些自定义通知的访问。
备注
文档中被标记为“virtual”的方法(即虚方法)可以被脚本覆盖重写。
一个经典的例子是 Object 中的 _init 方法。虽然它没有等效的 NOTIFICATION_* 通知,但是引擎仍然会调用该方法。大多数语言(C#除外)都将其用作构造函数。
那么,你究竟应该在什么时候使用这些通知(notifications)或虚函数(virtual functions)呢?
对比 _process、_physics_process、*_input
当你需要用到与帧率挂钩的帧间隔时间(delta time)时,就应该使用 _process() 。如果某些更新对象数据的代码需要尽可能频繁地刷新,那这里就是最合适的位置。像是一些周期性的逻辑判断和数据缓存,通常都会放在这里执行,但这最终还是取决于这些运算需要多久更新一次。如果它们不需要每一帧都跑一遍,那么改用计时器(Timer)的超时循环(timeout loop)会是另一个不错的选择。
# 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")
)
using Godot;
public partial class MyNode : Node
{
// Allows for recurring operations that don't trigger script logic
// every frame (or even every fixed frame).
public override void _Ready()
{
var timer = new Timer();
timer.Autostart = true;
timer.WaitTime = 0.5;
AddChild(timer);
timer.Timeout += () => GD.Print("This block runs every 0.5 seconds");
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
public:
// Allows for recurring operations that don't trigger script logic
// every frame (or even every fixed frame).
virtual void _ready() override {
Timer *timer = memnew(Timer);
timer->set_autostart(true);
timer->set_wait_time(0.5);
add_child(timer);
timer->connect("timeout", callable_mp(this, &MyNode::run));
}
void run() {
UtilityFunctions::print("This block runs every 0.5 seconds.");
}
};
当你需要用到与帧率无关的帧间隔时间(delta time)时,就应该使用 _physics_process() 。如果某些代码需要随着时间获得稳定的更新,而不管时间流逝的快慢如何,这里就是最合适的位置。周期性的运动学运算和物体变换操作,都应该放在这里执行。
虽然理论上你确实可以在这些回调函数里做输入检测,但为了获得最佳的性能,你应该尽量避免这样做。_process() 和 _physics_process() 只要一有机会就会触发(它们默认是不会 "休息" 的,每时每刻都在跑)。相比之下, *_input() 类的回调函数只会在引擎真正检测到有输入操作的那些帧里才会触发。
你同样可以在这些输入回调函数里检测输入动作。如果你需要用到帧间隔时间(delta time),随时可以通过相关的方法来获取。
# 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())
using Godot;
public partial class MyNode : Node
{
// Called every frame, even when the engine detects no input.
public void _Process(double 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:
if (Input.IsActionJustPressed("ui_accept"))
{
GD.Print(GetProcessDeltaTime());
}
break;
}
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
public:
// Called every frame, even when the engine detects no input.
virtual void _process(double p_delta) override {
if (Input::get_singleton->is_action_just_pressed("ui_select")) {
UtilityFunctions::print(p_delta);
}
}
// Called during every input event. Equally true for _input().
virtual void _unhandled_input(const Ref<InputEvent> &p_event) override {
Ref<InputEventKey> key_event = event;
if (key_event.is_valid() && Input::get_singleton->is_action_just_pressed("ui_accept")) {
UtilityFunctions::print(get_process_delta_time());
}
}
};
对比 _init、初始化、导出
如果脚本初始化它自己的没有场景的节点子树,则该代码将会在 _init() 中执行。其他属性或独立于 SceneTree 的初始化也应在此处运行。
备注
C# 中与 GDScript 的 _init() 方法等效的是构造函数。
_init() 在 _enter_tree() 或 _ready() 之前触发,但在脚本创建并初始化其属性之后。实例化场景时,属性值将按照以下顺序设置:
初始值赋值:为属性赋初始值,未指定初始值时赋默认值。Setter 函数即便存在也不会使用。
_init()赋值:在_init()中通过各种赋值改变属性的取值,会触发 setter 函数。导出值赋值:如果在“检查器”中修改了导出属性的值,就会再次修改该属性的值,会触发 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 you set test to "three" from the Inspector, it would trigger
# the setter, changing test's value from "two!" to "three!".
using Godot;
public partial class MyNode : Node
{
private string _test = "one";
[Export]
public string Test
{
get { return _test; }
set { _test = $"{value}!"; }
}
public MyNode()
{
// Triggers the setter, changing _test's value from "one" to "two!".
Test = "two";
}
// If you set Test to "three" in the Inspector, it would trigger
// the setter, changing _test's value from "two!" to "three!".
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
String test = "one";
protected:
static void _bind_methods() {
ClassDB::bind_method(D_METHOD("get_test"), &MyNode::get_test);
ClassDB::bind_method(D_METHOD("set_test", "test"), &MyNode::set_test);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "test"), "set_test", "get_test");
}
public:
String get_test() { return test; }
void set_test(String p_test) { test = p_test + "!"; }
MyNode() {
// Triggers the setter, changing _test's value from "one" to "two!".
set_test("two");
}
// If you set test to "three" in 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!")
using Godot;
public partial class MyNode : Node
{
private Node _parentCache;
public bool ConnectionCheck()
{
return _parentCache.HasUserSignal("InteractedWith");
}
public override void _Notification(int what)
{
switch ((long)what)
{
case NotificationParented:
_parentCache = GetParent();
if (ConnectionCheck())
{
_parentCache.Connect("InteractedWith", Callable.From(OnParentInteractedWith));
}
break;
case NotificationUnparented:
if (ConnectionCheck())
{
_parentCache.Disconnect("InteractedWith", Callable.From(OnParentInteractedWith));
}
break;
}
}
private void OnParentInteractedWith()
{
GD.Print("I'm reacting to my parent's interaction!");
}
}
using namespace godot;
class MyNode : public Node {
GDCLASS(MyNode, Node)
Node *parent_cache = nullptr;
void on_parent_interacted_with() {
UtilityFunctions::print("I'm reacting to my parent's interaction!");
}
public:
void connection_check() {
return parent_cache->has_user_signal("interacted_with");
}
void _notification(int p_what) {
switch (p_what) {
case NOTIFICATION_PARENTED:
parent_cache = get_parent();
if (connection_check()) {
parent_cache->connect("interacted_with", callable_mp(this, &MyNode::on_parent_interacted_with));
}
break;
case NOTIFICATION_UNPARENTED:
if (connection_check()) {
parent_cache->disconnect("interacted_with", callable_mp(this, &MyNode::on_parent_interacted_with));
}
break;
}
}
};