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