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
# (_physics_process, _unhandled_input) to the state.


signal state_changed(previous, new)

export var initial_state = NodePath()
var is_active = true setget set_is_active

onready var _state = get_node(initial_state) setget set_state
onready var _state_name = _state.name


func _init():
    add_to_group("state_machine")


func _ready():
    connect("state_changed", self, "_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.emit_signal("player_state_changed", _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")
    emit_signal("state_changed")

格式

編碼與特殊字元

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

  • 每個檔案都以 LF 換行字元來結束。 (編輯器預設值)

  • 使用不帶 BOM <https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E9%A0%86%E5%BA%8F%E8%A8%98%E8%99%9F>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_dir = {
    "Name": "Bob",
    "Age": 27,
    "Job": "Mechanic",
}

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

錯誤例 :

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

var character_dir = {
        "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)
    emit_signal("health_changed", health)


func take_damage(amount, effect=null):
    health -= amount
    health = max(0, health)
    emit_signal("health_changed", 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 = "fall" if not is_on_floor() else "idle"

避免不必要的括號

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

正確例 :

if is_colliding():
    queue_free()

錯誤例 :

if (is_colliding()):
    queue_free()

布林運算子

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

  • 使用 and 而不是 &&

  • 使用 or 而不是 ||

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

正確例 :

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

錯誤例 :

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

註釋中的空白

一般的註釋應該以空白開始,但註解掉的程式碼則不用。這樣一來比較能區分文字註解與停用掉的程式碼。

正確例 :

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

錯誤例 :

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

備註

在腳本編輯器中,可以按 Ctrl + K 來將選擇的程式碼註解或取消註解。這個功能會在所選行前加上一個 # 符號。

空白

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

正確例 :

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。以免降低數字的可讀性,並難以一眼區分與整數的不同。

Good:

var float_number = 0.234
var other_float_number = 13.0

Bad:

var float_number = .234
var other_float_number = 13.

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

Good:

var hex_number = 0xfb8c0b

Bad:

var hex_number = 0xFB8C0B

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

Good:

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

Bad:

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 KinematicBody

當將類別載入只常數與變數時也使用 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. exported variables
09. public variables
10. private variables
11. onready variables

12. optional built-in virtual _init method
13. built-in virtual _ready method
14. remaining built-in virtual methods
15. public methods
16. private methods

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

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

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

  2. 先寫 Public 再寫 Private。

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

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

類別宣告

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

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

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

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

class_name MyNode
extends Node
# A brief description of the class's role and functionality.
# Longer description.

訊號與屬性

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

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

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

signal spawn_player(position)

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

const MAX_LIVES = 3

export(Jobs) var job = Jobs.KNIGHT
export var max_health = 50
export var attack = 5

var health = max_health setget set_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():
    connect("state_changed", self, "_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.emit_signal("player_state_changed", _state.name)


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

靜態型別

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

宣告型別

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

var health: int = 0

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

func heal(amount: int) -> void:

推斷型別

大多數情況下,我們都直接使用 := 來讓編輯器推斷型別:

var health := 0  # The compiler will use the int type.

然而,有時候會因為上下文不足而讓編譯器無法得知函數的回傳型別。舉例來說,除非已經將場景或節點檔案載入至記憶體,否則 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")