基于Unity行为树设计与实现的尝试
查阅了一些行为树资料,目前最主要是参考了这篇文章,看完后感觉行为树实乃强大,绝对是替代状态机的不二之选。但从理论看起来很简单的行为树,真正着手起来却发现很多细节无从下手。
总结起来,就是:
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#不能多继承,抽象类可能会导致某些时候会很棘手,所以还是用接口。虽然目前还未发现接口的好处。
在进行抽象设计的时候,接口的纯粹性虽然看起来更加清晰,不过有时候遇到需要重复使用某些类函数的时候就挺麻烦,让人感觉有点不利于复用。虽然每一组都只是简单的居中,不过效果看起来还可以接受
然后从图中就可以看到问题了。所有正条件,都会有一个反条件,不这么做就无法在条件改变时,让当前节点返回FALSE,从而让行为树去寻找其他节点。而如果用状态机来做的话,条件肯定只用判断一次,比如
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<CharacterAttribute>(); 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 framevoid 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<WarriorPatrol>().enabled = false; break; case WarriorActon.eAttack: case WarriorActon.eCrazyAttack: GetComponent<WarriorAttack>().enabled = false; break; case WarriorActon.eRunAway: GetComponent<WarriorRunAway>().enabled = false; break; case WarriorActon.eAlert: GetComponent<WarriorAlert>().enabled = false; break; } switch (action) { case WarriorActon.ePatrol: GetComponent<WarriorPatrol>().enabled = true; break; case WarriorActon.eAttack: var attack = GetComponent<WarriorAttack>(); attack.revenge = false; attack.enabled = true; break; case WarriorActon.eCrazyAttack: var crazyAttack = GetComponent<WarriorAttack>(); crazyAttack.revenge = true; crazyAttack.enabled = true; break; case WarriorActon.eRunAway: GetComponent<WarriorRunAway>().enabled = true; break; case WarriorActon.eAlert: GetComponent<WarriorAlert>().enabled = true; break; case WarriorActon.eIdle: GetComponent<WarriorPatrol>().enabled = false; GetComponent<WarriorAttack>().enabled = false; GetComponent<WarriorRunAway>().enabled = false; break; } }