Руководство по стилю GDScript

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

Поскольку GDScript очень близок к Python, это руководство вдохновлено руководством по стилю программирования на Python`PEP 8 <https://www.python.org/dev/peps/pep-0008/>.

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

В целом, поддержание согласованности кода в ваших проектах и в команде важнее, чем следование этому руководству.

Примечание

Встроенный в 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. (редактор по умолчанию)

  • Используйте символ перевода строки в конце каждого файла. (editor default)

  • Используйте кодировку 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 Tiles {
    TILE_BRICK,
    TILE_FLOOR,
    TILE_SPIKE,
    TILE_TELEPORT,
}

Плохо:

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

var character_dict = {
        "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)

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

Примечание

We use a single line between classes and function definitions in the class reference and in short code snippets in this documentation.

Длина строки

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

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

Хорошо:

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")

Примечание

В редакторе скриптов, чтобы закомментировать выбранный код, нажмите Ctrl + K. Это добавит один символ # в начале выбранных строк.

Пробел

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

Хорошо:

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\"")

Номера

Не пропускайте нули в числах с плавающей точкой. Это делает их менее читаемыми, и более сложно отличимыми от обычных чисел.

Хорошо:

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 (слова из маленьких букв с подчеркиваниями) для имен файлов. Для именованных классов преобразуйте имя класса 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 KinematicBody

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

Порядок кода

Это первый раздел сфокусированный на порядке кода. Для форматирования смотрите Форматирование. Для соглашений по именованию смотрите Соглашения об именовании.

Мы предлагаем организовывать код на 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. Модификатор public идет перед модификатором private.

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

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

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

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

При необходимости используйте class_name. С помощью этого свойства вы можете превратить файл GDScript в глобальный тип вашего проекта. Для получения дополнительной информации см.:ref:doc_gdscript.

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

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

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

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

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

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

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

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``. Вы можете использовать это для кэширования зависимости узлов, иными словами, получить дочерние узлы в сцене, на которую опирается ваш класс. Пример выше показывает это.

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

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

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

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

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

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

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

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

Другие встроенные виртуальные обратные вызовы, такие как _unhandled_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 поддерживает optional static typing.

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

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

var health: int = 0

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

func heal(amount: int) -> void:

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

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

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

В случаях, когда контекст отсутствует — компилятор прерывает работу и указывает на неверный возвращаемый тип функции. К примеру, get_node() не может вывести тип, если сцена или файл узла не прогружены. В этом случае, задайте тип явно.

Хорошо:

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

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

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

Этот вариант также считается больше type-safe, чем первый.

Плохо:

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