分享

Flash基础理论课 第八章 缓动与弹性运动Ⅲ

 kunzhu 2015-03-05

返回“Flash基础理论课 - 目录”

弹簧

下面我们将几个弹性小球串联起来。在介绍缓动一节时,我们简单地讨论了鼠标跟随的概念,意思是说一个物体跟随鼠标,另一个物体再跟随这个物体,依此类推。当时没有给大家举例子,是因为这个效果现在看来有些逊色。但是,当我们在弹性运动中使用这个概念时,效果就截然不同了。

本程序的设计思想:创建三个小球,名为ball0,ball1,ball2。第一个小球,ball0的动作与上面例子中的效果是相同的。ball1向ball0 运动,ball2向ball1 运动。每个小球都受到重力的影响,所以它们都会向下坠。代码稍有些复杂,文档类 Chain.as:

package {
 import flash.display.Sprite;
 import flash.events.Event;
 public class Chain extends Sprite {
  private var ball0:Ball;
  private var ball1:Ball;
  private var ball2:Ball;
  private var spring:Number = 0.1;
  private var friction:Number = 0.8;
  private var gravity:Number = 5;
  public function Chain() {
   init();
  }
  private function init():void {
   ball0 = new Ball(20);
   addChild(ball0);
   ball1 = new Ball(20);
   addChild(ball1);
   ball2 = new Ball(20);
   addChild(ball2);
   addEventListener(Event.ENTER_FRAME,onEnterFrame);
  }
  private function onEnterFrame(event:Event):void {
   moveBall(ball0,mouseX,mouseY);
   moveBall(ball1,ball0.x,ball0.y);
   moveBall(ball2,ball1.x,ball1.y);
   graphics.clear();
   graphics.lineStyle(1);
   graphics.moveTo(mouseX,mouseY);
   graphics.lineTo(ball0.x,ball0.y);
   graphics.lineTo(ball1.x,ball1.y);
   graphics.lineTo(ball2.x,ball2.y);
  }
  private function moveBall(ball:Ball,targetX:Number,targetY:Number):void {
   ball.vx += (targetX - ball.x) * spring;
   ball.vy += (targetY - ball.y) * spring;
   ball.vy += gravity;
   ball.vx *= friction;
   ball.vy *= friction;
   ball.x += ball.vx;
   ball.y += ball.vy;
  }
 }
}

看一下 Ball 这个类,我们发现每个对象实例都有自己 vx 和 vy 属性,并且它们的初始值均为0。所以在init方法中,我们只需要创建小球并把它们加入显示列表。

然后在onEnterFrame函数中,实现弹性运动。这里我们调用了 moveBall方法,比复制三次运动代码要好用得多。该函数的参数分别为一个 ball 对象以及目标点的 x,y 坐标。每个小球都调用这个函数,第一个小球以鼠标的 x,y作为目标位置,第二第三个小球以第一第二个小球作为目标位置。

最后,在确定了所有小球的位置后,开始画线,画线的起点是鼠标位置,然后依次画到每个小球上,这样橡皮圈就连接上了所有的小球。注意,程序中的 friction 降为0.8 为了使小球能够很快稳定下来。

创建一个数组保存链中所有对象的引用,然后通过循环遍历数组中的每个小球并执行运动,使这个程序更加灵活。这里只需要做一些小小的改变。首先,需要两个新的变量代表数组和对象数目:

private var balls:Array;

private var numBalls:Number = 5;

函数 init 中,使用for循环创建所有对象,并将对象引用加入数组:

private function init():void {
balls = new Array();
    for(var i:uint = 0; i < numBalls; i++) {
        var ball:Ball = new Ball(20);
        addChild(ball);
        balls.push(ball);
    }
    addEventListener(Event.ENTER_FRAME,onEnterFrame);
}

最后,onEnterFrame方法的变化最大。首先设置线条,将绘图起点移动到鼠标位置,再到第一个小球,然后循环为剩下的小球设置位置并连线。通过改变 numBalls 变量,我们可以加入任意多个小球。

private function onEnterFrame(event:Event):void {
    graphics.clear();
    graphics.lineStyle(1);
    graphics.moveTo(mouseX,mouseY);
    moveBall(balls[0],mouseX,mouseY);
    graphics.lineTo(balls[0].x,balls[0].y);
    for(var i:uint = 1; i < numBalls; i++) {
        var ballA:Ball = balls[i-1];
        var ballB:Ball = balls[i];
        moveBall(ballB,ballA.x,ballA.y);
        graphics.lineTo(ballB.x,ballB.y);
    }
}

运行结果见图 8-3,大家可以在ChainArray.as找到这个类。

图8-3 弹簧链

多目标弹性运动

我们在第五章讨论速度与加速度时,曾说过如何使一个物体受到多种外力。如果每种力都是加速度,我们只需要把它们一个个都加到速度向量中去。因为弹力不过就是施加在物体上的一种加速度,因此在一个物体上添加多种弹力也是非常容易的。

下面是创建多目标弹簧的方法:我们需要三个控制点,这些点都是 Ball 类的实例,并且具有简单的拖拽功能,用它们作为小球弹性运动的控制点。小球会立即运动到点,并在两点间寻找平衡。换句话讲,每个目标都会对小球施加一定的外力,小球的运动速度就是这些外力相加的结果。

例子程序相当复杂,使用多个方法处理不同的事件。以下是代码(文档类 MultiSpring.as),看过后再进行分段讲解:

package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.MouseEvent;
 public class MultiSpring extends Sprite {
  private var ball:Ball;
  private var handles:Array;
  private var spring:Number = 0.1;
  private var friction:Number = 0.8;
  private var numHandles:Number = 3;
  public function MultiSpring() {
   init();
  }
  private function init():void {
   ball = new Ball(20);
   addChild(ball);
   handles = new Array();
   for (var i:uint = 0; i < numHandles; i++) {
    var handle:Ball = new Ball(10,0x0000ff);
    handle.x = Math.random() * stage.stageWidth;
    handle.y = Math.random() * stage.stageHeight;
    handle.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
    addChild(handle);
    handles.push(handle);
   }
   addEventListener(Event.ENTER_FRAME,onEnterFrame);
   addEventListener(MouseEvent.MOUSE_UP,onRelease);
  }
  private function onEnterFrame(event:Event):void {
   for (var i:uint = 0; i < numHandles; i++) {
    var handle:Ball = handles[i] as Ball;
    var dx:Number = handle.x - ball.x;
    var dy:Number = handle.y - ball.y;
    ball.vx += dx * spring;
    ball.vy += dy * spring;
   }
   ball.vx *= friction;
   ball.vy *= friction;
   ball.x += ball.vx;
   ball.y += ball.vy;
   graphics.clear();
   graphics.lineStyle(1);
   for (i = 0; i < numHandles; i++) {
    graphics.moveTo(ball.x,ball.y);
    graphics.lineTo(handles[i].x,handles[i].y);
   }
  }
  private function onPress(event:MouseEvent):void {
   event.target.startDrag();
  }
  private function onRelease(event:MouseEvent):void {
   stopDrag();
  }
 }
}

在init方法中,创建小球并用for循环创建三个控制点,随机安排位置,并为它们设置拖拽行为。

onEnterFrame方法循环取出每个控制点,使小球向该点方向运动。然后,用控制点的坐标设置小球的速度,反复循环,从小球开始向各个控制点画线。onPress方法的内容非常简单,但是请注意 onRelease函数,我们无法知道当前拖拽的是哪个小球。幸运的是,使用任何一个显示调用stopDrag方法,都可以停止所有的拖拽,所以只需要在文档类中直接调用该方法。

我们只要改变 numHandles 变量的值,就可以轻松地设置控制点的数量。运行结果如图 8-4所示。

图8-4 多目标弹性

到目前为止,我相信大家已经有了很多的心得与体会,并且开始尝试解决一些书中没有提到的问题。如果真是这样的话,那就太好了!这也正是我写这本书的目的。

目标偏移

我们拿到一个真正的弹簧——有弹性的金属圈——然后将它的一头固定起来,另一头放上小球或其它物体,那么物体运动的目标点是哪里?难道说目标点是固定弹簧的那头儿?不,这并不实际。小球永远也到不了这个点,因为它会受到弹簧自身的阻碍。一旦弹簧变回了正常的长度,它会对小球施加更大的力。因此,目标点就应该是弹簧展开后的末端。

要寻找目标点,首先要找到物体与固定点之间的夹角,然后沿这个角度从固定点向外展开一段长度——弹簧的长度。换句话讲,如果弹簧长度是 50,小球与固定点的夹角是 45 度的话,那么就要以 45 度的夹角向外运动 50 个像素,而这个点就是小球的目标点。图8-5 解释了这一过程。

图8-5 弹簧复位

寻找目标点的代码如下:

var dx:Number = ball.x - fixedX;
var dy:Number = ball.y - fixedY;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = fixedX + Math.cos(angle) * springLength;
var targetY:Number = fixedY + Math.sin(angle) * springLength;

运行结果是,物体向着固定点运动,但会在与目标点相差一段距离时停止移动。大家还要注意,虽然我们叫它“固定点”,只是代表弹簧固定到的某个点。而不是指这个点不能移动。也许最好的方法就是看代码。

我们继续使用鼠标位置作为固定点,弹簧的长度为100 像素。以下是文档类(OffsetSpring.as):

package {
 import flash.display.Sprite;
 import flash.events.Event;
 public class OffsetSpring extends Sprite {
  private var ball:Ball;
  private var spring:Number = 0.1;
  private var vx:Number = 0;
  private var vy:Number = 0;
  private var friction:Number = 0.95;
  private var springLength:Number = 100;
  public function OffsetSpring() {
   init();
  }
  private function init():void {
   ball = new Ball(20);
   addChild(ball);
   addEventListener(Event.ENTER_FRAME,onEnterFrame);
  }
  private function onEnterFrame(event:Event):void {
   var dx:Number = ball.x - mouseX;
   var dy:Number = ball.y - mouseY;
   var angle:Number = Math.atan2(dy,dx);
   var targetX:Number = mouseX + Math.cos(angle) * springLength;
   var targetY:Number = mouseY + Math.sin(angle) * springLength;
   vx += (targetX - ball.x) * spring;
   vy += (targetY - ball.y) * spring;
   vx *= friction;
   vy *= friction;
   ball.x += vx;
   ball.y += vy;
   graphics.clear();
   graphics.lineStyle(1);
   graphics.moveTo(ball.x,ball.y);
   graphics.lineTo(mouseX,mouseY);
  }
 }
}

虽然我们能够看到运行结果,但却不能真正发现这项技术的特殊用处。没关系,下一节会给大家一个特别的例子。

弹簧连接多个物体

我们知道如何用弹簧连接两个物体,还知道这个点不是固定的。但是,如果另一个物体上还有一个弹簧反作用在第一个物体上,又是怎样的呢?这里有两个物体之间由一根弹簧连接。其中一个运动了,另一个物体就要向该物体移动过来。

我开始认为制作这种效果会导致死循环从而无法实现,或者至少会引起错误。但我也没管那么多,勇敢地进行尝试。结果非常完美!

虽然前面已经描述了一些策略,但这里还要细致得说一下:物体A以物体B作为目标,并向它移动。物体B 反过来又以物体A作为目标。事实上,本例中目标偏移起了重要的作用。如果一个物体以其它物体直接作为目标,那么它们之就会相互吸引,最终聚集在一个点上。通过使用偏移目标,我们就可以使它们之间保持距离,如图 8-6所示。

193x104

图8-6 弹簧连接的两个物体

下面举一个例子,我们需要两个 Ball 类的实例。分别为ball0 和 ball1。ball0向ball1 偏移运动。ball1向ball0 偏移运动。为了不去反复写偏移弹性运动的代码,我们将这些功能写到函数 springTo 中,直接调用函数即可。如果想让 ball0向ball1 运动,只要写 springTo(ball0,ball1),然后再让 ball1向ball0 运动,就写 springTo(ball1,ball0)。还要设置两个变量,ball0Dragging 和 ball1Dragging,作为每个小球运动的开关。以下是文档类(DoubleSpring.as):

package {
 import flash.display.Sprite;
 import flash.events.Event;
 import flash.events.MouseEvent;
 public class DoubleSpring extends Sprite {
  private var ball0:Ball;
  private var ball1:Ball;
  private var ball0Dragging:Boolean = false;
  private var ball1Dragging:Boolean = false;
  private var spring:Number = 0.1;
  private var friction:Number = 0.95;
  private var springLength:Number = 100;
  public function DoubleSpring() {
   init();
  }
  private function init():void {
   ball0 = new Ball(20);
   ball0.x = Math.random() * stage.stageWidth;
   ball0.y = Math.random() * stage.stageHeight;
   ball0.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
   addChild(ball0);
   ball1 = new Ball(20);
   ball1.x = Math.random() * stage.stageWidth;
   ball1.y = Math.random() * stage.stageHeight;
   ball1.addEventListener(MouseEvent.MOUSE_DOWN,onPress);
   addChild(ball1);
   addEventListener(Event.ENTER_FRAME,onEnterFrame);
   stage.addEventListener(MouseEvent.MOUSE_UP,onRelease);
  }
  private function onEnterFrame(event:Event):void {
   if (!ball0Dragging) {
    springTo(ball0,ball1);
   }
   if (!ball1Dragging) {
    springTo(ball1,ball0);
   }
   graphics.clear();
   graphics.lineStyle(1);
   graphics.moveTo(ball0.x,ball0.y);
   graphics.lineTo(ball1.x,ball1.y);
  }
  private function springTo(ballA:Ball,ballB:Ball):void {
   var dx:Number = ballB.x - ballA.x;
   var dy:Number = ballB.y - ballA.y;
   var angle:Number = Math.atan2(dy,dx);
   var targetX:Number = ballB.x - Math.cos(angle) * springLength;
   var targetY:Number = ballB.y - Math.sin(angle) * springLength;
   ballA.vx += (targetX - ballA.x) * spring;
   ballA.vy += (targetY - ballA.y) * spring;
   ballA.vx *= friction;
   ballA.vy *= friction;
   ballA.x += ballA.vx;
   ballA.y += ballA.vy;
  }
  private function onPress(event:MouseEvent):void {
   event.target.startDrag();
   if (event.target == ball0) {
    ball0Dragging = true;
   }
   if (event.target == ball1) {
    ball1Dragging = true;
   }
  }
  private function onRelease(event:MouseEvent):void {
   ball0.stopDrag();
   ball1.stopDrag();
   ball0Dragging = false;
   ball1Dragging = false;
  }
 }
}

本例中,每个小球都是可以拖拽的。enterFrame函数负责为小球调用springTo函数。请注意,这两条语句都是由两条判断语句包围起来的,目的是要确认小球目前没被拖拽:

springTo(ball0,ball1);
springTo(ball1,ball0);

springTo函数用于产生运动,函数中的所有语句大家应该都很熟悉。首先,求出距离和角度,再计算目标点,然后向目标点运动。第二次调用函数时,参数相反,两个小球交换位置,开始的小球向另一个小球运动。这也许不是效率最高的代码,但是它可以最好地表现出运动的过程。

我们看到,小球不会依附在任何固定点上,它们都是自由飘浮的。小球之间唯一的约束就是彼此保持一定的距离。这种写法最好的地方是可以很容易地加入新的物体。例如,再创建第三个小球(ball2),同时为它设置一个变量(ball2Dragging),就可以这么添加:

if(!ball0Dragging) {
    springTo(ball0,ball1);
    springTo(ball0,ball2);
}
if(!ball1Dragging) {
    springTo(ball1,ball0);
    springTo(ball1,ball2);
}
if(!ball2Dragging) {
    springTo(ball2,ball0);
    springTo(ball2,ball1);
}

这样就建立了一个三角形结构,如图 8-7所示。大家熟练掌握后,很快就能做出四边形结构,直到一切复杂的弹簧结构。

166x117

图8-7 一根弹簧连接三个物体

本章重要公式总结

现在来回顾一下本章的重要公式

简单缓动,长形:

var dx:Number = targetX - sprite.x;
var dy:Number = targetY - sprite.y;
vx = dx * easing;
vy = dy * easing;
sprite.x += vx;
sprite.y += vy;

简单缓动,中形:

vx = (targetX - sprite.x) * easing;
vy = (targetY - sprite.y) * easing;
sprite.x += vx;
sprite.y += vy;

简单缓动,短形:

sprite.x += (targetX - sprite.x) * easing;
sprite.y += (targetY - sprite.y) * easing;

简单弹性,长形:

var ax:Number = (targetX - sprite.x) * spring;
var ay:Number = (targetY - sprite.y) * spring;
vx += ax;
vy += ay;
vx *= friction;
vy *= friction;
sprite.x += vx;
sprite.y += vy;

简单弹性,中形:

vx += (targetX - sprite.x) * spring;
vy += (targetY - sprite.y) * spring;
vx *= friction;
vy *= friction;
sprite.x += vx;
sprite.y += vy;

简单弹性,短形:

vx += (targetX - sprite.x) * spring;
vy += (targetY - sprite.y) * spring;
sprite.x += (vx *= friction);
sprite.y += (vy *= friction);

偏移弹性运动:

var dx:Number = sprite.x - fixedX;
var dy:Number = sprite.y - fixedY;
var angle:Number = Math.atan2(dy,dx);
var targetX:Number = fixedX + Math.cos(angle) * springLength;
var targetY:Number = fixedX + Math.sin(angle) * springLength;

声明:该文章系网友上传分享,此内容仅代表网友个人经验或观点,不代表本网站立场和观点;若未进行原创声明,则表明该文章系转载自互联网;若该文章内容涉嫌侵权,请及时向上学吧网站投诉>>

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多