如何实现海岛奇兵里的脚印效果

  海岛奇兵(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         }