Когда использовать сцены вместо скриптов
Мы уже рассмотрели, чем отличаются сцены и скрипты. Скрипты определяют расширение класса движка с императивным кодом, сцены - с декларативным кодом.
В результате возможности каждой системы различаются. Сцены могут определять способ инициализации расширенного класса, но не его фактическое поведение. Сцены часто используются вместе со скриптом: сцена объявляет композицию узлов, а скрипт добавляет поведение с помощью императивного кода.
Анонимные типы
Возможно полностью определить содержимое сцены, используя только скрипт. Это, по сути, то, что делает редактор 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.instantiate() # Different method call.
var my_inherited_scene = MyScene.instantiate(PackedScene.GEN_EDIT_STATE_MAIN) # Create scene inheriting from MyScene.
using Godot;
public partial class Game : Node
{
public static CSharpScript MyNode { get; } =
GD.Load<CSharpScript>("res://Path/To/MyNode.cs");
public static PackedScene MyScene { get; } =
GD.Load<PackedScene>("res://Path/To/MyScene.tscn");
private Node _node;
private Node _myNode;
private Node _myScene;
private Node _myInheritedScene;
public Game()
{
_node = new Node();
_myNode = MyNode.New().As<Node>();
// Different than calling new() or MyNode.New(). Instantiated from a PackedScene.
_myScene = MyScene.Instantiate();
// Create scene inheriting from MyScene.
_myInheritedScene = MyScene.Instantiate(PackedScene.GenEditState.Main);
}
}
Кроме того, скрипты будут работать немного медленнее, чем сцены, из-за разницы в скорости между движком и кодом скрипта. Чем больше и сложнее узел, тем больше причин строить его как сцену.
Именованные типы
Скрипты могут быть зарегистрированы как новый тип в самом редакторе. При этом он отображается как новый тип в диалоге создания узла или ресурса с опциональной иконкой. Таким образом, возможности пользователя по использованию скрипта становятся гораздо более удобными. Вместо того чтобы...
Знать базовый тип скрипта, который они хотели бы использовать.
Создавать экземпляр этого базового типа.
Добавлять скрипт в узел.
При наличии зарегистрированного скрипта создание заскриптованного типа становится опциональным, как и другие узлы и ресурсы в системе. В диалоге создания даже имеется строка поиска, позволяющая найти тип по имени.
Существует две системы регистрации типов:
-
Editor-only (Только редактор). Имена типов недоступны во время выполнения.
Не поддерживает унаследованные пользовательские типы.
Инструмент инициализации. Создает узел со скриптом. Ничего более.
Редактор не имеет никакого представления о типе скрипта или его связи с другими типами движков или скриптов.
Позволяет пользователям определять иконку.
Работает для всех скриптовых языков, потому что работает со скриптовыми ресурсами абстрактно.
Настраивается с помощью EditorPlugin.add_custom_type.
-
Доступны редактор и среда выполнения.
Полностью отображает отношения наследования.
Создаёт узел со скриптом, но также может изменять типы или расширять тип из редактора.
Редактор знает об отношениях наследования между скриптами, классами скриптов и классами C++ движка.
Позволяет пользователям определять иконку.
Разработчики движка должны добавлять поддержку языков вручную (как для отображения имени, так и для доступности во время выполнения).
Редактор сканирует папки проекта и регистрирует любые открытые имена для всех скриптовых языков. Каждый скриптовой язык должен реализовывать свою собственную поддержку для раскрытия этой информации.
Обе методологии добавляют имена в диалог создания, но классы скриптов, в частности, также позволяют пользователям получить доступ к названию типа без загрузки ресурса скрипта. Создание экземпляров и доступ к константам или статическим методам является жизнеспособным из любого места.
С такими функциями можно пожелать, чтобы их тип был скриптом без сцены из-за простоты использования, которую он предоставляет пользователям. Тем, кто разрабатывает плагины или создает собственные инструменты для дизайнеров, таким образом будет легче.
С другой стороны, это также означает необходимость использования в значительной степени императивного программирования.
Производительность сценариев и PackedScene
Последний аспект, который следует учитывать при выборе сцен и скриптов — это скорость выполнения.
По мере увеличения размера объектов необходимый размер скриптов для их создания становится намного больше. Создание иерархии узлов демонстрирует это. Логика каждого отдельного узла может состоять из нескольких сотен строк кода.
Приведенный ниже код создаёт новый узел Node, меняет его имя, назначает ему скрипт, устанавливает его будущего родителя в качестве его владельца, чтобы он сохранялся на диске вместе с ним, и, наконец, добавляет его в качестве дочернего элемента главного узла Main:
# main.gd
extends Node
func _init():
var child = Node.new()
child.name = "Child"
child.script = preload("child.gd")
add_child(child)
child.owner = self
using Godot;
public partial class Main : Node
{
public Node Child { get; set; }
public Main()
{
Child = new Node();
Child.Name = "Child";
var childID = Child.GetInstanceId();
Child.SetScript(GD.Load<Script>("res://Path/To/Child.cs"));
// SetScript() causes the C# wrapper object to be disposed, so obtain a new
// wrapper for the Child node using its instance ID before proceeding.
Child = (Node)GodotObject.InstanceFromId(childID);
AddChild(Child);
Child.Owner = this;
}
}
Такой код скрипта намного медленнее, чем код C++ на стороне движка. Каждое изменение вызывает отдельный вызов API сценариев, что приводит к множеству "поисков" в серверной части, чтобы найти логику для выполнения.
Сцены помогают избежать этой проблемы с производительностью. PackedScene, базовый тип, от которого наследуются сцены определяет ресурсы, использующие сериализованные данные для создания объектов. Движок может обрабатывать сцены группами на сервере и обеспечивать гораздо лучшую производительность, чем скрипты.
Заключение
В конце концов, лучший подход заключается в том, чтобы рассмотреть следующее:
Если кто-то хочет создать базовый инструмент, который будет повторно использоваться в нескольких различных проектах, и который, скорее всего, будут использовать люди всех уровней квалификации (включая тех, кто не называет себя "программистами"), то, скорее всего, это должен быть скрипт с пользовательским именем/иконкой.
Если кто-то хочет создать концепцию, характерную для его игры, то это всегда должна быть сцена. Сцены легче отслеживать/редактировать и обеспечивают большую безопасность, чем скрипты.
Если хочется дать сцене имя, то это можно сделать, объявив класс скрипта и присвоив ему сцену в качестве константы. Скрипт становится, по сути, пространством имён:
# game.gd class_name Game # extends RefCounted, so it won't show up in the node creation dialog. extends RefCounted const MyScene = preload("my_scene.tscn") # main.gd extends Node func _ready(): add_child(Game.MyScene.instantiate())
// Game.cs public partial class Game : RefCounted { public static PackedScene MyScene { get; } = GD.Load<PackedScene>("res://Path/To/MyScene.tscn"); } // Main.cs public partial class Main : Node { public override void _Ready() { AddChild(Game.MyScene.Instantiate()); } }