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

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

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

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

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

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

Одна из важнейших вещей, которую следует учитывать в ООП — это поддержка целевых классов единственного назначения со слабым зацеплением с другими частями кодовой базы. Это сохраняет размеры объектов небольшими (для удобства поддержки) и улучшает их переиспользование.

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

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

Если сцена должна взаимодействовать с внешним контекстом, опытные разработчики рекомендуют использовать Внедрение Зависимости. Эта техника включает в себя наличие высокоуровневого API, который предоставит зависимости для низкоуровневого API. Почему так стоит делать? Потому что классы которые полагаются на внешнее окружение могут ненароком вызвать баги и неожиданное поведение.

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

  1. Подключитесь к сигналу. Чрезвычайно безопасно, но должно использоваться только для "ответа" на поведение, а не для его запуска. Обратите внимание, что имена сигналов обычно являются глаголами прошедшего времени, например "вошел", "навык_активирован" или "предмет_собран".

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
  2. Вызов метода. Используется для запуска поведения.

    # 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).
    
  3. Инициализируйте свойство FuncRef. Безопасней чем метод так как владение методом не требуется. Используется для начального поведения.

    # 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).
    
  4. Инициализация ссылки на Node или другой Object.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. Инициализируйте NodePath ("Путь узла").

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    

Эти опции прячут точки доступа от дочернего узла. Этот ход оставляет дочерний элемент слабо связанным со своим окружением. Можно использовать это в другом контексте без дополнительных изменений в его API.

Примечание

Хотя примеры выше иллюстрируют отношения родительский элемент-дочерний элемент, идентичные принципы применимы ко всем связям объектов. Узлы которые являются соседними должны знать свои иерархии только когда предок посредничает в их коммуникациях и ссылках.

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

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

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

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

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

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

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

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

Выбор структуры дерева нод

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

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

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

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

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

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

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

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

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

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

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

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

... то следует создать автозагрузочный узел 'singleton'.

Примечание

Для небольших игр более простой альтернативой с меньшим контролем было бы иметь синглтон "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 формируют отношения агрегирования, а не композиции. Но несмотря на то, что у вас по прежнему есть возможность перемещать узлы, лучше всего, когда такие перемещения, по умолчанию, не нужны.