GDScript 风格指南
该样式指南列出了编写优雅GDScript的约定。目标是促进编写干净、可读的代码,并促进项目、讨论和教程之间的一致性。希望这也会促进开发自动格式化工具。
由于GDScript与Python非常接近,因此本指南的灵感来自Python的 PEP 8 编程风格指南。
风格指南并不是硬性的规则手册。有时,您可能无法应用下面的一些准则。当这种情况发生时,使用你最好的判断,并询问其他开发人员的见解。
一般来说,在项目和团队中保持代码的一致性比遵循本指南进行tee更为重要。
注解
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
。(编辑默认) - 在每个文件的末尾使用一个换行符。(编辑器默认设置)
- 使用不带 字节顺序标记(BOM) 的 UTF-8 编码。(编辑默认)
- 使用 Tabs 代替制表符进行缩进(称为“软制表符”)。(编辑默认)
缩进
每个缩进级别必须大于包含它的代码块。
良好的:
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)
函数内部使用一个空行来分隔逻辑部分。
Line length(可能是字符长度)
把每行代码控制在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")
注解
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\"")
Numbers
不要忽略浮点数中的前导或尾随零。 否则,这会使它们的可读性降低,很难一眼与整数区分开。
Good:
var float_number = 0.234
var other_float_number = 13.0
Bad:
var float_number = .234
var other_float_number = 13.
对于十六进制数字,请使用小写字母,因为它们较矮,使数字更易于阅读。
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 引擎风格。打破这些都会使你的代码与内置的命名约定冲突,导致风格不一致的代码。
File names
文件名用 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
将类加载到常量或变量时同样适用:
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
我们优化了顺序,使从上到下阅读代码变得容易,帮助第一次阅读代码的开发人员了解代码的工作原理,并避免与变量声明顺序相关的错误。
此代码顺序遵循四个经验法则:
- 首先是属性和信号,然后是方法。
- 公共变量优先于私有变量。
- 虚拟回调出现在类的接口之前。
- 对象的构造和初始化函数``_init``和``_ready``应该在运行时修改对象的函数之前。
类声明
如果代码要在编辑器中运行,请将 tool
关键字放在脚本的第一行。
如有必要,在后面加上 class_name。您可以使用此功能将GDScript文件转换为项目中的全局类型。有关更多信息,请参见 doc gdscript。
然后,如果类扩展了内置类型,则添加 extends 关键字。
然后,您应该添加类的可选文档字符串作为注释。您可以使用它来向您的团队解释类的角色、工作原理,以及其他开发人员应该如何使用它,下面举个例子。
class_name MyNode
extends Node
# A brief description of the class's role and functionality.
# Longer description.
信号和属性
Write signal declarations, followed by properties, that is to say, member variables, after the docstring.
枚举应该在信号之后,因为您可以将它们用作其他属性的导出提示。
然后,按该顺序写入常量、导出变量、公共变量、私有变量和 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变量。您可以使用它来缓存节点依赖项,也就是说,在您的类所依赖的场景中获取子节点。这就是上面的例子所展示的。
Member variables
如果变量只在方法中使用,勿声明其为成员变量,因为我们难以定位在何处使用了该变量。相反,你应该将它们在方法内部定义为局部变量。
Local variables
声明局部变量的位置离首次使用它的位置越近越好。这让人更容易跟上代码的思路,而不需要上下翻找该变量的声明位置。
方法和静态函数
在类的属性之后是方法。
从 _init()
回调方法开始,引擎将在创建内存对象时调用该方法。接下来是 _ready()
回调,当Godot向场景树添加一个节点时调用它。
These functions should come first because they show how the object is initialized.
其他内置的虚拟回调,如 _unhandling_input()
和 _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 # 编译器会使用int类型。
然而,在少数情况下,当上下文缺失时,编译器会回退到函数的返回类型。例如,在节点的场景或文件被加载到内存中之前,``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")