如何实现海岛奇兵里的脚印效果
海岛奇兵(boom beach,炸婊子)是一款热门的策略手游,我也是一名忠实爱好者。每当登陆一半,系统提示我的基地正在被人侵略时,心里总是凉飕飕的,等到终于登陆上游戏,基地建筑被人夷为平地,四处还留着敌人作案的脚印时,累觉不爱。回到正题,本文讨论的是这种脚印效果的实现。脚印效果很真实细腻,主要靠两点:
1、根据前进路线改变脚印的方向。
2、有左右脚交替的感觉。
最近我们游戏里也有在场景里留下脚印的需求,实现方法如下。
第1点非常好解决:在角色移动过程中,每帧记录角色位置,结合上一帧位置,计算出角色的移动方向角度。既然知道了移动角度,对脚印sprite做相应角度的旋转即可。
相应接口:Math.atan2(x:Number, y:Number):Number
要实现第2点效果则有些难度,需要理解并用到一点数学知识——三角函数!没错,就是耳熟能详的sin和cos。
2.5d视角游戏的左右脚交替的效果为什么难实现呢?
先从最简单情况分析,当角色从水平移动时,脚印只要考虑在y轴方向的进行偏移,脚印一上一下的,从而产生脚印效果;同理,当角色在垂直方向移动,脚印只要考虑在x轴方向进行偏移,脚印就会一左一右;那当移动方向不是单纯水平或垂直方向时,这时两只脚印之间要相对x,y轴都进行偏移,才能产生自然的脚印效果。而且,随着角度变化,相对x,y轴的偏移量也是不同的。因此,怎样根据移动方向,确定脚印相对x,y的具体偏移量,就是问题难点。
在这里,我们返璞归真,抛弃所有高深的概念,以日常经验来想象左右脚交替是怎么一回事。看下面草图。
假设以一个点为圆心画圆,当一只脚落在圆的任一处时,另一只脚必定落在另一处,两脚连线必定经过圆心,那两脚交替的效果不就出来了吗?
假设原点坐标为(x0,y0),如果左脚坐标为(x0+offsetX,y0+offsetY),那么我们就知道右脚坐标为(x0-offsetX,y0-offsetY)。到目前为止我们已经知道如何确定左右脚的相对位置了。
那么按照之前的分析,我们知道角色移动的角度,那如何根据移动角度,来确定脚印的坐标呢?这里就需要用到三角函数。
根据定义,三角函数(sinA)^2 +(cosA)^2=1。而圆的表达式则为x^2 + y^2 = 1。假设x=sinA,y=cosA,那(sinA)^2 +(cosA)^2=1在坐标系中的图像就是一个圆!
我们假设有一个半径为1的圆,且有一条斜角角度为A,经过圆心的直线,两者相交,那相交点的坐标是多少?如下图所示
答案是(cosA,sinA)和(-cosA,-sinA)。
根据百度百科:
正弦:在直角三角形中,任意一锐角∠A的对边与斜边的比叫做∠A的正弦,记作sinA(由英语sine一词简写得来),即sinA=角A的对边/斜边
余弦:角A的邻边比斜边 叫做∠A的余弦,记作cosA(由余弦英文cosine简写得来),即cosA=角A的邻边/斜边(直角三角形)。
而因为圆的半径为1,所以图中直角三角形的斜边边长为1。因此我们就知道了角A的对边边长为sinA,角A的邻边边长为cosA。
所以相交点的坐标当然就是(cosA,sinA)和(-cosA,-sinA)。
至此,问题完美解决了。我们将每一帧角色的位置点作为圆心,通过Math.atan2方法,计算得出移动角度;再通过Math.sin,Math.cos得出相应的正弦值和余弦值,便可知道两个脚印的坐标。
PS:在as3里实现,还需要注意一些细节。
1、Math.atan2(x:Number, y:Number):Number接口算出的移动角度是相对垂直方向而言的。也就是说,算出的角度,应该是上图中的角a的余角(90°-∠A),暂且称之为∠B。所以相交点的坐标,则是(sinB,cosB)和(-sinB,-cosB)。
2、as3的以左上角作为坐标系原点,y轴正向向下,x轴正向向右;而数学里的坐标系则是y轴正向向上,x轴正向向右。所以相同角度,两种坐标系得出的sin值刚好是相反数。所以在as3中,相交点的坐标应该是(-sinB,cosB)和(sinB,-cosB)。
最后贴出实现代码:
1 /** 脚印时间间隔,单位ms */ 2 private static const FOOTPRINT_INTERVAL:int = 100; 3 /** 脚印渐隐时间,单位s */ 4 private static const FOOTPRINT_FADE_OUT_TIME:Number = 2; 5 /** 两脚距离 */ 6 private static const FOOT_GAP:int = 9; 7 8 /** 系统场景层 */ 9 private var _layer:DisplayObjectContainer;//舞台 10 /** 玩家siprit */ 11 private var _role:DisplayObject; 12 /** 脚印容器 */ 13 private static var canvs:Sprite;//特效容器 14 15 private var _footprintList:Array = []; 16 17 /** 脚印mc类 */ 18 private var _footprintCls:Class; 19 20 /** 定时器id */ 21 private var _si:int; 22 23 /** 24 * 初始化脚印效果 25 * @param footprintCls 脚印mc类 26 * @param role 玩家mc 27 */ 28 public function init(footprintCls:Class, role:DisplayObject):void 29 { 30 this._footprintCls = footprintCls; 31 this._role = role; 32 33 canvs=new Sprite();//特效容器 34 _layer.addChildAt(canvs, 0);//创建并且添加特效容器 35 36 clearInterval(_si); 37 _si = setInterval(onInterval, FOOTPRINT_INTERVAL); 38 } 39 40 /** 停止脚印效果 */ 41 public function stop():void 42 { 43 clearAll(); 44 clearInterval(_si); 45 46 _roleLastLocation = null; 47 } 48 49 /** 角色之前的位置 */ 50 private var _roleLastLocation:Point; 51 52 protected function onInterval(event:Event = null):void 53 { 54 if(!_role)return; 55 56 if(_roleLastLocation) 57 { 58 if(_roleLastLocation.x == _role.x && _roleLastLocation.y == _role.y)return; 59 60 var offsetX:Number = _role.x - _roleLastLocation.x; 61 var offsetY:Number = _role.y - _roleLastLocation.y; 62 63 var angle:Number = Math.atan2(offsetY, offsetX); 64 var sin:Number = Math.sin(angle); 65 var cos:Number = Math.cos(angle); 66 67 _roleLastLocation.x = _role.x; 68 _roleLastLocation.y = _role.y; 69 70 addFootprint(_role.x, _role.y, sin, cos); 71 }else 72 { 73 _roleLastLocation = new Point(); 74 _roleLastLocation.x = _role.x; 75 _roleLastLocation.y = _role.y; 76 } 77 78 } 79 80 /** 左右脚交替 */ 81 private static var switchFoot:Boolean; 82 /** 83 * 添加一个脚印 84 * @param _x x坐标 85 * @param _y y坐标 86 */ 87 private function addFootprint(_x:int,_y:int, sin:Number, cos:Number):void 88 { 89 switchFoot = !switchFoot; 90 91 var footprint:MovieClip = ObjectPool.getObject(_footprintCls) as MovieClip; 92 footprint.alpha = 1; 93 footprint.mouseChildren = false; 94 footprint.mouseEnabled = false; 95 footprint.play(); 96 97 if(switchFoot) 98 { 99 footprint.x = _x + sin * FOOT_GAP; 100 footprint.y = _y - cos * FOOT_GAP; 101 } 102 else 103 { 104 footprint.x = _x - sin * FOOT_GAP; 105 footprint.y = _y + cos * FOOT_GAP; 106 } 107 108 _footprintList[_footprintList.length] = footprint; 109 canvs.addChild(footprint); 110 111 Tween.to(footprint, FOOTPRINT_FADE_OUT_TIME ,{x:footprint.x,y:footprint.y,alpha:0.1}, null, clear); 112 } 113 114 /** 清除一个脚印 */ 115 private function clear():void 116 { 117 if(_footprintList && _footprintList.length > 0) 118 { 119 var dpo:MovieClip = _footprintList.shift() as MovieClip; 120 if(dpo && dpo.parent) 121 dpo.parent.removeChild(dpo); 122 dpo.stop(); 123 124 ObjectPool.disposeObject(dpo); 125 dpo = null; 126 } 127 } 128 129 /** 清除所有脚印 */ 130 private function clearAll():void 131 { 132 if(_footprintList) 133 { 134 while(_footprintList.length){ 135 var dpo:MovieClip = _footprintList.shift() as MovieClip; 136 if(dpo && dpo.parent) 137 dpo.parent.removeChild(dpo); 138 dpo.stop(); 139 140 ObjectPool.disposeObject(dpo); 141 dpo = null; 142 } 143 } 144 }