GDScript 風格指南

本風格指南列出了撰寫優雅 GDScript 的公約。本指南的目標是要鼓勵寫出乾淨、可閱讀的程式碼,並促進在專案、討論與教學中使用一貫的樣式。希望此一教學也能促進開發自動格式化工具。

由於 GDScript 類似 Python,本指南啟發自 Python 的 PEP 8 程式碼風格指引。

風格指南並不是一種強制規範。有時候可能會無法符合下列的某些準則,這是則需要自行判斷最佳方法,或是詢問其他開發者的意見。

一般來說,在專案中或是在團隊成員間保持程式碼風格的一致性比嚴格遵守本指南還要重要。

備註

Godot 的內建腳本編輯器預設也使用了許多本公約的規範。可以使用編輯器的協助。

下列為遵守本方針的完整類別範例:

class_name StateMachine
extends Node
## Hierarchical State machine for the player.
##
## Initializes states and delegates engine callbacks ([method Node._physics_process],
## [method Node._unhandled_input]) to the state.

signal state_changed(previous, new)

@export var initial_state: Node
var is_active = true:
    set = set_is_active

@onready var _state = initial_state:
    set = set_state
@onready var _state_name = _state.name


func _init():
    add_to_group("state_machine")


func _enter_tree():
    print("this happens before the ready method!")


func _ready():
    state_changed.connect(_on_state_changed)
    _state.enter()


func _unhandled_input(event):
    _state.unhandled_input(event)


func _physics_process(delta):
    _state.physics_process(delta)


func transition_to(target_state_path, msg={}):
    if not has_node(target_state_path):
        return

    var target_state = get_node(target_state_path)
    assert(target_state.is_composite == false)

    _state.exit()
    self._state = target_state
    _state.enter(msg)
    Events.player_state_changed.emit(_state.name)


func set_is_active(value):
    is_active = value
    set_physics_process(value)
    set_process_unhandled_input(value)
    set_block_signals(not value)


func set_state(value):
    _state = value
    _state_name = _state.name


func _on_state_changed(previous, new):
    print("state changed")
    state_changed.emit()


class State:
    var foo = 0

    func _init():
        print("Hello!")

格式

編碼與特殊字元

  • 使用 LF 換行字元來換行,而非 CRLF 或 CR。(編輯器預設

  • 每個檔案結尾都應有一個 LF 換行字元。(編輯器預設

  • 使用不含 位元組順序標記UTF-8 編碼。(編輯器預設

  • 縮排請使用 Tab 字元,勿使用空白字元。(編輯器預設

縮排

縮排等級在每個區塊都應增加。

正確例 :

for i in range(10):
    print("hello")

錯誤例 :

for i in range(10):
  print("hello")

for i in range(10):
        print("hello")

使用兩層縮排來區分連續行與一般程式碼區塊。

正確例 :

effect.interpolate_property(sprite, "transform/scale",
        sprite.get_scale(), Vector2(2.0, 2.0), 0.3,
        Tween.TRANS_QUAD, Tween.EASE_OUT)

錯誤例 :

effect.interpolate_property(sprite, "transform/scale",
    sprite.get_scale(), Vector2(2.0, 2.0), 0.3,
    Tween.TRANS_QUAD, Tween.EASE_OUT)

但本規則的例外為陣列、字典與列舉型別。使用單一縮排等級來區分連續行:

正確例 :

var party = [
    "Godot",
    "Godette",
    "Steve",
]

var character_dict = {
    "Name": "Bob",
    "Age": 27,
    "Job": "Mechanic",
}

enum Tile {
    BRICK,
    FLOOR,
    SPIKE,
    TELEPORT,
}

錯誤例 :

var party = [
        "Godot",
        "Godette",
        "Steve",
]

var character_dict = {
        "Name": "Bob",
        "Age": 27,
        "Job": "Mechanic",
}

enum Tile {
        BRICK,
        FLOOR,
        SPIKE,
        TELEPORT,
}

結尾逗號

在陣列、字典與列舉型別的最後一行末尾加上逗號。這樣一來在重構時就比較容易,且在版本控制系統中比較差異時也更簡單,因為新增新元素時不需要修改上一行。

正確例 :

var array = [
    1,
    2,
    3,
]

錯誤例 :

var array = [
    1,
    2,
    3
]

單行列表中不需要加上結尾逗號,所以下列例子中未加上逗號。

正確例 :

var array = [1, 2, 3]

錯誤例 :

var array = [1, 2, 3,]

空白行

在函式與類別定義周圍加上兩個空行:

func heal(amount):
    health += amount
    health = min(health, max_health)
    health_changed.emit(health)


func take_damage(amount, effect=null):
    health -= amount
    health = max(0, health)
    health_changed.emit(health)

在函式中則使用單一空行來區分不同的邏輯區塊。

備註

在類參考和本文件的短程式碼片段中,我們會在類和函式定義之間使用單個空行。

單行長度

每行程式碼請控制在 100 字元內。

若可以的話,儘量將每行控制在 80 個字元內。這樣一來在小螢幕或是在外部文字編輯器中左右並排開啟兩個腳本時比較容易閱讀。舉例來說,需要對照兩個版本的時候。

一行一個敘述式

為了可讀性,避免在同一行內合併多個陳述式(包含條件判斷式)。請遵守 GDScript 程式風格指引,每行只寫一個陳述式。

正確例 :

if position.x > width:
    position.x = 0

if flag:
    print("flagged")

錯誤例 :

if position.x > width: position.x = 0

if flag: print("flagged")

唯一的例外是三元運算子:

next_state = "idle" if is_on_floor() else "fall"

為可讀性格式化多行敘述

如果你的 if 敘述特別長,或者是巢狀的三元運算式時,把它們拆分成多行可以提高可讀性。因為這些連續的行仍然屬於同一個運算式,應該縮進兩級而不是一級。

GDScript 允許使用括弧或反斜線將敘述拆成多行。本風格指南傾向於使用括弧,因為重構起來更簡單。使用反斜線的話,你必須保證最後一行的末尾沒有反斜線。而如果是括弧,你就不必擔心最後一行的反斜線問題。

把條件運算式拆分成多行時,andor 關鍵字應當放在下一行的開頭,而不是上一行的結尾。

正確例 :

var angle_degrees = 135
var quadrant = (
        "northeast" if angle_degrees <= 90
        else "southeast" if angle_degrees <= 180
        else "southwest" if angle_degrees <= 270
        else "northwest"
)

var position = Vector2(250, 350)
if (
        position.x > 200 and position.x < 400
        and position.y > 300 and position.y < 400
):
    pass

錯誤例 :

var angle_degrees = 135
var quadrant = "northeast" if angle_degrees <= 90 else "southeast" if angle_degrees <= 180 else "southwest" if angle_degrees <= 270 else "northwest"

var position = Vector2(250, 350)
if position.x > 200 and position.x < 400 and position.y > 300 and position.y < 400:
    pass

避免不必要的括號

避免在運算式與條件陳述式中加上不必要的括號。除非是為了運算子的順序,否則加上括號只會降低可讀性。

正確例 :

if is_colliding():
    queue_free()

錯誤例 :

if (is_colliding()):
    queue_free()

布林運算子

為了可存取性,應優先使用英語的布林運算子:

  • 使用 and 而不是 &&

  • 使用 or 而不是 ||

  • 使用 not 而不是 !

也可以在布林運算子周圍加上括號來避免歧義。這樣一來比較長的運算式就比較好讀。

正確例 :

if (foo and bar) or not baz:
    print("condition is true")

錯誤例 :

if foo && bar || !baz:
    print("condition is true")

註解空白

一般註解(#)與文件註解(##)應在符號後加上空格,不過被註解掉的程式碼則不需加空格。另外,程式區塊註解(#region/#endregion)必須嚴格遵循該語法,因此前面不能有空格。

在一般註解與文件註解後加上空格,有助於區分純文字註解與被停用(註解掉)的程式碼。

正確例 :

# This is a comment.
#print("This is disabled code")

錯誤例 :

#This is a comment.
# print("This is disabled code")

備註

在腳本編輯器中,可以按下 Ctrl + K 來註解或取消註解選取的程式碼。此功能會於選取行的開頭自動加上或移除一個 # 符號。

Prefer writing comments on their own line as opposed to inline comments (comments written on the same line as code). Inline comments are best used for short comments, typically a few words at most:

正確例 :

# This is a long comment that would make the line below too long if written inline.
print("Example") # Short comment.

錯誤例 :

print("Example") # This is a long comment that would make this line too long if written inline.

空白字元

運算子前後以及逗號之後,請務必加上一個空格。也請避免在字典存取與函式呼叫時加上多餘的空格。唯一的例外是單行字典宣告,這種情況下建議在開頭與結尾的大括號({})內各加上一個空格,這樣可以讓字典與陣列([])在大多數字型下更容易分辨。

正確例 :

position.x = 5
position.y = target_position.y + 10
dict["key"] = 5
my_array = [4, 5, 6]
my_dictionary = { key = "value" }
print("foo")

錯誤例 :

position.x=5
position.y = mpos.y+10
dict ["key"] = 5
myarray = [4,5,6]
my_dictionary = {key = "value"}
print ("foo")

請勿用空格來讓運算式垂直對齊:

x        = 100
y        = 100
velocity = 500

引號

儘量使用雙引號,除非使用單引號能在字串中跳脫比較少的字元。請參考下方範例:

# Normal string.
print("hello world")

# Use double quotes as usual to avoid escapes.
print("hello 'world'")

# Use single quotes as an exception to the rule to avoid escapes.
print('hello "world"')

# Both quote styles would require 2 escapes; prefer double quotes if it's a tie.
print("'hello' \"world\"")

數字

不要忽略浮點數中的前置或後置 0。以免降低數字的可讀性,並難以一眼區分與整數的不同。

正確例 :

var float_number = 0.234
var other_float_number = 13.0

錯誤例 :

var float_number = .234
var other_float_number = 13.

16 進位數字使用小寫字母,因為當字母比數字矮的時候比較好閱讀數字。

正確例 :

var hex_number = 0xfb8c0b

錯誤例 :

var hex_number = 0xFB8C0B

在字面值中通過 GDScript 的底線功能來讓大數更好閱讀。

正確例 :

var large_number = 1_234_567_890
var large_hex_number = 0xffff_f8f8_0000
var large_bin_number = 0b1101_0010_1010
# Numbers lower than 1000000 generally don't need separators.
var small_number = 12345

錯誤例 :

var large_number = 1234567890
var large_hex_number = 0xfffff8f80000
var large_bin_number = 0b110100101010
# Numbers lower than 1000000 generally don't need separators.
var small_number = 12_345

命名慣例

這些命名規範遵循 Godot Engine 樣式。若不遵循這些規則,將導致你的程式碼與內建命名慣例衝突,造成不一致。以下為摘要對照表:

型別

命名規則

範例

檔案名稱

snake_case

yaml_parser.gd

類別名稱

PascalCase

class_name YAMLParser

節點名稱

PascalCase

Camera3D, Player

函式

snake_case

func load_level():

變數

snake_case

var particle_effect

訊號

snake_case

signal door_opened

常數

CONSTANT_CASE

const MAX_SPEED = 200

列舉名稱

PascalCase

enum Element

列舉成員

CONSTANT_CASE

{EARTH, WATER, AIR, FIRE}

檔案名稱

檔案名稱請用 snake_case。有命名的類別則將 PascalCase 類別名稱轉換為 snake_case:

# This file should be saved as `weapon.gd`.
class_name Weapon
extends Node
# This file should be saved as `yaml_parser.gd`.
class_name YAMLParser
extends Object

這種命名方式與 Godot 原始碼中的 C++ 檔案保持一致,另外也能從 Windows 匯出到其他平台時發生區分大小寫的問題。

類別與節點

類別與節點名稱使用 PascalCase:

extends CharacterBody3D

當將類別載入至常數或變數時也使用 PascalCase:

const Weapon = preload("res://weapon.gd")

函式與變數

函式與變數名稱請使用 snake_case:

var particle_effect
func load_level():

在需要由使用者覆寫的虛擬方法、Private 函式、Private 變數前加上一個底線 (_):

var _counter = 0
func _recalculate_path():

訊號

訊號使用過去式來命名:

signal door_opened
signal score_changed

常數與列舉型別

常數請使用 CONSTANT_CASE,也就是全部大寫且用底線(_)分隔單字:

const MAX_SPEED = 200

Use PascalCase for enum names and keep them singular, as they represent a type. Use CONSTANT_CASE for their members, as they are constants:

enum Element {
    EARTH,
    WATER,
    AIR,
    FIRE,
}

列舉型別請每個項目各佔一行。這樣能更方便在每項目前方新增註解,同時在版本控制時,項目增刪時也能獲得更清楚的差異比較。

正確例 :

enum Element {
    EARTH,
    WATER,
    AIR,
    FIRE,
}

錯誤例 :

enum Element { EARTH, WATER, AIR, FIRE }

程式碼順序

本章節著重於程式碼順序。關於格式化,請參考 格式。命名規則請見 命名慣例

建議按照這種方法來組織 GDScript 程式碼:

01. @tool, @icon, @static_unload
02. class_name
03. extends
04. ## doc comment

05. signals
06. enums
07. constants
08. static variables
09. @export variables
10. remaining regular variables
11. @onready variables

12. _static_init()
13. remaining static methods
14. overridden built-in virtual methods:
    1. _init()
    2. _enter_tree()
    3. _ready()
    4. _process()
    5. _physics_process()
    6. remaining virtual methods
15. overridden custom methods
16. remaining methods
17. subclasses

類別中的方法與變數請依存取修飾子的不同,按照下列順序排列:

1. public
2. private

我們最佳化了順序,來讓程式碼從上到下閱讀時比較容易,也幫助首次閱讀程式碼的開發者能瞭解程式如何運作的,並避免因變數宣告順序導致的錯誤。

這個程式碼順序遵守了四個經驗法則:

  1. 先寫屬性與訊號,然後寫方法。

  2. 先寫 Public 再寫 Private。

  3. 先寫虛擬回呼函式,再寫類別介面。

  4. 先寫物件的結構與初始化函式 _init_ready ,再寫會在執行環境修改物件的函式。

類別宣告

若程式碼是要在編輯器中執行的,請在腳本的第一行加上 @tool 關鍵字。

Follow with the optional @icon then the class_name if necessary. You can turn a GDScript file into a global type in your project using class_name. For more information, see 將腳本註冊為類別.

接著,若類別繼承內建型別,加上 extends 關鍵字。

接著,您應該要有該類別的可選 文件註解。您可以使用它來解釋您的類別對隊友的作用、它的運作方式,以及其他開發人員應該如何使用它等等。

class_name MyNode
extends Node
## A brief description of the class's role and functionality.
##
## The description of the script, what it can do,
## and any further detail.

訊號與屬性

接著是訊號宣告,再來是屬性,也就是說成員變數要寫在 DocString 之後。

列舉型別應該寫在訊號之後,因為可以使用列舉性來匯出其他屬性的提示。

接著按照順序是常數、匯出變數、Public、Private 以及 Onready 變數。

signal player_spawned(position)

enum Job {
    KNIGHT,
    WIZARD,
    ROGUE,
    HEALER,
    SHAMAN,
}

const MAX_LIVES = 3

@export var job: Job = Job.KNIGHT
@export var max_health = 50
@export var attack = 5

var health = max_health:
    set(new_health):
        health = new_health

var _speed = 300.0

@onready var sword = get_node("Sword")
@onready var gun = get_node("Gun")

備註

GDScript 會在 _ready 回呼執行前初始化 @onready 變數。這可用於快取節點相依性,也就是取得類別所需的場景子節點。上方範例即為示範。

成員變數

如果某個變數只會在一個方法內使用,不要把它宣告成成員變數。這會讓程式碼難以追蹤。請直接在方法內宣告為區域變數。

區域變數

區域變數請盡量在首次使用的附近宣告。這樣能讓程式碼更易閱讀,也不用頻繁捲動去尋找變數的宣告位置。

方法與靜態函式

方法寫在類別的屬性後。

先寫 _init() 回呼函式,因為 Godot 會在記憶體中建立物件時呼叫 _init()。接著是 _ready() 回呼函式,Godot 會在節點被新增至場景樹時呼叫 _ready()。

應該先寫這幾個函式,因為這個順序是物件初始化的順序。

其他內建的虛擬回呼函式,如 _unhandled_input()_physics_process 則應該接在後面。這幾個函式控制了物件的主循環以及與遊戲引擎的互動方法。

剩下的則依序為類別介面、Public 與 Private 方法。

func _init():
    add_to_group("state_machine")


func _ready():
    state_changed.connect(_on_state_changed)
    _state.enter()


func _unhandled_input(event):
    _state.unhandled_input(event)


func transition_to(target_state_path, msg={}):
    if not has_node(target_state_path):
        return

    var target_state = get_node(target_state_path)
    assert(target_state.is_composite == false)

    _state.exit()
    self._state = target_state
    _state.enter(msg)
    Events.player_state_changed.emit(_state.name)


func _on_state_changed(previous, new):
    print("state changed")
    state_changed.emit()

靜態型別

GDScript 支援 可選靜態型別

宣告型別

若要宣告變數的型別,請使用 <變數>: <型別>

var health: int = 0

若要宣告函式的回傳型別,請使用 -> <型別>

func heal(amount: int) -> void:

推斷型別

In most cases, you can let the compiler infer the type using :=. Prefer := when the type is written on the same line as the assignment, otherwise prefer writing the type explicitly.

正確例 :

# The type can be int or float, and thus should be stated explicitly.
var health: int = 0

# The type is clearly inferred as Vector3.
var direction := Vector3(1, 2, 3)

當型別不明確時包含型別提示,當型別提示多餘時省略型別提示。

錯誤例 :

# Typed as int, but it could be that float was intended.
var health := 0

# The type hint has redundant information.
var direction: Vector3 = Vector3(1, 2, 3)

# What type is this? It's not immediately clear to the reader, so it's bad.
var value := complex_function()

然而,有時候會因為本文不足而讓編譯器無法得知函式的回傳型別。舉例來說,除非已經將場景或節點檔案載入至記憶體,否則 get_node() 無法進行型別推斷。這時候,就需要明確設定型別。

正確例 :

@onready var health_bar: ProgressBar = get_node("UI/LifeBar")

錯誤例 :

# The compiler can't infer the exact type and will use Node
# instead of ProgressBar.
@onready var health_bar := get_node("UI/LifeBar")

或者,你也可以使用 as 關鍵字來轉換返回型別,這個型別會被用於推導變數的型別。

@onready var health_bar := get_node("UI/LifeBar") as ProgressBar
# health_bar will be typed as ProgressBar

備註

This option is considered more type-safe than type hints, but also less null-safe as it silently casts the variable to null in case of a type mismatch at runtime, without an error/warning.