Когда использовать сцены по сравнению со скриптами

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

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

Анонимные типы

Возможно полностью определить содержимое сцены, используя только скрипт. Это, по сути, то, что делает редактор Godot, только в конструкторе C++ своих объектов.

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

const MyNode = preload("my_node.gd")
const MyScene = preload("my_scene.tscn")
var node = Node.new()
var my_node = MyNode.new() # Same method call
var my_scene = MyScene.instance() # Different method call
var my_inherited_scene = MyScene.instance(PackedScene.GEN_EDIT_STATE_MAIN) # Create scene inheriting from MyScene
using System;
using Godot;

public class Game : Node
{
    public readonly Script MyNodeScr = (Script)ResourceLoader.Load("MyNode.cs");
    public readonly PackedScene MySceneScn = (PackedScene)ResourceLoader.Load("MyScene.tscn");
    public Node ANode;
    public Node MyNode;
    public Node MyScene;
    public Node MyInheritedScene;

    public Game()
    {
        ANode = new Node();
        MyNode = new MyNode(); // Same syntax
        MyScene = MySceneScn.Instance(); // Different. Instantiated from a PackedScene
        MyInheritedScene = MySceneScn.Instance(PackedScene.GenEditState.Main); // Create scene inheriting from MyScene
    }
}

Кроме того, скрипты будут работать немного медленнее, чем сцены, из-за разницы в скорости между движком и кодом скрипта. Чем больше и сложнее узел, тем больше причин строить его как сцену.

Именованные типы

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

  1. Знать базовый тип скрипта, который они хотели бы использовать.

  2. Создайте экземпляр этого базового типа.

  3. Добавьте скрипт в узел.

    1. (Метод Drag-n-drop)

      1. Найдите скрипт в панели FileSystem.

      2. Перетащите скрипт на ноду, в панели Scene.

    2. (Метод свойства)

      1. Прокрутите вниз до нижней части инспектора, найдите свойство script и выберите его.

      2. В раскрывающемся списке выберите "Load" (Загрузить).

      3. Выберите скрипт в диалоге выбора файлов.

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

Существует две системы регистрации типов...

  • Пользовательские Типы

    • Editor-only (Только редактор). Имена типов недоступны во время выполнения.

    • Не поддерживает унаследованные пользовательские типы.

    • Инструмент инициализации. Создает узел со скриптом. Ничего более.

    • Редактор не имеет никакого представления о типе скрипта или его связи с другими типами движков или скриптов.

    • Позволяет пользователям определять значок.

    • Работает для всех скриптовых языков, потому что работает со скриптовыми ресурсами абстрактно.

    • Настройка с помощью EditorPlugin.add_custom_type.

  • Классы скриптов

    • Доступны редактор и среда выполнения.

    • Полностью отображает отношения наследования.

    • Создаёт узел со скриптом, но также может изменять типы или расширять тип из редактора.

    • Редактор знает об отношениях наследования между скриптами, классами скриптов и классами C++ движка.

    • Позволяет пользователям определять значок.

    • Разработчики движка должны добавлять поддержку языков вручную (как для отображения имени, так и для доступности во время выполнения).

    • Только Godot версии 3.1 и выше.

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

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

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

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

Производительность сценариев и PackedScene

Последний аспект, который следует учитывать при выборе сцен и скриптов, - это скорость выполнения.

По мере увеличения размера объектов необходимый размер скриптов для их создания становится намного больше. Создание иерархии узлов демонстрирует это. Логика каждого отдельного узла может состоять из нескольких сотен строк кода.

Приведенный ниже код создаёт новый узел Node, меняет его имя, назначает ему сценарий, устанавливает его будущего родителя в качестве его владельца, чтобы он сохранялся на диске вместе с ним, и, наконец, добавляет его в качестве дочернего элемента главного узла Main:

# Main.gd
extends Node

func _init():
    var child = Node.new()
    child.name = "Child"
    child.script = preload("Child.gd")
    child.owner = self
    add_child(child)
using System;
using Godot;

public class Main : Resource
{
    public Node Child { get; set; }

    public Main()
    {
        Child = new Node();
        Child.Name = "Child";
        Child.Script = ResourceLoader.Load<Script>("child.gd");
        Child.Owner = this;
        AddChild(Child);
    }
}

Такой код сценария намного медленнее, чем код C++ на стороне движка. Каждое изменение вызывает отдельный вызов API сценариев, что приводит к множеству "поисков" в серверной части, чтобы найти логику для выполнения.

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

Вывод

В конце концов, лучший подход заключается в том, чтобы рассмотреть следующее:

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

  • Если кто-то хочет создать концепцию, характерную для его игры, то это всегда должна быть сцена. Сцены легче отслеживать/редактировать и обеспечивают большую безопасность, чем скрипты.

  • Если кто-то хочет дать сцене имя, то он все равно может сделать это в 3.1, объявив класс скрипта и дав ему сцену в качестве константы. Скрипт становится, по сути, пространством имён:

    # game.gd
    extends Reference
    class_name Game # extends Reference, so it won't show up in the node creation dialog
    const MyScene = preload("my_scene.tscn")
    
    # main.gd
    extends Node
    func _ready():
        add_child(Game.MyScene.instance())