Оглавление
Проблема
Ваш проект начал усложняться. У вас есть несколько сцен, экземпляров и много узлов. Вероятно, вы уже столкнулись с написанием кода, подобного следующему:
get_node("../../SomeNode/SomeOtherNode")
get_parent().get_parent().get_node("SomeNode")
get_tree().get_root().get_node("SomeNode/SomeOtherNode")
Если вы продолжите так поступать, вы скоро обнаружите, что подобные ссылки на узлы легко ломаются. Как только вы измените что-то в своем дереве сцен, ни одна из этих ссылок может быть уже не действительной.
Обмен данными между узлами и сценами не обязательно должен быть сложным. Есть более хороший способ.
Решение
Как общее правило, узлы в Godot должны управлять своими дочерними элементами, а не наоборот. Если вы используете get_parent() или get_node(«..»), то, вероятно, вы направляетесь к проблемам. Пути к узлам подобного рода хрупки, что означает, что они легко ломаются. Три основные проблемы с этим подходом:
- Вы не можете тестировать сцену независимо. Если вы запускаете сцену отдельно или в тестовой сцене, которая не имеет точно такой же структуры узлов, get_node() вызовет сбой.
- Изменять что-то легко не удается. Если вы решите изменить порядок или переработать ваше дерево, пути больше не будут действительными.
- Порядок готовности — сначала дети, затем родитель. Это означает, что попытка доступа к свойству родителя в _ready() узла может завершиться неудачей, потому что родитель еще не готов.
В общем, узел или сцену должно быть возможно создавать в любом месте вашей игры, и они не должны делать предположений о том, что будет их родителем.
Мы рассмотрим подробные примеры позже в этом руководстве, но на данный момент вот «золотое правило» общения между узлами:
Call down, signal up.
- Если узел вызывает дочерний (то есть идет «вниз» по дереву), тогда get_node() — это подходящий способ.
- Если узлу нужно взаимодействовать «вверх» по дереву, вероятно, стоит использовать сигнал.
Если вы помните это правило, проектируя структуру своей сцены, вы уже на пути к созданию поддерживаемого и хорошо организованного проекта. И вы избежите использования громоздких путей к узлам, которые приводят к проблемам.
Теперь давайте рассмотрим каждую из этих стратегий вместе с примерами.
Использование get_node()
get_node() перемещается по дереву сцены, используя предоставленный путь, чтобы найти узел с указанным именем.
Пример использования get_node()
Давайте рассмотрим следующую распространенную конфигурацию:
Сценарий в узле Player должен уведомить AnimatedSprite2D, какую анимацию воспроизводить, основываясь на движении игрока. В этой ситуации get_node() работает хорошо:
extends CharacterBody2D
func _process(delta):
if speed > 0:
get_node("AnimatedSprite2D").play("run")
else:
get_node("AnimatedSprite2D").play("idle")
В GDScript вы можете использовать $ в качестве сокращения для get_node(), написав $AnimatedSprite2D вместо этого.
Использование сигналов
Сигналы следует использовать для вызова функций на узлах, которые находятся выше в дереве или на том же уровне (так называемые «соседние» узлы).
Вы можете соединить сигнал в редакторе (чаще всего для узлов, которые существуют до запуска игры) или в коде (для узлов, которые вы создаете во время выполнения). Синтаксис для соединения сигнала следующий:
signal_name.connect(target_node.target_function)
Взглянув на это, вы можете подумать: «Подождите, если я соединяюсь с соседом, мне все равно нужны пути к узлам типа ../Сосед?». Хотя вы могли бы это сделать, это нарушает наше вышеуказанное правило. Решение этой загадки заключается в том, чтобы убедиться, что соединения устанавливаются общим родителем.
Следуя правилу вызова вниз по дереву, узел, который является общим родителем для узлов, отправляющего и принимающего сигналы, будет по определению знать, где они находятся, и будет готов после обоих из них.
Пример сигнала
Очень частым случаем использования сигналов является обновление вашего пользовательского интерфейса. Каждый раз, когда изменяется переменная здоровья игрока, вы хотите обновить отображение метки (Label) или полосы состояния (ProgressBar). Однако ваши узлы интерфейса полностью отделены от вашего игрока (как это и должно быть). Игрок ничего не знает о том, где находятся эти узлы и как их найти.
Вот наш примерный вариант настройки:
Обратите внимание, что интерфейс пользователя представляет собой сцену, созданную в экземпляре, мы просто показываем содержащиеся узлы. Здесь часто можно увидеть такие вещи, как get_node(«../UI/VBoxContainer/HBoxContainer/Label).text = str(health), что мы хотим избежать.
Вместо этого игрок отправляет сигнал health_changed всякий раз, когда его здоровье увеличивается/уменьшается. Нам нужно отправить этот сигнал в функцию update_health() интерфейса, которая отвечает за установку значения метки (Label). В сценарии игрока мы используем следующий код, когда здоровье игрока изменяется:
health_changed.emit(health)
В сценарии UI у нас есть:
onready var label = $VBoxContainer/HBoxContainer/Label
func update_health(value):
label.text = str(value)
Теперь нам просто нужно соединить сигнал с функцией. Идеальное место для этого узел «World», который является общим родителем и знает, где находятся оба узла:
func _ready():
$Player.health_changed.connect($UI.update_health)
Использование групп
Группы — еще один способ декомпозиции, особенно когда у вас есть много похожих объектов, которые должны выполнять одно и то же действие. Узел может быть добавлен в любое количество групп и членство в них можно изменять динамически в любое время с использованием функций add_to_group() и remove_from_group().
Часто встречаемое недопонимание о группах заключается в том, что их рассматривают как некоторый вид объекта или массива, который «содержит» ссылки на узлы. Группы — это система маркировки. Узел «входит» в группу, если ему назначен этот тег. Дерево сцены отслеживает эти метки и предоставляет функции, такие как get_nodes_in_group(), чтобы помочь вам найти все узлы с определенной меткой.
Пример использования групп
Давайте рассмотрим космический шутер в стиле Galaga, где много врагов летает вокруг. Эти враги могут иметь различные типы и поведение. Вы хотели бы добавить улучшение «умной бомбы», которая, активируясь, уничтожает всех врагов на экране. С использованием групп, вы можете реализовать это с минимальным объемом кода.
Во-первых, добавьте всех врагов в группу «enemies». Вы можете сделать это в редакторе, используя вкладку «Node»:
Вы также можете добавлять узлы в группу в своем сценарии:
func _ready():
add_to_group("enemies")
Допустим, каждый враг имеет функцию explode(), которая обрабатывает события при его уничтожении (воспроизведение анимации, появление выпавших предметов и т. д.). Теперь, когда каждый враг находится в группе, мы можем реализовать нашу функцию умной бомбы следующим образом:
func activate_smart_bomb():
get_tree().call_group("enemies", "explode")
Использование owner
owner — это свойство узла, которое устанавливается автоматически при сохранении сцены. Каждый узел в этой сцене будет иметь свойство owner, установленное на корневой узел сцены. Это предоставляет удобный способ соединять сигналы дочерних узлов с главным узлом.
Пример использования owner
В сложном пользовательском интерфейсе вы часто сталкиваетесь с глубокой вложенной иерархией контейнеров и элементов управления. Узлы, с которыми пользователь взаимодействует, такие как Button, отправляют сигналы, и вы, возможно, захотите соединить эти сигналы с сценарием на корневом узле интерфейса.
Вот примерная настройка:
Сценарий на корневом узле CenterContainer содержит следующую функцию, которую мы хотим вызывать всякий раз, когда нажата любая кнопка:
extends CenterContainer
func _on_button_pressed(button_name):
print(button_name, " was pressed")
Кнопки здесь представляют собой экземпляры сцены Button, представляющей объект, который может содержать динамический код, устанавливающий текст кнопки или другие свойства. Или, возможно, у вас есть кнопки, которые динамически добавляются/удаляются из контейнера в зависимости от состояния игры. Независимо от этого все, что нам нужно для соединения сигнала кнопки, — это следующее:
extends Button
func _ready():
pressed.connect(owner._on_button_pressed.bind(name))
Неважно, где вы разместили кнопки в дереве — если вы добавляете больше контейнеров — CenterContainer остается владельцем.