本文主要讨论一种利用两层状态机将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可能会等不到路径计算成功就又开始重计算路径了,从而可能导致奇怪的移动、原地发呆等行为。