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#除外)都将其用作构造函数。
所以说,应该在哪些情况下使用这些通知或虚函数呢?
对比 _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")
)
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.");
}
};
当需要与帧速率无关的时间增量时,请使用 _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())
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()
assignment: the property's value is replaced by any assignments made in_init()
, triggering the 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 someone sets 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 someone sets 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) { return test = p_test; }
MyNode() {
// Triggers the setter, changing _test's value from "one" to "two!".
set_test("two");
}
// If someone sets 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 void _Notification(int what)
{
switch (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;
}
}
};