Организация сцены

Эта статья охватывает темы связанные с эффективной организацией содержания сцены. Какие узлы вам стоит использовать? Как их стоит располагать? Как они должны взаимодействовать?

Как эффективно строить зависимости

Когда пользователи Godot начинают создавать собственные сцены, они часто приходят к подобной проблеме:

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

Чтобы исправить эти проблемы, необходимо создать экземпляры подсцены, не требуя подробностей об их среде. Нужно быть уверенным в том, что подсцена создаст сама себя, не придавая значения тому, как её использовать.

Одна из важнейших вещей, которую следует учитывать в ООП — это поддержка целевых классов единственного назначения со `слабой зацеплением<https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D1%86%D0%B5%D0%BF%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)#%D0%A2%D0%B8%D0%BF%D1%8B_%D0%B7%D0%B0%D1%86%D0%B5%D0%BF%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F>`_ с другими частями кодовой базы. Это сохраняет размеры объектов небольшими (для удобства поддержки) и улучшает их переиспользование.

Этот наилучший метод ООП имеет некоторую причастность к наилучшему методу структурирования сцены и использования скриптов.

Если в целом это возможно, вам стоит создавать сцены так чтобы они не имели зависимостей. То есть, вам стоит создавать сцены, которые содержат всё, что им нужно, в пределах самих себя.

If a scene must interact with an external context, experienced developers recommend the use of Dependency Injection. This technique involves having a high-level API provide the dependencies of the low-level API. Why do this? Because classes which rely on their external environment can inadvertently trigger bugs and unexpected behavior.

To do this, one must expose data and then rely on a parent context to initialize it:

  1. Connect to a signal. Extremely safe, but should be used only to "respond" to behavior, not start it. Note that signal names are usually past-tense verbs like "entered", "skill_activated", or "item_collected".

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
    // Parent
    GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
    
    // Child
    EmitSignal("SignalName"); // Triggers parent-defined behavior.
    
  2. Call a method. Used to start behavior.

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
    // Parent
    GetNode("Child").Set("MethodName", "Do");
    
    // Child
    Call(MethodName); // Call parent-defined method (which child must own).
    
  3. Initialize a FuncRef property. Safer than a method as ownership of the method is unnecessary. Used to start behavior.

    # Parent
    $Child.func_property = funcref(object_with_method, "method_on_the_object")
    
    # Child
    func_property.call_func() # Call parent-defined method (can come from anywhere).
    
    // Parent
    GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
    
    // Child
    FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
    
  4. Initialize a Node or other Object reference.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
    // Parent
    GetNode("Child").Set("Target", this);
    
    // Child
    GD.Print(Target); // Use parent-defined node.
    
  5. Инициализируйте NodePath ("Путь узла").

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    
    // Parent
    GetNode("Child").Set("TargetPath", NodePath(".."));
    
    // Child
    GetNode(TargetPath); // Use parent-defined NodePath.
    

These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. One can re-use it in another context without any extra changes to its API.

Примечание

Although the examples above illustrate parent-child relationships, the same principles apply towards all object relations. Nodes which are siblings should only be aware of their hierarchies while an ancestor mediates their communications and references.

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");

public class Left : Node
{
    public Node Target = null;

    public void Execute()
    {
        // Do something with 'Target'.
    }
}

public class Right : Node
{
    public Node Receiver = null;

    public Right()
    {
        Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
        AddChild(Receiver);
    }
}

Те же принципы применимы и к объектам, не относящимся к Node, которые поддерживают зависимости от других объектов. Какому бы объекту ни принадлежали объекты, он должен управлять отношениями между ними.

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

Следует отдавать предпочтение хранению данных внутри дома (внутри сцены), хотя, поскольку установка зависимости от внешнего контекста, даже слабосвязанного, по-прежнему означает, что узел будет ожидать, что что-то в его среде будет истинным. Философия дизайна проекта должна предотвратить это. В противном случае присущие коду обязательства заставят разработчиков использовать документацию для отслеживания объектных отношений в микроскопическом масштабе; это иначе известно как Производственный ад. Написание кода, который полагается на внешнюю документацию для его безопасного использования, по умолчанию подвержено ошибкам.

Чтобы избежать создания и поддержки такой документации, нужно преобразовать зависимый узел ("дочерний" выше) в инструментальный скрипт, который реализует _get_configuration_warning(). Возврат из него непустой строки заставит док-станцию Scene сгенерировать значок предупреждения со строкой в качестве всплывающей подсказки для узла. Это тот же значок, который отображается для таких узлов, как узел Area2D, когда он не имеет дочерних узлов: CollisionShape2D, определенных. Затем редактор самостоятельно документирует сцену с помощью кода сценария. Никакого дублирования контента через документацию не требуется.

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

Итак, почему всё это сложное переключение работает? Ну, потому что сцены работают лучше всего, когда они работают в одиночку. Если вы не можете работать в одиночку, то лучше всего будет работать с другими анонимно (с минимальными жёсткими зависимостями, т.е. слабой связью). Неизбежно может потребоваться внести изменения в класс, и если эти изменения заставят его непредвиденным образом взаимодействовать с другими сценами, то все начнёт ломаться. Весь смысл всего этого косвенного обращения состоит в том, чтобы избежать ситуации, когда изменение одного класса приводит к неблагоприятному воздействию на другие классы.

Скрипты и сцены, как расширения классов движков, должны соответствовать всем принципам ООП. Примеры включают...

Choosing a node tree structure

Итак, разработчик начинает работу над игрой только для того, чтобы остановиться на огромных возможностях, которые перед ним открываются. Он может знать, что он хочет делать, какие системы он хочет иметь, но где их все разместить? Что ж, как делать свою игру, всегда зависит от него. Дерево узлов можно построить множеством способов. Но для тех, кто не уверен, это полезное руководство может дать им образец достойной структуры для начала.

В игре всегда должна быть своего рода "точка входа"; где-то разработчик может окончательно отследить, где что-то начинается, чтобы он мог следовать логике, как она продолжается в другом месте. Это место также служит для обзора всех остальных данных и логики программы с высоты птичьего полета. Для традиционных приложений это будет "главная" функция. В данном случае это будет главный узел (Main).

  • Узел "Main" (main.gd)

Сценарий main.gd будет тогда служить основным контроллером игры.

Затем есть свой настоящий игровой "Мир" (World) (2D или 3D). Это может быть дочерний элемент Main. Кроме того, для игры потребуется основной графический интерфейс, который управляет различными меню и виджетами, которые необходимы проекту.

  • Узел "Main" (main.gd)
    • Node2D / Пространственный "Мир" (game_world.gd)

    • Управление "GUI" (gui.gd)

При смене уровней можно извлечь из узла "World" дочерние элементы. :ref:`Смена сцен вручную <doc_change_scenes_manally>' дает пользователям полный контроль над тем, как они меняют игровой мир.

Следующий шаг - обдумать, какие игровые системы требуются для вашего проекта. Если есть система, которая....

  1. отслеживает все свои данные изнутри

  2. должна быть доступна глобально

  3. может существовать изолированно

...то следует создать :ref:`автозагрузочный узел 'singleton' <doc_singletons_autoload>'.

Примечание

Для небольших игр более простой альтернативой с меньшим контролем было бы иметь синглтон "Game", который просто вызывает метод SceneTree.change_scene() для замены содержимого основной сцены. Эта структура более или менее сохраняет "World" как главный игровой узел.

Любой графический интерфейс также должен быть одноэлементным; быть преходящей частью "World"; или быть добавленным вручную как прямой потомок корня. В противном случае узлы GUI также удалялись бы при переходе между сценами.

Если у вас есть системы, которые изменяют данные других систем, следует определять их как свои собственные сценарии или сцены, а не как автозагрузки. Дополнительные сведения о причинах см. в документации 'Автозагрузки против внутренних узлов'.

Каждая подсистема в игре должна иметь свой собственный раздел в SceneTree (дерево сцены). Отношения родитель-потомок следует использовать только в тех случаях, когда узлы фактически являются элементами своих родителей. Означает ли удаление родителя разумно, что нужно удалить и детей? Если нет, то он должен иметь свое собственное место в иерархии в качестве родного брата или другого родственника.

Примечание

В некоторых случаях требуется, чтобы эти отдельные узлы также располагались относительно друг друга. Для этой цели можно использовать узлы RemoteTransform / RemoteTransform2D. Они позволят целевому узлу условно наследовать выбранные элементы преобразования из удалённого узла. Чтобы назначить цель NodePath, используйте одно из следующего:

  1. Надёжная третья сторона, вероятно, родительский узел, для посредничества при назначении.

  2. Группа, чтобы легко получить ссылку на желаемый узел (при условии, что когда-либо будет только одна из целей).

Когда нужно это делать? Ну это субъективно. Дилемма возникает, когда нужно микроуправление, когда узел должен перемещаться по дереву сцены, чтобы сохранить себя. Например...

  • Добавьте узел "player" в "room".

  • Необходимо изменить комнаты, поэтому необходимо удалить текущую комнату.

  • Прежде чем комнату можно будет удалить, нужно сохранить и/или переместить игрока.

    Память - это проблема?

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

    Если да, то нужно...

    • Переместите игрока в другое место в дереве.

    • Удалить комнату.

    • Создать и добавить новую комнату.

    • Повторно добавьте игрока.

Проблема в том, что игрок здесь является "особым случаем"; тот, где разработчики должны знать, что им нужно обращаться с игроком таким образом для проекта. Таким образом, единственный способ надёжно поделиться этой информацией в команде - это задокументировать её. Однако хранить детали реализации в документации опасно. Это бремя обслуживания затрудняет читаемость кода и без надобности раздувает интеллектуальное содержание проекта.

В более сложной игре с более крупными ассетами может быть лучшей идеей просто полностью оставить игрока где-нибудь в другом месте в дереве сцены. Это приводит к:

  1. Большей согласованности.

  2. Нет "особых случаев", которые нужно где-то документировать и поддерживать.

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

Напротив, если кому-то когда-либо понадобится дочерний узел, который не наследует преобразование своего родителя, у него есть следующие варианты:

  1. Декларативное решение: поместите между ними Node. Как узлы без преобразования, узлы не будут передавать такую информацию своим потомкам.

  2. Императивное решение: используйте сеттер set_as_toplevel для узла CanvasItem или :ref:`Spatial <class_Spatial_method_set_as_toplevel> `. Это заставит узел игнорировать унаследованное преобразование.

Примечание

При создании сетевой игры имейте в виду, какие узлы и системы игрового процесса относятся ко всем игрокам, а не только к авторитетному серверу. Например, не всем пользователям нужно иметь копию логики "PlayerController" каждого игрока. Вместо этого им нужны только свои. Таким образом, их хранение в отдельной ветке от "world" может помочь упростить управление игровыми подключениями и т.п.

Ключ к организации сцены - рассматривать дерево сцены в реляционных терминах, а не в пространственных терминах. Зависимы ли узлы от существования их родителей? Если нет, то они могут развиваться сами по себе где-нибудь ещё. Если они зависимы, то логично предположить, что они должны быть потомками этого родителя (и, вероятно, частью сцены этого родителя, если они ещё не являются таковыми).

Означает ли это, что сами узлы являются компонентами? Конечно, нет. Деревья узлов Godot формируют отношения агрегирования, а не композиции. Но несмотря на то, что у вас по прежнему есть возможность перемещать узлы, лучше всего, когда такие перемещения, по умолчанию, не нужны.