Посібник зі стилю 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
# (_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. (за замовчуванням редактора)
  • Використовуйте символи зміни рядка в кінці кожного файлу. (за замовчуванням редактора)
  • Використовуйте кодування UTF-8 без маркера порядку байтів. (за замовчування редактора)
  • Використовуйте для відступів Tab замість пробілів. (за замовчуванням редактора)

Відступи

Кожен рівень відступу повинен бути на один відступ більшим, ніж блок, що містить його.

Правильно:

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 символів. Це допомагає читати код на невеликих дисплеях та з двома скриптами, відкритими поруч у зовнішньому текстовому редакторі. Наприклад, щоб переглянути на наявність різниці.

Одне твердження на рядок

Ніколи не комбінуйте кілька тверджень в одному рядку.

Правильно:

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

Конвенції іменування

Ці конвенції іменування відповідають стилю Godot Engine. Якщо їх порушити, ваш код зіткнеться із вбудованими конвенціями іменування, що призведе до непослідовного коду.

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

Також використовуйте 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. Загальнодоступні йдуть перед приватними.
  3. Віртуальні зворотні виклики йдуть перед інтерфейсом класу.
  4. Конструктори об'єктів та ініціалізації функцій, _init і _ready, перед функціями, що змінюють об'єкт під час виконання.

Оголошення класу

Якщо код призначений для запуску в редакторі, розмістіть ключове слово tool в першому рядку скрипта.

Далі, якщо потрібно, class_name. Ви можете перетворити файл GDScript у глобальний тип свого проекту за допомогою цієї функції. Для отримання додаткової інформації дивіться Основи GDScript.

Потім додайте ключове слово extens, якщо клас розширює вбудований тип.

Слідом за цим у вас можуть бути додаткові коментарі, які пояснюють клас. Ви можете використовувати їх для пояснення ролі свого класу товаришам по команді, та того, як він працює та як, наприклад, інші розробники повинні використовувати його.

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

Сигнали та властивості

Записуйте оголошення сигналу, далі властивості, тобто змінні-члени.

Перерахунки повинні йти за сигналами, оскільки ви можете використовувати їх як підказки щодо експорту інших властивостей.

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

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 викликає, коли додає вузол до дерева сцен.

Ці функції повинні бути першими, оскільки вони показують, як об'єкт ініціалізується.

Інші вбудовані віртуальні зворотні виклики, як-от _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 підтримує необов'язкову статичну типізацію.

Type hints

Place the colon right after the variable's name, without a space, and let the GDScript compiler infer the variable's type when possible.

Правильно:

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

When you let the compiler infer the type hint, write the colon and equal signs together: :=.

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

Add a space on either sides of the return type arrow when defining functions.

func heal(amount: int) -> void: