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

使用框架:AS3
任务描述:了解
游戏中新手的制作原理及流程

难度:3

 

本章源码下载:http://www.iamsevent.com/zb_users/UPLOAD/GuideManager/Test1.zip

 

有人问我,都两年过去了,AS3 Coder系列怎么才出了10篇文章都不到?答案很简单:我TM懒得写!原计划出到10篇就洗手不写了,现在还有最后两篇,加把劲冲刺一下吧!

新手引导基本上在每个游戏中都会出现,或长或短,或简单或复杂,当然,新手引导流程越长越容易出现BUG,且传统的新手引导做法会极大地破坏代码的耦合性,为了解决“不稳定”及“破坏耦合性”这两个问题,贫道想了一种相对较好一点的(到底是好还是不好,列位看完本文之后就仁者见仁智者见智了)方式,在本文中介绍给大家。

 

传统的新手引导制作方式

传统的新手引导方式一般是设置一个全局的静态变量来保存当前新手引导进度,然后在项目中每个可能出现新手引导的位置添加一句判断:若当前新手引导步骤等于我所期望的步骤就执行引导部分的逻辑。例如,一个游戏中的新手引导第四步是引导用户去打开一个A窗口,然后第五步引导则是引导用户点击A窗口中的某个按钮。那么在A窗口的“打开窗口”函数中就会加上对当前新手引导步骤的一个判断,若当前步骤等于5,就执行相应的新手引导逻辑(例如,给A中需要引导用户点击的按钮加上一个箭头神马的)

public function onOpen():void

{   

 if( GuideManager.currentStep == 5 )//GuideManager.currentStep是一个静态变量,用于存储当前新手引导进行到的步骤   

 {     //执行相应的引导逻辑   }

}

这种做法的弊端在于,它破坏了代码的耦合性,因为新手引导每涉及到一个组件,就需要在该组件中添加相应的判断语句及引导逻辑。而且当新手引导的步骤一多之后就会出现不稳定的情况,最烦人的是,一旦策划要求你在新手引导中插入几个步骤,那你几乎全部的涉及到新手引导的组件都会遭殃(原先if语句里的判断条件都会发生变动),而且你涉案组件那么多,难免会漏改几个位置。

 

基于接口的编程——降低耦合度的最佳方式

为了降低新手引导对项目耦合度的破坏性,有人提出了使用“继承”的方式,即游戏中的全部可视化组件都继承于同一个父类,然后在这个父类里面写上新手引导相关的逻辑代码,这样就可以一劳永逸了。不过话说回来,不一定全部涉及到新手引导的类都是继承自你这个共同的父类,而且使用你这种继承的方式来做,就会让全部继承自该共同父类的那些子类里面多出很多冗余的代码(因为只有极少的一部分子类会涉及到新手引导),尤其是在未启动新手引导时(玩家已经完成全部新手引导之后每次登陆游戏都是不会启动新手引导的)产生极大的浪费。

使用“接口”(Interface)的编程思想来做,就可以仅给有需要出现的新手引导的类添加相应的代码,最大限度地避免浪费的产生。

所谓“接口”的作用,就是让没有继承关系的类之间产生联系。让我们先来看一个小例子吧。现在我们有两个窗口类A与B,它们之间没有继承关系,虽然如此,但它们还是有共同之处的,就是都声明了一个名为“open”的打开窗口的方法,当执行该方法时,A/B窗口就会被打开。现在我想创建一个格式如下的方法:

public function openWindow( win:* ):void

{   

    win.open();

}

使用openWindow方法,我可以快速地打开一个窗口,而A和B类的对象都有可能被当做参数传入该方法中,但是由于A、B两个类之间没有共同的父类,所以我openWindow的参数类型只能写成通配符(*)。当然,使用通配符是存在隐患的,因为传入的参数很有可能不具备一个名为open的public方法,这样的话就会发生报错。

为了将A、B联系起来,我们此时可以声明一个接口,该接口中声明了A、B类中所拥有的那些个同名函数和属性(在该例中A、B所拥有的同名函数只有一个open方法):

public interface IWindow

{

    /** 执行打开窗口逻辑 */

    function open():void;

}

声明完该接口之后,需要让A、B实现该接口:

public class A extends AP implements IWindow

{    

    public function open():void    

    {    

         //do something    

    }

}

//--------------------------------------------------------------//

public class B extends BP implements IWindow

{    

    public function open():void    

    {     

        //do something    

    }

}

现在,A、B类中的open方法都是对于接口IWindow的一个实现。之后,不论一个对象是A类型还是B类型,我们都可以使用IWindow的类型来引用它,这样的话,我们之前定义的openWindow方法就可以改成:

public function openWindow( win:IWindow ):void

{   

    win.open();

}

参数win的类型不再是全部类型(*),而是将范围缩小到了所有实现了IWindow接口的对象,由于所有实现了IWindow接口的对象中都一定会有open方法,所以我们可以放心大胆地调用win.open()而不必担心再报错了。

  “接口”不同于“继承”,继承必须按照从上至下的层级顺序,且一个类只能于一个父类,但接口却不同,一个类可以实现多个接口,如下类就同时实现了两个接口:

public class A extends AP implements IWindow, IFucker

{    

    //class body

}

正是因为接口的这个特性,使我们“仅给需要类添加代码”的假设成为了可能。下面,我们就来写一个接口,该接口约定了一些新手引导过程中将会用到的方法和属性:

 /** * 如果某个面板将在新手引导中出现,那么它必须实现该接口  * @author S_eVent *  */public interface IGuideComponent{/** 处理新手引导相关事务 * @param data执行当前步骤时需用到的数据 */function guideProcess(data:Object=null):void;/** 执行新手引导卸载相关清理工作 */function guideClear():void;/** 注册到GuideManager中的实例名 */function get instanceName():String;function set instanceName(value:String):void;}

该接口定义了两个方法(guideProcess及guideClear)和一个属性(instanceName)。在新手引导过程中会用到的类,都需要实现该接口才可以。使用基于接口的编程的好处有以下三点:

1.避免冗余代码的产生,哪个类需要实现新手引导的功能就让哪个类来实现该接口;

2.方便查找:你只要在项目中搜索该接口的引用位置,就可以一次性找全全部涉及到新手引导的类;

3.方便管理:全部与新手引导有关的逻辑代码都存放于名为guideProcess的函数中,某组件涉及到的新手引导步骤执行完毕后的清理工作都放在guideClear函数中。

 

新手引导管理器

为了便于管理和查询新手引导各步骤,我创建了一张XML表guide.xml用于记录新手引导的各个步骤,其格式如下:

<?xml version="1.0" encoding="utf-8"?>
<guide>
<!-- Author:S_eVent
说明:节点名请保证使用step,否则将不被识别。
每个节点中的必须属性如下:
 sequence:显示此步骤出现的次序
 instanceName:此步骤所关联的实例名称
每个节点中的可选属性如下:
 subSeq:子步骤。某些界面可能会涉及到多次引导步骤,在每次步骤时执行的逻辑都不一样。此时用该属性来识别当前步骤该干嘛
 -->
 
 <step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
 <step sequence="1" instanceName="Window1" subSeq="1"/>
 <step sequence="2" instanceName="Window1" subSeq="2"/>
 <step sequence="3" instanceName="ButtomButtonBar" subSeq="2"/>
 
</guide>

sequence属性用以标示新手引导的步骤号;

instanceName则表示负责展示该步引导的实例名;

subSeq用于区分同一个组件展示出来的两个不同步骤。比如在上面的XML里面,“Window1”这个实例将负责展示步骤1和步骤2的新手引导,展示步骤1时,“Window1”这个窗口将引导用户点击窗口中的某个功能按钮(比如在该功能按钮上加一个箭头),而展示步骤2时,“Window1”就将引导用户点击窗口右上角的关闭按钮来关闭窗口。subSeq属性将会被传入"Window1"类的guideProcess方法中用于区分当前引导步骤应该执行哪个引导动作(是该引导用户点击窗口中某个功能按钮还是关闭按钮)。

 

在某个用户登录游戏时,服务器端会判断该用户是否需要进行新手引导,若该用户需要进行新手引导,那么咱们Flash前端就需要加载guide.xml以获取新手引导的步骤数据。实现代码如下:

private function onGameStart( e:Event ):void

{

    if( _needGuide )

        loadGuideXML();

}

private function onResize( e:Event ):void

{

    _globalVariables.stageWidth = stage.stageWidth;

    _globalVariables.stageHeight = stage.stageHeight;

}

private function loadGuideXML():void

{

    var loader:URLLoader = new URLLoader();

    loader.addEventListener(Event.COMPLETE, onGuideXMLLoadComp);

    loader.load( new URLRequest("guide.xml") );

}

private function onGuideXMLLoadComp(e:Event):void

{

    var data:XML = XML( (e.currentTarget as URLLoader).data );

    var guideData:Array = [];

    for each(var x:XML in data..step)

    {

        guideData.push( xml2Object(x) );

    }

    guideData.sortOn("sequence", Array.NUMERIC);

    function xml2Object( xml:XML ):Object

    {

        var obj:Object = {};

        var attributes:XMLList = xml.attributes();

        for each(var a:XML in attributes)

        {

            obj[a.name().toString()] = a.toString();

        }

        return obj;

    }

    GuideManager.setUp( guideData );

    GuideManager.start();

}

 加载完guide.xml之后我们需要将XML中的每一个标签都转换成相应的Object对象便于使用,最后把全部步骤对象放进一个数组中传递给我们接下来要介绍的新手引导管理器GuideManager类的setUp方法进行新手引导的启动工作,稍后,在需要开始新手引导时调用GuideManager.start方法开始新手引导的播放。

GuideManager类负责调度新手引导的暂停与播放,它提供了一系列static的静态方法,在项目中任意位置都可以调用到这些方法。

/** 
 * 新手引导管理器。请确保只有需要进入新手引导时才调用其setUp方法。
 * @author S_eVent
 */ 

public class GuideManager 

 /** 指示符容器。高亮边框、引导指针等指示符都会被添加于此容器之上。若不设置值,则无法显示指示符 */
 public static var stage:Stage;
 /** 新手引导完成一个步骤之后执行函数。此函数需接受一个Object型参数,代表当前完成步骤的配置数据 */
 public static var onStepFinish:Function;
 /** 新手引导播放完成后执行函数 */
 public static var onGuideFinish:Function;
 
 /** 注册成员地图。格式为{className1:IGuideComponent, className2:IGuideComponent, ......} */
 private static var _memberMap:Object = {};
 
 /** 新手引导播放队列,其中元素为每一步的实例 */
 private static var _guideQueue:Vector.<IGuideComponent>;
 
 private static var _isSetUp:Boolean = false;

 /** 当前执行的步骤索引 */
 private static var _currentStep:int=-1;
 
 /** 下一个将执行的步骤索引 */
 private static var _nextStep:int=0;
 
 /** 记录新手引导具体步骤的数组。其中元素为每一步的实例名 */
 private static var _sequenceArray:Array;
 
 /** 记录新手引导每步所包含数据的数组 */
 private static var _dataArray:Array;
 
 /** 完成步骤列表。键为步骤序号,值为true/false,表示是否完成 */
 private static var _finishList:Object;
 
 private static var _paused:Boolean;
 
 /** 存储一切当前使用的遮罩对象 */
 private static var _maskHome:Object = {};
 private static var _border:Shape;
 
 /** 启动新手引导 */
 public static function setUp( config:Array ):void
 {
  if( _isSetUp == false )
  {
   _isSetUp = true;
   _sequenceArray = [];
   _dataArray = [];
   _finishList = {};
   var len:int = config.length;
   
   for(var i:int=0; i<len; i++)
   {
_sequenceArray[i] = config[i].instanceName.toString();
_dataArray[i] = config[i];
   }
   
   _guideQueue = new Vector.<IGuideComponent>();
   for(i=0; i<len; i++)
   {
_guideQueue[i] = _memberMap[_sequenceArray[i]];
   }
  }
 }
 
 /** 卸载新手引导 */
 public static function uninstall():void
 {
  if( _isSetUp )
  {
   _isSetUp = false;
   if( _currentStep >= 0 )
doClear(_guideQueue[_currentStep]);
   _guideQueue = null;
   _currentStep = -1;
  }
 }
 
 /**
  * 注册一个 IGuideComponent 到GuideManager中,这样它就会出现在新手引导过程中。
  * GuideManager会根据注册对象的instanceName来注册对象类名。若注册时发现instanceName
  * 已被注册,则不执行接下来的注册过程
  * @param instance  欲注册对象
  * 
  */  
 public static function register(instance:IGuideComponent):void
 {
  if( instance )
  {
   var name:String = instance.instanceName;
if( _memberMap[name] )
{
 return;
}
_memberMap[name] = instance;

//注册的时候若是发现新手引导已经启动,则搜索当前注册对象是否是新手引导的其中
//一个步骤,若是,则加入到引导队列中
if( _isSetUp )
{
 var index:int = _sequenceArray.indexOf(name);
 while( index != -1 )
 {
  _guideQueue[index] = instance;
  //有时候,两个相邻步骤间会存在时间差。如步骤1执行完毕后调用nextStep发现步骤2
  //尚未注册,此时会导致GuideManager暂停运作,那么就等待步骤2在注册时重新启动
  //GuideManager的播放
  if( _nextStep == index )
  {
   nextStep(index);
  }
  
   index = _sequenceArray.indexOf(name, index+1);
 }
}
  }
 }
 
 /** 开始新手引导
  * @param from 从第几部开始 */
 public static function start(from:uint=0):void
 {
  nextStep(from);
 }
 
 /**
  * 进行下一步引导 
  * @param designedStep 跳到指定的步骤。若该值为-1,则走到当前步骤的下一步。若将跳转到的步骤不存在,则结束新手引导
  * 
  */  
 public static function nextStep(designedStep:int=-1):void
 {
  //若在暂停时调用nextStep,则自动执行resume方法继续播放新手引导
  if( _paused )
  {
   resume();
   return;
  }
  
  if( designedStep < 0 )
  {
   _nextStep = _currentStep+1;
  }
  else
  {
   _nextStep = designedStep;
  }
  
  //若该方法是由start方法调用情况下(此时_currentStep==-1)不需要让上一部引导完成:
  if( _nextStep > 0 && _currentStep >= 0 )
  {
   markFinish(_currentStep);
  }
  
  if(  _nextStep < _guideQueue.length && _guideQueue[_nextStep] )
  {
   var data:Object = _dataArray[_nextStep];
   _guideQueue[_nextStep].guideProcess(data);

   _currentStep = _nextStep;
  }
  //若无法执行欲跳转到的步骤,则不改变_currentStep的值
  else
  {
   //播放结束
   if( _nextStep == _sequenceArray.length )
   {

if( onGuideFinish != null )
 onGuideFinish();
uninstall();
   }
  }
 }
 
 /** 暂停引导播放 */
 public static function pause():void
 {
  if( !_paused )
  {
   _paused = true;
  }
 }
 
 /** 继续引导播放 */
 public static function resume():void
 {
  if( _paused )
  {
   _paused = false;
   
   _guideQueue[_nextStep].guideProcess(_dataArray[_nextStep]);
   _currentStep = _nextStep;
  }
 }
 
 /**
  * 显示全屏遮罩以限制交互范围
  * @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
 {
  if( !parent )
  {
   parent = stage;
   if( !parent )
return;
  }
  var mask:Sprite = _maskHome[maskName];
  if( !mask )
  {
   //遮挡物必须是能够响应鼠标事件的类,如Sprite。否则鼠标点击之将会穿透它以触发其挡住的对象的鼠标事件
   mask = new Sprite();
   _maskHome[maskName] = mask;
  }
  var w:Number = parent == stage ? stage.stageWidth : parent.width;
  var h:Number = parent == stage ? stage.stageHeight : parent.height;
  var g:Graphics = mask.graphics;
  g.clear();
  g.beginFill(maskColor, maskAlpha);
  g.drawRect(0, 0, w, h);
  if( showRect )
  {
   //利用Graphics重叠绘制会消去重叠区域像素的原理进行挖洞动作
   g.drawRect(showRect.x, showRect.y, showRect.width, showRect.height);
  }
  g.endFill();
  if( !parent.contains(mask) )
   parent.addChild(mask);
  
 }
 
 /**
  *隐藏全屏遮罩 
  * 
  */  
 public static function hideScreenMask(maskName:String="hotgirl"):void
 {
  var mask:Sprite = _maskHome[maskName];
  if( mask && mask.parent )
  {
   mask.parent.removeChild(mask);
  }
 }
 
 
 /**
  * 显示一个高亮矩形边框,该边框会被添加到当前正在播放新手引导的组件上
  * @param bounds 矩形边框显示位置。该矩形的参考系是当前正在播放新手引导的组件的父容器
  * @param parent  边框添加到的父容器。若留空,则父容器就等于GuideManager.indicatorContainer
  */  
 public static function showRectBorder( bounds:Rectangle, parent:DisplayObjectContainer=null ):void
 {
  if( !parent )
  {
   parent = stage;
   if( !parent )
return;
  }
  
  if( !_border )
  {
   _border = new Shape();
   _border.filters = [new GlowFilter(0xff911b, .8, 8, 8, 4, 2)];
  }
  if( !parent.contains(_border) )
  {
   parent.addChild(_border);
  }
  _border.graphics.clear();
  _border.graphics.lineStyle(1, 0xFFFF00);
  _border.graphics.drawRect(0, 0, bounds.width, bounds.height);
  _border.x = bounds.x;
  _border.y = bounds.y;
 }
 
 /**
  * 隐藏边框 
  * 
  */  
 public static function hideBorder():void
 {
  if( _border && _border.parent )
  {
   _border.parent.removeChild(_border);
  }
 }
 
//------------------------------------------private functions--------------------------------------------------//
 
 private static function doClear(step:IGuideComponent):void
 {
  if( step )
   step.guideClear();
  hideBorder();
  hideScreenMask();
 }

 private static function markFinish(sequence:int):void
 {
  if( !_finishList[sequence] )
  {
   doClear(_guideQueue[sequence]);
   if( onStepFinish != null )
onStepFinish(_dataArray[sequence]);
   _finishList[sequence] = true;
  }
 }
 
 /** 是否已启动新手引导 */
 public static function get isSetUp():Boolean
 {
  return _isSetUp;
 }

 /** 新手引导是否正被暂停 */
 public static function get paused():Boolean
 {
  return _paused;
 }

 /** 当前执行到的步骤 */
 public static function get currentStep():int
 {
  return _currentStep;
 }
 
 }

}

 

GuideManager是本章代码最多也是最复杂的一个类,如果你现在看不懂,没关系,你只需要学会如何使用就可以了,毕竟这个类也不是我一朝一夕就写出来的,也是经过了反复的修改才造就的。下图演示了GuideManager的大致工作原理:

image

主要需要解释的地方有如下几个:

1.加载guide.xml后得到的新手引导数据先会被存放进一个数组中,之后该数组被作为实参传给GuideManager.setUp()方法供GuideManager使用。细心的朋友会注意到,GuideManager在调度每一步的新手引导执行顺序的时候是根据每一步在数组中的索引,而并不是按照每一步的sequence属性。换句话说,如果我guide.xml里面的XML标签中sequence属性的最小值不是1,而是100,那么该标签代表的步骤仍然是首先被播放的:

<step sequence="100" instanceName="ButtomButtonBar" subSeq="1"/><!--第一步-->
 <step sequence="200" instanceName="Window1" subSeq="1"/><!--第二步-->
 <step sequence="300" instanceName="Window1" subSeq="2"/><!--第三步-->

 <step sequence="400" instanceName="ButtomButtonBar" subSeq="2"/><!--第四步-->

因此,根据我的这种方法,在guide.xml里面配置的新手引导步骤的sequence属性不必遵循从0开始的连贯数值,这样就便于插入新的步骤数据。比如我在设计新手引导步骤时,考虑到两个步骤间有可能在今后会插入一些新的步骤,那么我就可以让这两个步骤的sequence值差距大一些:

<step sequence="1" instanceName="ButtomButtonBar"/>
 <step sequence="20 instanceName="Window1"/>

2.由于涉及到新手引导的组件不可能在程序刚启动的时候都已经准备好展示新手引导(如实例化完成时、被添加到舞台上时、摆好位置时等等),所以我需要提供一个regist方法来让外部调用,在涉案组件准备好时才会被注册到引导管理器中。当一个组件被注册到引导管理器时,引导管理器会检查当前是否正在播放新手引导,若正在播放,则会检查当前播放到的引导步骤是否是由当前被注册组件负责展示的,若是,则马上开始展示当前引导

3.nextStep方法被调用时会让新手引导进入到下一步。若下一步的instanceName对应组件还未注册,则暂停引导,直到它被注册了再继续播放。若当前步是最后一步,则结束引导,执行卸载工作

 

小试牛刀

有了guide.xml,有了IGuideComponent和GuideManager之后我们的新手引导基本框架已经搭建完毕,接下来就是需要在咱们的项目中实际运用上这套框架来试试看效果如何了。

在下面这个例子里,我希望能够引导用户逐个点击我游戏右下角摆放着的按钮条(ButtomButtonBar)中的四个按钮。于是我可以这样设计guide.xml表的内容:

<step sequence="0" instanceName="ButtomButtonBar" subSeq="1"/>
 <step sequence="1" instanceName="ButtomButtonBar" subSeq="2"/>
 <step sequence="2" instanceName="ButtomButtonBar" subSeq="3"/>
 <step sequence="3" instanceName="ButtomButtonBar" subSeq="4"/>

下面是文档类和ButtonBar的代码:

/** 
 *   新手引导测试
 *   Created by S_eVent
 *   at 2013-5-27 
 */
[SWF(backgroundColor="0xFFFFFF")]
public class GuideTest extends Sprite
{
 private var _uiContainer:Sprite = new Sprite();

 private var _buttonBar:ButtonBar = new ButtonBar();
 
 private var _globalVariables:GlobalVariables = GlobalVariables.instance;
 
 private var _needGuide:Boolean = true;
 
 public function GuideTest()
 {
  initUI();
  
  if( stage )
   onAdded(null);
  else
   addEventListener(Event.ADDED_TO_STAGE, onAdded);
 }
 
 private function initUI():void
 {
  addChild(_uiContainer);
  
  var dp:Array = [];
  for(var i:int; i<6; i++)
  {
   dp[i] = {label:"按钮" + (i+1)};
  }
  _buttonBar.dataProvider = dp;
  _uiContainer.addChild(_buttonBar);
  
 }
 
 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;
  
  if( _needGuide )
   loadGuideXML();
 }
 
 private function onResize( e:Event ):void
 {
  _globalVariables.stageWidth = stage.stageWidth;
  _globalVariables.stageHeight = stage.stageHeight;
 }
 
 private function loadGuideXML():void
 {
  var loader:URLLoader = new URLLoader();
  loader.addEventListener(Event.COMPLETE, onGuideXMLLoadComp);
  loader.load( new URLRequest("guide.xml") );
 }
 
 private function onGuideXMLLoadComp(e:Event):void
 {
  var data:XML = XML( (e.currentTarget as URLLoader).data );
  var guideData:Array = [];
  
  for each(var x:XML in data..step)
  {
   guideData.push( xml2Object(x) );
  }
  
  guideData.sortOn("sequence", Array.NUMERIC);
  
  function xml2Object( xml:XML ):Object
  {
   var obj:Object = {};
   var attributes:XMLList = xml.attributes();
   for each(var a:XML in attributes)
   {
obj[a.name().toString()] = a.toString();
   }
   return obj;
  }
  
  GuideManager.setUp( guideData );
  GuideManager.stage = stage;
  GuideManager.onStepFinish = onStepFinish;
  GuideManager.onGuideFinish = onGuideFinish;
  GuideManager.start();
 }
 
 private function onStepFinish(data:Object):void
 {
  Message.show("您已完成第" + data.sequence + "步");
 }
 
 private function onGuideFinish():void
 {
  Message.show("恭喜您,您已完全部新手引导步骤!");
 }

}

 

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

 

/** 
 *   按钮条
 *   Created by S_eVent
 *   at 2013-5-27 
 */
public class ButtonBar extends Sprite implements IGuideComponent
{
 /** 当ButtonBar中的某个按钮被按下时调用。该函数接收一个代表按下按钮索引号的int型参数 */
 public var onBtnClick:Function;
 
 private var _dataProvider:Array;
 private var _buttons:Vector.<CustomButton> = new Vector.<CustomButton>();
 private var _gap:Number = 4;
 private var _globalVariables:GlobalVariables = GlobalVariables.instance;
 
 public function ButtonBar()
 {
  super();
  this.mouseEnabled = false;
  
  GuideManager.register(this);
  
  this.addEventListener(Event.ADDED_TO_STAGE, onAdded);
 }
 
 public function clear():void
 {
  var btn:CustomButton;
  while( _buttons.length > 0 )
  {
   btn = _buttons.pop();
   if( this.contains( btn) )
this.removeChild( btn );
  }
 }
 
//--------------------------------private functions-----------------------------------//
 
 private function onAdded( e:Event ):void
 {
  this.addEventListener(Event.REMOVED_FROM_STAGE, onRemoved);
  this.addEventListener(MouseEvent.CLICK, onClick);
  //侦听舞台尺寸发生变化事件
  _globalVariables.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPC);
 }
 
 private function onRemoved( e:Event ):void
 {
  this.removeEventListener(Event.REMOVED_FROM_STAGE, onRemoved);
  this.removeEventListener(MouseEvent.CLICK, onClick);
  _globalVariables.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onPC);
 }
 
 private function onClick( e:MouseEvent ):void
 {
  var btn:CustomButton = e.target as CustomButton;
  if( btn && onBtnClick != null )
  {
   onBtnClick( _buttons.indexOf(btn) );
  }
 }
 
 private function onPC( e:PropertyChangeEvent ):void
 {
  if( e.property == "stageWidth" || e.property == "stageHeight" )
  {
   this.x = _globalVariables.stageWidth - this.width;
   this.y = _globalVariables.stageHeight - this.height;
   if( _guideTarget )
   {
var maskArea:Rectangle = _guideTarget.getBounds(stage);
GuideManager.showScreenMask(maskArea);
   }
  }
 }
 
 private function layout():void
 {
  var crtW:Number = 0;
  for each(var btn:CustomButton in _buttons)
  {
   btn.x = crtW;
   crtW += btn.width + _gap;
  }
 }
 
//-------------------------------get / set functions----------------------------------//
 
 public function get dataProvider():Array
 {
  return _dataProvider;
 }

 public function set dataProvider(value:Array):void
 {
  _dataProvider = value;
  
  clear();
  
  var len:int = _dataProvider.length, btn:CustomButton;
  for(var i:int; i<len; i++)
  {
   btn = new CustomButton( _dataProvider[i].label );
   addChild( btn );
   _buttons[i] = btn;
  }
  
  layout();
 }

 public function get gap():Number
 {
  return _gap;
 }

 public function set gap(value:Number):void
 {
  _gap = value;
  layout();
 }

//-------------------------------interface implement----------------------------------//
 
 private var _instanceName:String = "ButtomButtonBar";
 private var _guideTarget:CustomButton;
 
 public function guideProcess(data:Object=null):void
 {
  _guideTarget = _buttons[data.subSeq-1];
  var maskArea:Rectangle = _guideTarget.getBounds(stage);
  GuideManager.showScreenMask(maskArea);
  _guideTarget.addEventListener(MouseEvent.CLICK, onNextStep);
 }
 
 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;
 }

}

 

对于文档类来说,它首先要做的,自然就是在需要启动新手引导的时候去加载guide.xml,之后将加载得到的XML数据转换成GuideManager能识别的Object数组并传递给GuideManager使用,之后马上开始新手引导的播放。

对于按钮条ButtonBar来说,要让它成为一个能够展示新手引导的组件,必须实现之前我们所说的IGuideComponent接口,然后在文件末尾处写上实现IGuideComponent接口的两个方法及一个属性。由于在我的项目中,ButtonBar只可能有一个实例,所以它的instanceName我就直接在它内部写死了。如果在项目中存在多个ButtonBar实例,那么我们需要在外部动态地为每个ButtonBar实例的instanceName属性赋值才行。在guideProcess方法中我需要写出轮到ButtonBar展示引导时会发生什么事情,在本例中,它要做的就是根据引导数据的子步骤sebSeq的不同而引导用户点击不同的按钮。为了方便,我这里就不加什么箭头来指示用户了,直接用GuideManager里面自带的全屏遮罩(实现原理可参考《使用绘图API绘制镂空矩形》)来限制用户的点击范围为我需要让用户点击的区域。

那么上述代码最终的实现效果如下:

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

 

经过这个例子,差不多我们熟悉了一点这套新手引导框架的使用方式了,那么在下一篇教程中,我们将考虑新手引导的更多方面:如与服务器端进行同步的问题、新手引导组件注册时机不对导致引导箭头指向位置不正确的问题以及如何使用开放式引导的问题等等。如果遇到这些问题,你知道该如何解决吗?i know what to do....

原文地址:https://www.cnblogs.com/keng333/p/3195497.html