CocosCreator_ECS/行为树决策的时效性.md

71 lines
3.8 KiB
Markdown
Raw Permalink Normal View History

2022-03-21 09:27:37 +00:00
# 行为树决策的时效性 -- honmono
最近在写ECS + BehaviorTree的框架, 研究BehaviorTree的时候遇到了一个问题.
    根据我看的一些文档, 对于行为树的决策是每一帧都要更新的, 比如现在有一个场景, 用户可以输入文本, 输入move让方块A向前移动10格子, 输入stop方块A停止移动. 那么对于行为树来说, 每一帧都要判断当前用户输入的是move,还是stop, 从而下达是移动还是停止的行为.
对于移动, 行为是sequence([用户输入move, 移动]); 用ActionA代替
对于停止, 行为是sequence([用户输入stop, 停止]);用ActionB代替
最终行为是select([ActionA, ActionB])
> sequence表示按顺序执行子行为, 如果遇到子行为执行失败, 那么立刻停止, 并返回失败, 如果全部执行成功, 那么返回成功.
> select表示按顺序执行子行为, 如果遇到子行为执行成功, 那么立即停止, 并返回成功. 如果全部执行失败, 那么返回失败.
假设现在用户点击一下, 那么每帧都需要从头一层层向下判断执行, 直到判断到移动再执行.
当然这是有必要的, 对于行为来说 确定自己能否应该执行是十分重要的.
但是这对执行一个 持续性的行为很不友好. 假设还是上面的场景, 用户输入sleep, 方块停止移动2s. 就是sequence([用户输入sleep, 停止移动2s]). 这个时候行为树是 select([ActionA, ActionB, ActionC]);
那么当我输入sleep, 方块停止移动的时间内, 输入move, 那么下一帧的决策就进入了ActionA, 导致方块移动. 停止移动2s的行为被打断了.
这个问题我称为行为树决策的时效性, 也就是行为得到的决策, 并不能维持一定时间.
这个决策其实目前只是sequence和selector 才拥有的.
## 如何解决:
因为我是自己想的, 所以解决方案可能不是最优的.
首先我为所有决策都添加了时效性, 也就是当前行为的决策一旦做出, 就必须在当前行为完全结束后才能被修改.
```
class NodeHandler {
onEnter:(node: NodeBase, context: ExecuteContext) => void;
onUpdate:(node: NodeBase, context: ExecuteContext) => void;
}
```
引入一丢丢代码, 在onEnter时, 将当前行为的状态置为 running, 在onUpdate时判断, 如果当前状态不是running就return. 所以一旦状态确定, 就不会再onUpdate中被修改, 直到下一次进入onEnter.
这个时候在带入上述的场景就没问题了, 当用户输入sleep时, 方块停止移动2s, 在2s内输入move, 并不会导致ActionC的决策更改, 直到停止移动2s的行为结束, 进入下一个周期后才会进入ActionA
但是这个同样也导致了另一个问题, 就是并行的行为. 比如假设一个场景, 士兵一边巡逻, 一边观察是否有敌人, 如果有敌人, 那么停止巡逻, 去追击敌人.
行为树如下:
ActionA = 巡逻
ActionB = sequence([观察是否有敌人, 追击敌人]);
repeat(sequence([ActionB, ActionA]))
因为上面的方法在行为结束前不会修改决策, 那么就会出现, 士兵先观察是否有敌人, 没有就去巡逻, 巡逻完了, 再去观察是否有敌人, 这就太蠢了.
我解决上面的问题的方案是添加一个新的决策Node, 这个Node就是处理并行行为的.
parallel 的能力是 顺序处理子节点, 但是并不需要等待前一个节点执行完毕后才能执行后一个. 当有行为返回失败时, 立即退出返回失败, 当所有行为返回成功时, 停止返回成功.
行为树如下:
repeat(selector([parallel([Inverter(观察是否有敌人), 巡逻]), 追击敌人]))
> Inverter表示取反
当前没有发现有敌人, 那么行为在巡逻, parallel还在running阶段, 因为巡逻不会失败, 所以最后一种情况是 如果发现敌人, 那么parallel立即返回失败, 那么行为就到了追击敌人了.
完美解决上述问题.