Руководство по стилю 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 ([method Node._physics_process],
## [method Node._unhandled_input]) to the state.

signal state_changed(previous, new)

@export var initial_state: Node
var is_active = true:
    set = set_is_active

@onready var _state = initial_state:
    set = set_state
@onready var _state_name = _state.name


func _init():
    add_to_group("state_machine")


func _enter_tree():
    print("this happens before the ready method!")


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


class State:
    var foo = 0

    func _init():
        print("Hello!")

Форматирование

Кодирование и специальные символы

  • Используйте символы подачи строк (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_dict = {
    "Name": "Bob",
    "Age": 27,
    "Job": "Mechanic",
}

enum Tile {
    BRICK,
    FLOOR,
    SPIKE,
    TELEPORT,
}

Плохо:

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

var character_dict = {
        "Name": "Bob",
        "Age": 27,
        "Job": "Mechanic",
}

enum Tile {
        BRICK,
        FLOOR,
        SPIKE,
        TELEPORT,
}

Висящая запятая (Последняя запятая)

Используйте висящую запятую на последней строке в массивах, словарях, и перечислениях. Такой подход позволяет упростить рефакторинг и улучшить сравнение в контроле версий, так как последняя строка не нуждается в изменении, когда добавляются новые элементы.

Хорошо:

var array = [
    1,
    2,
    3,
]

Плохо:

var array = [
    1,
    2,
    3
]

Висящие запятые не нужны в однострочных списках, поэтому в этом случае не добавляйте их.

Хорошо:

var array = [1, 2, 3]

Плохо:

var array = [1, 2, 3,]

Пустые строки

Окружите функции и определения классов двумя пустыми строками:

func heal(amount):
    health += amount
    health = min(health, max_health)
    health_changed.emit(health)


func take_damage(amount, effect=null):
    health -= amount
    health = max(0, health)
    health_changed.emit(health)

Используйте одну пустую строку внутри функций для отделения логических частей друг от друга.

Примечание

В справочнике классов и в коротких фрагментах кода в этой документации мы используем одну строку между классами и определениями функций.

Длина строки

Придерживайтесь длине строк кода не более 100 символов.

Если можете, постарайтесь придерживаться длине строк в 80 символов. Это поможет читать код на небольших дисплеях и с двумя скриптами открытыми бок о бок в стороннем текстовом редакторе. Например, чтобы посмотреть на различия.

Одно выражение на строку

Избегайте объединения нескольких операторов в одной строке, включая условные операторы, чтобы соответствовать правилам стиля GDScript для удобства чтения.

Хорошо:

if position.x > width:
    position.x = 0

if flag:
    print("flagged")

Плохо:

if position.x > width: position.x = 0

if flag: print("flagged")

Единственным исключением из этого правила является тернарный оператор:

next_state = "idle" if is_on_floor() else "fall"

Форматирование многострочных операторов для удобства чтения

Когда у вас особенно длинные операторы if или вложенные тернарные выражения, их перенос на несколько строк улучшает читаемость. Поскольку строки продолжения по-прежнему являются частью одного и того же выражения, следует использовать 2 уровня отступа вместо одного.

GDScript позволяет заключать инструкции в несколько строк, используя круглые скобки или обратную косую черту. В этом руководстве по стилю рекомендуется использовать круглые скобки, поскольку они упрощают рефакторинг. Используя обратную косую черту, вы должны убедиться, что последняя строка никогда не содержит обратной косой черты в конце. С круглыми скобками вам не нужно беспокоиться о том, что последняя строка будет иметь обратную косую черту в конце.

При переносе условного выражения на несколько строк ключевые слова and/or следует размещать в начале продолжения строки, а не в конце предыдущей строки.

Хорошо:

var angle_degrees = 135
var quadrant = (
        "northeast" if angle_degrees <= 90
        else "southeast" if angle_degrees <= 180
        else "southwest" if angle_degrees <= 270
        else "northwest"
)

var position = Vector2(250, 350)
if (
        position.x > 200 and position.x < 400
        and position.y > 300 and position.y < 400
):
    pass

Плохо:

var angle_degrees = 135
var quadrant = "northeast" if angle_degrees <= 90 else "southeast" if angle_degrees <= 180 else "southwest" if angle_degrees <= 270 else "northwest"

var position = Vector2(250, 350)
if position.x > 200 and position.x < 400 and position.y > 300 and position.y < 400:
    pass

Избегайте лишних скобок

Избегайте скобок в выражениях и условных операторах. Если это не требуется для порядка операций или переноса на несколько строк, они только ухудшают читаемость.

Хорошо:

if is_colliding():
    queue_free()

Плохо:

if (is_colliding()):
    queue_free()

Логические операторы

Отдавайте предпочтение простым английским версиям логических операторов, так как они наиболее доступны:

  • Используйте and вместо &&.

  • Используйте or вместо ||.

  • Используйте not вместо !.

Вы также можете использовать круглые скобки вокруг логических операторов во избежание неоднозначностей. Это может облегчить чтение длинных выражений.

Хорошо:

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

Плохо:

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

Интервал в комментариях

Обычные комментарии (#) и комментарии документации (##) должны начинаться с пробела, но не с кода, который вы комментируете. Кроме того, комментарии к областям кода (#region/#endregion) должны следовать этому точному синтаксису, поэтому они не должны начинаться с пробела.

Использование пространства для обычных и документационных комментариев помогает отличить текстовые комментарии от отключенного кода.

Хорошо:

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

Плохо:

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

Примечание

В редакторе скриптов, чтобы включить или отключить комментирование выделенного кода, нажмите Ctrl + K. Эта функция добавляет/удаляет одиночный символ # перед любым кодом в выделенных строках.

Предпочитайте писать комментарии на отдельной строке, а не на встроенных комментариях (комментариях, которые пишутся на той же строке, что и код). Встроенные комментарии лучше всего использовать для коротких комментариев, обычно состоящих максимум из нескольких слов:

Хорошо:

# This is a long comment that would make the line below too long if written inline.
print("Example") # Short comment.

Плохо:

print("Example") # This is a long comment that would make this line too long if written inline.

Пробел

Всегда используйте один пробел вокруг операторов и после запятых. Также избегайте лишних пробелов в ссылках на словари и вызовах функций. Исключением являются однострочные объявления словарей, где пробел следует добавлять после открывающей фигурной скобки и перед закрывающей. Это позволяет визуально отличить словарь от массива, поскольку символы [] в большинстве шрифтов выглядят почти как {}.

Хорошо:

position.x = 5
position.y = target_position.y + 10
dict["key"] = 5
my_array = [4, 5, 6]
my_dictionary = { key = "value" }
print("foo")

Плохо:

position.x=5
position.y = mpos.y+10
dict ["key"] = 5
myarray = [4,5,6]
my_dictionary = {key = "value"}
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

yaml_parser.gd

Имена классов

PascalCase

class_name YAMLParser

Имена узлов

PascalCase

Camera3D, Player

Функции

snake_case

func load_level():

Переменные

snake_case

var particle_effect

Сигналы

snake_case

signal door_opened

Константы

CONSTANT_CASE

const MAX_SPEED = 200

Имена перечислений (Enum)

PascalCase

enum Element

Члены перечисления ((Enum))

CONSTANT_CASE

{EARTH, WATER, AIR, FIRE}

Имена файлов

Используйте формат 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

Это согласуется с тем, как файлы C++ называются в исходном коде Godot. Это также позволяет избежать проблем с чувствительностью к регистру, которые могут возникнуть при экспорте проекта из Windows на другие платформы.

Классы и узлы

Используйте PascalCase для имен классов и узлов:

extends CharacterBody3D

Также используйте 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,
}

Пишите перечисления, каждый элемент которых находится на отдельной строке. Это упрощает добавление комментариев к документации над каждым элементом, а также способствует более четкому отображению различий в системе контроля версий при добавлении или удалении элементов.

Хорошо:

enum Element {
    EARTH,
    WATER,
    AIR,
    FIRE,
}

Плохо:

enum Element { EARTH, WATER, AIR, FIRE }

Порядок кода

В этом разделе рассматривается порядок кодирования. О форматировании см. Форматирование. О соглашениях об именовании см. Соглашения об именовании.

Мы предлагаем организовывать код на GDScript таким образом:

01. @tool, @icon, @static_unload
02. class_name
03. extends
04. ## doc comment

05. signals
06. enums
07. constants
08. static variables
09. @export variables
10. remaining regular variables
11. @onready variables

12. _static_init()
13. remaining static methods
14. overridden built-in virtual methods:
    1. _init()
    2. _enter_tree()
    3. _ready()
    4. _process()
    5. _physics_process()
    6. remaining virtual methods
15. overridden custom methods
16. remaining methods
17. inner classes

И расположите методы и переменные класса в следующем порядке в зависимости от их модификаторов доступа:

1. public
2. private

Мы оптимизировали порядок, чтобы упростить чтение кода сверху вниз и помочь разработчикам, которые читают этот код впервые, понять, как он работает и избежать ошибок, связанных с порядком объявления переменных.

Этот порядок кода следует четырем правилам большого пальца (эмпирическая закономерность):

  1. Свойства и сигналы идут первыми, сопровождаемые методами.

  2. Модификатор public идет перед модификатором private.

  3. Виртуальные функции обратного вызова идут перед интерфейсом класса.

  4. Создание объектов и инициализация функций, _init и _ready, идут перед функциями, которые изменяют объект во время выполнения.

Объявление класса

Если код предназначен для запуска в редакторе, поместите аннотацию @tool в первую строку скрипта.

После этого добавьте необязательный @icon, а затем, при необходимости, class_name. Вы можете преобразовать файл GDScript в глобальный тип в своём проекте с помощью class_name. Подробнее см. Регистрация именованных классов. Если класс должен быть abstract class, добавьте @abstract перед ключевым словом class_name.

Затем добавьте ключевое слово extends, если класс расширяет встроенный тип.

После этого у вас должны быть необязательные комментарии к классу documentation comments. Вы можете использовать их, например, чтобы объяснить коллегам по команде роль класса, как он работает и как его следует использовать другим разработчикам.

@abstract
class_name MyNode
extends Node
## A brief description of the class's role and functionality.
##
## The description of the script, what it can do,
## and any further detail.

Для внутренних классов используйте однострочные объявления:

## A brief description of the class's role and functionality.
##
## The description of the script, what it can do,
## and any further detail.
@abstract class MyNode extends Node:
    pass

Сигналы и свойства

Пишите объявления сигналов, затем свойства, иными словами, переменные-члены после строки документации.

Перечисления должны идти после сигналов, так как вы можете использовать их в качестве подсказок экспорта для других свойств.

Затем, пишите константы, экспортированные переменные, модификаторы public, private, и инициализированные переменные, в таком порядке.

signal player_spawned(position)

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

const MAX_LIVES = 3

@export var job: Job = Job.KNIGHT
@export var max_health = 50
@export var attack = 5

var health = max_health:
    set(new_health):
        health = new_health

var _speed = 300.0

@onready var sword = get_node("Sword")
@onready var gun = get_node("Gun")

Примечание

GDScript вычисляет переменные @onready непосредственно перед обратным вызовом _ready. Это можно использовать для кэширования зависимостей узлов, то есть для получения дочерних узлов в сцене, на которые опирается ваш класс. Это показано в примере выше.

Переменные-члены

Не объявляйте переменные-члены, если они используются только локально в методе, так как это затрудняет выполнение кода. Вместо этого объявите их как локальные переменные в теле метода.

Локальные переменные

Объявляйте локальные переменные как можно ближе к их первому использованию. Это облегчает отслеживание кода, не требуя слишком большой прокрутки, чтобы найти, где была объявлена переменная.

Методы и статические функции

После свойств класса идут методы.

Начните с метода обратного вызова _init(), который движок вызовет при создании объекта в памяти. Затем следует метод обратного вызова _ready(), который Godot вызывает при добавлении узла в дерево сцены.

Эти функции должны следовать первыми, так как они показывают, как инициализируется объект.

Другие встроенные виртуальные обратные вызовы, такие как _unhandled_input() и _physics_process, должны следовать дальше. Они контролируют основной цикл объекта и взаимодействие с игровым движком.

Остальной интерфейс класса, публичные и приватные методы — следуют после, в указанном порядке.

func _init():
    add_to_group("state_machine")


func _ready():
    state_changed.connect(_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.player_state_changed.emit(_state.name)


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

Статическая типизация

GDScript поддерживает :optional static typing.

Объявленные типы

Для объявление типа переменной, используйте конструкцию <переменная>: <тип>:

var health: int = 0

Для объявления возвращаемого типа функции, используйте конструкцию -> <тип>:

func heal(amount: int) -> void:

Определяемые типы

В большинстве случаев можно позволить компилятору определить тип с помощью :=. Если тип указан на той же строке, что и присваивание, предпочтительнее использовать :=, в противном случае предпочтительнее указывать тип явно.

Хорошо:

# The type can be int or float, and thus should be stated explicitly.
var health: int = 0

# The type is clearly inferred as Vector3.
var direction := Vector3(1, 2, 3)

Включите подсказку о типе, если тип неоднозначен, и опустите подсказку о типе, если она избыточна.

Плохо:

# Typed as int, but it could be that float was intended.
var health := 0

# The type hint has redundant information.
var direction: Vector3 = Vector3(1, 2, 3)

# What type is this? It's not immediately clear to the reader, so it's bad.
var value := complex_function()

В некоторых случаях тип необходимо указывать явно, иначе поведение будет не таким, как ожидалось, поскольку компилятор сможет использовать только возвращаемый тип функции. Например, 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")

В качестве альтернативы вы можете использовать ключевое слово as для приведения типа возвращаемого значения, и этот тип будет использоваться для определения типа переменной.

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

Примечание

Этот параметр считается более type-safe, чем подсказки типов, но также и менее безопасным по отношению к null, поскольку он молча приводит переменную к null в случае несоответствия типов во время выполнения, без ошибки/предупреждения.