GDScript 中的靜態型別

在本指南中,我們將學到:

  • 如何在 GDScript 中使用型別

  • 使用靜態型別可避免 Bug

要在哪裡使用靜態型別以及如何使用靜態型別全依個人:可以只在特定的 GDScript 檔案中使用、或是到處都使用靜態型別、也可以像平常一樣寫程式碼就好了!

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

備註

有型別的 GDScript 自 Godot 3.1 版後提供。

靜態型別簡介

在 GDScript 中使用型別,就能在寫程式碼時讓 Godot 偵測到更多錯誤!型別也能讓你與團隊成員獲得更多資訊,因為在呼叫方法時會顯示出引數的型別。

來想像一下開發物品列系統。先撰寫 Item 節點的程式,然後寫 Inventory 。要將物品新增到物品列上時,則需要將 Item 傳遞給 Inventory.add 方法。我們可以通過指定型別來強制 Inventory.add 只能接受 Item 型別:

# In 'Item.gd'.
class_name Item
# In 'Inventory.gd'.
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

另一個在 GDScript 使用型別的優勢為新的 警告系統 。自 3.1 版起,當 Godot 發現程式碼中出現了可能會在執行時發生問題的程式碼時會即時顯示警告,但是使用者依然可以決定要保留程式碼或是進行修改。稍後將詳細說明。

靜態型別也能讓程式碼補全更加完善。我們將在下方比較動態與靜態型別的補全選項,以 PlayerController 類別為例。

如果之前有將節點保存在變數過,輸入句點時不會顯示自動補全建議:

code completion options for dynamic

這是因為 body 的定義是動態的,Godot 無法知道傳入函式的節點或數值的型別。若明確聲明型別,則自動補全提示會顯示節點中所有 Public 的方法與變數:

code completion options for typed

將來,有型別的 GDScript 也能增加程式碼的效能:我們已經在計劃藍圖上加上 JIT 編譯與其他編譯器改進了!

整體上來說,加上型別能帶來更結構化的體驗,有助於避免錯誤以及讓腳本能自行說明功能。對於在團隊中合作或長期專案來說特別實用:研究指出,開發者花費較多的時間閱讀其他人或自己以前寫過但已經忘記的程式碼。程式碼越清楚、結構越明白,就越容易能理解,並能讓開發者更快開始工作。

如何使用靜態型別

要定義變數或常數的型別,只需在變數名稱後方加上逗號,再寫上型別。如 var heal: int 。這樣即可讓變數的型別保持相同:

var damage: float = 10.5
const MOVE_SPEED: float = 50.0

若只寫冒號而省略型別,則 Godot 會自動推斷型別:

var life_points := 4
var damage := 10.5
var motion := Vector2()

目前可以使用三種類型的型別:

  1. 內建型別

  2. 核心類型與節點 (Object, Node, Area2D, Camera2D …等)

  3. 自定類型。請參考新的 class_name 功能來瞭解如何將型別註冊到編輯器內。

備註

常數不需要寫型別,因為 Godot 會自動依據指派的值來推斷型別。但一樣可以寫上型別來讓程式碼更清楚。

自定變數型別

可以使用任何類別來作為型別,也包含自定類別。在腳本中使用類別作為型別有兩種方法,第一種方法是將要作為型別的腳本預先載入到常數中:

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

而第二種方法則是在建立類別時使用 class_name 關鍵字。如在上方的範例中,Rifle.gd 會變成這樣:

extends Node2D
class_name Rifle

若使用 class_name ,則 Godot 會將 Rifle 型別全域註冊到編輯器中,之後便可在任何地方使用而無需預載進常數:

var my_rifle: Rifle

變數型別轉換

型別轉換是型別語言的重要概念。型別轉換即為將數值從一種型別轉換至另一種型別。

試想一下遊戲中有 Enemy 物件 extends Area2D ,要與 Player (附加了 PlayerController 腳本的 KinematicBody2D) 碰撞,則可以使用 on_body_entered 訊號來檢測碰撞。若使用有型別的程式碼,則 _on_body_entered 回呼內偵測的是 PhysicsBody2D 而非 PlayerController

我們可以使用 as 型別轉換關鍵字來檢查 PhisicsBody2D 是否為 Player,並使用冒號 : 來強制變數使用這個型別。這樣一來便可強制變數保持為 PlayerController 型別:

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

    player.damage()

由於我們在處理的是自定型別,若 body 不是繼承 PlayerController ,則 player 變數會被設為 null 。可以藉由檢查 null 來確認 body 是否為玩家。另外,由於我們轉換了型別在輸入時也會有 Player 上完整的自動補全功能。

備註

若試著轉換型別為內建型別而失敗的話,Godot 會拋出錯誤。

安全行

我們可以使用型別轉換來確保某行程式碼為安全行。安全行是 Godot 3.1 版新增的新工具,能用來告訴使用者某行可能有歧義的程式碼是否為型別安全。由於我們能在程式碼中混合有型別與動態的程式碼,有時候 Godot 無法確保某個程式碼是否會在執行時產生錯誤。

Godot 無法推定型別的狀況通常發生在取得子節點時。以 Timer 為例,我們使用動態程式碼來取得節點並保存在 $Timer 中。GDScript 支援 鴨子型別 ,所以即使 Timer 是 Timer 型別,Timer 同時也會是其繼承的 NodeObject 兩個類別。使用動態 GDScript 時,只要節點上有我們需要的方法,就不需要去在意節點是什麼型別。

我們可以使用型別轉換來告訴 Godot 在取得節點的時候預期取得什麼型別,如 ($Timer as Timer), ($Player as KinematicBody2D) …等。Godot 會確保該型別是否有效,而有效的話則會將腳本編輯器左邊的行號變成綠色。

Unsafe vs Safe Line

非安全行 (第 7 行) vs 安全行 (第 6 行與第 8 行)

備註

可以在編輯器設定中關閉安全行或更改安全行的色彩。

使用箭頭 -> 來定義函式的回傳值型別

要定義函式的回傳值型別,可以在函式定義後加上一個減號與右角括號 -> ,再寫上回傳型別:

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

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

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

也可以使用自定節點來作為回傳型別:

# Inventory.gd

# 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

靜態或動態:只選擇一種風格

Typed GDScript and dynamic GDScript can coexist in the same project. But it's recommended to stick to either style for consistency in your codebase, and for your peers. It's easier for everyone to work together if you follow the same guidelines, and faster to read and understand other people's code.

使用型別程式碼可能會需要寫更多程式碼,但同時也能享受剛才提到的優點。下列為使用動態風格的空白腳本範例:

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_Area2D_body_entered(body):
    pass

而相同的回呼,加上型別定義:

func _on_area_entered(area: CollisionObject2D) -> void:
    pass

CollisionObject2D 可以自由替換為任何自定型別,並自動轉換參數的型別:

func _on_area_entered(bullet: Bullet) -> void:
    if not bullet:
        return

    take_damage(bullet.damage)

此例中的 bullet 變數可以是任何的 CollisionObject2D ,但這裡我們確保 bullet 是專案中的 Bullet 節點。若 bullet 是其他如 Area2D 之類沒有繼承 Bullet 的節點,則 bullet 變數會是 null

警告系統

備註

有關 GDScript 警告系統的文件已移至 GDScript 警告系統

無法指定型別的情況

在結束這篇教學前,我們再來說說無法使用型別定義的一些情況。接下來所有的例子都會 觸發錯誤

列舉類型不能作為型別定義:

enum MoveDirection {UP, DOWN, LEFT, RIGHT}
var current_direction: MoveDirection

無法為陣列中個別成員指定型別。下列例子會產生錯誤:

var enemies: Array = [$Goblin: Enemy, $Zombie: Enemy]

無法強制在 for 迴圈上指定型別,因為 for 關鍵字迴圈上的每個元素都有不同的型別。也就是說, 不能 這樣寫:

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

兩個腳本不能互相依賴,會導致循環:

# Player.gd

extends Area2D
class_name Player


var rifle: Rifle
# Rifle.gd

extends Area2D
class_name Rifle


var player: Player

總結

型別定義的 GDScript 是強力的工具。自 Godot 3.1 版起提供,型別定義能讓使用者撰寫更有結構的程式碼、避免場景錯誤以及能建立延展性更高的系統。在未來,靜態型別也能得益於編譯器的最佳化而有更好的效能。