【节点实现】
要想行为树的功能更加强大,需要有足够丰富的节点,我们先来实现各类节点:
控制节点
在控制节点中,需要知道当前执行到了哪个节点,需要增加一个字段标识
分别实现SequenceNode、ParallelNode、SelectorNode、RandomSelectorNode
这些是比较常见的控制节点,在前文中我们用顺序节点做了控制,但一些条件判断仍在动作节点中,这是不合适的。
动作节点应当尽量少的做条件判断,我们可以将常见的if else判断方式抽象为节点,这些是特殊的选择节点
结合前文,我们需要两类特殊的选择节点,从两个值做选择的比较节点,从不同值做选择的范围节点
分别实现BoolCompareSelectorNode、BoolCompareSelectorNode、IntCompareSelectorNode、IntRangeSelectorNode、FloatRangeSelectorNode
也可以结合实际需求,实现各类自定义的控制节点
public class ControlNode:Node
{
public List<Node> subNodes;
public int curSubIndex;
protected override NodeStatus OnUpdate()
{
return Update();
}
}
public class SequenceNode:ControlNode
{
protected override NodeStatus OnUpdate()
{
curSubIndex = -1;
foreach (var node in subNodes)//成功则继续
{
curSubIndex++;
var status = node.Update();
if (status != NodeStatus.Success)
return status;
}
return NodeStatus.Success;
}
}
public class SelectorNode:ControlNode
{
protected override NodeStatus OnUpdate()
{
curSubIndex = -1;
foreach (var node in subNodes)
{
curSubIndex++;
var status = node.Update();
if (status == NodeStatus.Success)//任意成功则返回
return status;
}
return NodeStatus.Success;
}
}
public class RandomSelectorNode:ControlNode
{
protected override NodeStatus OnUpdate()
{
curSubIndex = UnityEngine.Random.Range(0, subNodes.Count);
var status = subNodes[curSubIndex].Update();
return status;
}
}
public class BoolCompareSelectorNode:ControlNode
{
public bool targetValue;
private bool curValue;
protected override NodeStatus OnUpdate()
{
curSubIndex = targetValue == curValue ? 0 : 1;
var status = subNodes[curSubIndex].Update();
return status;
}
}
public class IntCompareSelectorNode : ControlNode
{
public int targetValue;
public Comparison comparison;
private int curValue;
protected override NodeStatus OnUpdate()
{
bool res = true;
switch(comparison)
{
case Comparison.SmallerThan: res = curValue<targetValue; break;
case Comparison.GreaterThan: res = curValue>targetValue; break;
case Comparison.Equal: res = curValue == targetValue;break;
case Comparison.SmallerThanOrEqual: res = curValue <= targetValue; break;
case Comparison.GreaterThanOrEqual: res = curValue >= targetValue; break;
}
curSubIndex = res?0:1;
var status = subNodes[curSubIndex].Update();
return status;
}
}
public class FloatCompareSelectorNode : ControlNode
{
public float targetValue;
public Comparison comparison;
private float curValue;
protected override NodeStatus OnUpdate()
{
bool res = true;
switch (comparison)
{
case Comparison.SmallerThan: res = curValue < targetValue; break;
case Comparison.GreaterThan: res = curValue > targetValue; break;
case Comparison.Equal: res = curValue == targetValue; break;
case Comparison.SmallerThanOrEqual: res = curValue <= targetValue; break;
case Comparison.GreaterThanOrEqual: res = curValue >= targetValue; break;
}
curSubIndex = res ? 0 : 1;
var status = subNodes[curSubIndex].Update();
return status;
}
}
public class IntRangeSelectorNode:ControlNode
{
public List<int> intRange;
private int curValue;
protected override NodeStatus OnUpdate()
{
for (int i = 0;i<intRange.Count;i++)
{
if (curValue < intRange[i])
{
curSubIndex = i;
break;
}
}
var status = subNodes[curSubIndex].Update();
return status;
}
}
public class FloatRangeSelectorNode : ControlNode
{
public List<float> intRange;
private float curValue;
protected override NodeStatus OnUpdate()
{
for (int i = 0; i < intRange.Count; i++)
{
if (curValue < intRange[i])
{
curSubIndex = i;
break;
}
}
var status = subNodes[curSubIndex].Update();
return status;
}
}
public enum Comparison
{
Equal,
SmallerThan,
SmallerThanOrEqual,
GreaterThan,
GreaterThanOrEqual
}
public class ParallelNode:ControlNode
{
protected override NodeStatus OnUpdate()
{
curSubIndex = -1;
int successCount = 0;
int failCount = 0;
foreach (var node in subNodes)
{
curSubIndex++;
var status = node.Update();
if(status == NodeStatus.Success)
{
successCount++;
}
else if(status == NodeStatus.Failure)
{
failCount++;
}
}
if (successCount == subNodes.Count)
return NodeStatus.Success;
if (failCount > 0)
return NodeStatus.Failure;
return NodeStatus.Success;
}
}
修饰节点
修饰节点的子节点通常只有一个,常见的修饰有结果取反、重复N次、重复直到失败、重复直到成功、直接返回失败、直接返回成功等
实现部分修饰节点需要知道行为树的状态,其状态由子节点组成,如果行为树得到的状态不是Running,表示当前行为树执行完毕了,需要重置节点数据
分别实现DecoratorNode、InverterNode、UntilFailureNode、UntilSuccessNode、RepeatNode、ReturnFailureNode、ReturnSuccessNode
public class DecoratorNode : Node
{
public Node subNode;
protected override NodeStatus OnUpdate()
{
return subNode.Update();
}
protected override void OnEnd()
{
subNode.End();
}
}
public class InverterNode: DecoratorNode
{
protected override NodeStatus OnUpdate()
{
var status = subNode.Update();
if(status == NodeStatus.Success )
{
status = NodeStatus.Failure;
}
else if (status == NodeStatus.Failure)
{
status = NodeStatus.Success;
}
return status;
}
}
public class UntilFailureNode: DecoratorNode
{
private NodeStatus lastState = NodeStatus.Success;
protected override NodeStatus OnUpdate()//失败则表示这个节点成功执行,成功执行后一直成功,也可以成功后重新执行
{
if(lastState == NodeStatus.Failure)
{
return NodeStatus.Success;
}
var status = subNode.Update();
lastState = status;
if (status == NodeStatus.Failure)
{
status = NodeStatus.Success;
}
return status;
}
protected override void OnEnd()
{
lastState = NodeStatus.Success;
}
}
public class UntilSuccessNode: DecoratorNode
{
private NodeStatus lastState = NodeStatus.Failure;
protected override NodeStatus OnUpdate()
{
if (lastState == NodeStatus.Success)
{
return NodeStatus.Success;
}
var status = subNode.Update();
lastState = status;
if (status == NodeStatus.Success)
{
status = NodeStatus.Success;
}
return status;
}
protected override void OnEnd()
{
lastState = NodeStatus.Failure;
}
}
public class ReturnFailureNode: DecoratorNode
{
protected override NodeStatus OnUpdate()
{
return NodeStatus.Failure;
}
}
public class ReturnSuccessNode : DecoratorNode
{
protected override NodeStatus OnUpdate()
{
return NodeStatus.Success;
}
}
public class RepeatNode : DecoratorNode
{
public int maxCount;
private int curCount;
protected override NodeStatus OnUpdate()
{
if(curCount<maxCount)
{
curCount++;
var status = subNode.Update();
return status;
}
return NodeStatus.Failure;
}
protected override void OnEnd()
{
curCount = 0;
}
}
public class BehaviorTree
{
public GameObject owner;
private Node rootNode;
public void Init(GameObject owner)
{
this.owner = owner;
}
public NodeStatus Update()
{
var status = rootNode.Update();
if(status != NodeStatus.Running)
{
rootNode.End();
}
return status;
}
public void Destroy()
{
}
}
public enum NodeStatus
{
Success,
Failure,
Running,
}
public class Node
{
public string nodeName;
public int nodeId;
public NodeStatus status;
public BehaviorTree owner;
public virtual void Init(string nodeName, int nodeId, BehaviorTree owner)
{
this.owner = owner;
this.nodeName = nodeName;
this.nodeId = nodeId;
}
public NodeStatus Update()
{
return OnUpdate();
}
public void End()//方法名字叫Exit也是一样的
{
OnEnd();
}
protected virtual NodeStatus OnUpdate()
{
return NodeStatus.Success;
}
protected virtual void OnEnd()
{
}
public virtual void Destroy()
{
}
}
【复杂需求】
现在有了一定数量的条件节点,我们让需求变得更为复杂来看一看。
NPC围绕固定的点50米内巡逻,当有多个敌人进入巡逻范围内或者和NPC距离小于20米时,会选择其中距离更近的敌人追击,如果和敌人距离小米3米,释放1技能攻击,如果在追击中距离敌人大于30米,NPC速度提高一倍,距离敌人小于3米,释放2技能攻击,如果和敌人距离大于50米或者距离中心点超过100米,则回到重新开始巡逻
按照步骤,首先做行为拆解:
- 固定50米巡逻
- 巡逻范围内有敌人或者和NPC距离小于20米可以追击
- 距离更近的敌人被选择
- 未加速时,距离敌人小于3米,使用1技能攻击
- 加速时,距离敌人小于3米,使用2技能攻击
- 距离敌人大于30米,移速加倍
- 距离敌人大于50米,或者中心点大于100米,回归巡逻
其次,划分状态,分为巡逻、追击、加速、未加速四个状态,可以发下加速和未加速时追击下的子状态
再次,对每个状态下的行为划分优先级,优先级高的行为先判断
最后,画出行为树导图,如下:
可以更明显的看到,行为逻辑是配置出来的,配置一般由策划负责,相对一般的数值配置来说,行为树的配置增添了逻辑性,更加考验策划。
而在以往的,一些逻辑,尤其是针对各类case的往往是程序负责实现的,而在行为树中,程序只需要负责实现具体的动作。两者分工更加明确。
【动作节点实现】
经过修改后NPC类和自定义实现的Action节点如下
public class NPC:MonoBehaviour
{
private BehaviorTree bt;
public bool idle;
public bool fastSpeed;
public Vector3 idleCenterPos;
public GameObject target;
public List<GameObject> enemys = new List<GameObject>();
public float speed;
void Start()
{
bt = new BehaviorTree();
bt.Init(this.gameObject);
//初始化bt数据
}
void Update()
{
bt.Update();
}
void OnDestroy()
{
bt.Destroy();
}
public void SearcheEnemys()
{
}
}
public class IdleNode : ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
if (npc.idle)
{
Debug.Log("执行巡逻时的动作");
res = NodeStatus.Success;
}
}
return res;
}
}
public class SearchEnemyNode: ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
if (npc.idle)
{
npc.enemys.Clear();
npc.SearcheEnemys();
if(npc.enemys.Count > 0)
{
res = NodeStatus.Success;
}
}
}
return res;
}
}
public class SelectEnemyNode:ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
npc.target = null;
float dis = float.MaxValue;
foreach( var item in npc.enemys )
{
var _dis = (item.transform.position - npc.gameObject.transform.position).sqrMagnitude;
if(_dis < dis)
{
dis = _dis;
npc.target = item;
}
}
if( npc.target != null )
{
res = NodeStatus.Success;
npc.idle = false;
}
}
return res;
}
}
public class ReturnIdleNode : ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
Debug.Log("执行回归巡逻的动作");
res = NodeStatus.Running;
//如果回归了
//npc.idle = false;
//npc.fastSpeed = false;
}
return res;
}
}
public class AddSpeedNode: ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
if(!npc.fastSpeed)
{
npc.speed *= 2;
npc.fastSpeed = true;
}
Debug.Log("执行寻路追踪的目标的动作");
res = NodeStatus.Success;
}
return res;
}
}
public class SkillOneAttackNode : ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
Debug.Log("执行技能1攻击");
res = NodeStatus.Success;
//如果回归了
//npc.state = NPC.NPCState.Idle;
}
return res;
}
}
public class SkillTwoAttackNode : ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
Debug.Log("执行技能2攻击");
res = NodeStatus.Success;
//如果回归了
//npc.state = NPC.NPCState.Idle;
}
return res;
}
}
public class PursueNode:ActionNode
{
protected override NodeStatus OnUpdate()
{
var npc = owner.owner.GetComponent<NPC>();
var res = NodeStatus.Failure;
if (npc != null)
{
Debug.Log("执行追踪敌人动作");
res = NodeStatus.Running;
//如果回归了
//npc.state = NPC.NPCState.Idle;
}
return res;
}
}
这样的实现包含了行为树基本的内核,但还需要一些其他功能让行为树更加完善,例如当前的实现面临以下问题:
- 通用节点参数配置化:对于存在参数的节点,其参数应该是配置的,作为一个通用节点,不应当插入业务代码去设置或获取具体的值
- 动作节点逻辑实现位置:实现既可以在动作节点内,也可以仅仅是调用下方法,哪种方式比较好
- 运行时参数获取:每个动作节点都要重复调用相同的代码获取参数的值,有没有一个通用的获取参数的方式
- 节点间通信:我们将一些数据放在NPC类中,但对于该类而言,有些数据不是必须的,这些放在一块势必导致NPC类不断膨胀,例如搜索敌人和选择敌人之间的通信。
我们在后文讨论如何实现更为通用的行为树