[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍

这篇具有很好参考价值的文章主要介绍了[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言

GraphView的节点(Node)和端口(Port)

GraphView的边(Edge)

关联Inspector窗口,显示数据

增加节点操作

构建节点图

删除与修改节点操作

创建节点的新建菜单栏

GraphView 复制粘贴操作实现


前言

        前置章节:

[Unity] 使用GraphView实现一个可视化节点的事件行为树系统(序章/Github下载)_Sugarzo的博客-CSDN博客_unity graphview

[Unity] GraphView 可视化节点的事件行为树(一) Runtime Node_Sugarzo的博客-CSDN博客 

[Unity] GraphView 可视化节点的事件行为树(二) UI Toolkit介绍,制作事件行为树的UI_Sugarzo的博客-CSDN博客

        在前面两个章节中,我们实现了两个部分的内容:Runtime部分的节点逻辑、UI Toolkit绘制Editor UI。这一章节将会用到GraphView框架,完成我们制作的事件行为树剩下的所有内容。

        GraphView是Unity内置的节点绘制系统,实际上Unity里内置的Visual Scripting(可视化脚本编辑),URP中的ShaderGraph,都是GraphView制作。

[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍

[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍

         GraphView也属于UI Toolkit的一部分。在Unity 2021版本中,GraphView是Experimental部分。需要引入命名空间:UnityEditor.Experimental.GraphView

using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;

public class FlowChartView : GraphView
{
    public new class UxmlFactory : UxmlFactory<FlowChartView, GraphView.UxmlTraits> { }
}

        回顾一下我们前两节的内容,给这一部分留下了什么内容。我们用UI Builder写了一个Editor窗口,里面有两个UI元素,是我们继承Visual Element新建的脚本元素。

        FlowCharView是左边的Inspector窗口,该窗口的目的是显示Runtime的节点数据(这里的Inspector用的是unity IMGUI默认的Inspector绘制)

[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍

        在Runtime部分,每个节点都是一个MonoBehaviour的脚本。忽视掉其他的Runtime逻辑,基类给Editor使用的数据只有:这个节点的位置(Vector2),以及它连接的下一个节点。

public abstract class NodeState : MonoBehaviour
    {
#if UNITY_EDITOR
        [HideInInspector]
        public Vector2 nodePos;
#endif
        //流向下一节点的流
        public MonoState nextFlow;
    }

        了解了这些数据,我们就可以开始正式进入GraphView了。 

GraphView的节点(Node)和端口(Port)

        创建脚本,继承GraphView.Node,这就是在GraphView中最基础的节点元素了。

        在这里,我们先创建一个节点View的基类。

        在下面的脚本里,一个节点View记录了三个数据:

        1.具体关联到的Runtime节点(这里指的是前文提到的NodeState)

        2.当被点击时触发的委托事件(这个委托要被转发到Inspector面板中)

        3.给节点View创建端口函数(Port)

public class BaseNodeView : UnityEditor.Experimental.GraphView.Node
    {
        /// <summary>
        /// 点击该节点时被调用的事件,比如转发该节点信息到Inspector中显示
        /// </summary>
        public Action<BaseNodeView> OnNodeSelected;

        public TextField textField;
        public string GUID;

        public BaseNodeView() : base()
        {
            textField = new TextField();
            GUID = Guid.NewGuid().ToString();
            
        }
        // 为节点n创建input port或者output port
        // Direction: 是一个简单的枚举,分为Input和Output两种
        public Port GetPortForNode(BaseNodeView n, Direction portDir, Port.Capacity capacity = Port.Capacity.Single)
        {
            // Orientation也是个简单的枚举,分为Horizontal和Vertical两种,port的数据类型是bool
            return n.InstantiatePort(Orientation.Horizontal, portDir, capacity, typeof(bool));
        }

        //告诉Inspector去绘制该节点
        public override void OnSelected()
        {
            base.OnSelected();
            Debug.Log($"{this.name}节点被点击");
            OnNodeSelected?.Invoke(this);
        }

        public abstract NodeState state { get; set; }
    }

        因为最后我们需要实现 触发器/行为/序列/判断 四个节点的View,我们这里创建好节点基类的泛型版本。

public class BaseNodeView<State> : BaseNodeView where State : NodeState
    {
        /// <summary>
        /// 关联的State
        /// </summary>
        private State _state;

        public override NodeState state 
        { 
            get
            {
                return _state;
            }
            set
            {
                if (_state != null)
                    _state.node = null;

                _state = (State)value;
            }
    }

        接着就可以继承泛型版本开始写四个节点的View脚本了。在本框架中,四个节点的样式和性质如下:

        触发器:只有一个输出端口,输出端口只能单连接。

        事件节点:一个输入端口,一个输出端口,输入端口可以多连接,输出端口只能单连接。

        条件节点:一种特殊的事件节点,有两个输出端口,都只能单连接。当条件满足时流向true,不满足时流向false

        序列节点:一种特殊的事件节点,有一个支持多连接的输出端口(也是目前框架里唯一一个支持输出端口多连接的节点)

[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍

public class TriggerNodeView : BaseNodeView<BaseTrigger>
{
    public TriggerNodeView()
    {
        title = state != null ? state.name : "TriggerNode";

        //Trigger只有一个输出端口
        Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
        output.portName = "output";
        outputContainer.Add(output);


    }
}
public class ActionNodeView : BaseNodeView<BaseAction>
{
    public ActionNodeView()
    {
        //Action有一个输出端口一个输入端口,输入接口可以多连接
        Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Multi);
        Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
        input.portName = "input";
        output.portName = "output";

        title = state != null ? state.name : "ActionNode";

        inputContainer.Add(input);
        outputContainer.Add(output);
    }
}
public class SequenceNodeView : BaseNodeView<BaseSequence>
{
    public SequenceNodeView()
    {
        //Sequence有一个输出端口一个输入端口,输入接口只能单连接,输出端口可以多连接
        Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Single);
        Port output = GetPortForNode(this, Direction.Output, Port.Capacity.Multi);
        input.portName = "input";
        output.portName = "output";

        title = state != null ? state.name : "SequenceNode";

        inputContainer.Add(input);
        outputContainer.Add(output);
    }
}
public class BranchNodeView : BaseNodeView<BaseBranch>
{
    public BranchNodeView()
    {
        //Sequence有两个输出端口一个输入端口
        Port input = GetPortForNode(this, Direction.Input, Port.Capacity.Multi);
        Port output1 = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
        Port output2 = GetPortForNode(this, Direction.Output, Port.Capacity.Single);
        input.portName = "input";
        output1.portName = "true";
        output2.portName = "false";

        title = state != null ? state.name : "IfNode";

        inputContainer.Add(input);
        outputContainer.Add(output1);
        outputContainer.Add(output2);
    }
}

        这样,我们就制作完成了四种类型的节点View了。

GraphView的边(Edge)

        对于每个节点View的端口(Port),都可以进行边Edge的连接。对于判断每个端口间是否可以连接,除了创建这个端口选择的枚举类型Port.Capacity.Single和Port.Capacity.Multi外,可以重写GraphView中的GetCompatiblePorts函数(这里是一个迭代器遍历,连接判断逻辑是一个点不能和自己连接)

        下列函数在FlowChartView:GraphView脚本中

//判断每个点是否可以相连
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
    return ports.ToList().Where(endPort =>
    endPort.direction != startPort.direction &&
    endPort.node != startPort.node).ToList();
}
//连接两个点
private void AddEdgeByPorts(Port _outputPort, Port _inputPort)
{
    if (_outputPort.node == _inputPort.node)
        return;

    Edge tempEdge = new Edge()
    {
        output = _outputPort,
        input = _inputPort
    };
    tempEdge.input.Connect(tempEdge);
    tempEdge.output.Connect(tempEdge);
    Add(tempEdge);
}

关联Inspector窗口,显示数据

        在InspectorView中给自己添加一个IMGUIContainer(也是Visual Element元素派生),添加Unity自带的Editor窗口。

public class InspectorView : VisualElement
{
    public new class UxmlFactory : UxmlFactory<InspectorView, UxmlTraits> { }

    Editor editor;

    public InspectorView()
    {
    }

    internal void UpdateSelection(BaseNodeView nodeView)
    {
        Clear();
        Debug.Log("显示节点的Inspector面板");
        UnityEngine.Object.DestroyImmediate(editor);
        editor = Editor.CreateEditor(nodeView.state);

        IMGUIContainer container = new IMGUIContainer(() => {
            if (nodeView != null && nodeView.state != null)
            {
                editor.OnInspectorGUI();
            }
        });
        Add(container);
    }
}

        将委托连接到NodeView上。其中FlowChartView.OnNodeSelected将在创建节点的函数中绑定。

public class FlowChartEditorWindow : EditorWindow
    {
        public static void OpenWindow()
        {
            FlowChartEditorWindow wnd = GetWindow<FlowChartEditorWindow>();
            wnd.titleContent = new GUIContent("FlowChart");
        }

        /// <summary>
        /// 当前选择的游戏物品
        /// </summary>
        public static GameObject userSeletionGo;

        FlowChartView flowChartView;
        InspectorView inspectorView;

        public void CreateGUI()
        {
            // Each editor window contains a root VisualElement object
            VisualElement root = rootVisualElement;

            
            // Import UXML
            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/SugarzoNode/Editor/UIBuilder/FlowChart.uxml");
            visualTree.CloneTree(root);

            // A stylesheet can be added to a VisualElement.
            // The style will be applied to the VisualElement and all of its children.
            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/SugarzoNode/Editor/UIBuilder/FlowChart.uss");
            root.styleSheets.Add(styleSheet);

            //设置节点视图和Inspector视图
            flowChartView = root.Q<FlowChartView>();
            inspectorView = root.Q<InspectorView>();

            flowChartView.OnNodeSelected = OnNodeSelectionChanged;
            flowChartView.userSeletionGo = userSeletionGo;
            flowChartView.window = this;

            //构造节点
            flowChartView.ResetNodeView();
        }

        void OnNodeSelectionChanged(BaseNodeView nodeView)
        {
            Debug.Log("Editor受到节点被选中信息");
            inspectorView.UpdateSelection(nodeView);
        }

增加节点操作

       在本框架中,所有节点都是作为一个Compoment附加到游戏物品上。在上一节中,我们以及写好了一个标记当前选择的游戏物品的脚本逻辑。 创建节点需要操作对于GO上的MonoBehaviour节点进行AddCompoment。

public class FlowChartView : GraphView
    {
        public GameObject userSeletionGo;
        public Action<BaseNodeView> OnNodeSelected;

        public FlowChartView()
        {
            userSeletionGo = userSeletionGo == null ? FlowChartEditorWindow.userSeletionGo : userSeletionGo;
    }
private void CreateNode(Type type, Vector2 pos = default)
{
    if (userSeletionGo == null)
        return;

    BaseNodeView nodeView = null;
    if (type.IsSubclassOf(typeof(BaseTrigger)))
        nodeView = new TriggerNodeView();
    if (type.IsSubclassOf(typeof(BaseAction)))
        nodeView = new ActionNodeView();
    if (type.IsSubclassOf(typeof(BaseSequence)))
        nodeView = new SequenceNodeView();
    if (type.IsSubclassOf(typeof(BaseBranch)))
        nodeView = new BranchNodeView();


    if (nodeView == null)
    {
        Debug.LogError("节点未找到对应属性的NodeView");
        return;
    }

    //添加Component,关联节点
    nodeView.OnNodeSelected = OnNodeSelected;
    nodeView.state = (NodeState)userSeletionGo.AddComponent(type);
    nodeView.SetPosition(new Rect(pos, nodeView.GetPosition().size));

    this.AddElement(nodeView);
}

构建节点图

        使用GetCompoments来获取游戏物品上的所有节点,再根据它的位置和下一个流的数据,逐个创建点和边还原出来。其中ResetNodeView函数可以由CreateGUI()调用。

        下列函数放在FlowChartView : GraphView中。

//重构布局
public void ResetNodeView()
{
    if (userSeletionGo != null)
    {
        Debug.Log("构建节点图");
        var list = userSeletionGo.GetComponents<NodeState>();
        foreach (var item in list)
            CreateBaseNodeView(item);
    }
    if (userSeletionGo != null)
    {
        Debug.Log("构建节点边的关系");
        CreateNodeEdge();
    }
}

//复原节点操作
private void CreateBaseNodeView(NodeState nodeClone)
{
    if (userSeletionGo == null || nodeClone == null)
        return;

    BaseNodeView nodeView = null;
    //判断需要复原的节点
    if (nodeClone is BaseTrigger trigger)
        nodeView = new TriggerNodeView();
    if (nodeClone is BaseAction action)
        nodeView = new ActionNodeView();
    if (nodeClone is BaseSequence sequence)
        nodeView = new SequenceNodeView();
    if (nodeClone is BaseBranch branch)
        nodeView = new BranchNodeView();

    if (nodeView == null)
    {
        Debug.LogError("节点未找到对应属性的NodeView");
        return;
    }

    nodeView.OnNodeSelected = OnNodeSelected;
    nodeView.state = nodeClone;
    nodeView.SetPosition(new Rect(nodeClone.nodePos, nodeView.GetPosition().size));

    nodeView.RefreshExpandedState();
    nodeView.RefreshPorts();

    AddElement(nodeView);
}

//复原节点的边
private void CreateNodeEdge()
{
    if (userSeletionGo == null)
        return;

    //这里有点像图的邻接表
    Dictionary<NodeState, BaseNodeView> map = new Dictionary<NodeState, BaseNodeView>();
    Dictionary<BaseNodeView, Port> inputPorts = new Dictionary<BaseNodeView, Port>();
    Dictionary<BaseNodeView, List<Port>> outputPorts = new Dictionary<BaseNodeView, List<Port>>();

    ports.ForEach(x =>
    {
        var y = x.node;
        var node = y as BaseNodeView;
        if (!map.ContainsKey(node.state))
        {
            map.Add(node.state, node);
        }
        if (!inputPorts.ContainsKey(node))
        {
            inputPorts.Add(node, x);
        }
        if (!outputPorts.ContainsKey(node))
        {
            outputPorts.Add(node, new List<Port>());
        }
        if (x.direction == Direction.Output)
            outputPorts[node].Add(x);
    });

    //只负责连接下面的节点
    foreach (var node in map.Keys)
    {

        if (node is BaseSequence sequence)
        {
            Port x = outputPorts[map[sequence]][0];
            foreach (var nextflow in sequence.nextflows)
            {
                Port y = inputPorts[map[nextflow]];
                AddEdgeByPorts(x, y);
            }
        }
        else if (node is BaseBranch branch)
        {
            var truePorts = outputPorts[map[branch]][0].portName == "true" ? outputPorts[map[branch]][0] : outputPorts[map[branch]][1];
            var falsePorts = outputPorts[map[branch]][0].portName == "false" ? outputPorts[map[branch]][0] : outputPorts[map[branch]][1];

            if (branch.trueFlow != null)
                AddEdgeByPorts(truePorts, inputPorts[map[branch.trueFlow]]);
            if (branch.falseFlow != null)
                AddEdgeByPorts(falsePorts, inputPorts[map[branch.falseFlow]]);
        }
        else if (node is MonoState state)
        {
            //普通的Action或者Trigger,只处理nextFlow就好了
            if (state.nextFlow != null)
                AddEdgeByPorts(outputPorts[map[state]][0], inputPorts[map[state.nextFlow]]);
        }

    }
}

删除与修改节点操作

        对于GraphView的删除,GraphView基类提供了一个委托接口,我们可以关联到它监听图的变化。当对于的点、边、位置被修改时,我们也需要实时更新Runtime节点的数据或者删除节点组件.

public FlowChartView()
{
    //当GraphView变化时,调用方法
    graphViewChanged += OnGraphViewChanged;
}
private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange)
{

if (graphViewChange.elementsToRemove != null)
{
    //对于每个被移除的节点
    graphViewChange.elementsToRemove.ForEach(elem =>
    {
        BaseNodeView BaseNodeView = elem as BaseNodeView;
        if (BaseNodeView != null)
        {
            GameObject.DestroyImmediate(BaseNodeView.state);
        }
        Edge edge = elem as Edge;
        if (edge != null)
        {
            BaseNodeView parentView = edge.output.node as BaseNodeView;
            BaseNodeView childView = edge.input.node as BaseNodeView;
            //If和Branch节点特判定
            if (edge.output.node is BranchNodeView view)
            {
                if (edge.input.portName == "true")
                {
                    (parentView.state as BaseBranch).trueFlow = null;
                }
                if (edge.input.portName == "false")
                {
                    (parentView.state as BaseBranch).falseFlow = null;
                }
            }
            else if (edge.output.node is SequenceNodeView sqView)
            {
                (parentView.state as BaseSequence).nextflows.Remove(childView.state as MonoState);
            }
            else
                parentView.state.nextFlow = null;
        }
    });
}
//对于每个被创建的边
if (graphViewChange.edgesToCreate != null)
{
    graphViewChange.edgesToCreate.ForEach(edge =>
    {
        BaseNodeView parentView = edge.output.node as BaseNodeView;
        BaseNodeView childView = edge.input.node as BaseNodeView;
        //If和Branch节点特判定
        if (edge.output.node is BranchNodeView view)
        {
            if (edge.output.portName.Equals("true"))
            {
                (parentView.state as BaseBranch).trueFlow = childView.state as MonoState;
            }
            if (edge.output.portName.Equals("false"))
            {
                (parentView.state as BaseBranch).falseFlow = childView.state as MonoState;
            }
        }
        else if (edge.output.node is SequenceNodeView sqView)
        {
            (parentView.state as BaseSequence).nextflows.Add(childView.state as MonoState);
        }
        else
            parentView.state.nextFlow = childView.state as MonoState;
    });
}
//遍历节点,记录位置点
nodes.ForEach((n) =>
{
    BaseNodeView view = n as BaseNodeView;
    if (view != null && view.state != null)
    {
        view.state.nodePos = view.GetPosition().position;
    }
});

return graphViewChange;
}

创建节点的新建菜单栏

        在右键后,新添一个创建节点的菜单。这里使用了Linq去遍历和寻找项目中的节点脚本。

[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍 

        在FlowChartView的构造函数中添加布局

public FlowChartView()
{
            //新建搜索菜单
            var menuWindowProvider = ScriptableObject.CreateInstance<SearchMenuWindowProvider>();
            menuWindowProvider.OnSelectEntryHandler = OnMenuSelectEntry;

            nodeCreationRequest += context =>
            {
                SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), menuWindowProvider);
            };
}
public class SearchMenuWindowProvider : ScriptableObject, ISearchWindowProvider
    {
        public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
        {
            var entries = new List<SearchTreeEntry>();
            entries.Add(new SearchTreeGroupEntry(new GUIContent("创建新节点")));                //添加了一个一级菜单

            entries.Add(new SearchTreeGroupEntry(new GUIContent("触发器")) { level = 1 });      //添加了一个二级菜单
            var triggers = GetClassList(typeof(BaseTrigger));
            foreach(var trigger in triggers)
            {
                entries.Add(new SearchTreeEntry(new GUIContent(trigger.Name)) { level = 2,userData = trigger });
            }

            entries.Add(new SearchTreeGroupEntry(new GUIContent("行为")) { level = 1 });
            var actions = GetClassList(typeof(BaseAction));
            foreach(var action in actions)
            {
                entries.Add(new SearchTreeEntry(new GUIContent(action.Name)) { level = 2, userData = action });
            }

            entries.Add(new SearchTreeGroupEntry(new GUIContent("分支")) { level = 1 });
            var branchs = GetClassList(typeof(BaseBranch));
            foreach (var action in branchs)
            {
                entries.Add(new SearchTreeEntry(new GUIContent(action.Name)) { level = 2, userData = action });
            }

            entries.Add(new SearchTreeGroupEntry(new GUIContent("序列")) { level = 1 });
            var sq = GetClassList(typeof(BaseSequence));
            foreach (var action in sq)
            {
                entries.Add(new SearchTreeEntry(new GUIContent(action.Name)) { level = 2, userData = action });
            }


            return entries;
        }


        public delegate bool SerchMenuWindowOnSelectEntryDelegate(SearchTreeEntry searchTreeEntry, SearchWindowContext context);            //声明一个delegate类

        public SerchMenuWindowOnSelectEntryDelegate OnSelectEntryHandler;                              //delegate回调方法

        public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
        {
            if (OnSelectEntryHandler == null)
            {
                return false;
            }
            return OnSelectEntryHandler(searchTreeEntry, context);
        }
        private List<Type> GetClassList(Type type)
        {
            var q = type.Assembly.GetTypes()
                .Where(x => !x.IsAbstract)
                .Where(x => !x.IsGenericTypeDefinition)
                .Where(x => type.IsAssignableFrom(x));

            return q.ToList();
        }
    }

GraphView 复制粘贴操作实现

        操作HandleEvent的事件句柄,检测"Paste"操作,可以使用UnityEditorInternal.ComponentUtility.PasteComponentValues复制组件的value

    protected BoolClass isDuplicate = new BoolClass();

    public override void HandleEvent(EventBase evt)
    {
        base.HandleEvent(evt);

        if (evt is ValidateCommandEvent commandEvent)
        {
            Debug.Log("Event:");
            Debug.Log(commandEvent.commandName);
            //限制一下0.2s执行一次  不然短时间会多次执行
            if (commandEvent.commandName.Equals("Paste"))
            {
                new EditorDelayCall().CheckBoolCall(0.2f, isDuplicate,
                    OnDuplicate);
            }
        }
    }
    /// <summary>
    /// 复制时
    /// </summary>
    protected void OnDuplicate()
    {
        Debug.Log("复制节点");
        //复制节点
        var nodesDict = new Dictionary<BaseNodeView, BaseNodeView>(); //新旧Node对照

        foreach (var selectable in selection)
        {
            var offset = 1;
            if (selectable is BaseNodeView baseNodeView)
            {
                offset++;
                UnityEditorInternal.ComponentUtility.CopyComponent(baseNodeView.state);

                BaseNodeView nodeView = null;
                var nodeClone = baseNodeView.state;
                //判断需要复原的节点
                if (nodeClone is BaseTrigger trigger)
                    nodeView = new TriggerNodeView();
                if (nodeClone is BaseAction action)
                    nodeView = new ActionNodeView();
                if (nodeClone is BaseSequence sequence)
                    nodeView = new SequenceNodeView();
                if (nodeClone is BaseBranch branch)
                    nodeView = new BranchNodeView();

                if (nodeView == null)
                    return;

                //新旧节点映射
                if (nodeView != null)
                {
                    nodesDict.Add(baseNodeView, nodeView);
                }

                nodeView.OnNodeSelected = OnNodeSelected;
                AddElement(nodeView);
                nodeView.state = (NodeState)userSeletionGo.AddComponent(baseNodeView.state.GetType());
                UnityEditorInternal.ComponentUtility.PasteComponentValues(nodeView.state);

                //调整一下流向
                //保持原来的流向算法好难写,还是全部设置成null把
                nodeView.state.nextFlow = null ;
                if(nodeView.state is BaseSequence sq)
                {
                    sq.nextflows = new List<MonoState>();
                }
                if (nodeView.state is BaseBranch br)
                {
                    br.trueFlow = null;
                    br.falseFlow = null;
                }

                //复制出来的节点位置偏移
                nodeView.SetPosition(new Rect(baseNodeView.GetPosition().position + (Vector2.one * 30 * offset),nodeView.GetPosition().size));
            }
        }

        for (int i = selection.Count - 1; i >= 0; i--)
        {
            //取消选择
            this.RemoveFromSelection(selection[i]);
        }

        foreach (var node in nodesDict.Values)
        {
            //选择新生成的节点
            this.AddToSelection(node);
        }
    }

        用到的Editor工具脚本:文章来源地址https://www.toymoban.com/news/detail-426414.html

using System;
using UnityEditor;
using UnityEngine;

public class EditorDelayCall
{
    public class BoolClass
    {
        public bool value;
    }
    /// <summary>
    /// 延迟秒数
    /// </summary>
    private float _delay;

    private Action _callback;
    private float _startupTime;

    public void Call(float delay, Action callback)
    {
        this._delay = delay;
        this._callback = callback;

        EditorApplication.update += Update;
    }

    public void CheckBoolCall(float delay, BoolClass boolClass,
            Action action)
    {
        if (!boolClass.value)
        {
            boolClass.value = true;
            action?.Invoke();
            Call(delay, delegate { boolClass.value = false; });
        }
    }

    // 主动停止
    public void Stop()
    {
        _startupTime = 0;
        _callback = null;

        EditorApplication.update -= Update;
    }

    private void Update()
    {
        // 时间初始化放在这里是因为如果在某些类的构造函数中获取时间是不允许的
        if (_startupTime <= 0)
        {
            _startupTime = Time.realtimeSinceStartup;
        }

        if (Time.realtimeSinceStartup - _startupTime >= _delay)
        {
            _callback?.Invoke();
            Stop();
        }
    }
}

到了这里,关于[Unity] GraphView 可视化节点的事件行为树(三) GraphView介绍的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity可视化Shader工具ASE介绍——6、通过例子说明ASE节点的连接方式

    阿赵的Unity可视化Shader工具ASE介绍目录   大家好,我是阿赵。继续介绍Unity可视化Shader编辑插件ASE的用法。上一篇已经介绍了很多ASE常用的节点。这一篇通过几个小例子,来看看这些节点是怎样连接使用的。   这篇的内容可能会比较长,最终是做了一个遮挡X光的效果,不

    2024年02月07日
    浏览(54)
  • 基于Python的淘宝行为数据可视化分析

    完成如下商业分析任务,通过数据分析和可视化展示,充分挖掘数据的价值,让数据更好地为业务服务: 流量分析 :PV/UV是多少,通过分析PV/UV能发现什么规律? 漏斗分析 :用户“浏览-收藏-加购-购买”的转化率是怎样的? 用户价值分析 :对电商平台什么样的用户是有价值

    2024年02月10日
    浏览(51)
  • 天池赛:淘宝用户购物行为数据可视化分析

    目录 前言 一、赛题介绍 二、数据清洗、特征构建、特征可视化 1.数据缺失值及重复值处理 2.日期分离,PV及UV构建 3.PV及UV可视化 4.用户行为可视化 4.1 各个行为的面积图(以UV为例) 4.2 各个行为的热力图 5.转化率可视化 三、RFM模型 1.构建R、F、M 2.RFM的数据统计分布 3.计算

    2024年01月22日
    浏览(46)
  • 基于Python电商用户行为的数据分析、机器学习、可视化研究

    有需要本项目的源码以及全套文档和相关资源,可以私信博主!!! 在数字化和互联网技术飞速发展的推动下,消费者的购买能力和消费观念呈现不断升级和变迁的趋势。用户消费数据的爆炸式增长,为我们提供了寻找潜在价值信息的机会。 本研究使用了阿里巴巴提供的淘

    2024年02月04日
    浏览(49)
  • 毕业设计 大数据电商用户行为分析及可视化(源码+论文)

    今天学长向大家介绍一个机器视觉的毕设项目,大数据电商用户行为分析及可视化(源码+论文) 项目运行效果: 毕业设计 基于大数据淘宝用户行为分析 项目获取: https://gitee.com/assistant-a/project-sharing 这是一份来自淘宝的用户行为数据,时间区间为 2017-11-25 到 2017-12-03,总计

    2024年02月22日
    浏览(50)
  • 【TIANCHI】天池大数据竞赛(学习赛)--- 淘宝用户购物行为数据可视化分析

    目录 前言 一、数据集的来源和各个字段的意义 二、数据分析 1.引入库 2.读入数据 3.查看数据数量级 4.PV(Page View)/UV访问量 5.漏斗模型 6.用户购买商品的频次分析。 7.ARPPU(average revenue per paying user)  计算 ARPPU  ARPPU出图 8.复购情况分析 计算用户购买频次 复购周期分析 总结

    2024年02月09日
    浏览(41)
  • python毕设选题 - flink大数据淘宝用户行为数据实时分析与可视化

    🔥 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。 为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天

    2024年02月01日
    浏览(72)
  • PyG基于DeepWalk实现节点分类及其可视化

    大家好,我是阿光。 本专栏整理了《图神经网络代码实战》,内包含了不同图神经网络的相关代码实现(PyG以及自实现),理论与实践相结合,如GCN、GAT、GraphSAGE等经典图网络,每一个代码实例都附带有完整的代码。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10 语言环

    2024年01月19日
    浏览(61)
  • 大数据毕设分享 flink大数据淘宝用户行为数据实时分析与可视化

    🔥 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。 为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天

    2024年01月21日
    浏览(61)
  • 【毕业设计】基于大数据的京东消费行为分析与可视化 - python 机器学习

    🔥 这两年开始毕业设计和毕业答辩的要求和难度不断提升,传统的毕设题目缺少创新和亮点,往往达不到毕业答辩的要求,这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师的要求。 为了大家能够顺利以及最少的精力通过毕设,学长分享优质毕业设计项目,今天

    2024年02月04日
    浏览(62)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包