博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
行为树的设计与实现
阅读量:6048 次
发布时间:2019-06-20

本文共 24507 字,大约阅读时间需要 81 分钟。

查阅了一些行为树资料,目前最主要是参考了这篇文章,看完后感觉行为树实乃强大,绝对是替代状态机的不二之选。但从理论看起来很简单的行为树,真正着手起来却发现很多细节无从下手。总结起来,就是:1、行为树只是单纯的一棵决策树,还是决策+控制树。为了防止不必要的麻烦,我目前设计成单纯的决策树。2、什么时候执行行为树的问题,也就是行为树的Tick问题,是在条件变化的时候执行一次,还是只要对象激活,就在Update里面一直Tick。前者明显很节省开销,但那样设计的最终结果可能是最后陷入事件发送的泥潭中。那么一直Tick可能是最简单的办法,于是就引下面出新的问题。目前采用了一直Tick的办法。3、基本上可以明显节点有 Composite Node、Decorator Node、Condition Node、Action Node,但具体细节就很头疼。比如组合节点里的Sequence Node。这个节点是不是在每个Tick周期都从头迭代一次子节点,还是记录正在运行的子节点。每次都迭代子节点,就感觉开销有点大。记录运行节点就会出现条件冗余问题,具体后面再讨论。目前采用保存当前运行节点的办法。4、条件节点(Condition Node)的位置问题。看到很多设计都是条件节点在最后才进行判断,而实际上,如果把条件放在组合节点处,就可以有效短路判断,不再往下迭代。于是我就采用了这种方法。设计开始在Google Code上看到的某个行为树框架,用的是抽象类做节点。考虑到C#不能多继承,抽象类可能会导致某些时候会很棘手,所以还是用接口。虽然目前还未发现接口的好处。在进行抽象设计的时候,接口的纯粹性虽然看起来更加清晰,不过有时候遇到需要重复使用某些类函数的时候就挺麻烦,让人感觉有点不利于复用。[csharp] view plaincopy public enum RunStatus  {      Completed,      Failure,      Running,  }    public interface IBehaviourTreeNode  {      RunStatus status { get; set; }      string nodeName { get; set; }      bool Enter(object input);      bool Leave(object input);      bool Tick(object input, object output);      RenderableNode renderNode { get; set; }      IBehaviourTreeNode parent { get; set; }      IBehaviourTreeNode Clone();  }    /************************************************************************/  /* 组合结点                                                             */  /************************************************************************/  public interface ICompositeNode : IBehaviourTreeNode  {      void AddNode(IBehaviourTreeNode node);      void RemoveNode(IBehaviourTreeNode node);      bool HasNode(IBehaviourTreeNode node);        void AddCondition(IConditionNode node);      void RemoveCondition(IConditionNode node);      bool HasCondition(IConditionNode node);        ArrayList nodeList { get; }      ArrayList conditionList { get; }  }    /************************************************************************/  /* 选择节点                                                             */  /************************************************************************/  public interface ISelectorNode : ICompositeNode  {    }    /************************************************************************/  /*顺序节点                                                              */  /************************************************************************/  public interface ISequenceNode : ICompositeNode  {    }    /************************************************************************/  /* 平行(并列)节点                                                             */  /************************************************************************/  public interface IParallelNode : ICompositeNode  {    }    //    /************************************************************************/  /* 装饰结点                                                             */  /************************************************************************/  public interface IDecoratorNode : IBehaviourTreeNode  {    }    /************************************************************************/  /* 条件节点                                                             */  /************************************************************************/  public interface IConditionNode  {      string nodeName { get; set; }      bool ExternalCondition();  }    /************************************************************************/  /* 行为节点                                                             */  /************************************************************************/  public interface IActionNode : IBehaviourTreeNode  {    }    public interface IBehaviourTree  {        }  很多节点的接口都是空的,目前唯一的作用就是用于类型判断,很可能在最后也没有什么实际的作用,搞不好就是所谓的过度设计。如果最终确定没有用再删掉吧。接口里出现了一个渲染节点,目的是为了能够更方便的把这个节点和负责渲染的节点联系到一起,方便节点的可视化。如果只有接口,每次实现接口都要重复做很多工作,为了利用面向对象的复用特性,就来实现一些父类[csharp] view plaincopy public class BaseNode  {      public BaseNode() { nodeName_ = this.GetType().Name + "\n"; }        protected RunStatus status_ = RunStatus.Completed;      protected string nodeName_;      protected RenderableNode renderNode_;      protected IBehaviourTreeNode parent_;        public virtual RunStatus status { get { return status_; } set { status_ = value; } }      public virtual string nodeName { get { return nodeName_; } set { nodeName_ = value; } }      public virtual RenderableNode renderNode { get { return renderNode_; } set { renderNode_ = value; } }      public virtual IBehaviourTreeNode parent { get { return parent_; } set { parent_ = value; } }      public virtual IBehaviourTreeNode Clone() {          var clone = new BaseNode();          clone.status_ = status_;          clone.nodeName_ = nodeName_;          clone.renderNode_ = renderNode_;          clone.parent_ = parent_;          return clone as IBehaviourTreeNode;      }  }    public class BaseActionNode : IActionNode  {      public BaseActionNode() { nodeName_ = this.GetType().Name + "\n"; }      protected RunStatus status_ = RunStatus.Completed;      protected string nodeName_;      protected RenderableNode renderNode_;      protected IBehaviourTreeNode parent_;      public virtual RunStatus status { get { return status_; } set { status_ = value; } }      public virtual string nodeName { get { return nodeName_; } set { nodeName_ = value; } }      public virtual RenderableNode renderNode { get { return renderNode_; } set { renderNode_ = value; } }      public virtual IBehaviourTreeNode parent { get { return parent_; } set { parent_ = value; } }      public virtual IBehaviourTreeNode Clone()      {          var clone = new BaseActionNode();          clone.status_ = status_;          clone.nodeName_ = nodeName_;          clone.renderNode_ = renderNode_;          clone.parent_ = parent_;          return clone as IBehaviourTreeNode;      }        public virtual bool Enter(object input)      {          status_ = RunStatus.Running;          return true;      }        public virtual bool Leave(object input)      {          status_ = RunStatus.Completed;          return true;      }        public virtual bool Tick(object input, object output)      {          return true;      }  }  public class BaseCondictionNode {      protected string nodeName_;      public virtual string nodeName { get { return nodeName_; } set { nodeName_ = value; } }      public BaseCondictionNode() { nodeName_ = this.GetType().Name+"\n"; }      public delegate bool ExternalFunc();      protected ExternalFunc externalFunc;      public static ExternalFunc GetExternalFunc(BaseCondictionNode node) {          return node.externalFunc;      }  }    public class Precondition : BaseCondictionNode, IConditionNode{      public Precondition(ExternalFunc func) { externalFunc = func; }      public Precondition(BaseCondictionNode pre) { externalFunc = BaseCondictionNode.GetExternalFunc(pre); }      public bool ExternalCondition()      {          if (externalFunc != null) return externalFunc();          else return false;      }  }    public class PreconditionNOT : BaseCondictionNode, IConditionNode  {      public PreconditionNOT(ExternalFunc func) { externalFunc = func; }      public PreconditionNOT(BaseCondictionNode pre) { externalFunc = BaseCondictionNode.GetExternalFunc(pre); }      public bool ExternalCondition()      {          if (externalFunc != null) return !externalFunc();          else return false;      }  }    public class BaseCompositeNode : BaseNode{      protected ArrayList nodeList_ = new ArrayList();      protected ArrayList conditionList_ = new ArrayList();      protected int runningNodeIndex = 0;      protected bool CheckNodeAndCondition() {          if (nodeList_.Count == 0)          {              status_ = RunStatus.Failure;              Debug.Log("SequenceNode has no node!");              return false;          }          return CheckCondition();      }      protected bool CheckCondition() {          foreach (var node in conditionList_)          {              var condiction = node as IConditionNode;              if (!condiction.ExternalCondition())                  return false;          }          return true;      }      public virtual void AddNode(IBehaviourTreeNode node) { node.parent = (IBehaviourTreeNode)this; nodeList_.Add(node); }      public virtual void RemoveNode(IBehaviourTreeNode node) { nodeList_.Remove(node); }      public virtual bool HasNode(IBehaviourTreeNode node) { return nodeList_.Contains(node); }        public virtual void AddCondition(IConditionNode node) { conditionList_.Add(node); }      public virtual void RemoveCondition(IConditionNode node) { conditionList_.Remove(node); }      public virtual bool HasCondition(IConditionNode node) { return conditionList_.Contains(node); }        public virtual ArrayList nodeList { get { return nodeList_; } }      public virtual ArrayList conditionList { get { return conditionList_; } }        public override IBehaviourTreeNode Clone()      {          var clone = base.Clone() as BaseCompositeNode;          clone.nodeList_.AddRange(nodeList_);          clone.conditionList_.AddRange(conditionList_);          clone.runningNodeIndex = runningNodeIndex;          return clone as IBehaviourTreeNode;      }  }  然后实现具体的节点,先是序列节点[csharp] view plaincopy public class SequenceNode : BaseCompositeNode, ISequenceNode  {      public SequenceNode(bool canContinue_ = false) { canContinue = canContinue_; }      public bool canContinue = false;          public bool Enter(object input)      {          var checkOk = CheckNodeAndCondition();          if (!checkOk) return false;          var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;          checkOk = runningNode.Enter(input);          if (!checkOk) return false;          status_ = RunStatus.Running;          return true;      }        public bool Leave(object input)      {          if (nodeList_.Count == 0)          {              status_ = RunStatus.Failure;              Debug.Log("SequenceNode has no node!");              return false;          }          var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;          runningNode.Leave(input);          if (canContinue)          {              runningNodeIndex++;              runningNodeIndex %= nodeList_.Count;          }          status_ = RunStatus.Completed;          return true;      }        public bool Tick(object input, object output)      {          if (status_ == RunStatus.Failure) return false;          if (status_ == RunStatus.Completed) return true;                    var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;          var checkOk = CheckCondition();          if (!checkOk)          {              return false;          }            switch (runningNode.status)          {              case RunStatus.Running:                  if (!runningNode.Tick(input, output))                  {                      runningNode.Leave(input);                      return false;                  }                    break;              default:                  runningNode.Leave(input);                  runningNodeIndex++;                  if(runningNodeIndex >= nodeList_.Count)break;                  var nextNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;                  var check = nextNode.Enter(input);                  if (!check) return false;                  break;          }          return true;      }        public override IBehaviourTreeNode Clone()      {          var clone = base.Clone() as SequenceNode;          clone.canContinue = canContinue;          return clone;      }  }  这就是序列节点的设计,但是明显看起来很不爽,里面还出现了一个别扭的变量canContinue 。为什么会出现这个?因为序列节点的特点就是遇到一个子节点FALSE,就会停止并返回FALSE,但是这里我想用序列节点来做根节点,如果是根节点遇到这种情况,那么就不会执行下一个节点,而我看了很多种对于几大节点的描述,似乎都没提到这个。很多都用序列节点做根节点,有些就直接说是根节点。那么要么根节点另外实现,要么改一下序列节点。因为如果序列节点是非根节点的情况下,如果不是每次都从头开始,似乎又会引来新的问题,虽然目前还没想到会出什么问题。不过最后实现执行起来之后发现,用选择节点其实是一样的。所以目前这样的设计,可能是有根本上的问题。希望哪位大神可以指点一下。然后是选择节点,根据了所有FALSE才返回FALSE的特点设计了[csharp] view plaincopy public class SelectorNode : BaseCompositeNode, ISelectorNode  {      public bool Enter(object input)      {          var checkOk = CheckNodeAndCondition();          if (!checkOk) return false;                   do          {              var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;              checkOk = runningNode.Enter(input);              if (checkOk) break;              runningNodeIndex++;              if (runningNodeIndex >= nodeList_.Count) return false;          } while (!checkOk);                    status_ = RunStatus.Running;          return true;      }        public bool Leave(object input)      {          var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;          runningNode.Leave(input);          runningNodeIndex = 0;          status_ = RunStatus.Completed;          return true;      }        public bool Tick(object input, object output)      {          if (status_ == RunStatus.Failure) return false;          if (status_ == RunStatus.Completed) return true;          var checkOk1 = CheckCondition();          if (!checkOk1) return false;          var runningNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;          switch (runningNode.status)          {              case RunStatus.Running:                  if (!runningNode.Tick(input, output))                  {                      runningNode.Leave(input);                      return false;                  }                    break;              default:                  runningNode.Leave(input);                  runningNodeIndex++;                  if (runningNodeIndex >= nodeList_.Count) return false;                    bool checkOk = false;                  do                  {                      var nextNode = nodeList_[runningNodeIndex] as IBehaviourTreeNode;                      checkOk = nextNode.Enter(input);                      if (checkOk) break;                      runningNodeIndex++;                      if (runningNodeIndex >= nodeList_.Count) return false;                  } while (!checkOk);                  break;          }          return true;      }  }  目前对于我的简单DEMO,组合节点只需要这两个就够了,实际上只需要选择节点、条件节点、动作节点就够了。所以说设计是不完全的,虽然能够实现目标需求,但是实际工作量仍挺大,具体接下来会说明。行为节点先放一些渲染节点的代码。实际上我基本上是第一次接触自己去渲染一种数据结构,看完网上的大牛们随随便便就能写出个数据结构的示意图,不得不佩服。我一时半会没想出怎么渲染出树状结构,于是就简单的把树按层分组,一层一层渲染,缺点就是不能很好的表现树的样子,父子关系不能很好的表示。这里放出来希望能抛砖引玉。我以后可能会去完事它,但是现在首先是要搞清楚行为树。实现这个完全是为了看看节点是否正确放置,以方便调试。[csharp] view plaincopy public class RenderableNode  {      public RenderableNode parent;      public IBehaviourTreeNode targetNode;      public Rect posRect = new Rect();      public string name;      public int layer;      public RunStatus staus;      public override string ToString()      {          return name + "\n" + staus.ToString();      }      public virtual void Render()      {          bool running = staus == RunStatus.Running;          var rect = posRect;          rect.y -= (posRect.height / 2);            var oldColor = GUI.color;           if (running)          {              GUI.color = Color.green;          }          GUI.Box(rect, ToString());          GUI.color = oldColor;            if (parent == null && targetNode != null && targetNode.parent!=null)          {              parent = targetNode.parent.renderNode;          }          if (parent != null)          {              Vector2 parentPos = new Vector2();              parentPos.x = parent.posRect.x + parent.posRect.width;              parentPos.y = parent.posRect.y;              GUIHelper.DrawLine(new Vector2(rect.x, rect.y + rect.height / 2), parentPos, running?Color.green:Color.yellow);          }                }  }    public class RenderableCondictionNode : RenderableNode  {      public IConditionNode targetCondictionNode;      public override string ToString() { parent = null; return name; }      public override void Render()      {          var rect = posRect;          rect.y -= (posRect.height / 2);            var oldColor = GUI.color;          if (targetCondictionNode.ExternalCondition())              GUI.color = Color.green;          else              GUI.color = Color.blue;          GUI.Box(rect, ToString());          GUI.color = oldColor;      }  }    public class EmptyNode : RenderableNode { public override void Render() { } }    public class NodeBox  {      public Rect posRect = new Rect();      public List
nodeList = new List
(); public void AddNode(RenderableNode node) { nodeList.Add(node); } public void Render() { posRect.y = Screen.height / 2; Rect rect = new Rect(); foreach (var node in nodeList) { var n = node; rect.height += (n.posRect.height + 1); rect.width = n.posRect.width + 10; } rect.height += 10; rect.x = posRect.x - rect.width / 2; rect.y = posRect.y - rect.height / 2; //GUI.Box(rect, ""); posRect.width = rect.width; posRect.height = rect.height; float height = 0; for (var i = 0; i < nodeList.Count; i++) { var n = nodeList[i]; n.posRect.y = rect.y + height + n.posRect.height / 2 + 5; n.posRect.x = rect.x + 5; n.Render(); height += n.posRect.height + 1; } } } 放一张渲染出来的效果虽然每一组都只是简单的居中,不过效果看起来还可以接受然后从图中就可以看到问题了。所有正条件,都会有一个反条件,不这么做就无法在条件改变时,让当前节点返回FALSE,从而让行为树去寻找其他节点。而如果用状态机来做的话,条件肯定只用判断一次,比如[csharp] view plaincopy if(run){ Run(); } else{ Walk(); } 那么可能就回到最初的组合节点的设计了,组合节点就不得不每次都扫描条件。其实本质上我是在担心开销问题,因为变成节点后,就不在是if else那么简单,而是变成了函数调用的开销。简单的AI还好,如果大量复杂的AI,每次对整棵树进行扫描估计够呛。但是目前的设计,条件节点就会非常多,条件不完备就会出现BUG,似乎也不是非常好的情况。最后放出一些细节[csharp] view plaincopy class PatrolAction : BaseActionNode { public PatrolAction() { nodeName_ += "巡逻行为"; } public override bool Tick(object input_, object output_) { // var input = input_ as WarriorInputData; var output = output_ as WarriorOutPutData; output.action = WarriorActon.ePatrol; return true; } } class RunAwayAction : BaseActionNode { public RunAwayAction() { nodeName_ += "逃跑行为"; } public override bool Tick(object input_, object output_) { // var input = input_ as WarriorInputData; var output = output_ as WarriorOutPutData; output.action = WarriorActon.eRunAway; return true; } } class AttackAction : BaseActionNode { public AttackAction() { nodeName_ += "攻击行为"; } public override bool Tick(object input_, object output_) { // var input = input_ as WarriorInputData; var output = output_ as WarriorOutPutData; output.action = WarriorActon.eAttack; return true; } } class CrazyAttackAction : BaseActionNode { public CrazyAttackAction() { nodeName_ += "疯狂攻击行为"; } public override bool Tick(object input_, object output_) { // var input = input_ as WarriorInputData; var output = output_ as WarriorOutPutData; output.action = WarriorActon.eCrazyAttack; return true; } } class AlertAction : BaseActionNode { public AlertAction() { nodeName_ += "警戒行为"; } public override bool Tick(object input_, object output_) { // var input = input_ as WarriorInputData; var output = output_ as WarriorOutPutData; output.action = WarriorActon.eAlert; return true; } } [csharp] view plaincopy private ICompositeNode rootNode = new SelectorNode(); private WarriorInputData inputData = new WarriorInputData(); private WarriorOutPutData outputData = new WarriorOutPutData(); // Use this for initialization public void Start() { inputData.attribute = GetComponent
(); rootNode.nodeName += "根"; //条件 var hasNoTarget = new PreconditionNOT(() => { return inputData.attribute.hasTarget; }); hasNoTarget.nodeName = "无目标"; var hasTarget = new Precondition(hasNoTarget); hasTarget.nodeName = "发现目标"; var isAnger = new Precondition(() => { return inputData.attribute.isAnger; }); isAnger.nodeName = "愤怒状态"; var isNotAnger = new PreconditionNOT(isAnger); isNotAnger.nodeName = "非愤怒状态"; var HPLessThan500 = new Precondition(() => { return inputData.attribute.health < 500; }); HPLessThan500.nodeName = "血少于500"; var HPMoreThan500 = new PreconditionNOT(HPLessThan500); HPMoreThan500.nodeName = "血大于500"; var isAlert = new Precondition(() => { return inputData.attribute.isAlert; }); isAlert.nodeName = "警戒"; var isNotAlert = new PreconditionNOT(isAlert); isNotAlert.nodeName = "非警戒"; var patrolNode = new SequenceNode(); patrolNode.nodeName += "巡逻"; patrolNode.AddCondition(hasNoTarget); patrolNode.AddCondition(isNotAlert); patrolNode.AddNode(new PatrolAction()); var alert = new SequenceNode(); alert.nodeName += "警戒"; alert.AddCondition(hasNoTarget); alert.AddCondition(isAlert); alert.AddNode(new AlertAction()); var runaway = new SequenceNode(); runaway.nodeName += "逃跑"; runaway.AddCondition(hasTarget); runaway.AddCondition(HPLessThan500); runaway.AddNode(new RunAwayAction()); var attack = new SelectorNode(); attack.nodeName += "攻击"; attack.AddCondition(hasTarget); attack.AddCondition(HPMoreThan500); var attackCrazy = new SequenceNode(); attackCrazy.nodeName += "疯狂攻击"; attackCrazy.AddCondition(isAnger); attackCrazy.AddNode(new CrazyAttackAction()); attack.AddNode(attackCrazy); var attackNormal = new SequenceNode(); attackNormal.nodeName += "普通攻击"; attackNormal.AddCondition(isNotAnger); attackNormal.AddNode(new AttackAction()); attack.AddNode(attackNormal); rootNode.AddNode(patrolNode); rootNode.AddNode(alert); rootNode.AddNode(runaway); rootNode.AddNode(attack); var ret = rootNode.Enter(inputData); if (!ret) { Debug.Log("无可执行节点!"); } } // Update is called once per frame void Update () { var ret = rootNode.Tick(inputData, outputData); if (!ret) rootNode.Leave(inputData); if (rootNode.status == RunStatus.Completed) { ret = rootNode.Enter(inputData); if (!ret) rootNode.Leave(inputData); } else if (rootNode.status == RunStatus.Failure) { Debug.Log("BT Failed"); enabled = false; } if (outputData.action != inputData.action) { OnActionChange(outputData.action, inputData.action); inputData.action = outputData.action; } } void OnActionChange(WarriorActon action, WarriorActon lastAction) { // print("OnActionChange "+action+" last:"+lastAction); switch (lastAction) { case WarriorActon.ePatrol: GetComponent
().enabled = false; break; case WarriorActon.eAttack: case WarriorActon.eCrazyAttack: GetComponent
().enabled = false; break; case WarriorActon.eRunAway: GetComponent
().enabled = false; break; case WarriorActon.eAlert: GetComponent
().enabled = false; break; } switch (action) { case WarriorActon.ePatrol: GetComponent
().enabled = true; break; case WarriorActon.eAttack: var attack = GetComponent
(); attack.revenge = false; attack.enabled = true; break; case WarriorActon.eCrazyAttack: var crazyAttack = GetComponent
(); crazyAttack.revenge = true; crazyAttack.enabled = true; break; case WarriorActon.eRunAway: GetComponent
().enabled = true; break; case WarriorActon.eAlert: GetComponent
().enabled = true; break; case WarriorActon.eIdle: GetComponent
().enabled = false; GetComponent
().enabled = false; GetComponent
().enabled = false; break; } }

原地址:http://blog.csdn.net/luyuncsd123/article/details/18351137

你可能感兴趣的文章
svmlight使用说明
查看>>
Swing 和AWT之间的关系
查看>>
Mysql设置自增长主键的初始值
查看>>
获取post传输参数
查看>>
ASP生成静态页面的方法
查看>>
HDU 1325 Is It A Tree? 判断是否为一棵树
查看>>
Bzoj 2252: [2010Beijing wc]矩阵距离 广搜
查看>>
Oracle 12c 多租户 手工创建 pdb 与 手工删除 pdb
查看>>
shell初涉
查看>>
[浪子学编程][MS Enterprise Library]ObjectBuilder之创建策略祥解(二)
查看>>
关于云栖,有点无语的几个地方,管理能不能管?
查看>>
Windows线程的同步与互斥
查看>>
C#进阶系列——MEF实现设计上的“松耦合”(四):构造函数注入
查看>>
linux系统下安装两个或多个tomcat
查看>>
ProtoBuffer 简单例子
查看>>
iOS多线程开发系列之(一)NSThread
查看>>
微信小程序初体验(上)- 腾讯ISUX社交用户体验设计成员出品
查看>>
SAP WM Physical Inventory Method ST & PZ
查看>>
一次快速的数据迁移感悟
查看>>
《ELK Stack权威指南(第2版)》一3.6 Java日志
查看>>