Руководство по стилю 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_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)

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

Длина строки

Придерживайтесь длине строк кода не более 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")

Примечание

В редакторе скриптов, чтобы закомментировать выбранный код, нажмите 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")

Плохо:

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