GDScript 基礎

簡介

GDScript 是一個動態型別的高級語言,用於進行內容創作。語法類似於 Python (如區塊都是基於縮排來判斷,以及很多關鍵字都相同)。GDScript 的目標是要做一個對 Godot Engine 最佳化的語言,並且能緊密地與引擎整合,進而讓我們更有彈性地製作內容與整合功能。

歷史

備註

關於 GDScript 歷史的文件移到了 常見問題

GDScript 範例

有些人瞭解語法後可以學得更好,所以以下是簡單的 GDScript 例子。

# A file is a class!

# Inheritance

extends BaseClass

# (optional) class definition with a custom icon

class_name MyClass, "res://path/to/optional/icon.svg"


# Member variables

var a = 5
var s = "Hello"
var arr = [1, 2, 3]
var dict = {"key": "value", 2: 3}
var typed_var: int
var inferred_type := "String"

# Constants

const ANSWER = 42
const THE_NAME = "Charly"

# Enums

enum {UNIT_NEUTRAL, UNIT_ENEMY, UNIT_ALLY}
enum Named {THING_1, THING_2, ANOTHER_THING = -1}

# Built-in vector types

var v2 = Vector2(1, 2)
var v3 = Vector3(1, 2, 3)


# Function

func some_function(param1, param2):
    var local_var = 5

    if param1 < local_var:
        print(param1)
    elif param2 > 5:
        print(param2)
    else:
        print("Fail!")

    for i in range(20):
        print(i)

    while param2 != 0:
        param2 -= 1

    var local_var2 = param1 + 3
    return local_var2


# Functions override functions with the same name on the base/parent class.
# If you still want to call them, use '.' (like 'super' in other languages).

func something(p1, p2):
    .something(p1, p2)


# Inner class

class Something:
    var a = 10


# Constructor

func _init():
    print("Constructed!")
    var lv = Something.new()
    print(lv.a)

如果你曾經有使用如 C、C++、或 C# 等靜態型別語言的經驗卻沒用過動態型別的話,建議你閱讀這個教學: GDScript:動態語言簡介

語言

接下來是 GDScript 的概要說明。關於陣列或其他物件有哪些方法可以用之類的詳細說明,請參考相關連結裡的類別介紹。

識別項

所有只包含英文字元( azAZ )、數字( 09 )、與 _ 的字串都算是一個識別項。另外,識別項不可以以數字開頭。識別項的大小寫有別( fooFOO 是不同的)。

關鍵字

下面是 GDScript 所支援的關鍵字列表。這些單字是保留字(符記,Token),所以不能當作識別項來用。有些運算子(如 innotand 、或是 or )與下一個章節會提到的內建型別的名稱也是保留字。

如果你想瞭解一下的話,關鍵字定義在 GDScript Tokenizer 裡。

關鍵字

說明

if

請參考 if/else/elif

elif

請參考 if/else/elif

else

請參考 if/else/elif

for

請參考 for

while

請參考 while

match

請參考 match

break

跳出目前的 for 或是 while 迴圈。

continue

馬上跳至 forwhile 迴圈的下一個迭代。

pass

當語法上需要有敘述句但不需要執行任何東西的時候可以使用。如:空函式。

return

從函式裡回傳數值。

class

定義一個內部類別。

class_name

為腳本定義一個類別名稱,以及一個可選的圖示。

extends

定義目前類別所要繼承的類別。

is

測試一個變數是否為繼承自給定的類別,或判斷其是否為指定的內建型別。

as

嘗試轉換為指定型別。

self

參照目前的類別實體。

tool

在編輯器中執行腳本。

signal

定義一個訊號。

func

定義一個函式。

static

定義一個靜態函式。不能用來定義靜態成員變數。

const

定義一個常數。

enum

定義一個 Enum(列舉型)。

var

定義一個變數。

onready

一旦腳本所附加的節點以及其子節點成為場景樹的一部分後,初始化變數。

export

將變數與變數所附加的資源一起保存,並令其在編輯器中可見與可修改。

setget

為變數定義 Setter 與 Getter 函式。

breakpoint

編輯器輔助功能,除錯工具中斷點。

preload

預先載入一個類別或變數。請參閱 以類別作為資源

yield

協同程式 (Coroutine) 支援。請參考 使用 yield 撰寫協同程式

assert

判定一個條件,當判定失敗的時候記錄錯誤。在非除錯用建置中會忽略。請參考 Assert 關鍵字

remote

網路 RPC 註解。請參考 高階多玩家說明文件

master

網路 RPC 註解。請參考 高階多玩家說明文件

puppet

網路 RPC 註解。請參考 高階多玩家說明文件

remotesync

網路 RPC 註解。請參考 高階多玩家說明文件

mastersync

網路 RPC 註解。請參考 高階多玩家說明文件

puppetsync

網路 RPC 註解。請參考 高階多玩家說明文件

PI

PI(圓周率)常數。

TAU

TAU 常數。

INF

無窮大常數。用於比較。

NAN

NAN(Not a Number,不是數字)常數。用於比較。

運算子

下面是支援的運算子與其優先度。

運算子

描述

x[index]

陣列索引(最高優先度)

x.attribute

屬性參照

foo()

呼叫函式

is

檢查實體型別

~

按位元 (Bitwise) NOT(非)

-x

負/一元否定

* / %

乘法/除法/餘數

這些運算子的行為與 C++ 中一樣。整數的除法會被截斷而不是回傳小數,而 % 運算子只可用於整數(浮點數用「fmod」)

+

加法/陣列的串聯

-

減法

<< >>

位元移位

&

按位元 AND(與)

^

按位元 XOR (互斥或)

|

按位元 OR(或)

< > == != >= <=

比較

in

檢查內容

! not

布林 NOT

and &&

布林 AND

or ||

布林 OR

if x else

三元 if/else

as

型別轉換

= += -= *= /= %= &= |=

賦值(最低優先度)

字面值

字面值

型別

45

10 進位整數

0x8f51

16 進位整數

0b101010

2 進位整數

3.1458.1e-10

浮點數(實數)

"Hello""Hi"

字串

"""Hello"""

多行字串

@"Node/Label"

NodePath 或 StringName

$NodePath

get_node("NodePath") 的簡寫

整數與浮點數可以用 _ 來分隔數字,讓數字的可讀性更高。下列這些數字格式都是有效的:

12_345_678  # Equal to 12345678.
3.141_592_7  # Equal to 3.1415927.
0x8080_0000_ffff  # Equal to 0x80800000ffff.
0b11_00_11_00  # Equal to 0b11001100.

註解

# 開始到行未的所有東西都會被忽略並當作註解。

# This is a comment.

內建型別

內建型別按堆疊配置 (Stack-allocated) 。傳遞時只傳遞值。也就是說每次賦值或作為參數傳給函式的時候都會建立一份複製。唯一的例外是 Array (陣列)與 Dictionary (字典),這兩種型別是以參照傳遞的,所以內容共用的。(如 PoolByteArray 等的 Pooled Array 仍以值傳遞。)

基礎內建型別

GDScript 的變數可以被指派為多種內建型別。

null

null 是一個沒有包含任何資訊的空資料型別,不能指派為其他任何的值。

bool

「布林 (Boolean)」的縮寫,只會是 truefalse

int

「整數 (Interger)」的縮寫。可以保存整數(正數與負數)。可以儲存 64 位元的值,相當於 C++ 的「int64_t」。

float

使用浮點數值,儲存包含小數的實數。保存為 64 位元的數值,相當於 C++ 中的「倍精確 (double)」型別。注意:目前,如 Vector2、Vector3、與 PoolRealArray 資料結構都儲存 32 位元的單精確「float(浮點)」值。

String

Unicode 格式 的字串。字串可以包含下列逸出序列:

逸出序列

會被解析為

\n

換行 (LF)

\t

水平 TAB 字元

\r

歸位字元

\a

警告 (警示嗶聲/鈴聲)

\b

退格鍵

\f

Formfeed 分頁字元

\v

縱向 TAB 字元

\"

雙引號

\'

單引號

\\

反斜線

\uXXXX

Unicode 字碼指標 XXXX (16進位,不區分大小寫)

GDScript 亦支援 GDScript 格式化字串

內建向量型別

Vector2

2D 向量包含 xy 欄位。也能用與陣列一樣的方式存取。

Rect2

2D 矩形包含兩個向量欄位 position (位置)與 size (大小)。也包含了一個 end 欄位,為 position + size

Vector3

3D 向量包含 xy 、與 z 欄位。也能用與陣列一樣的方式存取。

Transform2D

用於 2D 幾何變換的 3×2 矩陣。

Plane

包含 normal 向量欄位與 d 常量距離的標準形式的 3D 平面型別。

Quat

四元數 (Quaternion) 是一種用於表示 3D 旋轉的資料型別。進行內插旋轉時很有用。

AABB

座標軸對齊定界框 (AABB, Axis-aligned Bounding Box),或稱為 3D 框 (3D Box),包含了兩個向量欄位: position (位置)與 size (大小)。也包含了一個 end 欄位,即為 position + size

Basis

用於 3D 旋轉與縮放的 3x3 矩陣。包含了三個向量欄位( xy 、與 z ),一樣可以視為 3D 向量的陣列的來存取。

Transform

3D 變換包含了一個 Basis 欄位 basis 以及一個 Vector3 欄位 origin

引擎內建型別

Color

色彩資料型別包含 rgb 、與 a 欄位。也可以存取 hs 、與 v ,代表色相 (Hue) /飽和度 (Saturation)/明度 (Value)。

NodePath

主要用於場景系統中,代表到一個節點的已編譯路徑。也可以賦值為陣列或是從陣列轉換過來。

RID

資源 ID (RID, Resource ID)。伺服器使用通用 RID 來參照不透明資料。

Object

所有非內建型別的基礎類別。

內建容器型別

Array

可包含任意物件型別,也包含其他陣列或字典型別(詳見下方)。陣列的大小可以動態調整。陣列的索引從 0 開始。若使用負數索引則自尾端開始。

var arr = []
arr = [1, 2, 3]
var b = arr[1] # This is 2.
var c = arr[arr.size() - 1] # This is 3.
var d = arr[-1] # Same as the previous line, but shorter.
arr[0] = "Hi!" # Replacing value 1 with "Hi!".
arr.append(4) # Array is now ["Hi!", 2, 3, 4].

為求速度,GDScript 的陣列在記憶體中只會以線性進行分配。對於大型陣列(超過數萬元素),可能會導致記憶體片段化。若有需要考慮這類情況,可以使用其他特殊類型的陣列。這類型別只能存放單一資料型別、能避免記憶體片段化且使用更少記憶體。但這些型別具原子性且速度比通用陣列還來的慢。所以只推薦用於有大量資料的情況:

Dictionary

可以包含以獨立 Key 參照數值的關聯式容器。

var d = {4: 5, "A key": "A value", 28: [1, 2, 3]}
d["Hi!"] = 0
d = {
    22: "value",
    "some_key": 2,
    "other_key": [2, 3, 4],
    "more_key": "Hello"
}

也支援 Lua 風格的 Table 語法。Lua 風格使用 = 而非 : ,字串 Key 不需要使用引號(可以少寫一點)。但這種寫法的 Key 不能以數字開頭(就像 GDScript 的識別項)。

var d = {
    test22 = "value",
    some_key = 2,
    other_key = [2, 3, 4],
    more_key = "Hello"
}

要往現有字典新增 Key,只需要像存取現有 Key 一樣存取,然後賦值:

var d = {} # Create an empty Dictionary.
d.waiting = 14 # Add String "waiting" as a key and assign the value 14 to it.
d[4] = "hello" # Add integer 4 as a key and assign the String "hello" as its value.
d["Godot"] = 3.01 # Add String "Godot" as a key and assign the value 3.01 to it.

var test = 4
# Prints "hello" by indexing the dictionary with a dynamic key.
# This is not the same as `d.test`. The bracket syntax equivalent to
# `d.test` is `d["test"]`.
print(d[test])

備註

方括號語法不僅可用於 Dictionary,也可用來存取任何 Object 的屬性。請記得當嘗試存取不存在的屬性時會導致腳本錯誤。要避免錯誤,請使用 Object.get()Object.set() 替代。

資料

變數

變數可以作為類別成員或函式的區域變數存在。變數使用 var 關鍵字建立,並且可選擇性地在初始化時賦值。

var a # Data type is 'null' by default.
var b = 5
var c = 3.8
var d = b + c # Variables are always initialized in order.

變數也可選擇性地指定型別。當指定型別,變數將強制必須維持相同型別,當試著指派不相容的型別會造成錯誤。

在變數宣告時在變數名稱後使用 : (冒號)接上型別來執行型別。

var my_vector2: Vector2
var my_node: Node = Sprite.new()

若變數在宣告時即進行初始化,則型別會自動推定,故可省略型別名稱:

var my_vector2 := Vector2() # 'my_vector2' is of type 'Vector2'.
var my_node := Sprite.new() # 'my_node' is of type 'Sprite'.

型別推定僅可於指派的值有定義型別時發生,否則將發生錯誤。

有效的型別為:

  • 內建型別(Array、Vector2、int、String…等)。

  • 引擎類別(Node、Resource、Reference…等)。

  • 常數名稱,若該常數包含腳本資源(若宣告 const MyScript = preload("res://my_script.gd") 則可使用 MyScript )。

  • 在相同腳本內的其他類別,尊重作用域( InnerClass.NestedClass 若在 class InnerClass 內宣告 class NextedClass 則為相同作用域)。

  • 使用 class_name 關鍵字宣告的腳本類別。

型別轉換

當指派值給有型別的變數時,該值必須有相容的型別。若有需要強制轉換值為特定型別,特別是物件型別時,可以使用型別轉換運算子 as

在物件型別間進行型別轉換時,若值為相同型別或該轉換型別的子型別時,取得的結果將為相同型別。

var my_node2D: Node2D
my_node2D = $Sprite as Node2D # Works since Sprite is a subtype of Node2D.

若該值非子型別,則轉換操作會導致 null 值。

var my_node2D: Node2D
my_node2D = $Button as Node2D # Results in 'null' since a Button is not a subtype of Node2D.

對於內建型別,若可能的話將強制轉換,否則引擎將產生錯誤。

var my_int: int
my_int = "123" as int # The string can be converted to int.
my_int = Vector2() as int # A Vector2 can't be converted to int, this will cause an error.

當與場景樹互動時,型別轉換對於取得更加的型別安全 (Type-Safe) 變數也很有用:

# Will infer the variable to be of type Sprite.
var my_sprite := $Character as Sprite

# Will fail if $AnimPlayer is not an AnimationPlayer, even if it has the method 'play()'.
($AnimPlayer as AnimationPlayer).play("walk")

常數

常數是一種無法在遊戲執行時更改的數值。常數的值必須在編譯時期就已定好。使用 const 關鍵字即可為常數設定名稱。若在常數定義後試圖為常數賦值會回傳錯誤。

我們建議,當有不會更改的值時,一概使用常數。

const A = 5
const B = Vector2(20, 20)
const C = 10 + 20 # Constant expression.
const D = Vector2(20, 30).x # Constant expression: 20.
const E = [1, 2, 3, 4][0] # Constant expression: 1.
const F = sin(20) # 'sin()' can be used in constant expressions.
const G = x + 20 # Invalid; this is not a constant expression!
const H = A + 20 # Constant expression: 25 (`A` is a constant).

雖然常數的型別時依據指派的值來推定的,但也可以加上明確的型別指定:

const A: int = 5
const B: Vector2 = Vector2()

當指派的值的型別不相容時將產生錯誤。

備註

由於陣列與字典是以參照來傳遞的,因此常數為「扁平」的。這表示當宣告常數型陣列或字典,之後依然可以修改其內容。只是這個常數之後不能指派為其他值而已。

Enum 列舉類型

Enum 基本上是常數的簡寫,當需要給一些常數指派連續的整數時滿有用的。

若給 Enum 指派名稱,則會將所有的鍵都放入以該名稱為名的字典常數內。

重要

在 Godot 3.1 與之後的版本中,在有名稱的 Enum 中的鍵並不會註冊為全域常數。存取時必須要在前面加上 Enum 的名稱( Name.KEY )。請參考下方範例。

enum {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT}
# Is the same as:
const TILE_BRICK = 0
const TILE_FLOOR = 1
const TILE_SPIKE = 2
const TILE_TELEPORT = 3

enum State {STATE_IDLE, STATE_JUMP = 5, STATE_SHOOT}
# Is the same as:
const State = {STATE_IDLE = 0, STATE_JUMP = 5, STATE_SHOOT = 6}
# Access values with State.STATE_IDLE, etc.

函式

函式必須屬於一個 方法 。尋找變數的作用域優先度如下:區域 -> 類別成員 -> 全域。 self 變數永遠可用,為一個用來存取類別成員的選項,但並非強制必須使用(且與 Python 不同,GDScript 裡 不可 傳送為函式的第一個引數)。

func my_function(a, b):
    print(a)
    print(b)
    return a + b  # Return is optional; without it 'null' is returned.

函式可以於任何時機 return 。預設的回傳值為 null

函式也可以給引數與回傳值指定型別。引數的型別可用與變數類似的方法指定:

func my_function(a: int, b: String):
    pass

若函式引數有預設值,則可推斷型別:

func my_function(int_arg := 42, String_arg := "string"):
    pass

函式的回傳值可在引數列表後以箭頭符記 (->) 來指定:

func my_int_function() -> int:
    return 0

有回傳型別的函式 必須 回傳適當的值。將型別設為 void 則表示函式不回傳任何東西。Void 函式可以使用 return 關鍵字來提早回傳,但無法回傳任何值。

func void_function() -> void:
    return # Can't return a value

備註

非 Void 函式必須 總是 回傳一個值。若程式碼中有分歧陳述式(如 if/else 結構),則所有可能的路徑都必須要有 return。如,若在 if 內有 returnif 後卻沒有,則編輯器會產生錯誤,因為若區塊程式碼未被執行,則該函式將不會有有效的回傳值。

函數參照

與 Python 相反,GDScript 中的函式 不是 一級物件。這表示函式無法儲存於變數內、作為引數傳遞給另一個函式、或是從其他函式中回傳。這是由於考量到效能。

要在執行時以名稱參照函式(如:儲存於變數、或作為引數傳遞給另一個函式),則必須使用 callfuncref Helper:

# Call a function by name in one step.
my_node.call("my_function", args)

# Store a function reference.
var my_func = funcref(my_node, "my_function")
# Call stored function reference.
my_func.call_func(args)

靜態函式

函式可以宣告為靜態。當函式被宣告為靜態,將無法存取實體的成員變數或 self 。主要適用於製作函式庫或 Helper 函式:

static func sum2(a, b):
    return a + b

稱述句與流程控制

稱述句為標準,可以為複製、函式呼叫、流程結構…等(相見下方)。 作為稱述句分隔字元的 ; 是完全可選的。

if/else/elif

簡單的條件判斷可以使用 if/else/elif 語法來建立。可以在條件周圍加上括號,但並非必要。由於基於 Tab 排版的性質,可以使用 elif 代替 else/if 來維持縮排的等級。

if [expression]:
    statement(s)
elif [expression]:
    statement(s)
else:
    statement(s)

較短的陳述式可以與條件寫在同一行內:

if 1 + 1 == 2: return 2 + 2
else:
    var x = 3 + 3
    return x

有時候我們會想依據布林表達式來指派不同的初始值。這時候可以使用三元表達式:

var x = [value] if [expression] else [value]
y += 3 if y < 10 else -1

while

可以使用 while 語法來建立簡單的迴圈。迴圈可以使用 break 來中斷或是使用 continue 來繼續:

while [expression]:
    statement(s)

for

要在如陣列或表格中的一段範圍內迭代,可以使用 for 迴圈。當在陣列中迭代時,目前的陣列元素會儲存於迴圈變數中。當於字典中迭代時,保存在迴圈變數內的則會是 索引

for x in [5, 7, 11]:
    statement # Loop iterates 3 times with 'x' as 5, then 7 and finally 11.

var dict = {"a": 0, "b": 1, "c": 2}
for i in dict:
    print(dict[i]) # Prints 0, then 1, then 2.

for i in range(3):
    statement # Similar to [0, 1, 2] but does not allocate an array.

for i in range(1, 3):
    statement # Similar to [1, 2] but does not allocate an array.

for i in range(2, 8, 2):
    statement # Similar to [2, 4, 6] but does not allocate an array.

for c in "Hello":
    print(c) # Iterate through all characters in a String, print every letter on new line.

for i in 3:
    statement # Similar to range(3)

for i in 2.2:
    statement # Similar to range(ceil(2.2))

match

match 陳述式用於在程式內分歧執行。與其他許多語言內的 switch 表達式相同,但有些額外的功能。

基本語法:

match [expression]:
    [pattern](s):
        [block]
    [pattern](s):
        [block]
    [pattern](s):
        [block]

給熟悉 switch 陳述式的人的速成課程

  1. switch 取代為 match

  2. 移除 case

  3. 移除所有的 break 。若不想預設使用 break ,可以使用 continue 來往下執行。

  4. default 改為底線。

流程控制

搜尋模式會按照由上到下的順序來配對。若與搜尋模式相符,則會執行第一個對應的區塊。之後會繼續執行 match 陳述式下方的程式。若想往下執行,可以使用 continue 來停止執行目前的區塊,並往下搜尋其他符合的搜尋模式。

有六種搜尋模式:

  • 常數

    基本常數型別,如數字與字串:

    match x:
        1:
            print("We are number one!")
        2:
            print("Two are better than one!")
        "test":
            print("Oh snap! It's a string!")
    
  • 變數

    尋找與變數或列舉類型 (Enum) 相符合的內容:

    match typeof(x):
        TYPE_REAL:
            print("float")
        TYPE_STRING:
            print("text")
        TYPE_ARRAY:
            print("array")
    
  • 萬用字元

    尋找所有東西。寫成一個底線。

    用法與其他語言中 switchdefault 相同:

    match x:
        1:
            print("It's one!")
        2:
            print("It's one times two!")
        _:
            print("It's not 1 or 2. I don't care to be honest.")
    
  • 繫結

    繫結搜尋模式使用一個新變數。與萬用字元模式一樣會配對到所有東西,同時將該值指派給一個變數。對於陣列或字典模式很有用:

    match x:
        1:
            print("It's one!")
        2:
            print("It's one times two!")
        var new_var:
            print("It's not 1 or 2, it's ", new_var)
    
  • 陣列

    搜尋陣列。陣列中的每一個元素也都是搜尋模式,可以嵌套使用。

    會先檢查陣列的長度,長度必須與搜尋模式相同,否則將不會視為相符。

    開放結尾陣列 :通過將最後一個子搜尋模式設為 .. 可配對大於模式的陣列。

    所有子搜尋模式都以逗號分隔。

    match x:
        []:
            print("Empty array")
        [1, 3, "test", null]:
            print("Very specific array")
        [var start, _, "test"]:
            print("First element is ", start, ", and the last is \"test\"")
        [42, ..]:
            print("Open ended array")
    
  • 字典

    與陣列模式相同。所有索引鍵都必須為常數搜尋模式。

    會先檢查字典的大小,必須與搜尋模式相同,否則將不會視為相符。

    開放結尾字典 :通過將最後一個子搜尋模式設為 .. 可允許配對大於搜尋模式的字典。

    所有子模式都必須以逗號分隔。

    若不指定值,則只會檢查索引鍵是否存在。

    值搜尋模式使用 : 來與索引值搜尋模式區分。

    match x:
        {}:
            print("Empty dict")
        {"name": "Dennis"}:
            print("The name is Dennis")
        {"name": "Dennis", "age": var age}:
            print("Dennis is ", age, " years old.")
        {"name", "age"}:
            print("Has a name and an age, but it's not Dennis :(")
        {"key": "godotisawesome", ..}:
            print("I only checked for one entry and ignored the rest")
    
  • 多重搜尋模式

    也可以逗號區分來指定多個搜尋模式。這些搜尋模式中不允許有任何繫結。

    match x:
        1, 2, 3:
            print("It's 1 - 3")
        "Sword", "Splash potion", "Fist":
            print("Yep, you've taken damage")
    

類別

預設情況下,所有的腳本檔案都是沒有命名的類別。此時,要參照這些類別的唯一方法就是使用相對路徑或絕對路徑的檔案位置。如,若腳本檔案命名為 character.gd

# Inherit from 'Character.gd'.

extends "res://path/to/character.gd"

# Load character.gd and create a new node instance from it.

var Character = load("res://path/to/character.gd")
var character_node = Character.new()

但也可以設定類別名稱,並將該類別註冊為 Godot 編輯器中的新型別。使用 class_name 來設定類別名稱。也可以選擇性地在後方加上逗號與一個圖片路徑,該圖片會用作圖示。新定義的類別與圖示會在編輯器中顯示:

# Item.gd

extends Node
class_name Item, "res://interface/icons/item.png"
../../../_images/class_name_editor_register_example.png

警告

If the script is located in the res://addons/ directory, class_name will only cause the node to show up in the Create New Node dialog if the script is part of an enabled editor plugin. See Making plugins for more information.

下列為類別檔案範例:

# Saved as a file named 'character.gd'.

class_name Character


var health = 5


func print_health():
    print(health)


func print_this_script_three_times():
    print(get_script())
    print(ResourceLoader.load("res://character.gd"))
    print(Character)

備註

Godot 的類別語法很簡短,類別中只有成員變數與成員函式。函式可以為靜態函式,但變數則不可定義為靜態。同樣地,Godot 會在每次實體化時都初始化變數,包含陣列與字典。這樣一來能讓腳本在使用者不知情的情況下於不同的執行續中初始化,這正是執行緒安全的精神。

繼承

類別 (儲存為檔案) 可以繼承自:

  • 全域類別。

  • 另一個類別檔案。

  • 在另一個類別檔案中的內部類別。

無法多重繼承。

使用 extends 關鍵字來繼承:

# Inherit/extend a globally available class.
extends SomeClass

# Inherit/extend a named class file.
extends "somefile.gd"

# Inherit/extend an inner class in another file.
extends "somefile.gd".SomeInnerClass

可使用 is 關鍵字來檢查某個類別是否繼承了指定的實體:

# Cache the enemy class.
const Enemy = preload("enemy.gd")

# [...]

# Use 'is' to check inheritance.
if entity is Enemy:
    entity.apply_damage()

若要呼叫 母類別 中的函式 (也就是目前類別 extend 的類別),可在函式名稱前加上 .

.base_func(args)

當目前類別定義了與母類別中相同名稱的函式時此方法特別適用。若想呼叫母類別中的方法則可在前面加上 . (類似其他語言中的 super 關鍵字):

func some_func(x):
    .some_func(x) # Calls the same function on the parent class.

備註

_init 等預設函式、以及大多數的通知如 _enter_tree, _exit_tree, _process, _physics_process …等都會自動呼叫母類別中的函式。當多載這些函式時不需要明確呼叫。

Class constructor

類別建置函式 _init 會在類別實體化時呼叫。如同剛才提到的,繼承的類別會自動呼叫母類別建置函式,所以通常不需要明確呼叫 ._init()

上方的 .some_func 例子中可以看到與呼叫一般函式不同,若繼承類別的建置函式有引數,則會這樣傳遞:

func _init(args).(parent_args):
   pass

通過範例能更好理解,來看看下面這個情況:

# State.gd (inherited class)
var entity = null
var message = null


func _init(e=null):
    entity = e


func enter(m):
    message = m


# Idle.gd (inheriting class)
extends "State.gd"


func _init(e=null, m=null).(e):
    # Do something with 'e'.
    message = m

還有幾件事需要注意:

  1. 若被繼承類別 (State.gd) 定義了需要引數的建置函式 _init (此例中為 e) ,則繼承類別 (Idle.gd) 必須 也定義 _init 並將適當的參數傳遞從 State.gd 中傳遞給 _init

  2. Idle.gd 的引數數量可與母類別 State.gd 不同。

  3. 在上方的例子中,傳遞給 State.gd 建置函式的 e 與傳遞給 Idle.gde 相同。

  4. If Idle.gd's _init constructor takes 0 arguments, it still needs to pass some value to the State.gd parent class, even if it does nothing. This brings us to the fact that you can pass literals in the base constructor as well, not just variables, e.g.:

    # Idle.gd
    
    func _init().(5):
        pass
    

內類別

類別檔可以再包含內類別。內類別使用 class 關鍵字來定義。這些內部類別使用 類別名稱.new() 函式來實體化。

# Inside a class file.

# An inner class in this class file.
class SomeInnerClass:
    var a = 5


    func print_value_of_a():
        print(a)


# This is the constructor of the class file's main class.
func _init():
    var c = SomeInnerClass.new()
    c.print_value_of_a()

以類別當作資源

保存為檔案的類別可視為 資源 。這些類別必須從硬碟中載入後才可在其他類別中存取。可使用 loadpreload 函式來載入(詳見下方)。通過呼叫類別物件上的 new 方法來實體化載入的類別:

# Load the class resource when calling load().
var my_class = load("myclass.gd")

# Preload the class only once at compile time.
const MyClass = preload("myclass.gd")


func _init():
    var a = MyClass.new()
    a.some_function()

匯出

備註

有關匯出的說明文件已移至 GDScript 匯出

Setter 與 Getter

若能知道類別成員變數何時更改與為何更改可能很有用,我們也可能會希望以某種方法來封裝成員變數的存取。

為此,GDScript 內提供了使用 setget 關鍵字的 setter/getter 語法。 setget 的使用方法為直接接在變數定義之後:

var variable = value setget setterfunc, getterfunc

一旦 variable 的值被 外部 的來源更改 (指非類別內部),則會呼叫 setter 函式 (上方例子中為 setterfunc )。setter 函式是在數值變更 之前 呼叫的。 setter 方法必須決定要如何處理新的值。同樣地,存取 variable 值時, getter 函式 (上方例子中為 getterfunc) 必須決定要 return 什麼值。範例如下:

var my_var setget my_var_set, my_var_get


func my_var_set(new_value):
    my_var = new_value


func my_var_get():
    return my_var # Getter must return a value.

settergetter 方法可省略:

# Only a setter.
var my_var = 5 setget my_var_set
# Only a getter (note the comma).
var my_var = 5 setget ,my_var_get

Setter 與 Getter 對於在工具腳本或插件中 匯出變數 時檢查輸入很有用。

如同剛才提到的, 區域 存取 不會 觸發 setter 與 getter。說明如下:

func _init():
    # Does not trigger setter/getter.
    my_integer = 5
    print(my_integer)

    # Does trigger setter/getter.
    self.my_integer = 5
    print(self.my_integer)

工具模式

預設情況下,腳本並不會在編輯器中執行,且只能更改匯出的屬性。而某些情況下,我們會想在編輯器中執行腳本(只要這些腳本不會執行遊戲程式碼或手動避免執行遊戲腳本即可)。為此,可在腳本檔案的頂部加上 tool 關鍵字:

tool
extends Button


func _ready():
    print("Hello")

請參考 Running code in the editor 以瞭解詳情。

警告

在工具腳本中以 queue_free()free() 釋放節點時請特別謹慎(特別是該節點為腳本擁有者時)。由於工具腳本會在編輯器中執行程式碼,若錯誤使用這些方法可能會使編輯器當掉。

記憶體管理

If a class inherits from Reference, then instances will be freed when no longer in use. No garbage collector exists, just reference counting. By default, all classes that don't define inheritance extend Reference. If this is not desired, then a class must inherit Object manually and must call instance.free(). To avoid reference cycles that can't be freed, a WeakRef function is provided for creating weak references. Here is an example:

extends Node

var my_node_ref

func _ready():
    my_node_ref = weakref(get_node("MyNode"))

func _this_is_called_later():
    var my_node = my_node_ref.get_ref()
    if my_node:
        my_node.do_something()

或者,不適用參照時,亦可使用 is_instance_valid(實體) 來判斷一個物件是否已被釋放。

訊號

訊號是用來從物件發送可讓其他物件做出反應的一項工具。若要為類別建立自定訊號,請使用 signal 關鍵字。

extends Node


# A signal named health_depleted.
signal health_depleted

備註

訊號是一種 回呼 機制。訊號也充當了 Observer 角色 (一種常見的程式設計模式)。更多資訊請參考 Game Programming Patterns 電子書中的 Observer tutorial (英語)

可通過與內建節點訊號 (如 ButtonRigidBody ) 相同的方法來將自定訊號連接至方法。

在下方的例子中,我們將將 health_depleted 訊號從 Character 節點連接至 Game 節點。當 Character 節點送出訊號時會呼叫 Game 節點的 _on_Character_health_depleted

# Game.gd

func _ready():
    var character_node = get_node('Character')
    character_node.connect("health_depleted", self, "_on_Character_health_depleted")


func _on_Character_health_depleted():
    get_tree().reload_current_scene()

也可以與訊號一起送出任意數量的引數。

下列範例說明了如何有效使用這個功能。假設螢幕上有一個血槽,可以動畫顯示生命值的改變,但同時我們也想在場景樹中將使用者界面與玩家分開來。

Character.gd 腳本中,我們定義了 health_changed 訊號,並使用 Object.emit_signal() 送出訊號。而在場景樹中更高的 Game 節點中,我們使用 Object.connect() 方法來將訊號與 LifeBar 連接起來:

# Character.gd

...
signal health_changed


func take_damage(amount):
    var old_health = health
    health -= amount

    # We emit the health_changed signal every time the
    # character takes damage.
    emit_signal("health_changed", old_health, health)
...
# Lifebar.gd

# Here, we define a function to use as a callback when the
# character's health_changed signal is emitted.

...
func _on_Character_health_changed(old_value, new_value):
    if old_value > new_value:
        progress_bar.modulate = Color.red
    else:
        progress_bar.modulate = Color.green

    # Imagine that `animate` is a user-defined function that animates the
    # bar filling up or emptying itself.
    progress_bar.animate(old_value, new_value)
...

備註

要使用訊號,則類別必須繼承 Object 或是任何繼承了 Object 的型別,如 Node, KinematicBody, Control …等。

我們在 Game 中同時取得了 CharacterLifebar 節點,然後將送出訊號的 Characeter 連接到接收器,也就使本例中的 Lifebar 節點。

# Game.gd

func _ready():
    var character_node = get_node('Character')
    var lifebar_node = get_node('UserInterface/Lifebar')

    character_node.connect("health_changed", lifebar_node, "_on_Character_health_changed")

這樣一來便能讓 Lifebar 對生命值的更改做出反應而無需與 Character 節點耦合。

可於訊號定義後方的括號中填寫可選的引數名稱:

# Defining a signal that forwards two arguments.
signal health_changed(old_value, new_value)

這些引數會在編輯器的節點 Dock 中顯示,Godot 會使用這些引數來產生回呼函式。但,送出訊號時還是可以送出任意數量的引數,可自行決定是否要送出正確數量的引數。

../../../_images/gdscript_basics_signals_node_tab_1.png

在 GDScript 中可繫結陣列來連接訊號與方法。訊號送出後,回呼函式會收到綁定的值。每個連線繫結的引數都是獨立的,引數值則會保持不變。

當無法在送出的訊號中取得所有需要的資料時,可以使用這個陣列來為連接加上額外的常數資訊。

以上方的範例為基礎,假設我們需要在螢幕上顯示各個角色收到傷害的記錄,如 Player1 受到了 22 點傷害 之類的。 health_changed 訊號並未提供收到傷害的角色名稱,所以我們可以在連接訊號至遊戲界面時將角色名稱放在繫結的陣列引數上:

# Game.gd

func _ready():
    var character_node = get_node('Character')
    var battle_log_node = get_node('UserInterface/BattleLog')

    character_node.connect("health_changed", battle_log_node, "_on_Character_health_changed", [character_node.name])

BattleLog 節點會接收繫結陣列中的所有元素作為額外引數:

# BattleLog.gd

func _on_Character_health_changed(old_value, new_value, character_name):
    if not new_value <= old_value:
        return

    var damage = old_value - new_value
    label.text += character_name + " took " + str(damage) + " damage."

使用 yield 撰寫協同程式

GDScript 中可通過 yield 內建函式來支援 協同程式 (英文) <https://en.wikipedia.org/wiki/Coroutine> 。呼叫 yield() 會馬上從目前的函式回傳,並將目前函式的狀態凍結來作為回傳值。在回傳的物件上呼叫 resume() 則會繼續執行函式並回傳函式的回傳值。繼續執行後,狀態物件即變為無效。範例如下:

func my_func():
    print("Hello")
    yield()
    print("world")


func _ready():
    var y = my_func()
    # Function state saved in 'y'.
    print("my dear")
    y.resume()
    # 'y' resumed and is now an invalid state.

將印出:

Hello
my dear
world

也可以在 yield()resume() 間傳遞數值,例如:

func my_func():
    print("Hello")
    print(yield())
    return "cheers!"


func _ready():
    var y = my_func()
    # Function state saved in 'y'.
    print(y.resume("world"))
    # 'y' resumed and is now an invalid state.

將印出:

Hello
world
cheers!

使用多個 yield 時請記得保存新函式的狀態:

func co_func():
    for i in range(1, 5):
        print("Turn %d" % i)
        yield();


func _ready():
    var co = co_func();
    while co is GDScriptFunctionState && co.is_valid():
        co = co.resume();

協同程式與訊號

當與訊號一起使用時才可發揮 yield 真正的實力。 yield 可接受兩個引數,一個物件與一個訊號。當收到訊號後,即會繼續開始執行。請參考下列例子:

# Resume execution the next frame.
yield(get_tree(), "idle_frame")

# Resume execution when animation is done playing.
yield(get_node("AnimationPlayer"), "animation_finished")

# Wait 5 seconds, then resume execution.
yield(get_tree().create_timer(5.0), "timeout")

協同程式會在自己轉變為無效狀態後發出 completed 訊號,如:

func my_func():
    yield(button_func(), "completed")
    print("All buttons were pressed, hurray!")


func button_func():
    yield($Button0, "pressed")
    yield($Button1, "pressed")

my_func 只會在兩個按鈕都按下後才繼續執行。

也可以在物件送出訊號後取得訊號的引數:

# Wait for when any node is added to the scene tree.
var node = yield(get_tree(), "node_added")

當有多個引數時,yield 會回傳包含引數的陣列:

signal done(input, processed)

func process_input(input):
    print("Processing initialized")
    yield(get_tree(), "idle_frame")
    print("Waiting")
    yield(get_tree(), "idle_frame")
    emit_signal("done", input, "Processed " + input)


func _ready():
    process_input("Test") # Prints: Processing initialized
    var data = yield(self, "done") # Prints: waiting
    print(data[1]) # Prints: Processed Test

若不確定某個函式會不會 yield,或是某個函式是否會 yield 多次,則可以設定 yield 條件為 completed 訊號:

func generate():
    var result = rand_range(-1.0, 1.0)

    if result < 0.0:
        yield(get_tree(), "idle_frame")

    return result


func make():
    var result = generate()

    if result is GDScriptFunctionState: # Still working.
        result = yield(result, "completed")

    return result

這樣一來便確保該函式會回傳應該要回傳的值而不管內部是否有協同程式。請注意,不需要使用 while ,因為 completed 訊號只會在函式不再 yield 後才送出。

onready keyword

使用節點時,我們通常會想以變數來參照到場景的某個部分。由於場景只有在進入有效場景樹後才能保證有正確配置,所以在 Node._ready() 呼叫後才能取得子節點。

var my_label


func _ready():
    my_label = get_node("MyLabel")

這麼做優點麻煩,特別是當節點與外部參照很多時。為此,GDScript 中有 onready 關鍵字,會推遲成員變數的初始化,直到 _ready() 呼叫後。我們可以將上述代碼用這樣一行程式碼來取代:

onready var my_label = get_node("MyLabel")

Assert 關鍵字

assert 關鍵字可以用來在除錯建置中檢查條件。這些斷言會在非除錯建置中忽略。這表示作為引數傳遞的運算式在以發行模式匯出的專案中將不會被計算。因此,斷言 不可 包含有副作用的運算式。否則,腳本會因為專案是否於除錯建置中而有不同的行為。

# Check that 'i' is 0. If 'i' is not 0, an assertion error will occur.
assert(i == 0)

若從編輯器中執行專案,則專案會在斷言發生錯誤時暫停。