撰寫程式 (續)

處理

在 Godot 中有許多行為是通過回呼函式 (Callback) 或虛擬函式 (Virtual Function) 觸發的,所以不需要撰寫要持續執行的程式碼。

但是,會需要在每一幀上都執行的腳本還是很常見。有兩種處理方法:閒置處理 (Idle Processing) 與物理處理 (Physics Processing)。

閒置處理會在腳本中有 Node._process() 方法來開啟或關閉閒置處理。

該方法會在繪製每一幀時呼叫:

func _process(delta):
    # Do something...
    pass
public override void _Process(float delta)
{
    // Do something...
}

有一點很重要的是, _process() 的呼叫頻率會依據應用程式在執行時的 FPS (Frames Per Second,每秒幀數) 而定。這個頻率在不同設備下可能會不同。

為了解決各裝置上 FPS 不同的問題,可以使用 delta (時間差) 參數。該參數會包含上次呼叫 _process() 到目前為止的時間,單位為浮點數的秒。

delte 參數可以用來確保一些東西花費的時間不會因為 FPS 而有所不同。

舉例來說,移動的距離通常會使用 Delta 來計算,這樣可以確保移動速度相同,而且也不會因為幀率而變化。

使用 _physics_process() 來進行物理處理,但物理處理是在每個物理步驟前使用的,如控制角色。物理處理永遠都在物理步驟之前執行,而且會以相同的間隔呼叫,預設為每秒 60 次。可以在專案設定中的 [Physics] -> [Common] -> [Physics FPS] 來更改這個間隔。

然而,_process() 函式並不與物理處理同步。_process() 的幀率並不固定,且會根據硬體與遊戲的最佳化而有所不同。在單一執行緒的遊戲上,_process() 會在物理步驟後才被執行。

有一個簡單的方法可以讓我們瞭解 _process()。先建立一個有 Label 節點的場景,然後使用下列腳本:

extends Label

var accum = 0

func _process(delta):
    accum += delta
    text = str(accum) # 'text' is a built-in label property.
public class CustomLabel : Label
{
    private float _accum;

    public override void _Process(float delta)
    {
        _accum += delta;
        Text = _accum.ToString(); // 'Text' is a built-in label property.
    }
}

這樣就會顯示一個每一幀都會增加的計數器。

群組

Godot 中的群組就類似其他軟體中的標籤 (Tag)。一個節點可以被新增到多個群組裡面。群組功能對於管理大型場景很實用。把節點新增進群組有兩種方法。第一種方法是通過圖形界面,使用節點面板中的 [群組] 按鈕:

../../_images/groups_in_nodes.png

第二種方法則是使用程式碼。下面這個腳本會在目前節點一進入場景樹後立刻把它新增到 enemies (敵人) 群組中。

func _ready():
    add_to_group("enemies")
public override void _Ready()
{
    base._Ready();

    AddToGroup("enemies");
}

這樣一來,一旦玩家潛入祕密基地被發現,就能通過 SceneTree.call_group() 來讓所有敵人聽到警報聲:

func _on_discovered(): # This is a purely illustrative function.
    get_tree().call_group("enemies", "player_was_discovered")
public void _OnDiscovered() // This is a purely illustrative function.
{
    GetTree().CallGroup("enemies", "player_was_discovered");
}

上面的程式碼會呼叫群組 enemies 裡所有成員的 player_was_discovered 方法。

另外,也可以通過呼叫 SceneTree.get_nodes_in_group() 來取得所有 enemies 群組下的節點:

var enemies = get_tree().get_nodes_in_group("enemies")
var enemies = GetTree().GetNodesInGroup("enemies");

SceneTree 類別還提供了很多實用的方法,例如與場景、節點架構、群組等互動。也可以使用 SceneTree 來輕鬆切換場景、重新載入場景、結束遊戲、暫停以及取消暫停。SceneTree 甚至有一些很有趣的訊號。有空的話記得去看看 SceneTree 的手冊!

通知

Godot 中有通知系統。雖然因為這個功能太底層,而且通知的大部分功能都可以在虛擬函式中找到,所以這個功能通常在編寫腳本時用不到。但知道有通知這東西還是好處的。例如,可以為腳本新增一個 Object._notification() 函式:

func _notification(what):
    match what:
        NOTIFICATION_READY:
            print("This is the same as overriding _ready()...")
        NOTIFICATION_PROCESS:
            print("This is the same as overriding _process()...")
public override void _Notification(int what)
{
    base._Notification(what);

    switch (what)
    {
        case NotificationReady:
            GD.Print("This is the same as overriding _Ready()...");
            break;
        case NotificationProcess:
            var delta = GetProcessDeltaTime();
            GD.Print("This is the same as overriding _Process()...");
            break;
    }
}

類別參考手冊 中有寫出各個類別能接收什麼通知。然而,大部分情況下 GDScript 都提供了更簡單的可複寫函式。

可複寫函式

下列可複寫函式可用於節點:

func _enter_tree():
    # When the node enters the Scene Tree, it becomes active
    # and  this function is called. Children nodes have not entered
    # the active scene yet. In general, it's better to use _ready()
    # for most cases.
    pass

func _ready():
    # This function is called after _enter_tree, but it ensures
    # that all children nodes have also entered the Scene Tree,
    # and became active.
    pass

func _exit_tree():
    # When the node exits the Scene Tree, this function is called.
    # Children nodes have all exited the Scene Tree at this point
    # and all became inactive.
    pass

func _process(delta):
    # This function is called every frame.
    pass

func _physics_process(delta):
    # This is called every physics frame.
    pass
public override void _EnterTree()
{
    // When the node enters the Scene Tree, it becomes active
    // and  this function is called. Children nodes have not entered
    // the active scene yet. In general, it's better to use _ready()
    // for most cases.
    base._EnterTree();
}

public override void _Ready()
{
    // This function is called after _enter_tree, but it ensures
    // that all children nodes have also entered the Scene Tree,
    // and became active.
    base._Ready();
}

public override void _ExitTree()
{
    // When the node exits the Scene Tree, this function is called.
    // Children nodes have all exited the Scene Tree at this point
    // and all became inactive.
    base._ExitTree();
}

public override void _Process(float delta)
{
    // This function is called every frame.
    base._Process(delta);
}

public override void _PhysicsProcess(float delta)
{
    // This is called every physics frame.
    base._PhysicsProcess(delta);
}

如同稍早提到的,建議使用這些函式來代替通知系統。

建立節點

與其他以類別為基礎的資料型別一樣,可以呼叫 .new() 方法來用程式碼建立節點。例如:

var s
func _ready():
    s = Sprite.new() # Create a new sprite!
    add_child(s) # Add it as a child of this node.
private Sprite _sprite;

public override void _Ready()
{
    base._Ready();

    _sprite = new Sprite(); // Create a new sprite!
    AddChild(_sprite); // Add it as a child of this node.
}

不論是在節點的場景內還是場景外刪除節點都要使用 free()

func _someaction():
    s.free() # Immediately removes the node from the scene and frees it.
public void _SomeAction()
{
    _sprite.Free(); // Immediately removes the node from the scene and frees it.
}

釋放節點後,也會把該節點的所有子節點都釋放。由此可知刪除節點其實很簡單。只要刪除一個節點,在樹狀結構下的所有子節點也都會一起被刪除。

但有可能會發生一個狀況,就是我們要刪除的節點可能會被「封鎖 (Block)」,因為這個節點可能正在傳送訊號或是呼叫函式。如果把這個節點刪除了遊戲可能會當掉。而當有開啟 Godot 的除錯工具時通常會偵測到此一情況,並且會顯示相關警告。

要刪除一個節點最安全的方法是使用 Node.queue_free() 。這個方法會在閒置的時候安全地刪除節點。

func _someaction():
    s.queue_free() # Removes the node from the scene and frees it when it becomes safe to do so.
public void _SomeAction()
{
    _sprite.QueueFree(); // Removes the node from the scene and frees it when it becomes safe to do so.
}

實體化場景

使用程式碼來建立節點可以區分為兩個步驟。第一個步驟是從硬碟中載入場景:

var scene = load("res://myscene.tscn") # Will load when the script is instanced.
var scene = GD.Load<PackedScene>("res://myscene.tscn"); // Will load when the script is instanced.

把場景預先載入進來可能會更方便,因為預載是在解析時執行的 (僅可用於 GDScript):

var scene = preload("res://myscene.tscn") # Will load when parsing the script.

scene (場景) 還不是節點,目前還打包在一個叫做 PackedScene (打包場景) 的特殊資源內。要真正建立節點,必須呼叫 PackedScene.instance() 函式。這個函式會回傳一個可以被新增到有效場景內的節點樹:

var node = scene.instance()
add_child(node)
var node = scene.Instance();
AddChild(node);

拆成「載入 - 實體化」兩個步驟的優點是,載入過打包場景後就可以維持已載入的狀態,隨時拿來使用。這樣一來我們就可以根據需要建立任意數量的實體。特別適用於需要在有效場景內快速實體化多個敵人、子彈或是其他實體的情況。

將腳本註冊為類別

Godot 有一個「腳本類別」功能,可以在編輯器內註冊個別腳本。預設情況下,要存取未命名的腳本,就只能直接載入檔案。

You can name a script and register it as a type in the editor with the class_name keyword followed by the class's name. You may add a comma and an optional path to a PNG or SVG image to use as an icon (16×16 minimum, 32×32 recommended). You will then find your new type in the Node or Resource creation dialog. Note that the icon will only appear after restarting the editor.

extends Node

# Declare the class name here
class_name ScriptName, "res://path/to/optional/icon.svg"

func _ready():
    var this = ScriptName           # reference to the script
    var cppNode = MyCppNode.new()   # new instance of a class named MyCppNode

    cppNode.queue_free()
../../_images/script_class_nativescript_example.png

警告

在 Godot 3.1 中:

  • 只有 GDScript 與 NativeScript 如 C++ 等使用 GDNative 的語言可以註冊腳本。

  • 只有 GDScript 才會為每個已命名的腳本建立全域變數。