Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Руководство по стилю 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 в случае несоответствия типов во время выполнения, без ошибки/предупреждения.