何時使用場景 v.s. 腳本

我們已經解釋了場景與腳本有什麼不同。腳本通過了指令式程式碼定義了引擎類別的延伸,而場景則是用宣告性程式碼定義。

因此,每個系統的能力都不同。場景可以定義要如何擴充類別初始化,而不是類別實際的行為。場景通常與腳本一起結合使用,這樣場景就可以作為腳本中宣告性程式碼的擴充來使用。

匿名型別

完全 可以 只使用腳本來定義場景。從本質上來說,這就是 Godot Editor 做的,只是是使用物件的 C++ 建置函式而已。

但是,選擇要使用哪種方法可能很難決定。建立腳本實體就與建立引擎內類別相同,而處理場景則需修改 API:

const MyNode = preload("my_node.gd")
const MyScene = preload("my_scene.tscn")
var node = Node.new()
var my_node = MyNode.new() # Same method call
var my_scene = MyScene.instance() # Different method call
var my_inherited_scene = MyScene.instance(PackedScene.GEN_EDIT_STATE_MAIN) # Create scene inheriting from MyScene
using System;
using Godot;

public class Game : Node
{
    public readonly Script MyNodeScr = (Script)ResourceLoader.Load("MyNode.cs");
    public readonly PackedScene MySceneScn = (PackedScene)ResourceLoader.Load("MyScene.tscn");
    public Node ANode;
    public Node MyNode;
    public Node MyScene;
    public Node MyInheritedScene;

    public Game()
    {
        ANode = new Node();
        MyNode = new MyNode(); // Same syntax
        MyScene = MySceneScn.Instance(); // Different. Instantiated from a PackedScene
        MyInheritedScene = MySceneScn.Instance(PackedScene.GenEditState.Main); // Create scene inheriting from MyScene
    }
}

另外,使用腳本也會比場景慢一點,這就是引擎與腳本程式碼速度的不同。越大與越複雜的節點,就更應該使用場景來建立。

有名稱的型別

有些情況下,使用者可以在編輯器中將腳本註冊為新型別。這樣在節點或資源建立視窗中就可以顯示該腳本為新型別,並帶有一個可選的圖示。這些時候,使用者能對腳本做的事就更流暢了。而不需要...

  1. 知道要使用哪個腳本的基本型別。

  2. 建立該基礎型別的實體。

  3. 將腳本附加到節點。

    1. (拖放方法)

      1. 在檔案系統 Dock 中找到腳本。

      2. 將腳本拖放到場景 Dock 中的節點。

    2. (屬性方法)

      1. 下拉到屬性面板底部,並找到 script 屬性並選擇。

      2. 從下拉選單中點擊 [讀取]。

      3. 從檔案對話框中選擇腳本。

使用已註冊的腳本,則腳本型別就會與其他系統內的節點與資源一樣變成建立選項。我們就不需要再進行上述步驟了。建立對話框中甚至還有一個搜尋框可以按名稱搜尋型別。

有兩種系統可以註冊型別...

  • 自定型別

    • 限編輯器。型別名稱在執行時無法存取。

    • 不支援繼承自定型別。

    • 初始設定式工具。以腳本建立節點。就這樣。

    • 編輯器不會偵測腳本的型別或其與其他引擎型別或腳本的關係。

    • 使用者可以定義圖示。

    • 適用於所有腳本語言,因為該腳本是以抽象方法處理 Script 資源。

    • 使用 EditorPlugin.add_custom_type 設定。

  • 腳本類別

    • 編輯器與執行環境都可存取。

    • 顯示完整的繼承關係。

    • 使用腳本建立節點,但也可以在編輯器中更改型別或繼承型別。

    • 編輯器可以知道腳本、腳本類別與引擎 C++ 類別間的繼承關係。

    • 使用者可以定義圖示。

    • 引擎開發人員必須為語言手動新增支援 (名稱暴露與執行時的可存取型)。

    • 限適用於 Godot 3.1+ 版。

    • 編輯器會掃描專案資料夾並註冊所有腳本語言中所有暴露的名稱。每個腳本語言都必須要實作對這種暴露資訊的支援。

這兩種方法都會在建立視窗中加上名稱,但特別是腳本類別,還允許使用者在不載入腳本資源時存取型別名稱。在任何地方都可以建立實體與存取常數或靜態方法。

有了這樣的功能,因為很容易使用,於是我們便會想將型別做成無需場景的腳本。開發外掛或製作給設計師用的內部工具的開發人員也可以通過這種方法來省事。

但缺點是,這也代表必須要使用大量的指令式程式。

腳本效能 vs PackedScene 效能

在選擇場景與腳本時,最後一個需要考慮的點就是執行速度了。

隨著物件的大小增長,要建立物件所需的腳本也變得越來越大。建立節點階層即可重現此狀況。個別節點的邏輯可多達數百行程式碼。

下列程式碼會建立一個新的 Node 節點、更改名稱、為該節點指派腳本、然後將新節點的母節點設為目前節點,這樣一來該節點才會跟著一起保存到硬碟上,然後最後才將其加為 Main 節點的子節點:

# Main.gd
extends Node

func _init():
    var child = Node.new()
    child.name = "Child"
    child.script = preload("Child.gd")
    child.owner = self
    add_child(child)
using System;
using Godot;

public class Main : Resource
{
    public Node Child { get; set; }

    public Main()
    {
        Child = new Node();
        Child.Name = "Child";
        Child.Script = ResourceLoader.Load<Script>("child.gd");
        Child.Owner = this;
        AddChild(Child);
    }
}

像這樣的程式碼會比引擎端 C++ 程式碼還要慢很多。每個改動都會需要額外的腳本 API 呼叫來向後端「查詢」要執行什麼邏輯。

場景系統可以避免這種效能問題。 PackedScene ,即,場景繼承的基礎型別,是一種資源,可以用來序列化資料並建立物件。引擎可以在後端批次處理場景,並可提供比腳本好很多的效能。

結論

最後,最好的方法就是要考慮下列事項:

  • 如果想製作會在不同專案間重複使用,並且所有程度的人 (包含不把自己成為「程式設計師」的人) 都能使用的基礎工具,那麼這個工具很有可能會是腳本,並且有自定名稱/圖示。

  • 如果想建立專屬於自己的遊戲的概念,則應該使用場景。場景比較好追蹤與編輯,並且比起腳本提供更多安全性。

  • 如果想為場景起名,則還是可以在 3.1 版中通過宣告腳本類別並以常數來指定場景來實現類似的目的。該腳本就有了命名空間的效果:

    # game.gd
    extends Reference
    class_name Game # extends Reference, so it won't show up in the node creation dialog
    const MyScene = preload("my_scene.tscn")
    
    # main.gd
    extends Node
    func _ready():
        add_child(Game.MyScene.instance())