GDScript 风格指南

该样式指南列出了编写优雅 GDScript 的约定。目标是促进编写干净、可读的代码,促进项目、讨论和教程之间的一致性。希望这也会促进开发自动格式化工具。

由于 GDScript 与 Python 非常接近,因此本指南的灵感来自 Python 的 PEP 8 编程风格指南。

风格指南并不是硬性的规则手册。有时,您可能无法应用下面的一些规范。当这种情况发生时,请使用你最好的判断,并询问其他开发人员的见解。

一般来说,在项目和团队中保持代码的一致性比一板一眼地遵循本指南更为重要。

备注

Godot的内置脚本编辑器默认使用了很多这些约定. 让它帮助你.

下面是基于这些规范的完整的类的示例:

  1. class_name StateMachine
  2. extends Node
  3. # Hierarchical State machine for the player.
  4. # Initializes states and delegates engine callbacks
  5. # (_physics_process, _unhandled_input) to the state.
  6. signal state_changed(previous, new)
  7. export var initial_state = NodePath()
  8. var is_active = true setget set_is_active
  9. onready var _state = get_node(initial_state) setget set_state
  10. onready var _state_name = _state.name
  11. func _init():
  12. add_to_group("state_machine")
  13. func _ready():
  14. connect("state_changed", self, "_on_state_changed")
  15. _state.enter()
  16. func _unhandled_input(event):
  17. _state.unhandled_input(event)
  18. func _physics_process(delta):
  19. _state.physics_process(delta)
  20. func transition_to(target_state_path, msg={}):
  21. if not has_node(target_state_path):
  22. return
  23. var target_state = get_node(target_state_path)
  24. assert(target_state.is_composite == false)
  25. _state.exit()
  26. self._state = target_state
  27. _state.enter(msg)
  28. Events.emit_signal("player_state_changed", _state.name)
  29. func set_is_active(value):
  30. is_active = value
  31. set_physics_process(value)
  32. set_process_unhandled_input(value)
  33. set_block_signals(not value)
  34. func set_state(value):
  35. _state = value
  36. _state_name = _state.name
  37. func _on_state_changed(previous, new):
  38. print("state changed")
  39. emit_signal("state_changed")

格式

编码和特殊字符

  • 使用换行符(LF)换行,而不是 CRLF 或 CR。(编辑器默认)

  • 在每个文件的末尾使用一个换行符。(编辑器默认)

  • 使用不带字节顺序标记UTF-8 编码。(编辑器默认)

  • 使用制表符代替空格进行缩进。(编辑器默认)

缩进

每个缩进级别必须大于包含它的代码块.

良好的 :

  1. for i in range(10):
  2. print("hello")

糟糕的 :

  1. for i in range(10):
  2. print("hello")
  3. for i in range(10):
  4. print("hello")

使用2个缩进级别来区分续行与常规代码块.

良好的 :

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

糟糕的 :

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

此规则的例外是数组, 字典和枚举. 使用单个缩进级别来区分连续行:

良好的 :

  1. var party = [
  2. "Godot",
  3. "Godette",
  4. "Steve",
  5. ]
  6. var character_dict = {
  7. "Name": "Bob",
  8. "Age": 27,
  9. "Job": "Mechanic",
  10. }
  11. enum Tiles {
  12. TILE_BRICK,
  13. TILE_FLOOR,
  14. TILE_SPIKE,
  15. TILE_TELEPORT,
  16. }

糟糕的 :

  1. var party = [
  2. "Godot",
  3. "Godette",
  4. "Steve",
  5. ]
  6. var character_dict = {
  7. "Name": "Bob",
  8. "Age": 27,
  9. "Job": "Mechanic",
  10. }
  11. enum Tiles {
  12. TILE_BRICK,
  13. TILE_FLOOR,
  14. TILE_SPIKE,
  15. TILE_TELEPORT,
  16. }

行尾逗号

请在数组、字典和枚举的最后一行使用逗号。这将使版本控制中的重构更容易,Diff 也更美观,因为添加新元素时不需要修改最后一行。

良好的 :

  1. enum Tiles {
  2. TILE_BRICK,
  3. TILE_FLOOR,
  4. TILE_SPIKE,
  5. TILE_TELEPORT,
  6. }

糟糕的 :

  1. enum Tiles {
  2. TILE_BRICK,
  3. TILE_FLOOR,
  4. TILE_SPIKE,
  5. TILE_TELEPORT
  6. }

单行列表中不需要行尾逗号,因此在这种情况下不要添加它们。

良好的 :

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

糟糕的 :

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

空白行

用两个空行包围函数和类定义:

  1. func heal(amount):
  2. health += amount
  3. health = min(health, max_health)
  4. emit_signal("health_changed", health)
  5. func take_damage(amount, effect=null):
  6. health -= amount
  7. health = max(0, health)
  8. emit_signal("health_changed", health)

函数内部使用一个空行来分隔逻辑部分.

备注

在类参考和本文档的短代码片段中,我们会在类和函数定义之间使用单个空行。

行的长度

把每行代码控制在100个字符以内.

如果可以的话, 尽量把行控制在80个字符以下. 这有助于在小屏幕上阅读代码, 并在外部文本编辑器中并排打开两个脚本. 例如, 在查看差异修订时.

一条语句一行

不要在一行上合并多个语句. 不要像C语言那样, 不能使用单行条件语句(三元运算符除外).

良好的 :

  1. if position.x > width:
  2. position.x = 0
  3. if flag:
  4. print("flagged")

糟糕的 :

  1. if position.x > width: position.x = 0
  2. if flag: print("flagged")

该规则的唯一例外是三元运算符:

  1. next_state = "fall" if not is_on_floor() else "idle"

为可读性格式化多行语句

如果你的 if 语句特别长,或者是嵌套的三元表达式时,把它们拆分成多行可以提高可读性。因为这些连续的行仍然属于同一个表达式,应该缩进两级而不是一级。

GDScript 允许使用括号或反斜杠将语句拆成多行。本风格指南倾向于使用括号,因为重构起来更简单。使用反斜杠的话,你必须保证最后一行的末尾没有反斜杠。而如果是括号,你就不必担心最后一行的反斜杠问题。

把条件表达式拆分成多行时,andor 关键字应当放在下一行的开头,而不是上一行的结尾。

良好的 :

  1. var angle_degrees = 135
  2. var quadrant = (
  3. "northeast" if angle_degrees <= 90
  4. else "southeast" if angle_degrees <= 180
  5. else "southwest" if angle_degrees <= 270
  6. else "northwest"
  7. )
  8. var position = Vector2(250, 350)
  9. if (
  10. position.x > 200 and position.x < 400
  11. and position.y > 300 and position.y < 400
  12. ):
  13. pass

糟糕的 :

  1. var angle_degrees = 135
  2. var quadrant = "northeast" if angle_degrees <= 90 else "southeast" if angle_degrees <= 180 else "southwest" if angle_degrees <= 270 else "northwest"
  3. var position = Vector2(250, 350)
  4. if position.x > 200 and position.x < 400 and position.y > 300 and position.y < 400:
  5. pass

避免不必要的圆括号

避免表达式和条件语句中的括号。除非需要修改操作顺序或者是在拆分多行,否则它们只会降低可读性。

良好的 :

  1. if is_colliding():
  2. queue_free()

糟糕的 :

  1. if (is_colliding()):
  2. queue_free()

布尔运算

首选布尔运算符的英文版本,因为它们是最易懂的:

  • 使用 and 代替 &&

  • 使用 or 代替 ||

也可以在布尔运算符周围使用括号来清除任何歧义。这可以使长表达式更容易阅读。

良好的 :

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

糟糕的 :

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

注释间距

普通注释开头应该留一个空格,但如果是为了停用代码而将其注释掉则不需要留。这样可以用来区分文本注释和停用的代码。

良好的 :

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

糟糕的 :

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

备注

在脚本编辑器中,要切换已注释的选定代码,请按 Ctrl + K。此功能在选定行的开头添加一个 # 注释符号。

空格

请始终在运算符前后和逗号后使用一个空格。同时,请避免在字典引用和函数调用中使用多余的空格。

良好的 :

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

糟糕的 :

  1. position.x=5
  2. position.y = mpos.y+10
  3. dict ["key"] = 5
  4. myarray = [4,5,6]
  5. print ("foo")

不要使用空格来垂直对齐表达式:

  1. x = 100
  2. y = 100
  3. velocity = 500

引号

尽量使用双引号,除非单引号可以让字符串中需要转义的字符变少。见如下示例:

  1. # Normal string.
  2. print("hello world")
  3. # Use double quotes as usual to avoid escapes.
  4. print("hello 'world'")
  5. # Use single quotes as an exception to the rule to avoid escapes.
  6. print('hello "world"')
  7. # Both quote styles would require 2 escapes; prefer double quotes if it's a tie.
  8. print("'hello' \"world\"")

数字

不要忽略浮点数中的前导或后缀零。否则,这会使它们的可读性降低,很难一眼与整数区分开。

良好的

  1. var float_number = 0.234
  2. var other_float_number = 13.0

糟糕的

  1. var float_number = .234
  2. var other_float_number = 13.

对于十六进制数字,请使用小写字母,因为它们较矮,使数字更易于阅读。

良好的

  1. var hex_number = 0xfb8c0b

糟糕的

  1. var hex_number = 0xFB8C0B

利用 GDScript 的文字下划线,使大数字更易读。

良好的

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

糟糕的

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

命名约定

这些命名约定遵循 Godot 引擎风格. 打破这些都会使你的代码与内置的命名约定冲突, 导致风格不一致的代码.

文件命名

文件名用 snake_case 命名法,对于有名字的类,将其名字从 PascalCase 命名转化为 snake_case:

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

这种命名于 Godot 源码中的 C++ 文件命名保持了一致. 这也防止了由 Windows 导出到其他大小写敏感平台时发生的问题.

类与节点

对类和节点名称使用帕斯卡命名法(PascalCase):

  1. extends KinematicBody

将类加载到常量或变量时同样适用:

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

函数与变量

函数与变量使用 snake_case 命名:

  1. var particle_effect
  2. func load_level():

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

  1. var _counter = 0
  2. func _recalculate_path():

信号

用过去时态来命名信号:

  1. signal door_opened
  2. signal score_changed

常数和枚举

使用 CONSTANT_CASE, 全部大写, 用下划线(_)分隔单词 :

  1. const MAX_SPEED = 200

对枚举的名称使用 PascalCase,对其成员使用 CONSTANT_CASE, 因为它们是常量:

  1. enum Element {
  2. EARTH,
  3. WATER,
  4. AIR,
  5. FIRE,
  6. }

代码顺序

第一节主要讨论代码顺序. 有关格式, 请参见 格式. 有关命名约定, 请参见 命名约定.

我们建议按以下方式组织GDScript代码:

  1. 01. tool
  2. 02. class_name
  3. 03. extends
  4. 04. # docstring
  5. 05. signals
  6. 06. enums
  7. 07. constants
  8. 08. exported variables
  9. 09. public variables
  10. 10. private variables
  11. 11. onready variables
  12. 12. optional built-in virtual _init method
  13. 13. built-in virtual _ready method
  14. 14. remaining built-in virtual methods
  15. 15. public methods
  16. 16. private methods

我们优化了顺序, 使从上到下阅读代码变得容易, 帮助第一次阅读代码的开发人员了解代码的工作原理, 并避免与变量声明顺序相关的错误.

此代码顺序遵循四个经验法则:

  1. 首先是属性和信号, 然后是方法.

  2. 公共变量优先于私有变量.

  3. 虚回调出现在类的接口之前.

  4. 对象的构造和初始化函数 _init_ready 在修改对象的函数之前运行.

类声明

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

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

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

然后, 您应该添加类的可选文档字符串作为注释. 您可以使用它来向您的团队解释类的角色, 工作原理, 以及其他开发人员应该如何使用它, 下面举个例子.

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

信号和属性

先声明信号, 后跟属性(即成员变量), 它们都在文档注释(docstring)之后.

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

然后, 按该顺序写入常量, 导出变量, 公共变量, 私有变量和 onready 变量.

  1. signal spawn_player(position)
  2. enum Jobs {KNIGHT, WIZARD, ROGUE, HEALER, SHAMAN}
  3. const MAX_LIVES = 3
  4. export(Jobs) var job = Jobs.KNIGHT
  5. export var max_health = 50
  6. export var attack = 5
  7. var health = max_health setget set_health
  8. var _speed = 300.0
  9. onready var sword = get_node("Sword")
  10. onready var gun = get_node("Gun")

备注

GDScript编译器在 _ready 函数回调之前计算onready变量. 您可以使用它来缓存节点依赖项, 也就是说, 在您的类所依赖的场景中获取子节点. 这就是上面的例子所展示的.

成员变量

如果变量只在方法中使用, 勿声明其为成员变量, 因为我们难以定位在何处使用了该变量. 相反, 你应该将它们在方法内部定义为局部变量.

局部变量

声明局部变量的位置离首次使用它的位置越近越好. 这让人更容易跟上代码的思路, 而不需要上下翻找该变量的声明位置.

方法和静态函数

在类的属性之后是方法.

_init() 回调方法开始, 引擎将在创建内存对象时调用该方法. 接下来是 _ready() 回调, 当Godot向场景树添加一个节点时调用它.

这些函数应该声明在脚本最前面, 因为它们显示了对象是如何初始化的.

_unhandling_input()_physics_process 等其他内置的虚回调应该放在后面。它们控制对象的主循环和与游戏引擎的交互。

类的其余接口, 公共和私有方法, 都是按照这个顺序出现的.

  1. func _init():
  2. add_to_group("state_machine")
  3. func _ready():
  4. connect("state_changed", self, "_on_state_changed")
  5. _state.enter()
  6. func _unhandled_input(event):
  7. _state.unhandled_input(event)
  8. func transition_to(target_state_path, msg={}):
  9. if not has_node(target_state_path):
  10. return
  11. var target_state = get_node(target_state_path)
  12. assert(target_state.is_composite == false)
  13. _state.exit()
  14. self._state = target_state
  15. _state.enter(msg)
  16. Events.emit_signal("player_state_changed", _state.name)
  17. func _on_state_changed(previous, new):
  18. print("state changed")
  19. emit_signal("state_changed")

静态类型

从Godot 3.1开始,GDScript支持 可选的静态类型.

声明类型

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

  1. var health: int = 0

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

  1. func heal(amount: int) -> void:

推断类型

在大多数情况下, 你可以让编译器使用 := 来推断类型:

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

然而, 在少数情况下, 当上下文缺失时, 编译器会回退到函数的返回类型. 例如, 在节点的场景或文件被加载到内存中之前, get_node() 无法自动推断类型. 在这种情况下, 你应该明确地设置类型.

良好的 :

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

或者,你也可以使用 as 关键字来转换返回类型,这个类型会被用于推导变量的类型。

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

这种做法也比第一种更类型安全

糟糕的 :

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