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 通知
Every Object in Godot implements a
_notification method. Its purpose is to
allow the Object to respond to a variety of engine-level callbacks that may
relate to it. For example, if the engine tells a
CanvasItem to "draw", it will call
_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_draw()
: NOTIFICATION_DRAW
使用者可能 沒注意到 ,除了 Node 之外其他型別的節點也有通知:
Object::NOTIFICATION_POSTINITIALIZE :會在物件初始化時觸發的回呼。腳本無法存取。
Object::NOTIFICATION_PREDELETE :會在引擎刪除 Object 時觸發的回呼,即「解構函式」。
而且,Node 節點中許多 真實存在 的回呼都沒有專屬的方法,但這些回呼還是很實用。
Node::NOTIFICATION_PARENTED :會在每次將子節點加入另一個節點時觸發的回呼。
Node::NOTIFICATION_UNPARENTED :會在每次子節點從另一個節點中移除時觸發的回呼。
我們可以通過通用的 _notification
方法來存取這些自定通知。
備註
在說明文件中標記為「虛擬」的方法都是為了讓腳本覆寫而存在的。
A classic example is the
_init method in Object. While it has no
NOTIFICATION_*
equivalent, the engine still calls the method. Most languages
(except C#) rely on it as a constructor.
那麼,各種通知與虛擬函式都應該分別在什麼狀況下使用呢?
_process vs. _physics_process vs. *_input
當我們需要處理各影格之間與 FPS 有關的 delta 時,就用 _process
。若更新物件資料的程式碼需要儘可能頻繁更新,就適合在 _process
中處理。我們也通常會把重複性的邏輯檢查以及資料快取放在這裡執行,但還是得取決於是否有需要頻繁地計算。若不需要每一影格都執行的化,則可以實作一個 Timer-Yield-Timeout 循環來代替。
# 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 時間無關時,就可以用 _physics_process
。如果程式碼需要不管時間快還是慢,都隨著時間持續更新的話,就適合用 _physics_process
。重複的動力學與物件變換操作應該在這個函式內執行。
雖然可以在這些回呼中檢查輸入,但為了獲得最佳效能,應該避免這麼做。 _process
與 _physics_process
一有機會機會觸發 (預設情況下這些回呼都不「休息」)。相反地, *_input
回呼則只會在引擎實際偵測到輸入的影格上才會呼叫。
我們也可以在輸入回呼裡做一樣的輸入操作檢查。如果需要使用 delta 時間的話,則可以從相關的 delta 時間方法中取得。
# 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 vs. 初始化 vs. 匯出
If the script initializes its own node subtree, without a scene,
that code should execute in _init()
. Other property or SceneTree-independent
initializations should also run here.
備註
The C# equivalent to GDScript's _init()
method is the constructor.
_init()
triggers before _enter_tree()
or _ready()
, but after a script
creates and initializes its properties. When instantiating a scene, property
values will set up according to the following sequence:
Initial value assignment: the property is assigned its initialization value, or its default value if one is not specified. If a setter exists, it is not used.
_init()
assignment: the property's value is replaced by any assignments made in_init()
, triggering the setter.Exported value assignment: an exported property's value is again replaced by any value set in the Inspector, triggering the 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!".
};
As a result, instantiating a script versus a scene may affect both the initialization and the number of times the engine calls the setter.
_ready vs. _enter_tree vs. NOTIFICATION_PARENTED
當實體化一個連接到首次執行場景的場景時,Godot 會在場景樹中向下實體化 (呼叫 _init
) 並一直從根節點往深層建置。因為這樣,所以 _enter_tree
就是在場景樹中由頂層往深層級呼叫的。建置完整棵樹後,葉上的節點就會呼叫 _ready
。節點會在所有自節點都呼叫完 _ready
後才呼叫自己的 _ready
,因此在呼叫的時候就是反過來從樹最深層往回呼叫到根節點。
當實體化腳本或獨立的場景時,節點並不會在建立時被加到 SceneTree 上,所以不會觸發 _enter_tree
回呼,而只會有 _init
以及之後的 _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;
}
}
};