【AS3 Coder】任务九:游戏新手引导的制做原理(下)

上一篇教程中,咱们了解了一套我自创的新手引导管理框架的使用原理,那么在本篇教程中,咱们将考虑新手引导制做中可能遇到的一些棘手问题及探讨其解决方案。Are you ready my baby? Let`s go!html

 

新手引导组件注册时间不对致使引导指示器指示位置出错web

我在作一个游戏的新手引导的时候有时候会出现这样的一个问题,就是新手引导中指示玩家点击的位置是一个错误的位置,以下图所示:数据库

image

可能做者的本意是让箭头指示到右上角那个叉叉表明的关闭按钮处,结果却由于种种缘由让箭头指偏了位置,这是一个能够严重也能够不严重的问题。若是你使用的是强制性引导,就像我在上一篇教程中使用的那种使用全屏遮罩限制用户交互范围的方式的话,你一旦发生了位置偏移的问题,那么用户永远也没法点击到你期待他点击的东西了,这样就会形成引导进行不下去的严重后果。后端

在个人GuideManager中自带的showScreenMask方法能够产生全屏遮罩,它接受的showRect参数表明全屏遮罩中惟一显示出来的能接受交互的矩形区域缓存

 

/**
 * 显示全屏遮罩以限制交互范围
 * @param showRect 惟一显示出来的能接受交互的矩形区域
 * @param maskAlpha 遮罩透明度
 * @param maskColor 遮罩颜色
 * @param parent   遮罩添加到的父容器。若留空,则父容器就等于GuideManager.indicatorContainer
 */  
public static function showScreenMask( showRect:Rectangle=null, maskAlpha:Number=0.5, maskColor:uint=0, 
parent:DisplayObjectContainer=null, maskName:String="hotgirl" ):void
{ ... }框架

 

showRect指示的区域是相对于parent参数指示的容器的,通常来讲,只要我在这一点上没有弄错,显示出来的区域应该也不会出错。好比我将让全屏遮罩直接显示在stage对象上面,那么我就能够这么写:ide

var maskArea:Rectangle = _guideTarget.getBounds(stage);//getBounds方法的参数——参考系直接选stage对象函数

GuideManager.showScreenMask(maskArea, 0.5, 0, stage);//showScreenMask方法的parent参数也选择stage对象,与上面取矩形区域的参考系一致post

可是有时候每每会事与愿违,我如今想建立一个三步的引导:点击右下角按钮弹出窗口 ——>点击窗口中按钮——>关闭窗口,那么guide.xml写成这样必定是没有问题的:测试

<step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
<step sequence="1" instanceName="Window1" subSeq="1"/>

<step sequence="2" instanceName="Window1" subSeq="2"/>

 

下面是文档类主要代码:

 

private function initUI():void
{

 .....
 _buttonBar.onBtnClick = onButtonBarBtnClick;
.....
}

 

private function onButtonBarBtnClick(index:int):void
{
 var win:DisplayObject;
 switch(index)
 {
  case 0:
   win = PopUpManager.createPopUp(Window1);
   break;
 }
 
 PopUpManager.centerPopUp( Window1 );

}

 

这段代码给右下角按钮条添加了按钮点击侦听器,在侦听函数中咱们判断,若索引位置为0的按钮被点击了,就弹出一个Window1的窗口,弹出窗口以后将其居中。

 

下面给出Window1的代码:

 

public class Window1 extends Window implements IGuideComponent
{
 private var _btn:CustomButton;
 public function Window1()
 {
  super(200, 200, 0x000000, 1, "面板一号", false);
  showCloseButton = true;
  _btn = new CustomButton("按我以完成引导!");
  addChild( _btn );
  _btn.x = (this.width - _btn.width) / 2;
  _btn.y = (this.height - _btn.height) / 2;
  onClose = function():void{ PopUpManager.removePopUp(Window1); };
  
  GuideManager.register(this);
 }
 
 //-------------------------------interface implement----------------------------------//
 
 private var _instanceName:String = "Window1";
 private var _guideTarget:CustomButton;
 
 public function guideProcess(data:Object=null):void
 {
  if( data.subSeq == 1 )
  {
   _guideTarget = _btn;
  }
  else if( data.subSeq == 2 )
  {
   _guideTarget = closeButton;
  }
  
  _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
  var maskArea:Rectangle = _guideTarget.getBounds(stage);
  GuideManager.showScreenMask(maskArea);
 }
 
 public function guideClear():void
 {
  //没什么好作的这里
 }
 
 private function onNextStep( e:MouseEvent ):void
 {
  e.currentTarget.removeEventListener(MouseEvent.CLICK, onNextStep);
  GuideManager.nextStep();
 }
 
 public function get instanceName():String
 {
  return _instanceName;
 }
 
 public function set instanceName(value:String):void
 {
  _instanceName = value;
 }

}

 

这个的写法事实上是仿造的ButtonBar的写法:在构造函数里就执行了引导注册的工做。而后运行代码后发如今执行到引导第二步:引导用户去点击Window1中的按钮时,全屏遮罩中开放的交互区域位置发生了偏移

image

这是为何呢?为何呢?哪位同窗能够告诉我缘由?哪位同窗知道请举手,哦,奥特曼就别举手了,我怕死!

好吧,没人回答我,那仍是我来为各位同窗讲一下谜底吧。首先,咱们在上一章中介绍过GuideManager的工做流程:当执行nextStep方法跳转到下一步时,若下一步涉及组件未注册,则会暂停,直到下一步组件注册时才会从新开始播放引导。对于刚才案例中咱们的窗口组件Window1来讲,它的注册工做是在外部调用其构造函数时才去作的,即直到窗口打开时它才会被注册,那么在刚才的案例中咱们的操做流程就能够用下图来表示:

image

咱们看到,若是按照这个流程走,那么在Window1还未被弹出前全屏遮罩就会被添加到舞台上,此时,Window1因为还未被添加到舞台上,因此其stage属性为null,那么在Window1.guideProcess()方法中的_guideTarget.getBounds(stage)这条语句的执行结果确定会出现问题,这就直接致使了显示出的全屏遮罩中给出的可交互区域位置发生问题。因此,总结一下,可交互区域位置错误的主要缘由是由于Window1对象被注册的时间过早

既然找到了缘由,那么接下来就想办法拖延Window1注册到GuideManager中的时间就能够了,好比,咱们能够在Window1实例被弹出并居中后再执行注册操做:

 

private function onButtonBarBtnClick(index:int):void
{
 var win:DisplayObject;
 switch(index)
 {
  case 0:
   win = PopUpManager.createPopUp(Window1);
   break;
 }
 
 PopUpManager.centerPopUp( Window1 );
 if( win is IGuideComponent && GuideManager.isSetUp )
 {
  GuideManager.register(win as IGuideComponent);
 }

}

 

这样一来,全屏遮罩显示出的可交互区域位置就正确了,固然,你不用担忧同一个实例会被屡次重复注册,在GuideManager的register方法中会自动忽略已注册过的组件。

以上案例的在线演示地址:

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test2/GuideTest.html

源码下载:

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test2.zip

 

新手引导步骤的记录

在新手引导过程当中万一玩家没有完成引导就退出了游戏或者关闭了页面或者掉线了怎么办?为了让用户下次登录时可以“再续前缘”,咱们须要在每完成一步时都记录用户当前进行到的引导步骤。

通常来讲,当前进行到的引导步骤都会记录在后端的数据库里面,可是在本例中因为没有后端可让我通信,因此我暂时把数据保存在本地Flash缓存SharedObject中。下面给出的SOManager就是负责存取缓存记录的:

 

/** 
 *   本地存储管理器
 *   Created by S_eVent
 *   at 2013-5-30 
 */
public class SOManager
{
 private static var so:SharedObject = SharedObject.getLocal("GuideTest");
 
 /** 保存当前引导步骤 */
 public static function set step(value:int):void
 {
  so.data.step = value;
  so.flush();
 }
 
 /** 获取本地存储的引导步骤 */
 public static function get step():int
 {
  return so.data.step;
 }
 
 /** 清除本地存储记录 */
 public static function clear():void
 {
  so.clear();
 }

}

 

接下来,咱们须要在游戏启动时取出上一次玩家下线时保存的引导步骤,根据它的值来设置新手引导是否须要播放或者从哪一步开始播放。

 

private function onAdded( e:Event ):void
{
 stage.scaleMode = StageScaleMode.NO_SCALE;
 stage.align = StageAlign.TOP_LEFT;
 
 stage.addEventListener(Event.RESIZE, onResize);
 onResize(null);
 Message.stage = stage;

 //新手引导最后一步的sequence是2,若是以前已完成步骤大于这个值,则表示玩家已经
 //完成新手引导,不然表示玩家还未完成引导,须要加载引导数据并启动引导
 if( SOManager.step <= 2 )
  loadGuideXML()

}

 

.....

 

private function onGuideXMLLoadComp(e:Event):void
{
......
 
 GuideManager.setUp( _guideData );
 GuideManager.stage = stage;
 GuideManager.onStepFinish = onStepFinish;
 GuideManager.onGuideFinish = onGuideFinish;
 //从上次离线时记录的步骤开始
 GuideManager.start( getStepIndexBySequence(SOManager.step) );
}

//根据步骤号获取索引号
private function getStepIndexBySequence( s:int ):int
{
 var len:int = _guideData.length;
 for(var i:int; i<len; i++)
 {
  if( _guideData[i].sequence == s )
  {
   return i;
  }
 }
 
 return 0;
}

//根据索引号获取步骤号
private function getStepSequenceByIndex( index:int ):int
{

   var len:int = _guideData.length;
   if( index >= len )return len;


 if( _guideData[index] )
 {
  return _guideData[index].sequence;
 }
 
 return 0;
}

private function onStepFinish(data:Object):void
{
 Message.show("您已完成第" + data.sequence + "步");
 //当前步骤完成后须要将下一步步骤号存进本地缓存
 SOManager.step = getStepSequenceByIndex( GuideManager.currentStep+1 );

}

 

在理解上述代码时,我须要再提一下一个步骤的索引号和步骤号之间的区别。索引号指的新手引导各步骤的执行顺序,它是从0开始的连贯数值;而步骤号则等于guide.xml中配置的各步骤标签中的sequence属性,它是不连贯的数值,咱们仅依靠它来给所有步骤进行排序以获取各步骤的索引号。要保存在本地/后端数据库中的数据是步骤号,而能被GuideManager识别并使用的是索引号。因此,咱们在存取数据时还须要时刻记得进行它们二者之间的转换工做。

运行一下上述代码,看起来一切工做正常,那是否咱们就能够安枕无忧了呢?固然不是,考虑下面一种状况,我将进行的新手引导步骤以下:

点击按钮弹出窗口——>点击窗口中的功能按钮——>点击窗口的关闭按钮关闭窗口

若是我在进行到第二步的时候离线了,那么我本地/数据库中记录的步骤号为2,也就是说,下一次我登录时会从步骤2开始。可是,从步骤2开始有一个坏处就是步骤2是由一个我未打开的窗口负责展现的,此时我上线后发现界面上没有任何箭头或者神马东西指示我去开启这个窗口,那此时做为一个新手玩家的我就会很困惑了,我不知道下一步该怎么作,不知道该点哪一个按钮来打开步骤2所涉及的窗口。这一点不免会下降用户体验,为此,咱们须要找一个方式来解决该问题,咱们理想的状况是,当用户在未完成第三步以前离线,下次上线时依然从步骤1开始,由于步骤2和3是在刚上线时看不到的两个步骤,而步骤1则否则,要是我下次上线时给我从步骤1开始,我就能清楚地回想起我该点哪一个按钮以继续上次未作完的新手引导。

若是我想根据我以前的设想来作,那么就不能每一步引导作完后都去同步一下(意思就是将步骤号保存到本地/数据库),为了识别当前作完的步骤是否须要同步,咱们再guide.xml中为每一个步骤标签增长一个属性:noSynchro(完成该步时是否跳过与同步的工做,若标签中存在该属性且该属性非0,这表示在完成该步骤后不会进行同步工做)

那么此时咱们的guide.xml就能够写成这样:

 

 <step sequence="1" instanceName="ButtomButtonBar" subSeq="1" noSynchro="1"/>
<step sequence="2" instanceName="Window1" subSeq="1" noSynchro="1"/>
<step sequence="3" instanceName="Window1" subSeq="2"/>

 

 这样写的后果,就是当完成第一、2步时,不会作同步工做,即下次登录时不会从第2/3步开始。改完了guide.xml后咱们在文档类中再进行相应的修改:

 

private function onStepFinish(data:Object):void
{
 Message.show("您已完成第" + data.sequence + "步");
 //仅当不存在noSynchro属性或该属性值为0时才进行同步工做
 if( !data.noSynchro )
 {
  //当前步骤完成后须要将下一步步骤号存进本地缓存
  SOManager.step = getStepSequenceByIndex( GuideManager.currentStep+1 );
 }

}

 

只须要在进行同步工做以前加一条判断语句就能够了。此时,咱们就能够测试一下,看看结果是否正如咱们指望的那样。

在线演示地址:(在进行到步骤2或3时刷新页面,看看第二次打开加载完成后新手引导步骤是从第几步开始的)

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test3/GuideTest.html

源码下载:

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test3.zip

 

开放式引导

咱们以前的全部例子都属于强制性引导,即经过一个全屏遮罩或者别的方式来限制用户的可交互区域,强制用户点击你但愿他点击的区域。强制引导的好处有二:一,实现起来简单;二,不容易出BUG。然而它的坏处就在于限制了用户的操做自由,遮挡了大部分好看的区域,下降了用户体验。

为了加强用户体验,增长作新手引导时的自由度,有时咱们须要实现一个开放式的引导。开放式引导虽然不会像强制性引导那样仅开放很是小的一块可交互范围给用户,但也不会彻底开放用户的操做自由。开放式引导的主要难点在于,在某一时刻,哪些功能可用哪些功能不可用,那些不可用的功能又在何时会变成可用,这些事情实现起来是比较复杂的。

     首先让咱们考虑下面一种状况,有两个将要展现引导的窗口Window1和Window2,Window1先展现,Window2后展现:

点击按钮1打开Window1——>完成Window1中展现引导——>点击按钮2打开Window2——>完成Window2中展现引导

    那么我期待用户是点击按钮1先打开Window1,在作完了Window1中展现的引导后再去点击按钮2打开Window2,所以,我不但愿用户在完成第二步前去打开Window2,不然引导顺序将会乱套。可是,因为我在按钮2上添加了鼠标点击事件CLICK的事件侦听,并在事件处理函数中写了弹出Window2的相关逻辑。若是用户执意要点按钮2,那岂不必定会触发CLICK事件,弹出Window2?对此,我有一个解决方案,就是在未执行到第三步时给按钮2添加一个优先级较高的CLICK事件侦听器,一块儿来看以下代码:

//----------------------------ButtonBar.as-----------------------//

public function guideProcess(data:Object=null):void
{
 _guideTarget = _buttons[data.subSeq-1];
 var maskArea:Rectangle = _guideTarget.getBounds(stage);
 GuideManager.showRectBorder(maskArea);
 _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
 this.addEventListener(MouseEvent.CLICK, onClickWhenGuiding, false, 1);
}

private function onClickWhenGuiding( e:MouseEvent ):void
{
 if( e.target != _guideTarget )
 {
  e.stopImmediatePropagation();
  Message.show("别淘气!");
 }

}

 

addEventListener方法的第四个参数priority表明该事件侦听器的优先级,默认状况下优先级都是0,所以,若是咱们在注册事件侦听器的时候传入一个大于0的值给addEventListener方法的第四个参数,那么咱们此时注册的侦听函数就会在事件触发时优先被执行到。在事件处理函数中,咱们将判断点击目标是不是咱们指望用户点击的,若不是,就使用event.stopImmediatePropagation方法来当即中止事件的冒泡,其结果是除了当前事件处理函数外的其余事件处理函数都再也不会被调用。在上例中,onClickWhenGuiding事件处理函数在触发CLICK事件时会被优先调用,若在onClickWhenGuiding函数中调用了event.stopImmediatePropagation方法,那么一样侦听CLICK事件的onClick方法就再也不会被执行。使用这种方法就能够有效地限制用户进行那些不但愿他们作的动做了。(不要直接在onClick方法里面判断当前点击对象是不是_guideTarget,这样会增长耦合性,对于onClick方法来讲,它并不须要关心当前有没有在进行新手引导)

    使用这种方式来实现的开放式引导在线展现以下:

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/test4/GuideTest.html

源码下载:

http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test4.zip

 

    固然,上面只是说了一种限制用户交互的方式,可能还有更多的状况我没有考虑到,这还须要列位道友在实际开发过程当中本身开动脑筋,想出一种耦合性不高又可靠的方案。

    本期教程就到这里吧,但愿你们喜欢,我提出的这种新手引导方案不必定是最好的,但也但愿列位能仔细读一读,取其精华去其糟粕,若有任何意见也能够留言给我哦。出这篇教程的初衷在于让更多的人不用再为作新手引导而头疼,像我之前一个同事,新手引导步骤发生了一些改变,结果他一改就改了好几天,这样的结果是咱们谁都不肯意看到和亲身体会的。最后,祝你们六一儿童节快乐啦,哈哈!

相关文章
相关标签/搜索