看了这个标题,围观群众对今天讲解的话题表示不知,好吧,如果列位爱卿见过网站上那些倒计时的应用以及老-虎 机(也叫拉霸,苹果机等,whatever...)(也叫拉霸,苹果机等,whatever...)游戏,那你就能理解我为神马取这个标题了。
若是觉得此类应用太简单太轻松,那么请您向后……转!从外面把门锁上~若是觉得没有做过此类应用或者想了解一下用纯代码怎样实现,那么请继续往下看吧。
片头废话时间
许久没有发帖了,原因有二,一是现在新项目开发在即,需要整天埋头看书学习新项目要用到的知识,如果不加紧学习的话若是因为我个人原因拖慢了项目的整体开发进程那我肯定过意不去的。二是一直没有找到好的话题来和大家一起探讨一起制作,我希望我的每一篇帖子都能带个大家一些新的思想,新的算法,切忌知识点重复。不过最近我敬爱的导师:老木君做了一个倒计时的应用,看起来挺好玩的,而我之前又没有做过,就欲研究一二,自己也做个玩玩,顺便把研究成果带给列位爱卿一起分享一下,so....what are you waiting for? let`s fucking go!
倒数计时应用
倒计时应用列位应该见得很多了,在线演示效果请见:http://www.iamsevent.com/upload/TimerDemo.swf
对于此应用,我们主要需要解决的问题就是数字更替时候的文字切换效果。实现思路是,我们需要两个文本框,第一个文本框是用户所看见的时间文字,第二个隐藏在第一个的下面并处于可视范围之外,它负责显示下一个时间点的时间,当时间需要更新时让两个文本框一起向上移动,当第一个文本框完全移出可视范围后把它位置调整至第二个文本框下面并更新其显示时间为接下来一个时间点的时间。下面几张图表示了这个过程:
<ignore_js_op> <ignore_js_op>
听起来很简单吧?但是在实际编码过程中还是问题多多哦。第一个要解决的问题是如何控制可视范围,我们每向一个Sprite中添加一个子显示对象后Sprite都会扩展自己的长和宽并显示出此显示对象,那我要是想让用户一次只看到一个显示对象咋办?ALL RIGHT,是时候让我们的mask(遮障)爱将出手的时候了,我们在之前的教程中说到过它,它的能力就是可以让用户只看到遮障的实心区域覆盖的部分,来吧来吧来吧,让我们动手写代码吧,时间的格式是时、分、秒各需要两位数字显示,所以一共需要6位数字,如果你看过演示效果的话应该明白。所以我们为了让每一位数字都能够单独地滚动,就需要编写一个类来负责做显示一位数字以及播放数字更新动画的工作,代码如下:
MyTimer.as:
/** * 单个计时器视图 * @author S_eVent * */ public class MyTimer extends Sprite { /** * 当数字减少到0以下时抛出事件 */ public static const TIME_CHANGE_EVENT:String = "time change"; /** * 此计时视图显示的最大数字,个位数最大值一般是9,十位数最大值可用6,默认值9 */ public var maximum:int = 9; /** * 一次update改变的时间,单位:秒,默认值1 */ public var range:int = 1; /** * 每帧数字移动的像素值,默认值1 */ public var moveSpeed:Number = 1; private var _tf:TextField = new TextField(); private var _tf2:TextField = new TextField(); private var _currentTime:Number = 9;//当前显示时间 private var _mask:Shape = new Shape();//为了一次只显示一个数字而使用的遮障 private var _curVisibleTF:int = 1;//当前显示的TextField private var _fadeInTF:TextField;//逐渐进入视野的Textfield private var _fadeOutTF:TextField;//逐渐退出视野的Textfield /** * 构造函数,你可以为显示文字设置格式 * @param startTime 开始时间 * @param maximum 最大值 * @param textFormat 设置数字文字的格式 * */ public function MyTimer( startTime:int=0, maximum:int=9, textFormat:TextFormat=null) { this.maximum = maximum; if( textFormat ){ _tf.defaultTextFormat = textFormat; _tf2.defaultTextFormat = textFormat; } time = startTime; _tf.height = _tf2.height = _tf.textHeight + 5; _tf.width = _tf2.width = _tf.textWidth + 5; _tf2.y = _tf.height + 2;//设置第二个Textfield位于遮障之外,不让你看见,嘿嘿~气死你 addChild( _tf ); addChild( _tf2 ); //由于Sprite类的特殊性,它会根据子元件大小调整大小,要想让用户可视范围固定不变只能使用遮障 _mask.graphics.beginFill(0,0); _mask.graphics.drawRect(0, 0, _tf.width, _tf.height);//把可视范围控制在一个Textfield范围内 _mask.graphics.endFill(); addChild(_mask); this.mask = _mask; } /** * 更新时间 * */ public function update():void{ //如果已经到0则抛出事件 if( currentTime == 0 ){ dispatchEvent( new Event(TIME_CHANGE_EVENT) ); } //当前显示哪个Textfield就让哪个往上移动渐渐退出可视范围,让下一秒的数字来替代它显示 if( _curVisibleTF == 1 ){ _curVisibleTF = 2; _fadeOutTF = _tf; _fadeInTF = _tf2; }else{ _curVisibleTF = 1; _fadeOutTF = _tf2; _fadeInTF = _tf; } //开始移动 addEventListener(Event.ENTER_FRAME, onEF); } /** * 设置显示时间 * @param value * */ public function set time( value:int ):void{ currentTime = value; //_tf显示当前时间,_tf2显示下一次时间间隔到达时的时间 _tf.text = value.toString(); value -= range; _tf2.text = String( value < 0 ? maximum : value ); } private function onEF(e:Event):void{ _fadeInTF.y -= moveSpeed; _fadeOutTF.y -= moveSpeed; //已移动到目的地则结束移动并更新下一秒时间文字于隐藏着的Textfield上 if( _fadeInTF.y < 1 ){ _fadeInTF.y = 0; removeEventListener(Event.ENTER_FRAME, onEF); _fadeOutTF.y = _fadeInTF.y + _fadeInTF.height + 2;//插回到可视范围下方 //刷新下一次会出现的时间文本 var visibleTime:int = int( _fadeInTF.text ); _fadeOutTF.text = String(visibleTime - range < 0 ? maximum : visibleTime - range); currentTime -= range; } } private function set currentTime( value:int ):void{ _currentTime = value; if( _currentTime < 0 )_currentTime = maximum; } private function get currentTime( ):int{ return _currentTime; } }
解释一下代码的意义吧。在构造函数里我们初始化了两个文本显示数字并使用了一个遮障,这个遮障的长宽等于一个文本的长宽,这就保证了我们一次只能看见一个文本。
update函数是一个public的方法,外部需要更新时间的话就靠它了。在这个方法里首先要做的是判断当前时间是否为0,如果为0的话就表示下一个时间点将会发生退位,抛出一个事件让它的临位知道自己也该更新时间了。比如我设置了一个时间为19秒,就需要两个MyTimer实例来分别负责显示它们,一般情况下都是个位数在改变,由9变到8,由8变到7……等到变到0的时候,下一秒到来时十位数需要由1变到0了,那么在十位数这个MyTimer实例监听到个位数实例发出的事件通知后将会调用自己的update方法进行时间的更新。update方法中之后的代码要做的事情就是确定接下来在ENTER_FRAME事件侦听函数中需要移出和移入可视区域的文本分别是哪个。
创建time的set方法的本意是让开发者在设置时间的同时能够自动地改变两个文本框中的文字,在这里需要注意的是value -= range;这句,不能用currentTime来代替value,currentTime能且只能在文本移动动画播放完毕之后改变,否则将会造成之后的计算发生错误。与time的set方法一样,currentTime的set方法也是为了让我不用在每次对_currentTime变量进行减操作时进行小于0的判断,这样能让我少写不少重复代码。
最后看到onEF方法,这里做的就是改变两个文本框的y值让它们向上移动,这里需要注意的是_fadeOutTF的文本不能根据currentTime来设置,如:
currentTime -= 1; _fadeOutTF.text = currentTime;
这样会出现什么问题呢?假如我们第一个文本是0,第二个文本是它的下一秒时间9,currentTime等于0,这些都是在初始化的时候设置好了的。那么当调用update方法时0会往上移出可视范围,9会出现,当0完全移出可视范围后0的文本将会被改变成currentTime-1也就是9,那么我们将会看见两次9!
耶?今天星期天也能这么快通过审核?我还打算等到明天通过了审核再继续写的呢。既然通过了审核那就继续讲吧,老巫你这家伙速度倒快,占了我一楼害贫道不能连击……
刚才说完了MyTimer类,有了这些类我们就可以使用它们来组合成一个计时器了,因为时间由六位数字组成,所以我们就需要创建6个MyTimer单例放在一个容器里面,叫这个容器为TimerLayer好了。见代码:
TimerLayer.as:
public class TimerLayer extends Sprite { /** * 事件,倒计时结束时抛出 */ public static const TIMES_UP_EVENT:String = "times up event"; /** * 当前时间 */ public var currentTime:int; private var _timerList:Array; private var _timer:Timer = new Timer(1000); private var _timerTextCount:int = 6; private var tf:TextFormat = new TextFormat(null, 40); /** * * @param totalTime 总计时时间 * @param range 每次计时间隔来到时计时数字减少的幅度,单位:秒 * @param moveSpeed 数字移动的速度,单位:像素 * */ public function TimerLayer( totalTime:int, range:int=1, moveSpeed:Number = 1 ) { currentTime = totalTime; _timerList = new Array( _timerTextCount );//数组长度根据需要来创建,避免频繁push造成的效率降低 var tf:TextFormat = new TextFormat(null, 40); //初始化全部计时器文本 for( var i:int=0; i<_timerTextCount; i++ ){ var maximum:int; //小时的最大值为23,分、秒最大值为59 if( i == 0 ){ maximum = 2; }else if( i == 1 ){ maximum = 3; }else{ maximum = i % 2 == 0 ? 5 : 9; } var mt:MyTimer = new MyTimer( 0, maximum, tf ); mt.range = range; mt.moveSpeed = moveSpeed; mt.x = 10 + i * 45; addChild( mt ); _timerList[i] = mt; mt.name = i.toString();//name是DisplayObject一个自带的属性,我们可以用它来记录一些东西,比如索引号 } initTime( totalTime ); //为了省资源,不必为每个MyTimer实例都添加事件侦听,只需要为他们的父容器增加一个 //设置了第三个参数,即“捕获”参数为true的侦听即可对他们进行全局统一侦听 addEventListener( MyTimer.TIME_CHANGE_EVENT, onTimeChange, true ); _timer.addEventListener(TimerEvent.TIMER, onTimer); } /** *开始计时 * */ public function startTiming():void{ _timer.start(); } private function initTime( totalTime:int ):void{ var hour:int = totalTime / 3600; var hourStr:String = hour < 10 ? "0"+hour.toString() : hour.toString(); var minute:int = totalTime % 3600 / 60; var minuteStr:String = minute < 10 ? "0"+minute.toString() : minute.toString(); var secound:int = totalTime % 60; var secoundStr:String = secound < 10 ? "0"+secound.toString() : secound.toString(); var timeStr:String = hourStr + minuteStr + secoundStr; //初始化计时器文本起始显示时间 for( var i:int=0; i<_timerTextCount; i++ ){ _timerList[i].time = int( timeStr.charAt(i) ); } } private function onTimeChange( event:Event ):void{ //根据初始化时根据索引号设置的name拿到索引号 var targetIndex:int = int( event.target.name ); //若目标计时文本不是最首位则更新其前一位计时文本的值 if( targetIndex != 0 ){ _timerList[targetIndex - 1].update(); } } private function onTimer( event:TimerEvent ):void{ var temp:MyTimer = _timerList[_timerTextCount-1] as MyTimer; temp.update(); if( --currentTime == 0 ){//这句就等于先currentTime--; 然后判断currentTime的值是否等于0 _timer.stop(); dispatchEvent( new Event(TIMES_UP_EVENT) ); } } }
这段代码基本上没有什么难点,就是有几个小技巧在里面:
1,活用name属性:当退位事件被抛出后我们需要更新事件抛出者前一位的MyTimer实例的时间,所以需要在事件侦听函数中知道事件抛出者位于数组中的位置,但是我不愿意使用indexOf方法来求得索引号,这样会进行多次计算,太浪费资源,所以我选择使用name属性来记录每一个MyTimer实例的索引位置,Adobe官方创建name属性的初衷是为记录Flash时间轴上某个元件,但是在大多数情况下我们都不太会用到之,不如把此属性作为一个标签贴在对象上以便我们在一些地方可以识别。
2,活用事件机制:事件机制是Flash一个很基本的概念,我们知道一个事件被抛出后首先会进入捕获阶段,此时将从根节点到子节点检查事件。所以如果一个元件里抛出一个事件,那么它的父容器,爷容器都是可以拿到的,只需要你设置addEventListener的第三个参数useCapture为true即可,这样就不需要遵循死板的“谁抛出谁侦听”的原则了,只需要对所有MyTimer的共同父类添加一个事件侦听即可,这样就省去一次添加多个事件侦听器,节省了资源。
3,OnTimer里面有一句:--currentTime == 0,这是一种合二为一的写法,在改变了currentTime值的同时也对 currentTime减一后的结果和0进行了比较,注意不能写成currentTime-- == 0, 这样做的话,和0进行比较的就不是currentTime 减一后的值而是未减去一时的值了。据说这样合二为一的写法能增加运算效率,不过我没有验证过……
怎样?上面这段代码都看完了吧?好了,所有要用的工具都已Ready,那我们还等什么呢,速速用起!看到我们的文档类TimerDemo:
TimerDemo.as(application):
public class TimerDemo extends Sprite { private var alertText:TextField = new TextField(); public function TimerDemo() { var tl1:TimerLayer = new TimerLayer(65, 1, 6); tl1.x = 50; tl1.y = 50; addChild( tl1 ); tl1.startTiming(); tl1.addEventListener( TimerLayer.TIMES_UP_EVENT, onTimesUp ); alertText.x = 50; alertText.y = 110; addChild(alertText); } private function onTimesUp(event:Event):void{ alertText.htmlText = "<font size='16' color='#FF0000'>TIMES UP!</font>"; alertText.height = alertText.textHeight + 5; alertText.width = alertText.textWidth + 5; } }
运行效果看起来还行,不过感觉还缺了点什么,就看到干巴巴的6位数字,感觉作为一个“钟”来说,应该有会不停闪动的冒号隔开时、分、秒才好看一点。那么就写一个分割符类出来好了。
TimeSeparator.as:
/** * 时、分、秒分隔符 * @author S_eVent * */ public class TimeSeparator extends TextField { private var timer:Timer = new Timer(500); public function TimeSeparator( tf:TextFormat=null ) { if( tf )this.defaultTextFormat = tf; this.text = ":"; this.width = this.textWidth + 5; this.height = this.textHeight + 5; } /** * 开始闪烁 * */ public function startBlink():void{ if( !timer.hasEventListener(TimerEvent.TIMER) ){ timer.addEventListener(TimerEvent.TIMER, onTimer); } timer.start(); } /** * 停止闪烁 * */ public function stopBlink():void{ timer.stop(); } private function onTimer(e:TimerEvent):void{ this.visible = !this.visible; } }
好了,把它放到TimerLayer里面去吧:
public function TimerLayer( totalTime:int, range:int=1, moveSpeed:Number = 1 ) { …… initTime( totalTime ); drawSth();//画一点东西 …… } //画些冒号隔开时、分、秒 private function drawSth():void{ var timeSeparator:TimeSeparator = new TimeSeparator(tf); var timeSeparator2:TimeSeparator = new TimeSeparator(tf); addChild(timeSeparator); addChild(timeSeparator2); timeSeparator.x = _timerList[1].x + _timerList[1].width + 2; timeSeparator2.x = _timerList[3].x + _timerList[3].width + 2; timeSeparator.startBlink(); timeSeparator2.startBlink(); }
最终运行效果列位相信已经在一开始给出的演示地址里看过啦~
来点更加有难度的吧,老虎 机的制作
刚才那个倒计时的东西难点不多,关键在于细节的处理,既然我们已经知道了超级转转转的原理所在,那么老虎 机的转动效果相信也容易写出,但是真的那么好做么?做了才知道,let`s fucking go!go!go!
一如既往地先看效果演示再说:http://www.iamsevent.com/upload/SlotMathineDemo.swf
遵循和计时器一样的步骤,先要做的是单列图片列的显示以及滚动,我们应该发现了一点,那就是每一张图片它不仅是一张图片,它还记录了该图片的类别,当老虎 机的转动停止时,我们将取出位于指针所指那一列的图片作为被选中的项目组,此时我们就需要根据项目组中所有图片的类别信息进行判断用户用没有中奖(比如这一个项目组中全部项目的类别都一样),并把每个被选中的项目类别在下方以文字形式显示出来。所以我们这里的每个项目不能只是一个bitmap,我们要让它成为bitmap中的战斗机!请见贫道的编码结果:
public class Item extends Bitmap { /** * 记录了项目所对应的类别 */ public var type:String; public function Item(bitmapData:BitmapData=null, type:String="") { super(bitmapData); this.type = type; } }
怎样?是不是很有科技含量,很有杀气吧?哎呦~你干嘛打人捏你,吹个牛而已嘛犯得着这样么真是的……
接下来让我们开始创建图片列ItemList,我们将用它显示一列图片并负责滚动它们的逻辑。
ItemList.as:
/** * 可滚动图片列 * @author S_eVent * */ public class ItemList extends Sprite { /** * 转动停止时抛出的事件 */ public static const ITEM_LIST_STOP_EVENT:String = "item list stop event"; /** * 加速度,影响了滚动开始与停止的速度 */ public var acceleration:Number = 0.3; /** * 最大速度 */ public var maxSpeed:Number = 20; private var _itemList:Array; private var _imgWidth:Number; private var _imgHeight:Number; private var _mask:Shape = new Shape(); private var _maskWidth:Number; private var _maskHeight:Number; private var _scrollSpeed:Number = 0;//当前速度 private var _state:int = 0;//当前状态,0为停止,1为开始滚动,2为开始减速 private var _containerHeight:Number = 0;//总高度 private var _itemNum:int; /** * * @param bitmapDataList 一列上所拥有的所有项目的Item数组 * @param imgWidth 每个图片的宽度,默认为0,若为0,则宽度与对应位图数据的宽度一致 * @param imgHeight 每个图片的高度,默认为0,若为0,则高度与对应位图数据的宽度一致 * */ public function ItemList( itemList:Array, imgWidth:Number=0, imgHeight:Number=0 ) { _itemNum = itemList.length; _imgWidth = itemList[0].width; _imgHeight = itemList[0].height; _itemList = itemList; for(var i:int=0; i<_itemNum; i++){ var item:Item = itemList[i] as Item; if( imgWidth != 0 ){ _imgWidth = imgWidth; item.width = imgWidth; } if( imgHeight != 0 ){ _imgHeight = imgHeight; item.height = imgHeight; } _containerHeight += item.height; item.y = i * _imgHeight; addChild( item ); } addChild( _mask ); _maskWidth = this.width + 1; _maskHeight = _containerHeight; updateMask(); } /** * 开始滚动图片 * */ public function startScroll():void{ _state = 1; addEventListener(Event.ENTER_FRAME, onEF); } /** * 停止滚动图片 * */ public function stopScroll():void{ _state = 2; } /** * 设置遮障宽度 * */ public function set maskWidth( value:Number ):void{ _maskWidth = value; updateMask(); } /** *设置遮障高度 * */ public function set maskHeight( value:Number ):void{ _maskHeight = value; updateMask(); } private function updateMask():void{ _mask.graphics.clear(); _mask.graphics.beginFill(0); _mask.graphics.drawRect(0,0,_maskWidth,_maskHeight); _mask.graphics.endFill(); this.mask = _mask; } private function onEF( event:Event ):void{ //滚动图片 for each( var elem:Item in _itemList ){ elem.y -= _scrollSpeed; } //加速、滚动状态 if( _state == 1 ){ _scrollSpeed += acceleration; if( _scrollSpeed >= maxSpeed )_scrollSpeed = maxSpeed; } //减速,停止滚动状态 else if( _state == 2 ){ if( _scrollSpeed > 0){ _scrollSpeed -= acceleration; } else{ _scrollSpeed = 0; removeEventListener(Event.ENTER_FRAME, onEF); _state = 0; return; } } //超出最高位置的图片移至最下面一张图片的末尾,并让其在数组中的位置也移至队尾, //以保证在数组中每个图片的 y 轴位置由低到高排列 if( _itemList[0].y <= -_imgHeight ){ var firstOne:Item = _itemList.shift(); var lastOne:Item = _itemList[_itemList.length-1]; firstOne.y = lastOne.y + lastOne.height; _itemList.push( firstOne ); } } }
在构造函数中我们允许外部修改每一个图片项目的长和宽,这一个功能在计时器应用中也有,只不过它是通过传入一个TextFormat来达到目的的。我们还通过一个state变量来记录当前图片列所处状态,在OnEF中将会根据状态的不同进行不同的处理。注意这里我们引入了加速度和最大速度的概念,这些都是为了让效果更真实。另外为了让需最终选择结果更充满随机性,我们将在之后随机设置每一列的加速度和最大滚动速度。
之后我们还是像计时器应用一样写一个layer容器来存放这所有的图片列。
SlotMachineLayer.as:
/** * 老-虎-机 * @author S_eVent * */ public class SlotMachineLayer extends Sprite { private var _colNum:int; private var _colList:Array; /** * * @param SMImgList 老-虎-机显示图片列表,二维数组,其格式为[列图片数组,列图片数组.....] * @param imgWidth 每个图片的宽度,默认为0,若为0,则宽度与对应位图数据的宽度一致 * @param imgHeight 每个图片的高度,默认为0,若为0,则高度与对应位图数据的宽度一致 */ public function SlotMachineLayer( SMImgList:Array, imgWidth:Number=0, imgHeight:Number=0 ) { _colNum = SMImgList.length; _colList = new Array( _colNum ); for( var i:int=0; i<_colNum; i++ ){ var il:ItemList = new ItemList( SMImgList[i], imgWidth, imgHeight ); _colList[i] = il; il.x = i * 45; addChild(il); } } /** * 开始滚动 * */ public function letsFuckingGo():void{ for each( var elem:ItemList in _colList ){ elem.startScroll(); } } /** * 停止滚动 * */ public function stop():void{ for each( var elem:ItemList in _colList ){ elem.stopScroll(); } } /** * 设置某一图片 列的加速度与速度信息 * @param index 图片列列号,从0开始,自左往右依次增加 * @param acceleration 加速度,默认值为0,即不设置 * @param maxSpeed 最大速度,默认值为0,即不设置 * */ public function setItemListSpeed( index:int, acceleration:Number=0, maxSpeed:Number=0 ):void{ if( acceleration != 0 ){ _colList[index].acceleration = acceleration; } if( maxSpeed != 0 ){ _colList[index].maxSpeed = maxSpeed; } } /** * 设置遮障宽度 * @param value * */ public function set maskWidth( value:Number ):void{ for each( var elem:ItemList in _colList ){ elem.maskWidth = value; } } /** * 设置遮障高度 * @param value * */ public function set maskHeight( value:Number ):void{ for each( var elem:ItemList in _colList ){ elem.maskHeight = value; } } }
现在只有一系列的public方法可供外部调用,所以现在看来它只是所有ItemList的经纪人而已,在主应用里使用它一下看看效果如何:
public class SlotMathineDemo extends Sprite { [Embed(source="Vehicle.png")] private var resource:Class; private var sml:SlotMachineLayer; private var state:int = 0; private var btn:MyButton; private var colNum:int = 5; public function SlotMathineDemo() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; initSM(); initBtn(); } private function initSM():void{ var bmd:BitmapData = (new resource() as Bitmap).bitmapData; var bmdList:Array = new Array(8); //取素材 bmdList[0] = AnimationFactory.Cut( bmd, 32, 32, 1, 0 ); bmdList[1] = AnimationFactory.Cut( bmd, 32, 32, 1, 3 ); …… bmdList[7] = AnimationFactory.Cut( bmd, 32, 32, 5, 9 ); //设置所有类型 var typeList:Array = ["木船", "战舰", "氢气球", "空艇", "马", "车", "空矿车", "满矿车"]; //原始顺序为0-7,它将在之后被打乱后作为某一列的数据源 var tempAry:Array = new Array(8); for( var i:int=0; i<8; i++ ){ tempAry[i] = i; } //随机生成colNum个列的信息 var resultAry:Array = new Array(colNum); for( i=0; i<colNum; i++ ){ resultAry[i] = randomization( tempAry ); //resultAry中原本的格式是[[0,1,2....],[0,1,2...]....] //现将里面的数字都变成Item实例[[Item,Item...],[Item,Item....]....] for( var j:int=0; j<8; j++ ){ var index:int = resultAry[i][j]; resultAry[i][j] = new Item( bmdList[index], typeList[index] ); } } sml = new SlotMachineLayer( resultAry ); for(i=0; i<colNum; i++ ){ var randomA:Number = 0.1 + Math.random() * 0.4; var randomS:Number = 12 + Math.random() * 15; sml.setItemListSpeed(i, randomA, randomS); } sml.maskHeight = 225; addChild( sml ); sml.x = 50; sml.y = 50; } //打乱数组顺序 private function randomization( target:Array ):Array{ var clone:Array = target.concat();//复制一个副本 var len:int = clone.length; var newAry:Array = new Array(len);//创建一个数组以存放打乱了的元素 for( var i:int=0; i<len; i++ ){ //随机取一个元素放入新数组中并把它从原来所在数组中删除 var newIndex:int = Math.random() * clone.length >> 0;// >>0是对一个数字进行向下取整操作 newAry[i] = clone[newIndex]; clone.splice( newIndex, 1 ); } return newAry; } private function initBtn():void{ btn = new MyButton("GO!", 40, 20); btn.x = 300; btn.y = 100; addChild( btn ); btn.addEventListener(MouseEvent.CLICK, onClick); } private function onClick(e:MouseEvent):void{ if( state == 0 ){ state = 1; sml.letsFuckingGo(); btn.label = "Stop!"; }else{ state = 0; sml.stop(); btn.label = "GO!"; } } }
对于这个文档类的代码,可能有人看不懂,我一一解释吧。我们嵌入的那张图片资源原貌是这样的:
<ignore_js_op>
我们使用的AnimationFactory类在之前曾多次用到,它提供了一些切图技术所用到的方法,如果看过我前面教程的朋友应该不会陌生。我们使用它提供的Cut方法去上面那张素材上切8张图标作为图片列素材。这里用到的MyButton类在之前也用到过,这里我稍作改建后使用:
/** * 我的把疼 * @author S_eVent * */ public class MyButton extends Sprite { private var Label:TextField = new TextField(); public function MyButton( label:String, Number, height:Number, UpColor:uint=0x999999, overColor:uint=0x000099 ) { Label.selectable = false; this.label = label; Label.mouseEnabled = false; updateBtn( UpColor, width, height ); Label.x = Label.y = 1; this.addChild( Label ); this.addEventListener(MouseEvent.ROLL_OVER, function(event:MouseEvent):void{ updateBtn( overColor, width, height); }); this.addEventListener(MouseEvent.ROLL_OUT, function(event:MouseEvent):void{ updateBtn( UpColor, width, height); }); this.buttonMode = true; this.useHandCursor = true; } private function updateBtn( color:uint, w:Number, h:Number ):void{ this.graphics.clear(); this.graphics.lineStyle(1); this.graphics.beginFill( color ); this.graphics.drawRect( 0, 0, w, h ); this.graphics.endFill(); } public function set label( value:String ):void{ Label.htmlText = value; Label.width = Label.textWidth + 4; Label.height = Label.textHeight + 4; } public function get label():String{ return Label.text; } }
用它来作为按钮使用非常方便。
之后来解释一下随机化算法。我们之前设置了一个图片素材数组和一个类型数组,它们是一一对应的关系,之后将一起被当做参数传入Item的构造函数中。不过我们不希望老-虎-机中所有的列中的Item都按照0-7的顺序排列下来,我们要打乱数组以造成Itme排布的随机性。那么怎样打乱数组呢?看看randomization这个方法,它的步骤分3步:
1,建立一个原始数组的副本以便于之后所做的数据更改不会对原始数组造成影响,再建一个数组以保存随机结果;
2,开始打乱数组:从克隆数组中随机取一个数字出来剪切至结果数组中,如此反复直至克隆数组中元素被取完;过程如下图所示
<ignore_js_op> <ignore_js_op>
3,将结果数组返回
我们的最终目的是得到一个随机的Item数组,因此我们需要随机出每个Item所使用的素材图片或类型在它们的数组中所对应的索引。创建一个索引数组,其中存放从0到bmdList.length或者typeList.length的数字,这些数字对应了bmdList或typeList的索引号,打乱此数组后就可以根据索引数组中的索引号去创建Item了。
还要注意一点,在这里我设置了老-虎-机的遮障高度:sml.maskHeight = 225,而不是它默认的30*8=240,不取它默认值是因为如果我们这么做,用户将会在第一张图片消失之际将会看到最下方会突然出现一张图片,这样咱就穿帮了不是,现在用的图片素材小可能你还没有在意到,如果用大一点的图片就会看得很明显了。所以我就把遮障的高度减小一些以遮挡住最下方的一张图片不让用户看出猫腻^_^
最后运行一下应用程序,你可能会发现停止时最上面的一张图片并没有和上边缘对齐,这可不行哦,我们得想点办法让它对齐。
<ignore_js_op>
回忆一下,在我们以前玩到过的老-虎-机游戏机中,如果一列转动快停止时在最后的一小段时间里该列一般会以极慢的速度上移直到把上面数下来第二张图片与上边缘对齐。为什么是第二张图片而不是第一张图片呢?因为第一张图片往往已经有一部分越出了上边缘。因此,我们欲把效果做成这样就得设置一个最小速度, 当速度减小到最小速度以下时便开始判断第二张图片是否已和上边缘对齐,若没有则让移动速度等于最小速度继续滚动,若有,则停止滚动。好了,让我们来看看ItemList类要怎么改:
…… * 最小速度,当速度减小到此速度以下时如果最上面一张图片没有和顶部对齐则会 * 以此速度进行缓慢移动以调整到对齐为止 */ public var minSpeed:Number = 1; …… private function onEF( event:Event ):void{ …… //减速,停止滚动状态 else if( _state == 2 ){ if( _scrollSpeed > minSpeed ){ _scrollSpeed -= acceleration; } else{ //如果第二张图片未和顶点对齐则继续移动到对齐为止 if( _itemList[1].y > 0 ){ _scrollSpeed = minSpeed; }else{ _scrollSpeed = 0; removeEventListener(Event.ENTER_FRAME, onEF); _state = 0; return; } } } …… }
这样就可以了。
接下来我们需要开始判断老-虎-机转动停止后有哪些项目被选中了,一般老-虎-机的游戏规则是让停在中间箭头所指的那一行项目为被选中项目,那么我们就需要设置一条水平线,看哪些项目图片停在了这条线上哪些项目就被选中了。再次修改ItemList的代码,添加以下东东:
…… private var _stopLine:Number = 0;//停止线位置 …… /** * 设置停止线的Y轴位置,在滚动停止时停在此位置的项目将作为被选中项目 * @param YPos * */ public function set stopLine( YPos:Number ):void{ _stopLine = YPos; } …… private function onEF( event:Event ):void{ …… //减速,停止滚动状态 else if( _state == 2 ){ if( _scrollSpeed > minSpeed ){ _scrollSpeed -= acceleration; } else{ //如果第二张图片未和顶点对齐则继续移动到对齐为止 if( _itemList[1].y > 0 ){ _scrollSpeed = minSpeed; }else{ _scrollSpeed = 0; removeEventListener(Event.ENTER_FRAME, onEF); _state = 0; //寻找被选中的项目并通过事件传递出去 var choosenItem:Item = findTheChoosenItem(); dispatchEvent( new ItemListStopEvent(ITEM_LIST_STOP_EVENT, choosenItem) ); return; } } } …… } private function findTheChoosenItem():Item{ for each(var elem:Item in _itemList){ if( elem.y <= _stopLine && (elem.y+elem.height) >= _stopLine ){ return elem; } } return null; }
这里我用的是最笨的办法,就是通过遍历项目数组依次检查每个项目,如果停止线落在某个项目的上边缘与下边缘直接那就说明该项目为被选中的项目,之后我将通过一个事件将被选中项目传递出去,该事件是个自定义事件ItemListStopEvent:
ItemListStopEvent.as:
public class ItemListStopEvent extends Event { public var choosenItem:Item; public function ItemListStopEvent(type:String, choosenItem:Item=null, bubbles:Boolean=false, cancelable:Boolean=false) { super(type, bubbles, cancelable); this.choosenItem = choosenItem; } }
这个事件将会由一个“信息面板”负责监听,并把监听到的结果以文字形式显示出来,一起来看看信息面板类的代码:
InfoPanel.as:
public class InfoPanel extends TextField { private var _target:ItemList; public function InfoPanel( Number=40, height:Number=20 ) { this.selectable = false; this.border = true; this.width = width; this.height = height; } public function set target( value:ItemList ):void{ if( value == null ){ _target.removeEventListener( ItemList.ITEM_LIST_STOP_EVENT, onStop ); }else{ value.addEventListener( ItemList.ITEM_LIST_STOP_EVENT, onStop ); } _target = value; } public function get target():ItemList{ return _target; } private function onStop( e:ItemListStopEvent ):void{ this.text = e.choosenItem.type; } }
之后,我们将在SlotMachineLayer中启用它们,并设置一个小箭头来指示stopLine所在位置:
public class SlotMachineLayer extends Sprite { …… private var _useArrow:Boolean; private var _arrow:Shape = new Shape(); /** * * @param SMImgList 老-虎-机显示图片列表,二维数组,其格式为[列图片数组,列图片数组.....] * @param imgWidth 每个图片的宽度,默认为0,若为0,则宽度与对应位图数据的宽度一致 * @param imgHeight 每个图片的高度,默认为0,若为0,则高度与对应位图数据的宽度一致 * @param useArrow 是否显示箭头,默认为false */ public function SlotMachineLayer( SMImgList:Array, imgWidth:Number=0, imgHeight:Number=0, useArrow:Boolean = false ) { _colNum = SMImgList.length; _colList = new Array( _colNum ); for( var i:int=0; i<_colNum; i++ ){ var il:ItemList = new ItemList( SMImgList[i], imgWidth, imgHeight ); _colList[i] = il; il.x = i * 45; addChild(il); var ip:InfoPanel = new InfoPanel(); ip.target = il; ip.x = il.x; ip.y = il.y + il.height; addChild( ip ); } this.useArrow = useArrow; }
/** * 是否使用箭头 * */ public function set useArrow( value:Boolean ):void{ _useArrow = value; _arrow.graphics.clear(); if( _useArrow ){ _arrow.graphics.beginFill(0xe0ae28); _arrow.graphics.lineTo(20, -10); _arrow.graphics.lineTo(20, 10); _arrow.graphics.lineTo(0, 0); _arrow.graphics.endFill(); if( !this.contains(_arrow) ){ _arrow.x = this.width; addChild( _arrow ); } }else if( this.contains(_arrow) ){ removeChild( _arrow ); } } /** * 设置箭头的Y轴位置 ,默认为0 * */ public function set arrowYPos( value:Number ):void{ _arrow.y = value; for each( var elem:ItemList in _colList ){ elem.stopLine = value; } } }
呼……好了,终于只剩最后一步了,在文档类里面开启“启用箭头”的开关吧!
public class SlotMathineDemo extends Sprite { private function initSM():void{ …… sml = new SlotMachineLayer( resultAry ); sml.useArrow = true;//启用箭头 sml.arrowYPos = 110;//设置停止线位置为中间点 for(i=0; i<colNum; i++ ){ …… } }
OK,全部搞定,运行一下结果,正和一开始给出的演示效果一样,怎么样,很有成就感吧?其实回头看看代码,有些东西虽然表面上看起来很简单,实现原理也能说出个大概,但是到实际编码过程中却问题多多,我写这个案例也花掉了两天的空闲时间,一些细节方面的问题常常会困扰我很久。这篇教程给出了一种做“滚动效果”的思路和方法,在实际开发过程中列位爱卿可以以此为参考,也可以直接把我的源码download过去略加修改即可(别忘了改掉作者名哦),下面放出源码,请笑纳: <ignore_js_op> scrollApps.rar