本文主要讨论一种利用两层状态机将Astar寻路功能在dots下实现的功能。
首先说下为什么要使用Astar这个插件。这个插件的功能很多其实不依赖于unity monobehavior 这一套体系,因此在转为dots使用时可以更加方便,并且可以最大程度的不在场景中通过gameobject来获得寻路功能。Unity本身自带的寻路目前没有更多的API,如果要使用Unity自身的导航系统,估计只能使用GameObject Injecte 方式,在dots世界内外都保存一个副本,如此效率显然十分低下。
为了将寻路转化为dots下的系统,需要对寻路功能进行拆分。这里将寻路拆成两部分,一部分是计算路径,另一部分是根据路径来实现移动。因此可以考虑实现两个独立的状态机,路径状态机控制对路径的计算,并根据计算结果来更新移动状态。而移动状态则仅控制移动。这种实现的好处在于,一方面移动的代码简单的情况可以自己实现,从而可以利用burst来加速;另一方面基本可以将大多数涉及Astar的代码打包进一个系统,而基本涉及Astar的代码部分都是只能在主线程运行的。
路径状态机的具体实现上,考虑到class类型的数据无法保存在dots组件之中,则需要在system中保存以entity为索引的路径map。以recastGraph为例,生成路径方面需要使用 RichPath 这个类来实现路径数据的生成及保存,而Astar本身就是多线程运行,因此路径状态机的主要功能就是将路径的生成放入Astar的计算队列,然后在路径完成时通知移动状态机改变状态机进行移动。
public enum RichPathComputeState
{
WaitingForDestination,
NeedInitPath,
NeedUpdate,
Computing,
ReadyToMove,
Finish
}
Entities
.WithoutBurst()
.ForEach((Entity e, ref AstarState state) =>
{
switch (state.pathState)
{
case RichPathComputeState.NeedInitPath:
OnInitPath(e, ref state);
break;
case RichPathComputeState.Computing:
OnComputingPath(e, ref state);
break;
case RichPathComputeState.NeedUpdate:
OnPathNeedUpdate(e, ref state);
break;
case RichPathComputeState.Finish:
OnPathFinish(e, ref state);
break;
default:
break;
}
}).Run();
移动状态机的具体实现上,一种思路是直接将所有的路径点全部拷贝进行移动,但是事实上移动状态机移动是一个移动的过程,假如移动的过程中离开了原始路径,那么当恢复时其实是要重新计算路径的,因此将所有路径点全部拷贝移动不是一个好的思路。这里可以利用RichFunnel.Update()这个接口,来获取最近的两个移动点。当然,如上的理由,这个接口应放在路径状态机中实现。因此整体的移动逻辑是:向第一个点移动=》抵达第一个点=》向路径状态机申请计算下次路径=》向第二个点移动=》路径状态机通知移动状态机新的移动点=》再次向第一个点移动。
public enum RichPathMoveState
{
WaitingForPath,
MoveToFirstCorner,
MoveToSecondCorner,
MoveToLastCorner
}
float time = Time.DeltaTime;
Entities
.ForEach((ref AstarState state, ref Translation pos, ref AstarCorners corner) =>
{
switch (state.moveState)
{
case RichPathMoveState.WaitingForPath:
OnWaitingForPath(ref state, ref pos);
break;
case RichPathMoveState.MoveToFirstCorner:
OnMoveToFirstCorner(ref state, ref pos, in corner, time);
break;
case RichPathMoveState.MoveToSecondCorner:
OnMoveToSecondCorner(ref state, ref pos, in corner, time);
break;
case RichPathMoveState.MoveToLastCorner:
OnMoveToLastCorner(ref state, ref pos, ref corner, time);
break;
default:
break;
}
}).Schedule();
最后简单的测试下性能,直接简单粗暴放500个entity测试效果。移动过程还是比较丝滑,不过当500个entity同时改变目标计算新的路径时明显帧率有个大幅下滑。这里用一个简单的计数器来控制每帧更新路径计算的entity数量,当超出计数时忽略余下的entity,留待下一帧处理。此种方法对控制帧率效果比较明显,但有个副作用就是如果目标变化过于频繁,排序靠后的entity可能会等不到路径计算成功就又开始重计算路径了,从而可能导致奇怪的移动、原地发呆等行为。