查阅了一些行为树资料,目前最主要是参考了这篇文章,看完后感觉行为树实乃强大,绝对是替代状态机的不二之选。但从理论看起来很简单的行为树,真正着手起来却发现很多细节无从下手。
总结起来,就是:
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#不能多继承,抽象类可能会导致某些时候会很棘手,所以还是用接口。虽然目前还未发现接口的好处。
在进行抽象设计的时候,接口的纯粹性虽然看起来更加清晰,不过有时候遇到需要重复使用某些类函数的时候就挺麻烦,让人感觉有点不利于复用。
- 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
- {
-
- }
很多节点的接口都是空的,目前唯一的作用就是用于类型判断,很可能在最后也没有什么实际的作用,搞不好就是所谓的过度设计。如果最终确定没有用再删掉吧。
接口里出现了一个渲染节点,目的是为了能够更方便的把这个节点和负责渲染的节点联系到一起,方便节点的可视化。
如果只有接口,每次实现接口都要重复做很多工作,为了利用面向对象的复用特性,就来实现一些父类
然后实现具体的节点,先是序列节点
- 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的特点设计了
- 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,组合节点只需要这两个就够了,实际上只需要选择节点、条件节点、动作节点就够了。所以说设计是不完全的,虽然能够实现目标需求,但是实际工作量仍挺大,具体接下来会说明。
行为节点
先放一些渲染节点的代码。实际上我基本上是第一次接触自己去渲染一种数据结构,看完网上的大牛们随随便便就能写出个数据结构的示意图,不得不佩服。我一时半会没想出怎么渲染出树状结构,于是就简单的把树按层分组,一层一层渲染,缺点就是不能很好的表现树的样子,父子关系不能很好的表示。这里放出来希望能抛砖引玉。我以后可能会去完事它,但是现在首先是要搞清楚行为树。实现这个完全是为了看看节点是否正确放置,以方便调试。
- 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<RenderableNode> nodeList = new List<RenderableNode>();
- 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;
-
- 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,从而让行为树去寻找其他节点。而如果用状态机来做的话,条件肯定只用判断一次,比如
- if(run){
- Run();
- }
- else{
- Walk();
- }
那么可能就回到最初的组合节点的设计了,组合节点就不得不每次都扫描条件。其实本质上我是在担心开销问题,因为变成节点后,就不在是if else那么简单,而是变成了函数调用的开销。简单的AI还好,如果大量复杂的AI,每次对整棵树进行扫描估计够呛。但是目前的设计,条件节点就会非常多,条件不完备就会出现BUG,似乎也不是非常好的情况。
最后放出一些细节
- class PatrolAction : BaseActionNode {
-
- public PatrolAction() { nodeName_ += "巡逻行为"; }
-
- public override bool Tick(object input_, object output_)
- {
-
- 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 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 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 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 output = output_ as WarriorOutPutData;
- output.action = WarriorActon.eAlert;
- return true;
- }
- }