双层状态机实现避免卡死的指引

该指引系统的主要特性:

  1. 引入状态机实现指引系统,使指引状态切换和步骤控制更加清晰。
  2. 指引系统不是绝对强制的指引,达成跳过条件时,指引是可以跳过的。
  3. 指引有意外挂起检测,当指引因为某些原因无法进行下去时(例如所在界面不对),会自动重置该条指引。
  4. 指引完成条件的达成依赖于服务器回复,而不是简单的客户端点击事件。
  5. 指引步骤封装,一条指引的多个步骤只用一个指令实现,使指引的流程在程序可控的范围内。

实例与主要思路:

假设有一条指引:引导建造一个食品厂。完成第该指引要分成两步,第一步,选择可建造空地并打开建造界面;第二步,点击建筑按钮完成建造食品厂的指引。
对于上述描述的情形可以抽象两层状态机。第一层状态机控制一条指引的开始、运行、完成的流程然后转到下一条指引,我称之为流程状态机。第二层状态机控制某条指引的具体步骤:选择空地、开打界面、点击按钮等,我称之为步骤状态机

伪代码:

由于该指引系统应用到公司的实际项目,这里只给出伪代码和完整注释。

  • 流程状态机的状态:
public enum GuideState
{ 
    none = 0,               //default
    start = 1,              //指引开始
    running = 2,            //指引进行中,此时唤醒了一个步骤状态机
    pause = 3,              //指引被暂停
    complete = 4,           //指引完成
    hangup = 5,             //指引内部主动挂机
    waitResponse = 6,       //等待服务器回复,收到服务器回复后完成指引
}
  • 步骤状态机的状态:
public enum GuideStep
{
    prepare = 0,            //准备
    step01 = 1,             //以下10个状态表示10个不同的步骤
    step02 = 2,
    step03 = 3,
    step04 = 4,
    step05 = 5,
    step06 = 6,
    step07 = 7,
    step08 = 8,
    step09 = 9,
    step10 = 10,
    playerInput = 99,       //等待玩家输入
    hangup = 100,           //主动挂起
    hangupUnexpect = 101,   //意外挂起
    complete = 102,         //完成
}
  • 指引类的变量申明及初始化,可略过:
public class BGuide : MonoBehaviour
{
    private static BGuide instance;
    [SerializeField]
    private GuideState guideState = GuideState.none;

    private GuideState GuideState
    {
        get { return guideState; }
        set
        {
            if (guideState == GuideState.none && value != GuideState.start)
                return;
            guideState = value;
        }
    }

    private Dictionary<string, Guide> guideMapper = new Dictionary<string, Guide>();        //待完成指引的字典
    [HideInInspector]
    public List<string> happendList;        //已完成指引列表

    private string spawnGuideId = string.Empty;     //activeGuide的spwans索引

    private Guide activeGuide = null;               //当前激活的指引
    [SerializeField]
    private GuideStep guideStep = GuideStep.prepare;

    private int guideStepCount = 0;

    private float guideInterval = 0.5f;

    private float guideStepInterval = 0.3f;

    private Coroutine stepStateMachine;

    private GameObject arrowGo = null;

    private GuideMask guideMask = null;

    private Sequence activeGuideCompleteSequence = null;

    public static BGuide Instance
    {
        get
        {
            if (instance == null)
            {
                instance = new GameObject("BGuide").AddComponent<BGuide>();
            }
            return instance;
        }
    }

    public void Init()
    {
        GuideState = GuideState.none;

        happendList = GetHappenedGuideList();      //初始化已完成指引列表

        //获取所有待完成指引,并排序
        Dictionary<string, Guide> dic = GetTotalGuideMapper();
        List<Guide> guideList = new List<Guide>();
        foreach (KeyValuePair<string, Guide> kv in dic)
        {
            Guide guide = kv.Value;
            if (!happendList.Contains(guide.id) && guide.weak != 2)
            {
                guideList.Add(guide);
            }

        }

        guideList.Sort
            (
                (Guide a, Guide b) =>
                {
                    return a.guideOrder - b.guideOrder;
                }
            );

        guideMapper.Clear();
        for (int i = 0; i < guideList.Count; i++)
        {
            guideMapper.Add(guideList[i].id, guideList[i]);
        }
    }
    //......
}
  • 流程状态机实现:
    #region guide state machine

    public void NetOperateDetect(int result, GuideStep failGotoStep, NetOperateType netOperate)
    {
        if (IsGuideWaitResponse() && netOperate == (NetOperateType)activeGuide.operateType)
        {
            if (result == 0)
            {
                SetGuideStep(GuideStep.complete);
                ActiveGuideComplete();
            }
            else
            {
                SetGuideStep(failGotoStep);
            }
        }
    }

    protected void Update()
    {
        switch (GuideState)
        {
            case GuideState.start:
            case GuideState.complete:       //流程状态机开始或上一条指引完成,取出一条指引运行
                {
                    Guide guide = null;

                    if (guideMapper.TryGetValue(spawnGuideId, out guide) && (bool)ExecuteExpression(guide.guideTrigger))//判断上一条完成指引的spawns字段索引的指引是否能触发
                    {
                        //上一条指引能spwans一条指引
                        if (GuideRunningValid())//流程状态机进入running状态,环境合法性检测
                        {
                            GuideState = GuideState.running;
                            activeGuide = guide;

                            if (activeGuide.skipCondition != "" && (bool)ExecuteExpression(activeGuide.skipCondition))
                            {
                                guideMapper.Remove(activeGuide.id);
                                AGuide.SendGuideCompletedMessage(activeGuide.id);
                                happendList.Add(activeGuide.id);
                            }
                            else
                            {
                                spawnGuideId = string.Empty;
                                ActiveGuideStart();
                                guideMapper.Remove(activeGuide.id);
                            }
                        }
                    }
                    else
                    {
                        //上一条指引不能spwans一条新指引
                        if (GuideRunningValid())//流程状态机进入running状态,环境合法性检测
                        {
                            List<string> weakGuideCache = new List<string>();

                            foreach (KeyValuePair<string, Guide> kv in guideMapper)     //遍历guideMapper
                            {
                                guide = kv.Value;

                                if ((bool)ExecuteExpression(guide.guideTrigger))//找出一条可触发的指引
                                {
                                    GuideState = GuideState.running;
                                    activeGuide = guide;

                                    if (activeGuide.skipCondition != "" && (bool)ExecuteExpression(activeGuide.skipCondition))//判断指引的跳过条件
                                    {
                                        //跳过该指引
                                        weakGuideCache.Add(activeGuide.id);
                                    }
                                    else
                                    {
                                        //执行该指引
                                        ActiveGuideStart();     //指引开始,该函数的实现在下文
                                        guideMapper.Remove(activeGuide.id);
                                        break;
                                    }
                                }
                                else if (guide.weak == 1)       //weak == 1的指引不满足触发条件就跳过
                                {
                                    weakGuideCache.Add(guide.id);
                                }
                            }

                            for (int i = 0; i < weakGuideCache.Count; i++)      //weakGuideCache里的指引全跳过
                            {
                                guideMapper.Remove(weakGuideCache[i]);
                                BGuide.SendGuideCompletedMessage(weakGuideCache[i]);
                                happendList.Add(weakGuideCache[i]);
                            }
                        }
                    }

                }break;

            case GuideState.pause:      //指引被暂停,打开EventSystem和FingerGesture
                {
                    SetPlayerInput(true);
                }break;
        }

        //自动跳过条件检测,强制完成该条指引
        if (IsGuideRunning() && activeGuide != null && activeGuide.skipCondition != "" && (bool)ExecuteExpression(activeGuide.skipCondition))
        {
            ActiveGuideComplete();
        }

        //流程状态机非法状态检测,强制完成该条指引
        if (!IsGuideStateValid())
        {
            ActiveGuideComplete();
        }
    }

    public void ActiveGuideStart()
    {
        if (activeGuideCompleteSequence != null)
        {
            activeGuideCompleteSequence.Kill();
        }

        try
        {
            SetPlayerInput(false);              //关闭EventSystem和FingerGesture
            guideStep = GuideStep.prepare;      //步骤状态机进入prepare状态

            foreach (string expression in activeGuide.moduleID)     //依次执行activeGuide.moduleID里的表达式
            {
                ExecuteExpression(expression);      //例子里的expression = "",通过中缀表达式解析器解析后会执行public void GuideBuilding(string buildingName)这个函数。
            }
        }
        catch (Exception e)     //出错,强制完成该条指引
        {
            GuideCloseAllDialogs();
            ActiveGuideComplete();
        }
    }

    #endregion

上段代码中ExecuteExpression(expression)函数的实现可参考该文章:
可自定义函数、并且函数可任意嵌套的中缀表达式解析器

  • 步骤状态机的实现:
    #region build
    public void GuideBuilding(string buildingName)
    {
        GuideCloseAllDialogs();
        SetGuideStep(GuideStep.step01);     //设置步骤状态机状态:step01

        this.stepStateMachine = StartCoroutine(GuideBuildingCoroutine(buildingName));       //起一个步骤状态机
    }

    private IEnumerator GuideBuildingCoroutine(string buildingName)
    {
        while(true)
        {
            switch (guideStep)
            {
                case GuideStep.step01:
                    {
                        SetPlayerInput(false);
                        GuideFindSlotAndBuild           //找到可建造空地,并打开建造界面,完成后进入第二步
                            (buildingName,
                            () =>
                            {
                                SetGuideStep(GuideStep.step02);
                            });

                        HangupGuideStep();
                    }break;

                case GuideStep.step02:
                    {
                        SetPlayerInput(false);

                        yield return new WaitForSeconds(guideStepInterval);
                        ClickButton                                                     //点击建筑界面的建造按钮,点击后关闭EventSystem和FingerGesture并等待服务器回复
                            ("buildView", "btn_name", 0, 200,
                            () =>
                            {
                                SetPlayerInput(false);
                                WaitResponse();
                            }
                            );

                        WaitPlayerInput();      //等待玩家输入
                    }break;
            }
            yield return null;

            //合法性检测
            if (IsGuideStepWaitPlayerInput(GuideStep.step02) && !IsCurrentDialogValid("buildView"))     //在第二步等待玩家输入并且当前界面不是建造界面,删除指引箭头和mask,步骤状态机进入意外挂起状态
            {
                ClearArrow();
                ClearGuideMask();
                HangupGuideStepUnexpect();
            }

            if (guideStep == GuideStep.hangupUnexpect && AUIManager.GetCurrentDialog().dialogName == "HUD")     //步骤状态机进入意外挂起状态,并且当前界面是主界面则重新开始该指引
            {
                GuideCloseAllDialogs();
                SetGuideStep(GuideStep.step01);
            }
            yield return null;
        }
    }

猜你喜欢

转载自blog.csdn.net/u010213226/article/details/54883362