Библиотека для создания кастомных нодовых редакторов.
- Собственные ассеты для графов
- Кастомизация UI редактора
- Выполнение графов: синхронное и асинхронное
- Работает в Runtime и в Editor
Для установки в проект необходимо поместить содержимое репозитория в любую папку внутри Assets. Требуется версия Unity не ниже 2019.1
В самом минимальном виде вам нужно сделать следующее:
Для этого вам нужно создать класс, реализующий интерфейс IGraphType
public class MinimalGraphType : IGraphType
{
private BaseConnectionType _defaultConnectionType = new BaseConnectionType();
// название графа, которое будет отображаться в окне редактора
public string Name => "Minimal Graph";
// список всех типов узлов, которые могут содержаться в графе.
// здесь мы создаем список сами, но можно использовать для этого отдельный реестр INodeTypeRegistry. об этом подробнее дальше.
public IReadOnlyList<INodeType> AvailableNodes { get; } = new List<INodeType>() { new MinimalNodeType() };
// возвращает тип соединения, которое может соединить выходной порт с типом from и входной порт с типом to
// если указанные типы соединить нельзя, то метод должен вернуть null
public IConnectionType FindConnectionType( Type from, Type to )
{
return _defaultConnectionType;
}
// вызывается после того, как граф был полностью загружен и инициализирован
public void PostLoad( GraphModel graph )
{
}
}
Для этого вам понадобится класс, унаследованный от BaseGraphAssetImporter
// обязательный атрибут. он задает какому типу ассетов соответствует этот класс
[ScriptedImporter(0, ConstExtension, -1)]
public class MinimalGraphAssetImporter : BaseGraphAssetImporter
{
// расширение файла, в котором будут хранится ассеты этого типа. должно быть уникальным, т.к. по нему Unity определяет, каким образом импортировать ассет
public const string ConstExtension = "mingraph";
// это свойство нужно для работы базового класса и должно возвращать то же расширение файла
public override string Extension => ConstExtension;
// задает тип окна редактора. если вам понадобится расширять возможности окна редактирования - укажите здесь тип своего окна
public override Type EditorWindowType => typeof( BaseGraphEditorWindow );
// пункт меню для создания нового ассета в проекте. этот метод не обязателен, вы можете создавать ассеты любым другим способом, но таким способом проще
[MenuItem( "Assets/Create/Minimal Graph", false, 208 )]
public static void CreateGraph()
{
var type = new MinimalGraphType();
var graph = new GraphModel( type );
CreateNewGraphAsset( graph, $"New Build Graph.{ConstExtension}" );
}
}
Для этого нужно создать класс, реализующий интерфейс INodeType
. Но намного проще - унаследоваться от BaseNodeType
:
public class MinimalNodeType : BaseNodeType
{
// объявляем входной порт типа object и id = "In"
public InputPort<object> In { get; }
// объявляем выходной порт типа object и id = "Out"
public OutputPort<object> Out { get; }
}
Все. Этого кода достаточно, чтобы можно было создать Asset в окне Project и открыть окно редактирования с помощью двойного клика по файлу.
Вы можете автоматически обнаруживать доступные типы узлов через рефлексию с помощью класса BaseNodeTypeRegistry
.
Для этого вам нужно унаследоваться от него и переопределить несколько методов:
public class MinimalNodeTypeRegistry : BaseNodeTypeRegistry
{
public BuildNodeTypeRegistry()
{
// Опционально отключает поиск в Editor-only сборках
ExcludeEditorAssemblies = true;
}
// Опционально фильтрует сборки, в которых следует искать типы
protected override bool ShouldProcessAssembly( Assembly asm )
{
return asm.FullName.Contains( "MyProjectNodes" );
}
// Опционально фильтрует типы, которые могут быть использованы
protected override bool ShouldProcessType( Type type )
{
return typeof( MinimalNodeType ).IsAssignableFrom( type );
}
}
Затем используйте этот класс в вашем GraphType
:
public class MinimalGraphType : IGraphType
{
private INodeTypeRegistry _nodeTypeRegistry = new MinimalNodeTypeRegistry();
public IReadOnlyList<INodeType> AvailableNodes => _nodeTypeRegistry.AvailableNodes;
// остальной код ....
}
Редактировать граф хорошо, а использовать полученный граф для каких-то практических целей - еще лучше. Для этого существуют методы GraphModel.Execute
и GraphModel.ExecuteAsync
. Но для того, чтобы их вызвать, нам надо добавить еще немного кода.
Добавим метод Execute в наш тип узла (MinimalNodeType
из предыдущего шага):
public void Execute( INodeExecutionContext ctx )
{
// получаем значение из входного порта
var inputValue = In.Get( ctx );
// рассчитываем выходное значение
var outputValue = inputValue.ToString();
// выставляем значение в выходной порт
Out.Set( ctx, outputValue );
}
Создадим собственный тип редактора графа. Он нужен, чтобы добавить дополнительные кнопки в тулбар.
public class MinimalGraphEditorView : BaseGraphEditorView
{
public MinimalGraphEditorView()
{
// добавляем в тулбар окна кнопку, которая будет вызывать Execute нашего графа
Toolbar.Add( new ToolbarButton( ExecuteGraph ) { text = "Execute Graph" } );
}
private void ExecuteGraph()
{
// вызываем выполнение текущего графа
Graph.Execute( ExecuteNode );
}
// этот метод будет вызываться для каждого узла графа
private void ExecuteNode( INodeExecutionContext ctx)
{
// получаем тип нашейго узла
var nodeType = ctx.Node.Type as MinimalNodeType;
// вызываем Execute конкретного типа узла
nodeType.Execute( ctx );
}
}
Создадим собственный тип окна редактора:
public class MinimalGraphEditorWindow : BaseGraphEditorWindow
{
protected override BaseGraphEditorView CreateEditorView()
{
// создаем наш собственный редактор, вместо дефолтного
return new MinimalGraphEditorView();
}
}
Укажем наш тип окна в Importer'е ассета:
public class MinimalGraphAssetImporter : BaseGraphAssetImporter
{
// ...
public override Type EditorWindowType => typeof( MinimalGraphEditorWindow );
// ...
}
Теперь мы можем выполнить наш граф, нажав на кнопку Execute Graph в окне редактора. Но мы получим ошибку, т.к. все наши ноды имеют входы. Чтобы ошибок не было, нужно реализовать ноду, у которой нет входов и использовать ее в самом начале графа.
Выполнение графа - это операция обхода узлов и соединений графа в определенном порядке.
Сначала выполняются узлы, у которых нет входов. Эти узлы вычисляют выходные значения, которые передаются через соединения в следующие узлы. Для соединений тоже можно задать метод, который будет вызван при прохождении через каждое из них.
Дальше находится узел, у которого вычислены все входные значения и выполняется он. Этот шаг повторяется пока мы не обойдем все узлы, соединенные с начальными.
Если в графе останутся узлы, у которых не вычислены все входные значения, будет выведено сообщение в лог.
При асинхронном выполнении происходят те же действия. Отличие лишь в том, что для каждого элемента (узла или соединения) вызывается функция, возвращающая Task
, и сам метод ExecuteAsync
также возвращает Task
.
Каждый узел, порт и соединение рисуются с помощью отдельной View. Вы можете использовать собственные классы для отображения этих элементов.
Для этого вам надо переопределить соответствующие методы в классе редактора графа:
public override BaseNodeView CreateNodeView( NodeModel node, IEdgeConnectorListener edgeConnectorListener )
{
return new MinimalGraphNodeView( node, edgeConnectorListener );
}
public override BaseConnectionView CreateConnectionView( ConnectionModel connection )
{
return new MinimalGraphConnectionView( connection );
}
И во View узла:
protected virtual BasePortView CreatePortView( PortModel port )
{
return new MinimalPortView( port );
}
Созданные вами View должны наследоваться от BaseNodeView
, BaseConnectionView
и BasePortView
соответственно.
У каждого типа узлов могут быть свои собственные параметры, которые доступны для редактирования в окне графа. По умолчанию никаких параметров не создается, но это несложно исправить.
Необходимо переопределить метод CreatePropertyBlock
в классе BaseNodeType
. Этот метод должен вернуть объект класса наследника PropertyBlock
. Предпочтительным способом является создание вложенного класса внутри вашего NodeType
:
public class MinimalNodeType : BaseNodeType
{
protected override PropertyBlock CreatePropertyBlock( NodeModel node )
{
return new Properties();
}
public class Properties
{
public string MyProperty;
}
}
Теперь в каждом узле этого типа будет отображаться редактор свойств. По умолчанию он поддерживает большинство простых типов, типы Unity (Vector3
, Color
и т.п.), списки, и вложенные объекты.
Но вы не ограничены редактором по умолчанию. Вы можете написать полностью свою View, которая будет отображаться в редакторе.
Чтобы ее подключить необходимо переопределить метод CreatePropertiesView
в классе BaseNodeView
:
public override VisualElement CreatePropertiesView()
{
return new MyCustomView( Node.PropertyBlock );
}