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)来换行,而不是 CRLFCR。(编辑默认)
  • 在每个文件的末尾使用一个换行符。(编辑器默认设置)
  • 使用不带 字节顺序标记(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\"")

命名约定

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

File names

Use snake_case for file names. For named classes, convert the PascalCase class name to 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

This is consistent with how C++ files are named in Godot’s source code. This also avoids case sensitivity issues that can crop up when exporting a project from Windows to other platforms.

类与节点

对类和节点名称使用帕斯卡命名法(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

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

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

  1. 首先是属性和信号,然后是方法。
  2. 公共变量优先于私有变量。
  3. 虚拟回调出现在类的接口之前。
  4. 对象的构造和初始化函数``_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

Don’t declare member variables if they are only used locally in a method, as it makes the code more difficult to follow. Instead, declare them as local variables in the method’s body.

Local variables

Declare local variables as close as possible to their first use. This makes it easier to follow the code, without having to scroll too much to find where the variable was declared.

方法和静态函数

在类的属性之后是方法。

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

这些函数应首先出现,因为它们显示如何初始化对象。

其他内置的虚拟回调,如 _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支持 可选的静态类型

类型提示

将冒号放在变量名称之后,不带空格,并在尽可能让GDScript编译器推断变量的类型。

良好的

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

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

糟糕的

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

当让编译器推断类型提示时,将冒号和等号一起写: :=

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

定义函数时,在返回类型箭头的两侧添加一个空格。

func heal(amount: int) -> void: