因为受到罡少的影响

决定使用NPBehave来作为MDDSkillEngine中的技能逻辑组成部分

因为NPBehave没有可视化界面,所以我们要制作一个。(基于罡少编程)

本篇使用XNode + Odin的方案为NPBehave提供一个可视化编辑界面,借鉴了罡少的思路

需要解决的问题

其实可视化编辑是个老生常谈的问题

我们无论如何都避不开在运行时申请行为树的runtime代码空间

所以我们可以选择在运行时构建行为树

那么我们的问题就来到了如何编辑构建行为树所需要的数据上

在这之前

我们梳理一下NPBehave的结构

NPBehave结构

简单的行为树

可以看到,一颗行为树的结构其实就是一个多叉树

NPBehave有三大基础节点

1.组合节点

常见的有

Sequence--顺序执行节点

Parallel--并行节点

组合节点的特征是有一个父亲和多个孩子

2.修饰节点

常见的有

Codition

BlackboardCondition

Root

CoolDown

修饰节点的特征是只有一个孩子,是孩子的前置节点,所以常作为是否执行孩子节点的判断节点使用

3.任务节点

常见的有

Action

Wait

任务节点一定是行为树的末端叶子节点,他是行为树经过诸多条件判断后最后的执行节点。

初始化顺序

对行为树结构有了一个基础理解以后

我们发现,初始化行为树其实就是一个初始化多叉树的过程

我这里的选择是在Editor阶段处理好数据排序

在runtime阶段以一个固定的顺序读取初始化就好

所以分为两个阶段

1.Editor排序关键步骤

一,子节点顺序调整

我们在编辑行为树的过程中会频繁的拖动节点,频繁的创建和删除节点

在这个过程中,我们要保证数据运行时的执行顺序是我们编辑时的视觉空间顺序

所以我们要时常对每个节点的孩子进行排序

/// <summary>
        /// 给所有的孩子节点排序 通过坐标确定顺序
        /// </summary>
        private void SortAllChildrenNode()
        {
            if (graph == null)
                return;

            foreach (var node in graph.nodes)
            {
                foreach (var port in node.Outputs)
                {
                    port.Sort();
                }
            }
        }


        public void Sort()
        {
            connections.Sort((y, x) =>
            {
                return x.node.position.y.CompareTo(y.node.position.y);
            });
        }
需要调整的子节点

二,行为树按层次遍历顺序存储

选择层次遍历的理由是此遍历方法符合我们对行为树的直观思维

遍历结构

比如这个结构,我们初始化时先实例化叶子节点,再实例化父节点,父节点就可以直接获取到孩子的实例

最后统一设置父节点即可

关键代码如下

graph.nodes列表元素按照深度高的节点在前的顺序存储

 Queue<XNode.Node> queue = new Queue<XNode.Node>(100); //将队列初始化大小为100
 Stack<XNode.Node> stack = new Stack<XNode.Node>(100);


        /// <summary>
        /// 行为树排序 并给ID自动赋值 层次遍历
        /// </summary>
        /// <param name="head"></param>
        private void SortTree(XNode.Node head)
        {
            if (head == null)
            {
                Debug.LogError("根节点为空");
                return;
            }   
            
            XNode.Node tMTreeNode;
            
            //将根节点入队列
            queue.Enqueue(head);

            //采用层次优先遍历方法,借助于队列
            while (queue.Count != 0) //如果队列q不为空
            {
                tMTreeNode = queue.Dequeue(); //出队列

                //孩子入队
                foreach (var v in tMTreeNode.Outputs)
                {
                    foreach (var v1 in v.GetConnections())
                    {
                        queue.Enqueue(v1.node);
                    }
                }

                stack.Push(tMTreeNode); //将p入栈
            }

            int i = 0;

            while (stack.Count != 0) //不为空
            {
                i++;
                tMTreeNode = stack.Pop(); //弹栈
                NP_NodeBase node = tMTreeNode as NP_NodeBase;
                node.Id = i;
            }

            graph.nodes.Sort((x,y)=> 
            {
                NP_NodeBase nodex = x as NP_NodeBase;
                NP_NodeBase nodey = y as NP_NodeBase;

                return nodex.Id.CompareTo(nodey.Id);
            });
        }

    }

总排序方法

       /// <summary>
        /// 行为树排序方法
        /// </summary>
        public void Sort()
        {
            if (graph == null)
                return;

            SortAllChildrenNode();
            SortTree(graph.GetRootNode());
        }

至此,我们获得了一个顺序调整好的节点列表。

Runtime初始化时直接食用即可。

初始化方法及数据设计

数据设计--基于XNode--基于Odin

1.Editor数据基类

public abstract class NP_NodeBase : XNode.Node
	{
        [FoldoutGroup("节点Editor数据")]
        [ReadOnly]
        public int Id;
       

        [FoldoutGroup("节点Editor数据")]
        [ReadOnly]
        public NodeType nodeType;

        [FoldoutGroup("节点Editor数据")]
        public string des;


    }

2.三大节点Editor数据基类

    public abstract class NP_CompositeNodeBase : NP_NodeBase
    {
        protected override void Init()
        {
            nodeType = NodeType.Composite;
        }

        [Input(ShowBackingValue.Unconnected, ConnectionType.Override)]
        public bool input;

        [Output(ShowBackingValue.Never, ConnectionType.Multiple)]
        public bool output;

    }

   public abstract class NP_DecoratorNodeBase : NP_NodeBase
    {
        protected override void Init()
        {
            nodeType = NodeType.Decorator;
        }

        [Input(ShowBackingValue.Unconnected, ConnectionType.Override)]
        public bool input;

        [Output(ShowBackingValue.Never, ConnectionType.Override)]
        public bool output;
    }

   public abstract class NP_TaskNodeBase : NP_NodeBase
    {
        protected override void Init()
        {
            nodeType = NodeType.Task;
        }

        [Input(ShowBackingValue.Unconnected, ConnectionType.Override)]
        public bool input;       
    }

3.Runtime数据基类

 [FoldoutGroup("结点数据")]
    public abstract class NP_NodeDataBase 
    {

        /// <summary>
        /// 获取结点
        /// </summary>
        /// <returns></returns>
        public virtual Node NP_GetNode()
        {
            return null;
        }


        /// <summary>
        /// 创建组合结点
        /// </summary>
        /// <returns></returns>
        public virtual Composite CreateComposite(Node[] nodes)
        {
            return null;
        }

        /// <summary>
        /// 创建装饰结点
        /// </summary>
        /// <param name="unitId">行为树归属的Unit</param>
        /// <param name="runtimeTree">运行时归属的行为树</param>
        /// <param name="node">所装饰的结点</param>
        /// <returns></returns>
        public virtual Decorator CreateDecoratorNode(object m_object, NP_Tree runtimeTree, Node node)
        {
            return null;
        }

        /// <summary>
        /// 创建任务节点
        /// </summary>
        /// <param name="unitId">行为树归属的Unit</param>
        /// <param name="runtimeTree">运行时归属的行为树</param>
        /// <returns></returns>
        public virtual Task CreateTask(object unit, NP_Tree runtimeTree)
        {
            return null;
        }
      
    }

    public enum NodeType
    {
        Composite,
        Decorator,
        Task,
    }

4.黑板值数据类

[BoxGroup("黑板数据配置")]
    [HideLabel]
    [ShowOdinSerializedPropertiesInInspector]
    public class ClassForBlackboard
    {
        [LabelText("字典键")]
        [ValueDropdown("GetBBKeys")]
        [OnValueChanged("OnBBKeySelected")]
        public string BBKey;

        [LabelText("指定的值类型")]
        [ReadOnly]
        public string NP_BBValueType;

        [LabelText("是否可以把值写入黑板,或者是否与黑板进行值对比")]
        public bool WriteOrCompareToBB;

        [ShowIf("WriteOrCompareToBB")]
        public Variable NP_BBValue;


#if UNITY_EDITOR
        private IEnumerable<string> GetBBKeys()
        {
            if (NPBlackBoardEditorInstance.BBValues != null)
            {
                return NPBlackBoardEditorInstance.BBValues.Keys;
            }

            return null;
        }

        private void OnBBKeySelected()
        {
            if (NPBlackBoardEditorInstance.BBValues != null)
            {
                foreach (var bbValues in NPBlackBoardEditorInstance.BBValues)
                {
                    if (bbValues.Key == this.BBKey)
                    {
                        NP_BBValue = bbValues.Value.DeepCopy();
                        NP_BBValueType = this.NP_BBValue.Type.ToString();
                    }
                }
            }
        }
#endif

        /// <summary>
        /// 获取目标黑板对应的此处的键的值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T GetBlackBoardValue<T>(Blackboard blackboard)
        {
            return blackboard.Get<T>(this.BBKey);
        }

        /// <summary>
        /// 获取配置的BB值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T GetTheBBDataValue<T>()
        {
            Variable<T> var = NP_BBValue as Variable<T>;

            return var.Value;
        }

        /// <summary>
        /// 自动根据预先设定的值设置值
        /// </summary>
        /// <param name="blackboard">要修改的黑板</param>
        public void SetBlackBoardValue(Blackboard blackboard)
        {
            blackboard.Set(BBKey, NP_BBValue);

            
        }
     }

5.存储Action的数据基类

[BoxGroup("用于包含Action的数据类"), GUIColor(0.961f, 0.902f, 0.788f, 1f)]
    [HideLabel]
    public class NP_ClassForAction
    {
        /// <summary>
        /// 归属的对象
        /// </summary>
        [HideInEditorMode] public object owner;

        /// <summary>
        /// 归属的运行时行为树实例
        /// </summary>
        [HideInEditorMode]
        public NP_Tree BelongtoRuntimeTree;

        [HideInEditorMode]
        public System.Action Action;

        [HideInEditorMode]
        public Func<bool> Func1;

        [HideInEditorMode]
        public Func<bool, Action.Result> Func2;

        /// <summary>
        /// 获取将要执行的委托函数,也可以在这里面做一些初始化操作
        /// </summary>
        /// <returns></returns>
        public virtual System.Action GetActionToBeDone()
        {
            return null;
        }

        public virtual System.Func<bool> GetFunc1ToBeDone()
        {
            return null;
        }

        public virtual System.Func<bool, Action.Result> GetFunc2ToBeDone()
        {
            return null;
        }

        public  Action _CreateNPBehaveAction()
        {
            Action action;

            GetActionToBeDone();
            if (this.Action != null)
            {
                 action = ReferencePool.Acquire<Action>();
                 action.SetAction(Action);
                return action;
            }

            GetFunc1ToBeDone();
            if (this.Func1 != null)
            {
                action = ReferencePool.Acquire<Action>();
                action.SetSingleFunc(Func1);
                return action;
            }

            GetFunc2ToBeDone();
            if (this.Func2 != null)
            {
                action = ReferencePool.Acquire<Action>();
                action.SetFunc1(Func2);
                return action;
            }
           
            return null;
        }
    }

6.具体Action数据类 Log方法

  public class NP_LogAction : NP_TaskNodeBase
    {
        public override string Name => "NP_LogAction";


        public NP_ActionNodeData data = new NP_ActionNodeData()
        {
            NpClassForAction = new MDDGameFramework.Runtime.NP_LogAction()
        };


        public override NP_NodeDataBase NP_GetNodeData()
        {
            return data;
        }
    }

7.具体节点数据类

 public class NP_ActionNodeData : NP_NodeDataBase
    {
        [HideInEditorMode] private Action m_ActionNode;

        public NP_ClassForAction NpClassForAction;

        public override Task CreateTask(object owner, NP_Tree runtimeTree)
        {
            NpClassForAction.owner = owner;
            NpClassForAction.BelongtoRuntimeTree = runtimeTree;
            m_ActionNode = NpClassForAction._CreateNPBehaveAction();
            return m_ActionNode;
        }

        public override Node NP_GetNode()
        {
            return this.m_ActionNode;
        }
    }
  [HideLabel]
    public class NP_BlackBoardConditionNodeData : NP_NodeDataBase
    {
        BlackboardCondition m_BlackboardCondition;

        [BoxGroup("黑板条件节点配置")]
        [LabelText("运算符号")]
        public Operator Ope = Operator.IS_EQUAL;

        [BoxGroup("黑板条件节点配置")]
        [LabelText("终止行为")]
        public Stops Stop = Stops.IMMEDIATE_RESTART;

        public ClassForBlackboard blackboardData = new ClassForBlackboard();


        public override Decorator CreateDecoratorNode(object m_object, NP_Tree runtimeTree, Node node)
        {
            this.m_BlackboardCondition = BlackboardCondition.Create(blackboardData.BBKey, Ope, blackboardData.NP_BBValue,Stop, node);
            return this.m_BlackboardCondition;
        }

        public override Node NP_GetNode()
        {
            return this.m_BlackboardCondition;
        }
    }

8.具体Editor节点数据类

public class NP_BlackboardConditionNode : NP_DecoratorNodeBase
    {
        protected override void Init()
        {
            base.Init();
            nodeType = NodeType.Decorator;
        }

        public override string Name => "NP_BlackboardCondition";

       
        public NP_BlackBoardConditionNodeData data = new NP_BlackBoardConditionNodeData();

        public override NP_NodeDataBase NP_GetNodeData()
        {
            return data;
        }       
    }

Runtime简单的构建

我们在Editor阶段已经处理好了nodes数据

在Runtime时我们需要关注的是数据引用关系处理的问题

但本篇不做细致的处理,因为还没讲到具体实用场景

    Root root;

    public NPBehaveGraph np;

    public void InitTree()
    {
        foreach (var v in np.nodes)
        {
            NP_NodeBase data = v as NP_NodeBase;

            switch (data.nodeType)
            {
                case NodeType.Task:
                    data.NP_GetNodeData().CreateTask(null, null);
                    break;
                case NodeType.Decorator:
                    Node node = null;
                    foreach (var v1 in data.Outputs)
                    {
                        node = (v1.Connection.node as NP_NodeBase).NP_GetNodeData().NP_GetNode();
                    }

                    data.NP_GetNodeData().CreateDecoratorNode(null, null, node);

                    break;
                case NodeType.Composite:
                    List<Node> nodes = new List<Node>();
                    foreach (var v1 in data.Outputs)
                    {
                        foreach (var v2 in v1.GetConnections())
                        {
                            nodes.Add((v2.node as NP_NodeBase).NP_GetNodeData().NP_GetNode());
                        }
                    }
                    data.NP_GetNodeData().CreateComposite(nodes.ToArray());
                    break;
            }

            if (data.Name == "根节点")
            {
                root = data.NP_GetNodeData().NP_GetNode() as Root;
            }


        }

        //设置根节点
        root.SetRoot(root);

        root.Start();
    }

Editor痛点处理

1.基于Odin的MenuTree制作

2.节点数据配置规范

3.实时黑板数据库

NPBehave性能优化及扩展(除了Debug支持,runtime扩展其余已经做好,感兴趣的兄弟可以自行查阅MDDSkillEngine源码)

1.解决黑板赋值GC问题

2.事件缓存

3.行为树节点池化

4.Runtime时动态扩展行为树

5.Debug可视化支持--基于XNode

内容待填充出去散步先.........


记录历程,整理思路,共享知识,分享思维。