一、简介
"StateTree 是一种通用分层状态机,组合了行为树中的 选择器(Selectors) 与状态机中的 状态(States) 和 过渡(Transitions) 。用户可以创建非常高效、保持灵活且井然有序的逻辑。
StateTree包含以树结构布局的状态。状态选择可以在树中的任意位置触发,但它最初从根开始。在选择过程中,将对每个状态的 进入条件(Enter Conditions) 求值。如果通过,选择将前进到该状态的子状态(如果可用)。如果没有子状态可用,将激活当前状态。" -- 官方文档
StateTree在5.1版本进行了重构,官方文档中的内容和编辑器展示的效果也有所不同:
-
将Evaluator从状态内部提出到了State Tree的全局中,所有State可以共用同一套Evaluator
-
新增基于Propertybag的Parameter系统,可以类似黑板功能的声明属性
-
将Condition数据单独剥离数来为SharedInstanceData,使得多个实例可以共享一份数据。
二、UML
三、StateTree设计分析
3.1 享元模式下,私有数据机制
-
索引构建流程
-
compile时,会将EditorData中的数据写入StateTree中。此过程中会构建索引结构,其中,
-
DefaultInstanceData/SharedInstanceData用来存放所有Evaluator/Task/Condition/Transition的默认值。
-
索引Nodes用来存储索引值
-
InstanceIndex:DefaultInstanceData的索引值
-
DataViewIndex:DataView的索引值。
-
-
States/Transition/EvaluatorBegin/EvaluatorEnd中记录Nodes的索引值。
-
-
运行时,
-
可以直接根据EvaluatorBegin/End-》Nodes(InstanceIndex)-》InstanceData中找到真实数据
-
CustomEvaluator如何找到在私有空间中的数据?在上边Evaluator遍历找到真实数据的同时,会构建DataView映射表,将DataViewIndex和真实数据建立映射,这样,在CustomEvaluator::Enter\Tick的时候,就可以通过CustomEvaluator中DataViewIndex找到真实数据,获取方法如下:
-
-
ExecutionContext数据驱动流程
-
StateTree默认值初始化Component的InstancedData
-
InstanceData数据实例,每个Component维护一个独有的数据空间,用于存放Task、Evaluator产生的数据。其中,condition的数据空间可以在不同的Component间共享,所以,此数据空间可以放在StateTree上的SharedInstanceData中。
-
初始化:FStateTreeExecutionContext::Start-》UpdateInstanceData
-
-
InstancedData数据读写
-
3.2 数据流
-
StateTree外部数据传入
a. 基于PropertyBag的Parameter设计
PropertyBag是5.1版本推出的黑板机制,可以在蓝图中动态新增、删除任意类型的成员。
-
实现原理:其内部还是通过一个InstancedStruct来实现的, 但是,用来记录内存信息的ScriptStruct使用了一个新增的UPropertyBag来实现,当新增成员时,会对应创建一个FPropertyBagProertyDesc,记录成员的名称、数据类型和内存信息,从而,可以通过FPropertyBagPropertyDesc+StructMemory获取数据值。
-
编辑器编辑功能:在蓝图中可以定义FInstancedPropertyBag成员变量,StructUtilsEditor插件提供了其编辑器显示的自定义UI:FPropertyBagDetails,UI布局注册位置如下:
编辑器中UI效果如下:
-
序列化功能:序列化顺序为:PropertyDesc序列化-》PropertyDesc的大小-》StructScript+StructMemory利用SerializeItem进行序列化-》内存大小-》总大小。
-
暂不支持网络序列化
b. 生产者-消费者模式的Event机制
-
实现原理:
-
事件定义:StateTreeEvent中,使用GameplayTag记录事件名称,使用InstancedStruct记录事件参数。
-
事件入口(生产):通过UStateTreeComponent::SendStateTreeEvent,将StateTreeEvent传递给Context,Context内部将Event存入InstancData中。
-
事件消费:FStateTreeExecutionContext::Tick中,每次tick执行完毕后,销毁InstancData中的Event
-
-
-
StateTree内部数据流转
a. 执行上下文设计
每次驱动StateTree时,都会新建一个执行上下文来实现对私有数据的初始化、绑定数据的更新、及方便业务层快速获取Instance数据的DataView映射表的构建。
b. 数据绑定机制
FStateTreePropertyBindings,简要概述就是记录绑定数据的FProperty,当拷贝时,根据DataView中source和target的内存地址进行拷贝。从而实现StateTree内部不同Node之间数据的传递。
3.3 状态管理![](https://i-blog.csdnimg.cn/blog_migrate/cff54f1df7cace1fa6baa01add95848e.jpeg)
-
执行上下文状态说明
-
Running: 状态运行时,此状态下,tick时才会执行TickEvaluators以及TickTasks,当TickTask实行时修改了Running状态,则调用StateCompleted,此处处理Task完毕逻辑
-
Failed:失败状态,会触发OnStateFailed||OnStateCompelete Trigger,执行Transition。
-
Succeeded:如果是成功状态,会触发OnStateSuccessed||OnStateCompelete Trigger,执行Transition。
-
Unset: 状态机初始状态设置为Unset
-
-
执行逻辑(5.2版本)
-
Start时,从Root节点出发,递归查找可执行状态,如果没有可激活的状态,导致Tick时,所有的evaluator、task都不会执行
-
Tick时:执行当前激活状态内的Tasks,FStateTreeTaskBase::Tick 返回非Running状态时,触发StateCompleted。
-
Transition:有5次遍历机会,每次从激活状态数组中,倒序遍历检测可跳转的状态。如果没有可跳转的状态,状态树报错。注意:由Transition触发的EnterState,EnterState的返回值如果是非Running,会继续进行Transition迭代(一共5次),因此,EnterState根据实际需求合理返回数值。
-
四、StateTree和BehaviorTree区别
行为树与状态机之间的一大区别是,状态机通常在执行沿树向下推进时提交状态选择,而行为树则尝试查找合适的叶节点。
一般来说,即使已经选择一个状态,行为树也会继续执行状态选择逻辑。这是在状态之间过渡的唯一方法。
StateTree基于过渡按需运行状态选择过程。在第一个Tick上,会隐式过渡到根状态,这将选择要运行的第一个状态。选择该状态之后,过渡会指示何时及在何处执行选择逻辑。
五、多线程SharedInstanceData
-
由UStateTreeComponent::TickComponent进行驱动的StateTree的tick,引擎ActorComponent::TickComponent驱动的流程是:World在执行Tick的时候,触发了FNamedTaskThread线程去执行任务(FTickFunctionTask),任务FTickFunctionTask具体的工作内容就是执行ActorComponent的Tick函数。其中,FNamedTaskThread被指定在GameThread中执行。所以,对于由UStateTreeComponent来驱动的状态树,只有一份SharedInstanceData。
-
UAITask_UseGameplayInteraction::TickTask驱动StateTree的tick,不确认此TickTask是否是多线程,如果是多线程,StateTree会为每个线程Copy一份SharedInstanceData,避免多线程下数据读写异常。
四、参考文档
[1] 官方文档: StateTree Overview