撰寫腳本

簡介

在 3.0 版以前的 Godot 中只能使用 GDScript 來為遊戲編寫程式。但現在 Godot 有四種 (沒錯,四種!) 官方語言,且還可以動態新增其他腳本語言!

這樣很棒,因為這樣彈性就更大了,但另一方面,要支援各個語言也需要做更多的工作。

雖然,Godot 的「主要」語言是 GDScript 與視覺腳本 (VisualScript)。選擇這兩個語言的主要原因就是與 Godot 的高度整合可以讓開發流程變得更順暢。這兩個語言都有完整的編輯器整合,而 C# 與 C++ 則需要用不同的 IDE 來進行開發。如果你很愛靜態型別語言的話,可以使用 C# 與 C++。

GDScript

就像剛才說的, GDScript 是 Godot 中的主要語言。因為與 Godot 高度整合,用 GDScript 比起其他語言有更多優點:

  • GDScript 很簡單、優雅,而且對於其他語言的使用者如 Lua, Python, Squirrel…等來說會感到很親切。

  • 超快速的載入與編譯。

  • 編輯器整合讓作業過程變得更開心。節點、訊號以及其他與正在編輯的場景有關的項目都能進行程式碼補全。

  • 有內建 Vector 型別 (如 Vector, Transform…等),當需要大量線性代數運算時更有效率。

  • 跟靜態型別語言一樣支援多執行緒 — 這是我們不選擇使用如 Lua, Squirrel…等語言 VM 的其中一個原因。

  • 不使用記憶體回收行程。以決定性 (Determinism) 取代一小部分的自動化 (大部分物件都使用引用計數)。

  • 由於動態特性,需要更高性能時可以把一部分程式碼用 C++ 來最佳化 (通過 GDNative),且不用重新編譯整個引擎。

如果你還沒決定好要用哪個語言,而且有過寫程式的經驗——特別是動態型別語言,選擇 GDScript 吧!

視覺腳本 (VisualScript)

Godot 從 3.0 版開始提供 視覺腳本 (VisualScript) 。視覺腳本是一種典型的「方塊與連接 (Blocks and Connections)」語言實作,只是改成了 Godot 版。

對於非程式設計師來說,以圖形化寫程式是一個很好用的工具。甚至對於有經驗的程式設計師來說,當想要讓其他如遊戲設計師或是美術家之類的人接觸程式碼時也很有用。

程式設計師也可以用視覺腳本來製作狀態機或是自定視覺節點工作流程 (如:對話框系統)。

.NET / C#

微軟的 C# 是很受遊戲開發者歡迎的語言,所以 Godot 也新增了對 C# 的官方支援。C# 是一個成熟的語言,有許多程式都是用 C# 寫的。要感謝微軟慷慨的捐款讓我們能夠支援 C#。

C# 在易用度與效能間的平衡做得非常優秀,雖然需要注意一下記憶體回收行程。

Godot 使用了 Mono .NET 執行環境,理論上任一第三方 .NET 函式庫或框架都可以拿來在 Godot 裡使用。同樣的,理論上其他 CLI (Common Language Infrastructure,通用語言基礎結構) 相容的程式語言也都能使用,如 F#, Boo 或是 ClojureCLR。但 C# 是唯一官方支援的 .NET 選項。

GDNative / C++

最後是 3.0 版所新增的亮點:GDNative。使用 GDNative 就可以在不需要重新編譯 (甚至重新啟動) Godot 的情況下以 C++ 來寫腳本。

可以使用任一版本的 C++。而因為使用了內部 C API Bridge,也可以正常使用由不同編譯器產生的共用函式庫。

若要追求效能,GDNative / C++ 是最好的選擇。而且不需要整個遊戲都改用 GDNative / C++,其他部分還是可以用 GDScript 或視覺腳本 (Visual Script) 來寫。話雖如此,GDNative 的 API 還是很清楚簡單的,因為大部分都是直接用 Godot 原生 API。

還能通過 GDNative 的介面來支援更多語言,但請記得 Godot 對其他語言並無官方支援。

為場景編寫腳本

我們會在這篇教學接下來的部分建立一個 GUI 場景,其中有按鈕與標籤。按下按鈕後會更新標籤。我們會示範:

  • 撰寫腳本並把腳本附加到節點上。

  • 通過訊號 (Signal) 來串聯 UI 元素。

  • 撰寫能夠存取場景中其他節點的腳本。

在這之前,請記得要先閱讀 GDScript 參考手冊並將 GDScript 頁面加到瀏覽器書籤內。GDScript 被設計得很簡單,而且這個參考手冊被區分成了多個段落,能夠很容易掌握各種概念。

場景設定

若上一個教學中的「實體化」專案還開著的話,請先關閉該專案 ([專案] -> [退出至專案列表]) 並建立新專案。

從場景分頁 (或是鍵盤 Ctrl + A ) 裡打開「新增子節點」視窗來建立下列架構的節點:

  • 面板

    • 標籤

    • 按鈕

場景樹應該會長這樣:

../../_images/scripting_scene_tree.png

使用 2D 編輯器來調整 Button 跟 Label 的位置與大小,讓這兩個節點看起來像下面這樣。可以從屬性面板裡設定文字內容。

../../_images/label_button_example.png

最後,保存場景為 sayhello.tscn 之類的名稱。

新增腳本

右鍵點擊 Panel 節點,然後在右鍵選單中選擇 [附加腳本]:

../../_images/add_script.png

接著會彈出腳本建立視窗。在這個視窗裡可以設定腳本所使用的語言、類別名稱以及其他相關設定。

GDScript 的話,一個檔案就是一個類別,所以 [類別名稱] 欄位會沒辦法編輯。

我們正在把腳本附加到 Panel 節點上,所以 [繼承] 欄位會自動填寫為「Panel」。我們在做的正是使用腳本來擴充 Panel 節點的功能。

最後,輸入腳本路徑然後點擊 [建立]:

../../_images/script_create.png

新增腳本後會腳本被附加到節點上。在場景分頁裡的節點旁邊會出現「打開腳本」圖示,屬性面板裡的腳本 (Script) 屬性也可以看到相同的選項:

../../_images/script_added.png

上圖這兩個按鈕中的任一個都可以用來編輯腳本。點擊後會打開腳本編輯器,裡面會有預設的腳本樣板:

../../_images/script_template.png

這裡沒很多東西。節點及其所有子節點進入有效場景後會呼叫 _ready() 函式。 請注意: _ready() 不是建置函式,建置函式是 _init()

腳本的功能

腳本可以增加節點的行為、控制節點的功能以及與決定如何與其他節點互動 (子節點、母節點、同級節點等)。腳本的區域作用域 (Local Scope) 為節點。換句話說,腳本會繼承節點的功能。

../../_images/brainslug.jpg

處理訊號

當特定類型的動作發生後,會「發送」一個訊號,我們可以通過訊號來將這個動作與任何一個腳本實體函式連在一起。訊號通常在 GUI 節點內使用,但其他節點也會有訊號。甚至可以在腳本內自定訊號。

在這個步驟,我們要把「按下 (Pressed)」訊號連接到自定義函式。第一個部分是構成連接,第二個部分則是要定義函式。Godot 中有兩個方法可以建立連線,第一個方法是通過編輯器視覺化界面,第二個方法則是通過程式碼。

雖然在這個教學中接下來的部分我們都會使用程式碼,但這裡還是來介紹一下要如何用編輯器界面來建立連接。

從場景樹中選擇 Button 節點,然後再按 [節點] 分頁。接下來,確定一下有選中 [訊號] 選項。

../../_images/signals.png

接下來選擇「BaseButton」底下的「pressed()」,然後點擊右下角的 [連接…] 打開建立連接對話框。

../../_images/connect_dialogue.png

對話框的頂部會顯示出場景中的節點列表,而觸發訊號的節點名稱則會以藍色顯示。這裡先選擇「Panel」節點。

對話框底部會顯示正在建立的方法名稱。預設方法名稱會包含觸發訊號的節點名稱 (這個例子中為「Button」),格式為 _on_[觸發節點名]_[訊號名稱]

到這裡就是如何使用視覺化界面的教學了。但這篇教學是腳本教學,所以,為了多學一點,來看看如何手動建立訊號連接吧!

為了手動建立連接,接下來我們要介紹 Node.get_node() ,這個函式可能所有 Godot 程式設計師最常用的函式。Node.get_node() 可以使用相對於該腳本節點的路徑來取得場景中任一節點。

但為了方便起見,先把所有 extends Panel 下面所有的東西都刪除。接下來我們要手動填寫剩下的腳本。

由於 Button 與 Label 為腳本所附加之 Panel 節點的同級節點,所以可以在 _ready() 函式中輸入以下內容來取得 Button 節點:

func _ready():
    get_node("Button")
public override void _Ready()
{
    GetNode("Button");
}

接下來,寫一個按鈕按下後要呼叫的函式:

func _on_Button_pressed():
    get_node("Label").text = "HELLO!"
public void _OnButtonPressed()
{
    GetNode<Label>("Label").Text = "HELLO!";
}

最後,通過 Object.connect() 方法來把按鈕的「pressed」訊號連接至 _on_Button_pressed() 函式。

func _ready():
    get_node("Button").connect("pressed", self, "_on_Button_pressed")
public override void _Ready()
{
    GetNode("Button").Connect("pressed", this, nameof(_OnButtonPressed));
}

腳本最終看起來應該長這樣:

extends Panel

func _ready():
    get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
    get_node("Label").text = "HELLO!"
using Godot;

// IMPORTANT: the name of the class MUST match the filename exactly.
// this is case sensitive!
public class sayhello : Panel
{
    public override void _Ready()
    {
        GetNode("Button").Connect("pressed", this, nameof(_OnButtonPressed));
    }

    public void _OnButtonPressed()
    {
        GetNode<Label>("Label").Text = "HELLO!";
    }
}

執行場景然後點擊按鈕,應該會得到下列結果:

../../_images/scripting_hello.png

不錯,Hello!恭喜你為第一個場景寫了腳本。

備註

在這個教學中,很對人會誤解 get_node(path) 的使用方法。在一個節點中, get_node(path) 會搜尋該節點的直接子代。在上面的程式碼中,Button 必須是 Panel 的子節點。如果 Button 是 Label 的子節點的話,那麼程式碼要寫成這樣:

# Not for this case,
# but just in case.
get_node("Label/Button")
// Not for this case,
// but just in case.
GetNode("Label/Button")

另外,也請記得參照的是節點名稱而不是型別。

備註

連接對話框中的「進階」面板中可用來將特定數值對應成函式參數,可以新增或移除不同型別的數值。

若使用程式碼的方式來連接節點,則可以使用第四個 Array 型別的參數,這個參數預設為空陣列。更多資訊可以閱讀 Object.connect 的說明來瞭解。