该指引系统的主要特性:
引入状态机实现指引系统,使指引状态切换和步骤控制更加清晰。指引系统不是绝对强制的指引,达成跳过条件时,指引是可以跳过的。指引有意外挂起检测,当指引因为某些原因无法进行下去时(例如所在界面不对),会自动重置该条指引。指引完成条件的达成依赖于服务器回复,而不是简单的客户端点击事件。指引步骤封装,一条指引的多个步骤只用一个指令实现,使指引的流程在程序可控的范围内。实例与主要思路:
假设有一条指引:引导建造一个食品厂。完成第该指引要分成两步,第一步,选择可建造空地并打开建造界面;第二步,点击建筑按钮完成建造食品厂的指引。 对于上述描述的情形可以抽象两层状态机。第一层状态机控制一条指引的开始、运行、完成的流程然后转到下一条指引,我称之为流程状态机。第二层状态机控制某条指引的具体步骤:选择空地、开打界面、点击按钮等,我称之为步骤状态机。
伪代码:
由于该指引系统应用到公司的实际项目,这里只给出伪代码和完整注释。
流程状态机的状态: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; } }新闻热点
疑难解答