NodeCanvas 是一款很棒的插件,包含了行为树、对话树、状态机等数据结构,提供可视化的图形服务,依此我们可以扩展一种 FlowTree 的流式图。

# 思考

# FlowTree

FlowTree 是我想要实现的一种数据结构,即流程树结构;在流程树中的所有节点会根据检查条件选择适合的分支依次执行。这其实和官方提供的 DialogTree 对话树的逻辑是差不多的;但是在对话树基本节点是对话节点。

为了能够执行一些列操作,而这一些操作可能包含对话也可能只是纯粹的 Action,故而需要一种新的流程树形式。

image-20240403103133264

从上图中可以看出,我们可以不再依赖于对话节点,我们可以 行为、条件、子图作为起始,也可以根据这些元素作为结尾任意的连接数据。

# 实现

目录结构

以上是一个基本树的目录组成结构,分别包括:

  • FlowTreeOwner 树持有者,继承自 MonoBehaviour
  • FlowTree 树数据,继承自 Graph
  • FTNode 基础节点数据,继承自 Node
  • FTConnection 基础连接数据,继承自 Connection
FlowTreeOwner
public class FlowTreeOwner: GraphOwner<FlowTree>
{
}
FlowTree
[CreateAssetMenu(menuName = "ParadoxNotion/NodeCanvas/Flow Tree Asset")]
public class FlowTree : Graph
{
    public override Type baseNodeType => typeof(FTNode);
    public override bool requiresAgent => false;
    public override bool requiresPrimeNode => true;
    public override bool isTree => true;
    public override PlanarDirection flowDirection => PlanarDirection.Horizontal;
    public override bool allowBlackboardOverrides => true;
    public override bool canAcceptVariableDrops => false;
    private float _intTime;
    private float _updateTime;
    private Status _rootStatus;
    private FTNode _currentNode;
    protected override void OnGraphStarted()
    {
        Debug.Log($"FlowTree {name} 启动");
        _currentNode = primeNode as FTNode;
        EnterNode(_currentNode != null ? _currentNode : (FTNode)primeNode);
    }
    protected override void OnGraphUpdate()
    {
        if ( _currentNode is IUpdatable ) {
            ( _currentNode as IUpdatable )?.Update();
        }
    }
    protected override void OnGraphStoped()
    {
        _currentNode = null;
        
    }
    public void GoNext(int index = 0)
    {
        if (index < 0 || index > _currentNode.outConnections.Count - 1)
        {
            Stop(true);
            
            return;
        }
        _currentNode.outConnections[index].status = Status.Success; //editor vis
        EnterNode(_currentNode.outConnections[index].targetNode as FTNode);
    }
    private void EnterNode(FTNode node)
    {
        _currentNode = node;
        _currentNode.Reset(false);
        if (_currentNode.Execute(agent, blackboard) == Status.Error)
        {
            Stop(false);
        }
    }
}
FTNode
public abstract class FTNode : Node
    {
        public override int maxInConnections => -1;
        public override int maxOutConnections => 1;
        public override Type outConnectionType => typeof(FTConnection);
        public override bool allowAsPrime => true;
        public override bool canSelfConnect => false;
        public override Alignment2x2 commentsAlignment => Alignment2x2.Right;
        public override Alignment2x2 iconAlignment => Alignment2x2.Right;
        /// <summary>
        /// 流程树
        /// </summary>
        protected FlowTree FlowTree => (FlowTree)graph;
#if UNITY_EDITOR
        protected override void OnNodeInspectorGUI()
        {
            base.OnNodeInspectorGUI();
        }
        protected override UnityEditor.GenericMenu OnContextMenu(UnityEditor.GenericMenu menu)
        {
            menu.AddItem(new GUIContent("Breakpoint"), isBreakpoint, () => { isBreakpoint = !isBreakpoint; });
            return menu;
        }
#endif
    }
FTConnection
public class FTConnection:Connection
{
    
}

# 树步进和条件

有了数据结构还不行,我们还需要让数据按我们期望的方式流转起来,其实也就是我们需要告诉我们的树何时应该执行下一步。

树步进
public void GoNext(int index = 0)
{
    if (index < 0 || index > _currentNode.outConnections.Count - 1)
    {
        Stop(true);
        
        return;
    }
    _currentNode.outConnections[index].status = Status.Success; //editor vis
    EnterNode(_currentNode.outConnections[index].targetNode as FTNode);
}
private void EnterNode(FTNode node)
{
    _currentNode = node;
    _currentNode.Reset(false);
    if (_currentNode.Execute(agent, blackboard) == Status.Error)
    {
        Stop(false);
    }
}

在 FlowTree 中有这样一个方法用于控制树步进,传入一个 index 代表选择的链接分支索引,用于进入下一个节点,如果节点返回失败则停止图,如果成功则进入下一个。

我们需要在所有需要进入下一个节点的地方调用 GoNext

例如当 Action 节点执行完时:

void OnActionEnd(bool success) {
    if ( success ) {
        status = Status.Success;
        FlowTree.GoNext();
        return;
    }
    status = Status.Failure;
    FlowTree.Stop(false);
}

例如当条件节点执行完时:

protected override Status OnExecute(Component agent, IBlackboard bb)
{
    if (outConnections.Count == 0)
    {
        return Error("There are no connections on the Dialogue Condition Node");
    }
    if (Condition == null)
    {
        return Error("There is no Conidition on the Dialoge Condition Node");
    }
    var isSuccess = Condition.CheckOnce(agent.transform, graphBlackboard);
    status = isSuccess ? Status.Success : Status.Failure;
    FlowTree.GoNext(status== Status.Success ? 0 : 1);
    return status;
}

例如当子图执行完成时:

void OnDLGFinished(bool success)
{
    if (status == Status.Running)
    {
        status = success ? Status.Success : Status.Failure;
        
        FlowTree.GoNext(); // 让流程树继续
    }
}

对于条件节点的处理,我们只需要提供一个 ConditionTask 用于处理即可:

ConditionNode
[global::ParadoxNotion.Design.Icon("Condition")]
    [Color("b3ff7f")]
    public class ConditionNode : FTNode, ITaskAssignable<ConditionTask>
    {
        [SerializeField] private ConditionTask _condition;
        public ConditionTask Condition
        {
            get => _condition;
            set => _condition = value;
        }
        public Task task
        {
            get => Condition;
            set => Condition = (ConditionTask)value;
        }
        public override int maxOutConnections => 2;
        protected override Status OnExecute(Component agent, IBlackboard bb)
        {
            if (outConnections.Count == 0)
            {
                return Error("There are no connections on the Dialogue Condition Node");
            }
            if (Condition == null)
            {
                return Error("There is no Conidition on the Dialoge Condition Node");
            }
            var isSuccess = Condition.CheckOnce(agent.transform, graphBlackboard);
            status = isSuccess ? Status.Success : Status.Failure;
            FlowTree.GoNext(status== Status.Success ? 0 : 1);
            return status;
        }
#if UNITY_EDITOR
        public override string GetConnectionInfo(int i)
        {
            return i == 0 ? "Then" : "Else";
        }
#endif
    }

# 子图 / 外接图

对于子图来说,其实也是属于一个节点,所以本质是继承自 FTNode

FTNodeNested
[Category("SubGraphs")]
[Color("ffe4e1")]
public abstract class FTNodeNested<T> : FTNode, IGraphAssignable<T> where T : Graph
{
    [SerializeField] private List<BBMappingParameter> _variablesMap;
    
    public abstract BBParameter subGraphParameter { get; }
    public T currentInstance { get; set; }
    public abstract T subGraph { get; set; }
    Graph IGraphAssignable.currentInstance { get => currentInstance;
        set => currentInstance = (T)value;
    }
    
    Graph IGraphAssignable.subGraph { get => subGraph;
        set => subGraph = (T)value;
    }
    public Dictionary<Graph, Graph> instances { get; set; }
    public List<BBMappingParameter> variablesMap { get => _variablesMap;
        set => _variablesMap = value;
    }
}

这里我们考虑使 FlowTree 中可以接入子图 DialogTree :

NestedDT
[Name("Sub Dialogue")]
[Description("Executes a sub Dialogue Tree. Returns Running while the sub Dialogue Tree is active. You can Finish the Dialogue Tree with the 'Finish' node and return Success or Failure.")]
[global::ParadoxNotion.Design.Icon("Dialogue")]
[DropReferenceType(typeof(DialogueTree))]
public class NestedDT : FTNodeNested<DialogueTree>
{
    [SerializeField, ExposeField, Name("Sub Tree")]
    private readonly BBParameter<DialogueTree> _nestedDialogueTree = null;
    public override DialogueTree subGraph
    {
        get => _nestedDialogueTree.value;
        set => _nestedDialogueTree.value = value;
    }
    public override BBParameter subGraphParameter => _nestedDialogueTree;
    //
    protected override Status OnExecute(Component agent, IBlackboard blackboard)
    {
        if (subGraph == null || subGraph.primeNode == null)
        {
            return Status.Optional;
        }
        if (status == Status.Resting)
        {
            status = Status.Running;
            this.TryStartSubGraph(agent, OnDLGFinished);
        }
        if (status == Status.Running)
        {
            currentInstance.UpdateGraph(this.graph.deltaTime);
        }
      
        return status;
    }
    void OnDLGFinished(bool success)
    {
        if (status == Status.Running)
        {
            status = success ? Status.Success : Status.Failure;
            
            FlowTree.GoNext(); // 让流程树继续
        }
    }
    protected override void OnReset()
    {
        if (currentInstance != null)
        {
            currentInstance.Stop();
        }
    }
}
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Fasty 微信支付

微信支付

Fasty 支付宝

支付宝

Fasty 贝宝

贝宝