Godot 通知
Godot 中的每個 _notification 方法都是由 Object 實作的。其目的是讓 Object 能夠回應各種可能與其相關的引擎層級回呼。例如,如果引擎告知一個 CanvasItem <class_CanvasItem>`「繪製」,它將會呼叫 ``_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 方法來存取這些自定通知。
備註
在說明文件中標記為「虛擬」的方法都是為了讓腳本覆寫而存在的。
一個經典的範例是 Object 中的 _init 方法。雖然它沒有對應的 NOTIFICATION_* ,引擎仍然會呼叫這個方法。大多數程式語言(除了 C#)都依賴它作為建構子。
那麼,各種通知與虛擬函式都應該分別在什麼狀況下使用呢?
_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. 匯出
如果腳本初始化自己的節點子樹(沒有場景),該程式碼應該在 _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 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 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;
}
}
};