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.

Запуск кода в редакторе

Что такое @tool?

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

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

  • Если у вас есть пушка, стреляющая ядрами, на которые влияет физика (гравитация), вы можете нарисовать траекторию полета ядра в редакторе, что значительно упростит дизайн уровней.

  • Если у вас есть прыжковые платформы с различными высотами прыжка, для каждой из них вы сможете нарисовать максимальную высоту на которую игрок может подпрыгнуть, что также делает дизайн уровней лёгким.

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

Опасно

Скрипты @tool запускаются внутри редактора и позволяют получить доступ к дереву сцены, редактируемой в данный момент. Это мощная функция, которая, однако, имеет свои недостатки, поскольку редактор не предусматривает защиты от возможного несанкционированного использования скриптов @tool. Будьте крайне осторожны при работе с деревом сцены, особенно с помощью Node.queue_free, так как это может привести к сбоям, если вы освободите узел во время выполнения редактором связанной с ним логики.

Как использовать @tool

Чтобы превратить скрипт в инструмент, добавьте аннотацию @tool в начало кода.

Чтобы проверить, находитесь ли вы в данный момент в редакторе, используйте: Engine.is_editor_hint().

Например, если вы хотите выполнить какой-то код только в редакторе, используйте:

if Engine.is_editor_hint():
    # Code to execute when in editor.

С другой стороны, если вы хотите выполнить код только в игре, просто отмените тот же оператор:

if not Engine.is_editor_hint():
    # Code to execute when in game.

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

Вот как может выглядеть функция _process():

func _process(delta):
    if Engine.is_editor_hint():
        # Code to execute in editor.

    if not Engine.is_editor_hint():
        # Code to execute in game.

    # Code to execute both in editor and in game.

Важная информация

Общее правило заключается в том, что любой другой GDScript, который использует ваш скрипт инструмента, *также* должен быть инструментом. Редактор не может создавать экземпляры из файлов GDScript без @tool, что означает, что вы не можете вызывать методы или ссылаться на них переменные-члены иным образом. Однако, поскольку статические методы, константы и перечисления могут использоваться без создания экземпляра, их можно вызывать или ссылаться на них из скрипта @tool в других скриптах, не являющихся инструментами. Исключением являются static variables. Если вы попытаетесь прочитать значение статической переменной в скрипте, в котором нет @tool, он всегда вернет null, но при этом не выведет предупреждение или ошибку. Это ограничение не применяется к статическим методам, которые могут быть вызваны независимо от того, находится ли целевой скрипт в режиме инструмента.

Расширение скрипта @tool не делает расширяющий скрипт автоматически @tool. Отсутствие @tool в расширяющем скрипте отключит поведение инструмента из суперкласса. Поэтому расширяющий скрипт также должен содержать аннотацию @tool.

Изменения в редакторе являются постоянными, их невозможно отменить или повторить. Например, в следующем разделе, когда мы удалим скрипт, узел сохранит свое вращение. Будьте осторожны и не вносите нежелательных изменений. Рассмотрите возможность настройки version control, чтобы избежать потери работы в случае совершения ошибки.

Отладка

While the debugger and breakpoints cannot be used directly with tool scripts, it is possible to launch a new instance of the editor and debug from there. To do this, navigate to Debug > Customize Run Instances... and specify --editor in Main Run Args.

Дополнительную информацию см. в Обзор инструментов отладки.

Кроме того, вы можете использовать операторы вывода для отображения содержимого переменных.

Попробуйте @tool

Добавьте узел Sprite2D в сцену и установите текстуру на иконку Godot. Присоедините и откройте скрипт, затем измените его следующим образом:

@tool
extends Sprite2D

func _process(delta):
    rotation += PI * delta

Сохраните скрипт и вернитесь в редактор. Вы должны увидеть как ваш объект вращается. Если вы запустите игру, он также будет вращаться.

Предупреждение

Вам может потребоваться перезагрузить редактор. Это известная ошибка, найденная во всех версиях Godot 4: GH-66381.

../../_images/rotating_in_editor.gif

Примечание

Если вы не увидели изменений, перезагрузите сцену (закройте и откройте снова).

Теперь давайте выберем когда и какой код запустится. Измените функцию _process() чтобы она выглядела вот так:

func _process(delta):
    if Engine.is_editor_hint():
        rotation += PI * delta
    else:
        rotation -= PI * delta

Сохраните скрипт. Теперь объект будет вращаться по часовой стрелке в редакторе, но если вы запустите игру он будет вращаться против часовой стрелки.

Редактирование переменных

Добавьте и экспортируйте переменную скорость в скрипт. Чтобы обновить скорость и сбросить угол поворота, добавьте сеттер set(new_speed), который выполняется при получении входных данных от инспектора. Измените _process(), чтобы включить скорость поворота.

@tool
extends Sprite2D


@export var speed = 1:
    # Update speed and reset the rotation.
    set(new_speed):
        speed = new_speed
        rotation = 0


func _process(delta):
    rotation += PI * delta * speed

Примечание

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

Получение уведомлений об изменении массивов или словарей

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

@tool
class_name MyTool
extends Node

@export var my_array = []:
    set(new_array):
        my_array = new_array
        print("My array just changed!")

@export var my_dictionary = {}:
    set(new_dictionary):
        my_dictionary = new_dictionary
        print("My dictionary just changed!")

Получение уведомлений об изменении ресурсов

Иногда требуется, чтобы ваш инструмент использовал ресурс. Однако при изменении свойства этого ресурса в редакторе метод set() вашего инструмента не будет вызван.

@tool
class_name MyTool
extends Node

@export var resource: MyResource:
    set(new_resource):
        resource = new_resource
        _on_resource_set()

# This will only be called when you create, delete, or paste a resource.
# You will not get an update when tweaking properties of it.
func _on_resource_set():
    print("My resource was set!")

Чтобы обойти эту проблему, вам сначала придется сделать свой ресурс инструментом и заставить его выдавать сигнал changed каждый раз, когда устанавливается свойство:

# Make Your Resource a tool.
@tool
class_name MyResource
extends Resource

@export var property = 1:
    set(new_setting):
        property = new_setting
        # Emit a signal when the property is changed.
        changed.emit()

Затем необходимо подключить сигнал при установке нового ресурса:

@tool
class_name MyTool
extends Node

@export var resource: MyResource:
    set(new_resource):
        resource = new_resource
        # Connect the changed signal as soon as a new resource is being added.
        if resource != null:
            resource.changed.connect(_on_resource_changed)

func _on_resource_changed():
    print("My resource just changed!")

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

@export var resource: MyResource:
    set(new_resource):
        # Disconnect the signal if the previous resource was not null.
        if resource != null:
            resource.changed.disconnect(_on_resource_changed)
        resource = new_resource
        if resource != null:
            resource.changed.connect(_on_resource_changed)

Предупреждения о конфигурации узла

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

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

# Use setters to update the configuration warning automatically.
@export var title = "":
    set(p_title):
        if p_title != title:
            title = p_title
            update_configuration_warnings()

@export var description = "":
    set(p_description):
        if p_description != description:
            description = p_description
            update_configuration_warnings()


func _get_configuration_warnings():
    var warnings = []

    if title == "":
        warnings.append("Please set `title` to a non-empty value.")

    if description.length() >= 100:
        warnings.append("`description` should be less than 100 characters long.")

    # Returning an empty array means "no warning".
    return warnings

Запуск одноразовых скриптов с помощью EditorScript

Иногда вам нужно запустить код всего один раз, чтобы автоматизировать определённую задачу, которая изначально недоступна в редакторе. Вот несколько примеров:

  • Используйте как площадку для написания сценариев GDScript или C# без необходимости запуска проекта. Вывод print() отображается на панели вывода редактора.

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

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

В Godot это доступно через расширение EditorScript в скрипте. Это позволяет запускать отдельные скрипты в редакторе без необходимости создания плагина для редактора.

Чтобы создать EditorScript, щёлкните правой кнопкой мыши по папке или пустому месту в доке файловой системы и выберите New > Script.... В диалоговом окне создания скрипта щёлкните по значку дерева, чтобы выбрать объект, который будет расширяться (или введите EditorScript непосредственно в поле слева, но учтите, что регистр учитывается):

Создание скрипта редактора в диалоговом окне создания редактора скриптов

Создание скрипта редактора в диалоговом окне создания редактора скриптов

Это автоматически выберет шаблон скрипта, подходящий для EditorScripts, с уже вставленным методом _run():

@tool
extends EditorScript

# Called when the script is executed (using File -> Run in Script Editor).
func _run():
    pass

Метод _run() выполняется при использовании любого из 4 подходов для запуска EditorScript:

  • Используйте Файл > Выполнить в верхней части редактора скриптов, когда EditorScript открыт в текущей вкладке.

  • Нажмите сочетание клавиш Ctrl + Shift + X, когда EditorScript открыт в текущей вкладке. Это сочетание клавиш действует только при фокусе на редакторе скриптов.

  • Щёлкните правой кнопкой мыши по скрипту в доке FileSystem и выберите Run.

  • Добавьте class_name <name> в начало скрипта, вызовите палитру команд, нажав Ctrl + Shift + P, и введите имя класса для его запуска. Запись будет названа в соответствии с именем класса с автоматическим применением капитализации.

Скрипты, расширяющие EditorScript, должны быть скриптами @tool для работы.

Примечание

EditorScript можно запускать только из редактора скриптов Godot. Если вы используете внешний редактор, используйте один из двух последних подходов для запуска скрипта.

Примечание

C# EditorScript нельзя запускать из редактора скриптов, так как он поддерживает только GDScript. Используйте альтернативные подходы, описанные выше, для запуска пользовательских C# EditorScript.

Имейте в виду, что C#-скрипты инструментов будут отображаться в палитре команд только при наличии атрибута GlobalClass.

Опасно

В EditorScripts нет функций отмены/повтора действий, поэтому обязательно сохраните сцену перед ее запуском, если скрипт предназначен для изменения каких-либо данных.

Для доступа к узлам в текущей редактируемой сцене используйте метод EditorInterface.get_edited_scene_root(), который возвращает корневой узел текущей редактируемой сцены. Вот пример, который рекурсивно получает все узлы в текущей редактируемой сцене и удваивает диапазон всех узлов OmniLight3D:

@tool
# Thanks to the class name, we can run this script by bringing up
# the command palette and searching "Scale Omni Lights".
class_name ScaleOmniLights
extends EditorScript

func _run():
    for node in EditorInterface.get_edited_scene_root().find_children("", "OmniLight3D"):
        # Don't operate on instantiated subscene children, as changes are lost
        # when reloading the scene.
        # See the "Instantiating scenes" section below for a description of `owner`.
        var is_instantiated_subscene_child = node != get_scene() and node.owner != get_scene()
        if not is_instantiated_subscene_child:
            node.omni_range *= 2.0
            EditorInterface.mark_scene_as_unsaved()

В приведённом выше примере мы также вызываем EditorScript.mark_scene_as_unsaved() после любого изменения, влияющего на состояние сцены. Это позволяет редактору отображать сцену как "несохранённую" (т.е. со звёздочкой рядом с именем). Таким образом, вы также получаете подтверждение при попытке закрыть сцену с несохранёнными изменениями.

Совет

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

Instantiating scenes

Вы можете создавать упакованные сцены обычным образом и добавлять их к сцене, открытой в данный момент в редакторе. По умолчанию узлы или сцены, добавленные с помощью Node.add_child(node), не видны в доке дерева сцены и не сохраняются на диск. Если вы хотите, чтобы узел или сцена были видны в доке дерева сцены и сохранялись на диск при сохранении сцены, вам нужно установить свойство дочернего узла owner на редактируемый корень сцены.

Если вы используете @tool:

func _ready():
    var node = Node3D.new()
    add_child(node) # Parent could be any node in the scene

    # The line below is required to make the node visible in the Scene tree dock
    # and persist changes made by the tool script to the saved scene file.
    node.owner = get_tree().edited_scene_root

Если вы используете EditorScript:

func _run():
    # `parent` could be any node in the scene.
    var parent = get_scene().get_node("Parent")
    var node = Node3D.new()
    parent.add_child(node)

    # The line below is required to make the node visible in the Scene tree dock
    # and persist changes made by the tool script to the saved scene file.
    node.owner = get_scene()

Примечание

Изменения, внесённые скриптами инструментов и EditorScript (например, добавление узлов или изменение свойств), не помечают сцену как несохранённую автоматически. Чтобы отобразить звёздочку (*) и предотвратить случайную потерю данных, вызовите EditorInterface.mark_scene_as_unsaved() после изменений или используйте EditorUndoRedoManager для поддержки отмены.

Предупреждение

Неправильное использование @tool может привести к множеству ошибок. Рекомендуется сначала написать код так, как вам нужно, и только затем добавлять аннотацию @tool в начало. Также обязательно разделяйте код, выполняемый в редакторе, от кода, выполняемого в игре. Это упростит обнаружение ошибок.