Godot 通知

Godot 中每個物件都實作了一個 _notification 。這個方法的目的是要讓 Object 物件能回應各種來自引擎層級,與該物件有關的回呼。舉例來說,若引擎告要叫 CanvasItem 進行「繪製 (draw)」,則引擎就會呼叫 _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

  • _input() : NOTIFICATION_INPUT

  • _unhandled_input() : NOTIFICATION_UNHANDLED_INPUT

  • _draw() : NOTIFICATION_DRAW

使用者可能 沒注意到 ,除了 Node 之外其他型別的節點也有通知:

而且,Node 節點中許多 真實存在 的回呼都沒有專屬的方法,但這些回呼還是很實用。

我們可以通過通用的 _notification 方法來存取這些自定通知。

備註

在說明文件中標記為「虛擬」的方法都是為了讓腳本覆寫而存在的。

典型的例子就是 Object 物件中的 _init 方法。雖然沒有等效的 NOTIFICATION_* 通知,但引擎還是會呼叫這個方法。大多數語言 (除了 C#) 都依賴這個方法來當作建置函式。

那麼,各種通知與虛擬函數都應該分別在什麼狀況下使用呢?

_process vs. _physics_process vs. *_input

當我們需要處理各幀之間與 FPS 有關的 delta 時,就用 _process 。若更新物件資料的程式碼需要儘可能頻繁更新,就適合在 _process 中處理。我們也通常會把重複性的邏輯檢查以及資料快取放在這裡執行,但還是得取決於是否有需要頻繁地計算。若不需要每一幀都執行的化,則可以實作一個 Timer-Yield-Timeout 循環來代替。

# Infinitely loop, but only execute whenever the Timer fires.
# Allows for recurring operations that don't trigger script logic
# every frame (or even every fixed frame).
while true:
    my_method()
    $Timer.start()
    yield($Timer, "timeout")

當需要的操作跟每幀之間的 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())
public class MyNode : Node
{

    // Called every frame, even when the engine detects no input.
    public void _Process(float 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 keyEvent:
                if (Input.IsActionJustPressed("ui_accept"))
                    GD.Print(GetProcessDeltaTime());
                break;
            default:
                break;
        }
    }

}

_init vs. 初始化 vs. 匯出

如果腳本會初始化自己的節點子樹而不依賴場景的話,那麼程式碼應該在 _init 中執行。其他屬性或是與 SceneTreee 無關的初始化行為也都應該在這裡執行。這個回呼會在 _ready_enter_tree 之前、腳本建立與初始化其屬性後觸發。

在初始化期間,腳本可以有三種類型的屬性賦值:

# "one" is an "initialized value". These DO NOT trigger the setter.
# If someone set the value as "two" from the Inspector, this would be an
# "exported value". These DO trigger the setter.
export(String) var test = "one" setget set_test

func _init():
    # "three" is an "init assignment value".
    # These DO NOT trigger the setter, but...
    test = "three"
    # These DO trigger the setter. Note the `self` prefix.
    self.test = "three"

func set_test(value):
    test = value
    print("Setting: ", test)
public class MyNode : Node
{
    private string _test = "one";

    // Changing the value from the inspector does trigger the setter in C#.
    [Export]
    public string Test
    {
        get { return _test; }
        set
        {
            _test = value;
            GD.Print("Setting: " + _test);
        }
    }

    public MyNode()
    {
        // Triggers the setter as well
        Test = "three";
    }
}

在初始化場景時,屬性值會依據下列順序來設定:

  1. 初始值賦值: 實體化會賦予初始化值或是 Init 賦值的值。Init 賦值的優先度高於初始化值。

  2. 匯出值賦值: 若是從場景中實體化而非腳本,則 Godot 會將匯出的值賦值來取代腳本中定義的初始值。

結果,實體化腳本對比場景來說,會 一起 影響初始化值 以及 引擎呼叫 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.has_user_signal("interacted_with")

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

func _on_parent_interacted_with():
    print("I'm reacting to my parent's interaction!")
public class MyNode : Node
{
    public Node ParentCache = null;

    public void ConnectionCheck()
    {
        return ParentCache.HasUserSignal("InteractedWith");
    }

    public void _Notification(int what)
    {
        switch (what)
        {
            case NOTIFICATION_PARENTED:
                ParentCache = GetParent();
                if (ConnectionCheck())
                    ParentCache.Connect("InteractedWith", this, "OnParentInteractedWith");
                break;
            case NOTIFICATION_UNPARENTED:
                if (ConnectionCheck())
                    ParentCache.Disconnect("InteractedWith", this, "OnParentInteractedWith");
                break;
        }
    }

    public void OnParentInteractedWith()
    {
        GD.Print("I'm reacting to my parent's interaction!");
    }
}