因为受到罡少的影响
决定使用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
内容待填充出去散步先.........
Comments | NOTHING