在上一篇教程中,咱們瞭解了一套我自創的新手引導管理框架的使用原理,那麼在本篇教程中,咱們將考慮新手引導製做中可能遇到的一些棘手問題及探討其解決方案。Are you ready my baby? Let`s go!html
新手引導組件註冊時間不對致使引導指示器指示位置出錯web
我在作一個遊戲的新手引導的時候有時候會出現這樣的一個問題,就是新手引導中指示玩家點擊的位置是一個錯誤的位置,以下圖所示:數據庫
可能做者的本意是讓箭頭指示到右上角那個叉叉表明的關閉按鈕處,結果卻由於種種緣由讓箭頭指偏了位置,這是一個能夠嚴重也能夠不嚴重的問題。若是你使用的是強制性引導,就像我在上一篇教程中使用的那種使用全屏遮罩限制用戶交互範圍的方式的話,你一旦發生了位置偏移的問題,那麼用戶永遠也沒法點擊到你期待他點擊的東西了,這樣就會形成引導進行不下去的嚴重後果。後端
在個人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中的按鈕時,全屏遮罩中開放的交互區域位置發生了偏移
這是爲何呢?爲何呢?哪位同窗能夠告訴我緣由?哪位同窗知道請舉手,哦,奧特曼就別舉手了,我怕死!
好吧,沒人回答我,那仍是我來爲各位同窗講一下謎底吧。首先,咱們在上一章中介紹過GuideManager的工做流程:當執行nextStep方法跳轉到下一步時,若下一步涉及組件未註冊,則會暫停,直到下一步組件註冊時纔會從新開始播放引導。對於剛纔案例中咱們的窗口組件Window1來講,它的註冊工做是在外部調用其構造函數時纔去作的,即直到窗口打開時它纔會被註冊,那麼在剛纔的案例中咱們的操做流程就能夠用下圖來表示:
咱們看到,若是按照這個流程走,那麼在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
固然,上面只是說了一種限制用戶交互的方式,可能還有更多的狀況我沒有考慮到,這還須要列位道友在實際開發過程當中本身開動腦筋,想出一種耦合性不高又可靠的方案。
本期教程就到這裏吧,但願你們喜歡,我提出的這種新手引導方案不必定是最好的,但也但願列位能仔細讀一讀,取其精華去其糟粕,若有任何意見也能夠留言給我哦。出這篇教程的初衷在於讓更多的人不用再爲作新手引導而頭疼,像我之前一個同事,新手引導步驟發生了一些改變,結果他一改就改了好幾天,這樣的結果是咱們誰都不肯意看到和親身體會的。最後,祝你們六一兒童節快樂啦,哈哈!