事件驱动行为树:不重复唤醒,也别忘了正在运行的动作
目标位置连续写入十次,行为树就应当重算十次吗?如果十次值都一样,通知可能只制造额外调度。但把整棵树挂起,也不意味着移动、超时和动画事件都能停止处理。
事件驱动需要明确两件事:什么变化值得唤醒,以及哪些任务本来就需要推进。
变化通知应当有比较语义
“写入发生了”与“值发生变化”不是同一事件。对实体句柄可以比较稳定ID和代次,对浮点位置则需根据用途决定阈值,不能用一个万能epsilon处理所有键。
Unreal行为树概览说明事件驱动避免无意义的整树轮询,并通过观察者打断相关分支。它没有说所有动作都不再需要周期执行。
合并的是通知,不是事实历史
一帧内同一键多次变化,决策层可能只需要最后值;审计或回放层却可能需要所有变更。把两者存到不同通道,避免为了省通知而破坏事件历史。
取消订阅也需要生命周期保护。角色已经销毁时,仍在队列里的回调不能写入新复用对象;订阅token或代次检查应参与派发。
先观察一帧只唤醒一次
下面用set合并脏键。相同值不会新增通知,不同值在同一帧改变多次只留下一个脏键。flush之后下一次变化才产生下一批通知。它不处理线程,也不吞掉事件日志。
事件先按键合并再唤醒决策;正在运行的动作仍保留自己的推进路径。
把关键规则缩成一个可运行例子
下面使用 Python 3 标准库表达算法规则,保存为example.py后运行python3 example.py。它便于观察输入和输出,不替代引擎中的 C++、Shader 或模型实现。
class Blackboard: def __init__(self): self.values, self.dirty = {}, set() def set(self, key, value): if key not in self.values or self.values[key] != value: self.values[key] = value self.dirty.add(key) def flush(self): changed = sorted(self.dirty) self.dirty.clear() return changed b = Blackboard() for value in [7, 7, 8, 8]: b.set("target", value) assert b.flush() == ["target"] assert b.flush() == [] b.set("target", 8) assert b.flush() == [] print("one dirty key for a frame; unchanged writes stay quiet")本例输出:
one dirty key for a frame; unchanged writes stay quiet优化前后应该比较实际唤醒数、分支重搜数和动作推进数,再看目标设备耗时。没有这些中间量,“CPU减少97.7%”很难解释,也很难复现。
适用范围
仅验证单线程脏键合并,未运行Unreal观察者、定时器或真实Agent负载;没有零开销或亚毫秒响应保证。
相关资料
- Unreal Engine行为树概览