Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

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 換行字元來結束。 (編輯器預設值)

  • 使用不帶 BOMUTF-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 Tiles {
    TILE_BRICK,
    TILE_FLOOR,
    TILE_SPIKE,
    TILE_TELEPORT,
}

錯誤例 :

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

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

enum Tiles {
        TILE_BRICK,
        TILE_FLOOR,
        TILE_SPIKE,
        TILE_TELEPORT,
}

結尾逗號

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

正確例 :

enum Tiles {
    TILE_BRICK,
    TILE_FLOOR,
    TILE_SPIKE,
    TILE_TELEPORT,
}

錯誤例 :

enum Tiles {
    TILE_BRICK,
    TILE_FLOOR,
    TILE_SPIKE,
    TILE_TELEPORT
}

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

正確例 :

enum Tiles {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT}

錯誤例 :

enum Tiles {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT,}

空行

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

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 個字元內。這樣一來在小螢幕或是在外部文字編輯器中左右並排開啟兩個腳本時比較容易閱讀。舉例來說,需要對照兩個版本的時候。

一行一個陳述式

千萬不要將多個陳述式放在單行內。C 語言程式設計師們,請別這麼做,也不要把條件陳述式寫在單行內。

正確例 :

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 而不是 ||

  • 使用 or 而不是 ||

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

正確例 :

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

錯誤例 :

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

註釋中的空白

Regular comments (#) and documentation comments (##) should start with a space, but not code that you comment out. Additionally, code region comments (#region/#endregion) must follow that precise syntax, so they should not start with a space.

Using a space for regular and documentation comments helps differentiate text comments from disabled code.

正確例 :

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

錯誤例 :

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

備註

In the script editor, to toggle the selected code commented, press Ctrl + K. This feature adds a single # sign at the start of the selected lines.

空格

在運算元周圍與逗號後加上一個空白。另外,也請避免在字典參照與函式呼叫中加上多餘的空白。

正確例 :

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

錯誤例 :

position.x=5
position.y = mpos.y+10
dict ["key"] = 5
myarray = [4,5,6]
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 。有名稱的類別則將 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

列舉型別的 名稱 使用 PascalCase,列舉的成員則使用 CONSTANT_CASE,因為這些成員為常數:

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

程式碼順序

第一個段落為程式碼順序,有關格式,請參考 格式 。有關命名公約,請參考 naming_coventions

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

01. @tool
02. class_name
03. extends
04. # docstring

05. signals
06. enums
07. constants
08. @export variables
09. public variables
10. private variables
11. @onready variables

12. optional built-in virtual _init method
13. optional built-in virtual _enter_tree() method
14. built-in virtual _ready method
15. remaining built-in virtual methods
16. public methods
17. private methods
18. subclasses

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

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

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

  2. 先寫 Public 再寫 Private。

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

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

類別宣告

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

接著若有必要,加上 class_name 。可以使用這個功能來將 GDScript 檔案轉為專案中的全域型別。更多資訊請參考 資料偏好

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

接下來則是可選擇性地加上 DocString 註解。DocString 註解可以用來向團隊成員解釋這個類別的是什麼角色以及其他開發者該如何使用這個類別…等。

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 Jobs {KNIGHT, WIZARD, ROGUE, HEALER, SHAMAN}

const MAX_LIVES = 3

@export var job: Jobs = Jobs.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 變數的值。可以使用 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()

靜態型別

自 Godot 3.1 版以後,GDScript 支援 可選的靜態型別定義

宣告型別

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

var health: int = 0

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

func heal(amount: int) -> void:

推斷型別

在大多數情況下,您可以使用“:=”讓編譯器推斷型別。當型別與賦值寫在同一行時,首選“:=”,否則首選明確編寫型別。

正確例 :

var health: int = 0 # The type can be int or float, and thus should be stated explicitly.
var direction := Vector3(1, 2, 3) # The type is clearly inferred as Vector3.

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

錯誤例 :

var health := 0 # Typed as int, but it could be that float was intended.
var direction: Vector3 = Vector3(1, 2, 3) # The type hint has redundant information.

# 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")

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

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

這種做法也比第一種更:ref:型別安全<doc_gdscript_static_typing_safe_lines>

錯誤例 :

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