本风格指南列出了编写优雅 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。(编辑器默认)
  • 在每个文件的末尾使用一个换行符。(编辑器默认)
  • 使用没有字节顺序标记的UTF-8编码。(编辑器默认)
  • 使用制表符代替空格进行缩进。(编辑器默认)

缩进

每个缩进级别应该比包含它的块大一个。

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

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

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

使用 2 个缩进级别来区分连续行和常规代码块。

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

数字

不要省略浮点数中的前导零或尾随零。否则,这会降低它们的可读性,并且很难一眼就将它们与整数区分开来。

var float_number = 0.234
var other_float_number = 13.0

var float_number = .234
var other_float_number = 13.

对十六进制数字中的字母使用小写字母,因为它们较低的高度使数字更易于阅读。

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`.
extends Node
class_name Weapon
# This file should be saved as `yaml_parser.gd`.
extends Object
class_name YAMLParser

这与 Godot 源代码中 C++ 文件的命名方式一致。这也避免了在将项目从 Windows 导出到其他平台时可能出现的区分大小写问题。

类和节点

对类名和节点名使用 PascalCase:

extends KinematicBody

在将类加载到常量或变量中时也使用 PascalCase:

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

函数和变量

使用 snake_case 来命名函数和变量:

var particle_effect
func load_level():

在用户必须覆盖的虚拟方法函数、私有函数和私有变量前添加一个下划线 (_):

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,
}

代码顺序

我们建议这样组织 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. 公共先于私人。
  3. 虚拟回调出现在类的接口之前。
  4. 对象的构造和初始化函数,_init和 _ready,在运行时修改对象的函数之前。

类声明

如果代码要在编辑器中运行,请将tool关键字放在脚本的第一行。

如有必要,请使用class_name。您可以使用此功能将 GDScript 文件转换为项目中的全局类型。有关更多信息,请参阅 GDScript 基础知识

然后,如果类扩展内置类型,则添加extends关键字。

之后,您应该将类​​的可选文档字符串作为注释。例如,您可以使用它来向您的团队成员解释您的类的角色、它是如何工作的以及其他开发人员应该如何使用它。

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

信号和属性

在文档字符串之后编写信号声明,然后是属性,即成员变量。

枚举应该在信号之后,因为您可以将它们用作其他属性的导出提示。

然后,按顺序编写常量、导出变量、公共、私有和就绪变量。

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 变量。您可以使用它来缓存节点依赖项,也就是说,在您的类所依赖的场景中获取子节点。这就是上面的例子所显示的。

成员变量

如果成员变量仅在方法中局部使用,则不要声明成员变量,因为这会使代码更难以理解。相反,将它们声明为方法主体中的局部变量。

局部变量

尽可能在第一次使用时声明局部变量。这样可以更轻松地遵循代码,而不必滚动太多以找到声明变量的位置。

方法和静态函数

在类的属性之后是方法。

_init()回调方法开始,引擎将在内存中创建对象时调用该方法。跟随_ready()回调,Godot 在向场景树添加节点时调用。

这些函数应该放在最前面,因为它们显示了对象是如何初始化的。

其他内置的虚拟回调,如_unhandled_input()and _physics_process,应该紧随其后。这些控制对象的主循环和与游戏引擎的交互。

类的其余接口,公共和私有方法,按此顺序在此之后。

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 支持可选的静态类型

声明类型

要声明变量的类型,请使用:<variable>: <type>

var health: int = 0

要声明函数的返回类型,请使用:-> <type>

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