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
備註
對於常數來說,
=與:=沒有差別。常數不一定要寫型別提示,Godot 會自動根據賦值推斷型別。不過,如果你要讓程式碼意圖更明確,還是可以寫上型別。特別是陣列,如果需要使用有型別的陣列(如
const A: Array[int] = [1, 2, 3]),就很有幫助,因為預設是無型別陣列。
哪些內容可以作為型別提示
以下是所有可以用來當作型別提示的項目:
Variant。代表任何型別。通常和未指定型別差異不大,但能提升可讀性。若指定為回傳型別,則強制函式必須有回傳值。(僅用於回傳型別)
void。代表函式不會有回傳值。內建型別。
原生類別(如
Object、Node、Area2D、Camera2D等)。全域類別。
內部類別。
全域、核心與自訂命名的 enum。請注意 enum 實際上是
int型別,無法保證它的值一定來自 enum 列舉值集合。常數(包含區域常數),只要內容是預載入的類別或 enum。
你可以用任何類別(包含自訂類別)作為型別。腳本中指定自訂類別有兩種方式,第一種是在常數中預先載入要當型別用的腳本:
const Rifle = preload("res://player/weapons/rifle.gd")
var my_rifle: Rifle
第二種方法是在類別腳本中使用 class_name 關鍵字。以上述範例來說, rifle.gd 會像這樣:
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)與其他運算子(如 ==)仍不支援型別。你可以用內建型別、核心類別、自訂類別、enum 當作元素型別。不支援巢狀陣列型別(如 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 型別。
型別轉換
型別轉換是有型別語言很重要的觀念,意思是將一個值從一種型別轉成另一種型別。
假設你的遊戲中有一個繼承自 Area2D 的 Enemy,你希望它能和附有 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,不會報錯也不會警告。這種行為有時很方便,但也可能導致 bug。除非你確定這是你要的行為,否則建議用更安全的 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 型別,它同時也是 Node 和 Object。用動態 GDScript,只要該節點有你要呼叫的方法,型別不是重點。
你可以用型別轉換讓 Godot 知道你預期取得什麼型別的節點,例如 ($Timer as Timer)、($Player as CharacterBody2D) 等。Godot 會檢查型別是否正確,如果正確,腳本編輯器左側的行號會變綠色。
不安全行(第 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_entered(area: CollisionObject2D) -> void:
pass
警告系統
備註
關於 GDScript 警告系統的詳細說明請參見 GDScript 警告系統。
Godot 會在你寫程式時即時顯示警告。引擎會標示出可能在執行時發生問題的程式碼區段,但最終是否要修改還是保留原樣,由你自己決定。
系統中有不少警告特別針對使用靜態型別 GDScript 的用戶。這些警告預設是關閉的,你可以在專案設定(偵錯 > GDScript,記得先開啟 進階設定)裡啟用。
如果你想要強制全程使用靜態型別,可以啟用 UNTYPED_DECLARATION 警告。若你偏好更可讀、更可靠但較冗長的語法,也可以啟用 INFERRED_DECLARATION 警告。
UNSAFE_* 警告會讓不安全的操作比安全行標記更顯眼。目前 UNSAFE_* 警告還沒有涵蓋所有安全行會標示的狀況。
常見的不安全操作與對應的安全寫法
UNSAFE_PROPERTY_ACCESS 和 UNSAFE_METHOD_ACCESS 警告
假設有一個物件掛有 class_name MyScript、繼承自 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_ACCESS 和 UNSAFE_METHOD_ACCESS 警告,因為這些屬性和方法並不存在於 Node2D。安全做法是先用 is 關鍵字檢查型別,再宣告一個 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 警告
在下例中,我們想讓一個碰撞區域內的 label 顯示區域名稱。當有物件進入碰撞區時,物理系統會傳給我們一個 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
無法指定型別的情境
最後補充無法使用型別提示的情況。以下做法都會造成 語法錯誤 。
你不能對陣列或字典的個別元素指定型別:
var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy] var character: Dictionary = { name: String = "Richard", money: int = 1000, inventory: Inventory = $Inventory, }
目前不支援巢狀型別:
var teams: Array[Array[Character]] = []
總結
靜態型別的 GDScript 是一項強大的工具。它能幫助你寫出結構更清晰的程式、減少常見錯誤,並建立更具擴充性與可靠性的系統。靜態型別也能提升 GDScript 效能,未來還會有更多最佳化。