Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

GDScript 的靜態型別

在本指南中,你將學到:

  • 如何在 GDScript 中使用靜態型別;

  • 靜態型別可以幫助你避免錯誤;

  • 靜態型別可以提升你在編輯器中的使用體驗。

你可以完全自由決定在哪裡以及如何使用這個語言特性:你可以只在某些關鍵的 GDScript 檔案使用、全專案都使用,或完全不使用。

你可以在變數、常數、函式、參數及回傳型別上使用靜態型別。

靜態型別簡介

有了靜態型別,GDScript 可以在不執行程式碼的情況下偵測出更多錯誤。型別提示也能在你或團隊成員撰寫程式時,於方法呼叫時顯示參數型別,讓大家獲得更多資訊。靜態型別還能提升編輯器的自動補全與 腳本註解文件 的效果。

想像你正在撰寫一個物品欄系統。你寫了一個 Item 類別,然後再寫一個 Inventory。要把物品加入物品欄,使用你程式碼的人應該永遠把 Item 傳給 Inventory.add() 方法。有了型別,你可以強制這件事:

class_name Inventory


func add(reference: Item, amount: int = 1):
    var item := find_item(reference)
    if not item:
        item = _instance_item_from_db(reference)
    item.amount += amount

靜態型別也能帶來更優秀的程式碼自動補全體驗。下面可以看到動態型別與靜態型別的補全選項差異。

你也許遇過在輸入「.」後沒有自動補全建議:

動態型別程式碼的補全選項。

這是因為動態程式碼下,Godot 無法知道你傳遞給函式的值是什麼型別。如果你明確寫出型別,補全就會顯示該型別的所有方法、屬性、常數等:

靜態型別程式碼的補全選項。

小訣竅

如果你偏好靜態型別,建議啟用 文字編輯器 > 自動完成 > 新增型別提示 編輯器設定,也可考慮啟用預設停用的 部分警告

此外,使用靜態型別的 GDScript 在編譯時若已知運算元/參數型別,會自動使用最佳化的位元碼,提升執行效能。未來還會加入更多最佳化,例如 JIT/AOT 編譯。

總體來說,靜態型別讓你的程式架構更明確,有助於防止錯誤,也讓腳本更容易自我說明。在團隊協作或長期專案中特別實用:研究顯示,多數開發者花最多的時間在閱讀他人或自己過去寫但早已忘記的程式碼。程式碼越清晰、結構越明確,理解就越快,也能讓開發更順暢。

如何使用靜態型別

要定義變數、參數或常數的型別,在名稱後面寫上冒號,接著是型別。例如 var health: int。這會強制該變數的型別保持不變:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0
func sum(a: float = 0.0, b: float = 0.0) -> float:
    return a + b

若你寫了冒號卻省略型別,Godot 會嘗試推斷型別:

var damage := 10.5
const MOVE_SPEED := 50.0
func sum(a := 0.0, b := 0.0) -> float:
    return a + b

備註

  1. 對於常數來說,=:= 沒有差別。

  2. 常數不一定要寫型別提示,Godot 會自動根據賦值推斷型別。不過,如果你要讓程式碼意圖更明確,還是可以寫上型別。特別是陣列,如果需要使用有型別的陣列(如 const A: Array[int] = [1, 2, 3]),就很有幫助,因為預設是無型別陣列。

哪些內容可以作為型別提示

以下是所有可以用來當作型別提示的項目:

  1. Variant。代表任何型別。通常和未指定型別差異不大,但能提升可讀性。若指定為回傳型別,則強制函式必須有回傳值。

  2. (僅用於回傳型別) void。代表函式不會有回傳值。

  3. 內建型別

  4. 原生類別(如 ObjectNodeArea2DCamera2D 等)。

  5. 全域類別

  6. 內部類別

  7. 全域、核心與自訂命名的 enum。請注意 enum 實際上是 int 型別,無法保證它的值一定來自 enum 列舉值集合。

  8. 常數(包含區域常數),只要內容是預載入的類別或 enum。

你可以使用任何類別(包含自訂類別)作為型別。在腳本中有兩種使用方式。第一種是先在常數中預載要作為型別的腳本:

const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle

The second method is to use the class_name keyword when you create the script. For the example above, your rifle.gd would look like this:

class_name Rifle
extends Node2D

如果使用 class_name,Godot 會在編輯器中將 Rifle 型別註冊為全域,你就可以在任何地方使用它,而不需要先預載到常數:

var my_rifle: Rifle

用箭頭 -> 指定函式的回傳型別

若要定義函式的回傳型別,在宣告後加上一個連字號與右角括號 ->,接著寫上回傳型別:

func _process(delta: float) -> void:
    pass

型別 void 代表函式不回傳任何東西。就像變數一樣,你可以使用任何型別:

func hit(damage: float) -> bool:
    health_points -= damage
    return health_points <= 0

你也可以使用自己的類別作為回傳型別:

# Adds an item to the inventory and returns it.
func add(reference: Item, amount: int) -> Item:
    var item: Item = find_item(reference)
    if not item:
        item = ItemDatabase.get_instance(reference)

    item.amount += amount
    return item

協變與逆變

繼承基底類別方法時,應遵循 Liskov 替換原則

協變(Covariance): 當你覆寫繼承的方法時,可以指定比父類別更具體(子型別)的回傳型別。

逆變(Contravariance): 當你覆寫繼承的方法時,可以指定比父類別更寬泛(父型別)的參數型別。

範例:

class_name Parent


func get_property(param: Label) -> Node:
    # ...
class_name Child extends Parent


# `Control` is a supertype of `Label`.
# `Node2D` is a subtype of `Node`.
func get_property(param: Control) -> Node2D:
    # ...

指定 Array 元素的型別

要定義 Array 的型別,請把型別名稱放在 [] 內。

陣列的元素型別會套用到 for 迴圈變數, 以及某些運算子, 如 [][...] = (指定) 與 +。不過, 陣列方法 (例如 push_back) 以及其他運算子 (例如 ==) 仍是不具型別的。元素型別可以是內建型別、原生與自訂類別, 也可以是列舉。巢狀陣列型別 (例如 Array[Array[int]]) 目前不支援。

var scores: Array[int] = [10, 20, 30]
var vehicles: Array[Node] = [$Car, $Plane]
var items: Array[Item] = [Item.new()]
var array_of_arrays: Array[Array] = [[], []]
# var arrays: Array[Array[int]] -- disallowed

for score in scores:
    # score has type `int`

# The following would be errors:
scores += vehicles
var s: String = scores[0]
scores[0] = "lots"

自 Godot 4.2 起,也可以在 for 迴圈中為迴圈變數指定型別。例如:

var names = ["John", "Marta", "Samantha", "Jimmy"]
for name: String in names:
    pass

陣列本身仍然是無型別,但 for 內的 name 變數會被固定為 String 型別。

指定 Dictionary 的元素型別

要定義 Dictionary 的鍵與值的型別,請將型別名稱包在 [] 內,並以逗號分隔鍵與值的型別。

字典的值型別會套用到 for 迴圈中的變數, 以及某些運算子, 如 [][...] = (指定)。回傳值的字典方法與其他運算子 (例如 ==) 仍是不具型別的。元素型別可以是內建型別、原生與自訂類別, 或列舉。巢狀具型別的集合 (例如 Dictionary[String, Dictionary[String, int]]) 目前不支援。

var fruit_costs: Dictionary[String, int] = { "apple": 5, "orange": 10 }
var vehicles: Dictionary[String, Node] = { "car": $Car, "plane": $Plane }
var item_tiles: Dictionary[Vector2i, Item] = { Vector2i(0, 0): Item.new(), Vector2i(0, 1): Item.new() }
var dictionary_of_dictionaries: Dictionary[String, Dictionary] = { { } }
# var dicts: Dictionary[String, Dictionary[String, int]] -- disallowed

for fruit in fruit_costs:
    # `fruit` has type `String`

# The following would be errors:
fruit_costs["pear"] += vehicles
var s: String = fruit_costs["apple"]
fruit_costs["orange"] = "lots"

型別轉換

型別轉換是有型別語言很重要的觀念,意思是將一個值從一種型別轉成另一種型別。

假設你的遊戲中有一個繼承自 Area2DEnemy,你希望它能和附有 PlayerController 腳本的 CharacterBody2D 玩家碰撞,並用 body_entered 訊號來偵測。這時,型別化的程式碼中你取得到的 body 其實是通用的 PhysicsBody2D,而不是你自訂的 PlayerController

你可以用 as 關鍵字檢查這個 PhysicsBody2D 是否是你的 Player,並再次使用冒號 : 來強制變數採用該型別。這會讓變數被固定為 PlayerController 型別:

func _on_body_entered(body: PhysicsBody2D) -> void:
    var player := body as PlayerController
    if not player:
        return

    player.damage()

因為這裡處理的是自訂型別,如果 body 不是 PlayerController 的子類,player 變數就會是 null。你可以用這個特性來判斷 body 是否為玩家。轉型之後,player 變數也會有完整的自動補全。

備註

as 關鍵字在執行階段若型別不相符,會在沒有錯誤或警告的情況下,將變數悄悄轉為 null。雖然某些情況下很方便,但也可能造成錯誤。只有在你確實想要這種行為時才使用 as。較安全的替代作法是使用 is 關鍵字:

if not (body is PlayerController):
    push_error("Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

你也可以使用 is not 運算子讓程式碼更簡潔:

if body is not PlayerController:
    push_error("Bug: body is not PlayerController")

或者,你也可以使用 assert() 敘述:

assert(body is PlayerController, "Bug: body is not PlayerController.")

var player: PlayerController = body
if not player:
    return

player.damage()

備註

如果你嘗試轉型成內建型別但失敗,Godot 會直接拋出錯誤。

安全行

你也可以用型別轉換來確保安全行。安全行是 Godot 的一種輔助工具,用來告訴你某行有歧義的程式碼在型別上是否安全。因為你可以混用有型別和動態程式碼,有時 Godot 無法判斷某行指令在執行時會不會錯。

這種情況通常發生在你取得子節點時。以 Timer 為例,動態寫法可以用 $Timer 直接取得節點。GDScript 支援 鴨子型別,所以即使 Timer 是 Timer 型別,它同時也是 NodeObject。用動態 GDScript,只要該節點有你要呼叫的方法,型別不是重點。

你可以用型別轉換讓 Godot 知道你預期取得什麼型別的節點,例如 ($Timer as Timer)($Player as CharacterBody2D) 等。Godot 會檢查型別是否正確,如果正確,腳本編輯器左側的行號會變綠色。

不安全 v.s. 安全行

不安全行(第 7 行)v.s. 安全行(第 6、8 行)

備註

所謂的安全行並不一定代表程式碼更好或更可靠;請參考上面關於 as 關鍵字的說明。例如:

@onready var node_1 := $Node1 as Type1 # Safe line.
@onready var node_2: Type2 = $Node2 # Unsafe line.

即使 node_2 宣告被標記為不安全,但其實比 node_1 更可靠。因為如果你在場景裡變更節點型別,在腳本裡沒同步更新,載入場景時會立刻報錯。反之,node_1 會靜默被轉成 null,錯誤會延後發現。

備註

你可以在編輯器設定中關閉安全行功能,或改變其顏色。

靜態型別或動態型別:建議統一風格

靜態型別和動態型別的 GDScript 可以在同一個專案共存,但建議你選擇一種風格統一使用,有助於團隊協作和維護。大家遵循同一套規範,彼此閱讀、理解程式碼會更有效率。

帶型別的寫法會多一些字,但你能獲得上述的好處。以下是相同的空白腳本,以動態風格撰寫:

extends Node


func _ready():
    pass


func _process(delta):
    pass

改用靜態型別則如下:

extends Node


func _ready() -> void:
    pass


func _process(delta: float) -> void:
    pass

如你所見,也可以在引擎的虛擬方法上使用型別。訊號回呼(和任何方法一樣)也可以使用型別。以下是以動態風格撰寫的 body_entered 訊號:

func _on_area_2d_body_entered(body):
    pass

同樣的回呼,加入型別提示:

func _on_area_2d_body_entered(body: PhysicsBody2D) -> void:
    pass

警告系統

備註

關於 GDScript 警告系統的詳細說明請參見 GDScript 警告系統

Godot 會在你寫程式時即時顯示警告。引擎會標示出可能在執行時發生問題的程式碼區段,但最終是否要修改還是保留原樣,由你自己決定。

系統中有不少警告特別針對使用靜態型別 GDScript 的用戶。這些警告預設是關閉的,你可以在專案設定(偵錯 > GDScript,記得先開啟 進階設定)裡啟用。

如果你想要強制全程使用靜態型別,可以啟用 UNTYPED_DECLARATION 警告。若你偏好更可讀、更可靠但較冗長的語法,也可以啟用 INFERRED_DECLARATION 警告。

UNSAFE_* 警告會讓不安全的操作比安全行標記更顯眼。目前 UNSAFE_* 警告還沒有涵蓋所有安全行會標示的狀況。

常見的不安全操作與對應的安全寫法

Global scope methods

The following global scope methods are not statically typed, but they have typed counterparts available. These methods return statically typed values:

Method

Statically typed equivalents

abs()

ceil()

clamp()

floor()

lerp()

round()

sign()

snapped()

When using static typing, use the typed global scope methods whenever possible. This ensures you have safe lines and benefit from typed instructions for better performance.

UNSAFE_PROPERTY_ACCESSUNSAFE_METHOD_ACCESS 警告

在這個例子中, 我們想要對一個物件設定屬性並呼叫方法; 該物件掛有使用 class_name MyScript 、且 extends Node2D 的腳本。若我們手上的參考是 Node2D (例如由物理系統傳入), 可以先檢查該屬性與方法是否存在, 若存在再設定與呼叫:

if "some_property" in node_2d:
    node_2d.some_property = 20  # Produces UNSAFE_PROPERTY_ACCESS warning.

if node_2d.has_method("some_function"):
    node_2d.some_function()  # Produces UNSAFE_METHOD_ACCESS warning.

然而,這段程式碼會產生 UNSAFE_PROPERTY_ACCESSUNSAFE_METHOD_ACCESS 警告,因為所參考的型別(本例為 Node2D)並沒有這個屬性與方法。為了讓操作安全,你可以先用 is 關鍵字檢查物件是否為 MyScript 型別,然後宣告一個 MyScript 型別的變數,再對其設定屬性並呼叫方法:

if node_2d is MyScript:
    var my_script: MyScript = node_2d
    my_script.some_property = 20
    my_script.some_function()

或者,你可以先宣告一個變數,並使用 as 運算子嘗試轉型。接著檢查該變數是否已被賦值,以確認轉型是否成功:

var my_script := node_2d as MyScript
if my_script != null:
    my_script.some_property = 20
    my_script.some_function()

UNSAFE_CAST 警告

在此例中,我們希望當有物件進入碰撞區域時,與該物件關聯的標籤能顯示此區域的名稱。當物件進入碰撞區域時,物理系統會送出帶有 Node2D 物件的訊號;最直接(但不是靜態型別)的作法如下:

func _on_body_entered(body: Node2D) -> void:
    body.label.text = name  # Produces UNSAFE_PROPERTY_ACCESS warning.

這段程式碼會產生 UNSAFE_PROPERTY_ACCESS 警告,因為 Node2D 中未定義 label。要解決這點,可以先檢查 label 屬性是否存在,並在設定其文字屬性之前將其轉型為 Label

func _on_body_entered(body: Node2D) -> void:
    if "label" in body:
        (body.label as Label).text = name  # Produces UNSAFE_CAST warning.

不過,這會產生 UNSAFE_CAST 警告,因為 body.label 的型別是 Variant。要安全地以你想要的型別取得該屬性,可以使用 Object.get() 方法取得它;該方法會回傳 Variant 值,若屬性不存在則回傳 null。接著使用 is 關鍵字判斷是否為正確型別,最後以該物件宣告一個靜態型別變數:

func _on_body_entered(body: Node2D) -> void:
    var label_variant: Variant = body.get("label")
    if label_variant is Label:
        var label: Label = label_variant
        label.text = name

無法指定型別的情境

最後補充無法使用型別提示的情況。以下做法都會造成 語法錯誤

  1. 你不能為陣列或字典的個別元素指定型別:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]
var character: Dictionary = {
    name: String = "Richard",
    money: int = 1000,
    inventory: Inventory = $Inventory,
}
  1. 目前不支援巢狀型別:

var teams: Array[Array[Character]] = []

總結

靜態型別的 GDScript 是一項強大的工具。它能幫助你寫出結構更清晰的程式、減少常見錯誤,並建立更具擴充性與可靠性的系統。靜態型別也能提升 GDScript 效能,未來還會有更多最佳化。