Ultrashock Tutorials > Flash 8 > Wave Motion  
 
by Paul Lewis Download Source Files  
 
Wave Motion
 
 Introduction: Physics Tutorial - Wave Motion 
 Step 1: Use the Force (You knew I'd use it at some point) 
 Step 2: Acceleration and Force 
 Step 3: Building the Wave 
 Step 4: Understanding the Code 
 Step 5: What’s left? Friction and connecting the dots 
 Step 6: Conclusion 

Author:
 Paul Lewis 

 www.anygivenfriday.com 

- discuss this tutorial -

Introduction:
Physics Tutorial - Wave Motion

Physics is such an important topic. It allows us to accurately model all sorts of behaviour and, as in the case of Flash, it allows us to render those results on screen. The best thing, though, is making the physics react to user input. Making a ball bounce because someone 'knocked it' with their mouse, or seeing something wiggle elastically in response to input from a microphone.

I think a lot of people are overwhelmed by physics when, in actual fact, once you get into the right way of thinking, the problems are usually quite easy to solve. I'm going to explain the water experiment I recently completed, which you can see below.


(Move mouse to interact with the "water")

1: Use the Force (You knew I'd use it at some point)

Forces act on everything around us. It causes things to move (or slow down), to rotate and to deform. We're not going to consider deformation because it's such a huge area and would make this unnecessarily complex. We're going to stick to the world of something called rigid body dynamics. What this means is that the forces control and influence the body or object but they don't cause any deformation; consider a bowling ball being thrown and a ball of jelly being thrown. The bowling ball could be modelled as not deforming when it bounces, whereas your jelly rather noisily splats on the floor. The point here is there is a force (your arm or a cannon or something imparting a force on the ball) and that whatever that force does, it certainly won't change our object's shape or mass.

2: Acceleration and Force

Ok, so here's the maths bit:

Sir Isaac Newton explained that Force = mass x acceleration. This is the most basic principle of what we're doing, and actually all we need to know to model any physics is what forces are applying and to what. So here's an example: I try and push a beach ball with a force. The beach ball rolls away pretty easily. Now I push a cannon ball with the same force. It doesn't move. Why? It has a much, much larger mass than the beach ball. According to the equation, acceleration = Force / mass. With a larger mass, acceleration will be smaller for the same force.

The bottom line here is if I use force on an object I will give it acceleration. The more force I give it the more it accelerates and the smaller mass it has, the more it will accelerate. The same is true in reverse; the heavier something is, the harder I push to make it move at the same speed as something lighter.

Now we've covered the forces side of things, I'll get us set up and then I will show you the code and explain what's going on.

3: Building the Wave

Step 1. Create a new Flash Document, adjust FPS to something good (like 41fps)

Step 2. Change the dimensions to something equally fun, like 800 x 600

Step 3. Let's create a particle and a wave:

Draw a simple, small circle about 10px diameter. Convert it to a MovieClip (F8), call it mcParticle and then make sure you name the instance of it mcParticle, too:

Now move it off to the left and top of the Stage so it's out of the way. From here on we'll be referring to it in the code alone.

Next we need to create a new, blank MovieClip. Press Ctrl+F8 and put all this stuff in:

We now need to create a load of particles on our timeline. So, on the first frame of the timeline put this code:

attachMovie("mcWave", "mcWave", -10); var particles = new Array();
var l = 40;
for(var i:Number = 0; i <=l; i++){
   duplicateMovieClip("mcParticle", "mcParticle"+i, i);
   this["mcParticle"+i]._x = -50+i*((Stage.width+100)/l);
   this["mcParticle"+i]._y = Stage.height/2;
   particles.push(this["mcParticle"+i]);
}

This code attaches a copy of our wave MovieClip (which as you will have seen in the linkage refers to a class called Wave (which we're about to create), then creates an array of particles, duplicated from our original particle.

So we’ve got the particles. Let's go to creating the class.

Step 4. File New -> ActionScript File...

This creates a new blank ActionScript file which you will need to save in the following structure: com\anygivenfriday\physics and it needs to be called Wave.as (with an upper case W). This is crucial or Flash won't be able to find the class. The com folder needs to live in the same folder as the FLA file.

Into the Wave.as file place the following code:

/* WAVE Class
* _-^-_-^-_-^-_-^-_-^-_-^

* * Author: Paul Lewis (http://www.anygivenfriday.com)
* You may use this class and distribute it freely but
* if used, you must include an acknowledgement somewhere
* on the site and not alter this header

* * Have Fun!

* * Paul
* _-^-_-^-_-^-_-^-_-^-_-^
* paul@anygivenfriday.com
*/

class com.anygivenfriday.physics.Wave extends MovieClip{

   var mcParticles:Array;
   var mcParticleSprings:Array;
   var fK:Number = 0.95;

   function Wave(){
      trace("_-^-_-^- WAVE -^-_-^-_");
      this.mcParticleSprings = new Array();
   }

   public function setEnds(mcParticles:Array):Void{
      this.mcParticles = mcParticles;
      for(var i:Number = 0; i < this.mcParticles.length; i++){
         this.mcParticles[i].fXPos = this.mcParticles[i]._x;
         this.mcParticles[i].fYAccel = 0;
         this.mcParticles[i].fYVel = 0;
         this.mcParticles[i].fYPos = this.mcParticles[i]._y;
         this.mcParticles[i].fBaseYPos = this.mcParticles[i]._y;
         this.mcParticles[i].iMass = 10;
         var mListener:Object = new Object();
         mListener.daddy = this;
         mListener.hitting = this.hitTest(_root._xmouse, _root._ymouse);
         mListener.mYPos = _root._ymouse;
         mListener.showParticles = false;
         mListener.firstRun = true;
         mListener.onMouseMove = function(){
            if(this.firstRun){
               this.hitting = this.daddy.hitTest(_root._xmouse, _root._ymouse);
               this.firstRun = false;
            }
            if(this.hitting != this.daddy.hitTest(_root._xmouse, _root._ymouse)){
               // work out mouse speed
               var iTarg:Number = Math.round((_root._xmouse/Stage.width)*(this.daddy.mcParticles.length-1));
               this.daddy.mcParticles[iTarg-2].fYVel = (_root._ymouse-this.mYPos)/6;
               this.daddy.mcParticles[iTarg-1].fYVel = (_root._ymouse-this.mYPos)/5;
               this.daddy.mcParticles[iTarg].fYVel = (_root._ymouse-this.mYPos)/3;
               this.daddy.mcParticles[iTarg+1].fYVel = (_root._ymouse-this.mYPos)/5;
               this.daddy.mcParticles[iTarg+2].fYVel = (_root._ymouse-this.mYPos)/6;
            }
            this.hitting = this.daddy.hitTest(_root._xmouse, _root._ymouse);
            this.mYPos = _root._ymouse;
         }
         Mouse.addListener(mListener);
      }
   }

   public function activate(){
      // add a spring between each particle
      for(var u:Number = 0; u < this.mcParticles.length-1; u++) this.mcParticleSprings.push({iLengthY:(this.mcParticles[u+1]._y - this.mcParticles[u]._y)});
      this.onEnterFrame = function(){
         for(var u:Number = this.mcParticles.length-1; u >=0; u--){
            // work out what forces are applying
            var fExtensionY:Number = 0;
            var fForceY:Number = 0;

            if(u>0){
               fExtensionY = this.mcParticles[u-1]._y - this.mcParticles[u]._y - this.mcParticleSprings[u-1].iLengthY;
               fForceY += -this.fK * fExtensionY;
            }
            if(u<this.mcParticles.length-1){
               fExtensionY = this.mcParticles[u]._y - this.mcParticles[u+1]._y - this.mcParticleSprings[u].iLengthY;
               fForceY += this.fK * fExtensionY;
            }

            fExtensionY = this.mcParticles[u]._y - this.mcParticles[u].fBaseYPos;
            fForceY += this.fK/15 * fExtensionY;

            // now update the position of each particle, but don't render (so we don't cascade changes early)
            this.mcParticles[u].fYAccel = -fForceY/this.mcParticles[u].iMass;
            this.mcParticles[u].fYVel += this.mcParticles[u].fYAccel;
            this.mcParticles[u].fYPos += this.mcParticles[u].fYVel;
            this.mcParticles[u].fYVel /= 1.04; // friction

         }

         // time to render our wave
         this.clear();
         this.lineStyle(1, 0x0, 100);
         this.beginFill(0x0, 100);
         for(var u:Number = 0; u < this.mcParticles.length; u++){ // use each particle as a control for the curve to and then use halfway points as actual anchors... took me AGES to work that out :-S
            if(u==0) this.moveTo(this.mcParticles[u].fXPos + (this.mcParticles[u + 1].fXPos - this.mcParticles[u].fXPos) / 2, this.mcParticles[u].fYPos + (this.mcParticles[u + 1].fYPos - this.mcParticles[u].fYPos) / 2);
            else if(u < this.mcParticles.length-1){
               this.curveTo(this.mcParticles[u].fXPos, this.mcParticles[u].fYPos, this.mcParticles[u].fXPos + (this.mcParticles[u + 1].fXPos-this.mcParticles[u].fXPos)/2, this.mcParticles[u].fYPos + (this.mcParticles[u + 1].fYPos - this.mcParticles[u].fYPos)/2);
            }
            this.mcParticles[u]._x = this.mcParticles[u].fXPos;
            this.mcParticles[u]._y = this.mcParticles[u].fYPos;
         }
         this.lineTo(Stage.width+50, Stage.height+50);
         this.lineTo(-50, Stage.height+50);
         this.lineTo(-50, Stage.height/2);
         this.endFill();
      }
   }
}

Ok, that's the code in. You could run it right now and you'd see a load of particles and it would behave as planned. Actually, if you delve into the particle MovieClip you can turn the alpha down to zero, or just delete the circle shape we made. However, it's nice to leave it as it will give you an idea of how it's all behaving under the hood. When you're done, of course, you may as well get rid of it.

4: Understanding the Code

Let's talk about the code. I'll start with the setEnds function and work my way down.

this.mcParticles = mcParticles;

for(var i:Number = 0; i < this.mcParticles.length; i++){
   this.mcParticles[i].fXPos = this.mcParticles[i]._x;
   this.mcParticles[i].fYAccel = 0;
   this.mcParticles[i].fYVel = 0;
   this.mcParticles[i].fYPos = this.mcParticles[i]._y;
   this.mcParticles[i].fBaseYPos = this.mcParticles[i]._y;
   this.mcParticles[i].iMass = 10;

The code sets acceleration, velocity and position for the particles. Note that it also stores the default, or base, position for the particle. We need this because we will want the particle to try to return to this position:

It’s also worth noting at this point that I’m only dealing with the particles in one dimension (the Y dimension or up and down), although the code will work just fine in the X dimension, too. You just combine the two dimensions and it will behave itself. But we do need the X position just so we can draw the line properly later on.

Our particles now have all the basic information to keep a track on where they are. How do we do the bit where the mouse can ‘knock’ the particles out of the way and cause some waves? Here’s how:

var mListener:Object = new Object();
mListener.daddy = this;
mListener.hitting = this.hitTest(_root._xmouse, _root._ymouse);
mListener.mYPos = _root._ymouse;
mListener.showParticles = false;
mListener.firstRun = true;
mListener.onMouseMove = function(){
   if(this.firstRun){
      this.hitting = this.daddy.hitTest(_root._xmouse, _root._ymouse);
      this.firstRun = false;
   }
   if(this.hitting != this.daddy.hitTest(_root._xmouse, _root._ymouse)){
      // work out mouse speed
      var iTarg:Number = Math.round((_root._xmouse/Stage.width)*(this.daddy.mcParticles.length-1));
      this.daddy.mcParticles[iTarg-2].fYVel = (_root._ymouse-this.mYPos)/6;
      this.daddy.mcParticles[iTarg-1].fYVel = (_root._ymouse-this.mYPos)/5;
      this.daddy.mcParticles[iTarg].fYVel = (_root._ymouse-this.mYPos)/3;
      this.daddy.mcParticles[iTarg+1].fYVel = (_root._ymouse-this.mYPos)/5;
      this.daddy.mcParticles[iTarg+2].fYVel = (_root._ymouse-this.mYPos)/6;
   }
   this.hitting = this.daddy.hitTest(_root._xmouse, _root._ymouse);
   this.mYPos = _root._ymouse;
}
Mouse.addListener(mListener);

So here we are using the mouse to control a particle’s velocity. The main area to take in is the onMouseMove function. If we’ve crossed over the boundary (marked by the fact that the hitting variable doesn’t match the hitTest – trace it out if you want to understand it a bit more) then we work out what the mouse’s speed is. We can then apply the velocity to the particle nearest to the mouse and it’s four neighbours, two either side. You could just apply it to one particle, but I found that you get a much nicer shape by applying it to the particle and its neighbours.

Ok, on to where the business end of this class starts:

// add a spring between each particle
for(var u:Number = 0; u < this.mcParticles.length-1; u++)this.mcParticleSprings.push(iLengthY:(this.mcParticles[u+1]._y - this.mcParticles[u]._y)});

What this code does is create a fictitious spring between each particle. We create a spring with a default Y length set to be the distance between two adjacent particles, in this case nothing. Seeing as we are developing a generic solution, however, we'll leave that alone and not assume that it will be nothing.

What this means is that however the particles start in relation to each other is considered as the base position, i.e. the position of rest. This, in combination with the fact that each particle knows its own base position, means that we can work out how far away the particles are from a) each other and b) their individual home positions.

Moving onto the constructor for the class, Wave():

this.onEnterFrame = function(){
   for(var u:Number = this.mcParticles.length-1; u >=0; u--){
      // work out what forces are applying
      var fExtensionY:Number = 0;
      var fForceY:Number = 0;

      if(u > 0){
         fExtensionY = this.mcParticles[u-1]._y - this.mcParticles[u]._y - this.mcParticleSprings[u-1].iLengthY;
         fForceY += -this.fK * fExtensionY;
      }
      if(u < this.mcParticles.length-1){
         fExtensionY = this.mcParticles[u]._y - this.mcParticles[u+1]._y - this.mcParticleSprings[u].iLengthY;
         fForceY += this.fK * fExtensionY;
      }

      fExtensionY = this.mcParticles[u]._y - this.mcParticles[u].fBaseYPos;
      fForceY += this.fK/15 * fExtensionY;

What the onEnterFrame does is go through each particle and work out, compared to its neighbours (one to the left and to the right), what extension has occurred on the springs between the particle itself and the neighbours in the Y direction.

Don’t forget that there is a spring between each two particles and that it has a default length. If all the particles start off at the same Y position, this spring length will be zero. Therefore, if any particle moves, the spring has extended by the distance between the two (again, only concerning the Y amount). Take a look at the image below to see what I mean:

Notice in the code that the index (in my code it’s u) must be greater than the zeroth particle to take into account the zeroth particle and it has to be less than the final particle to take into account the final particle. This is because the zeroth particle hasn’t got a neighbour to the left and the final particle is just that: final.

So you see in this bit:

if(u>0){
   fExtensionY = this.mcParticles[u-1]._y - this.mcParticles[u]._y - this.mcParticleSprings[u-1].iLengthY;
   fForceY += -this.fK * fExtensionY;
}

We are working out the force acting on a particle (u) based on the extension of the spring between it and its neighbour to the left (u-1). One other thing to note here is this.fK. This is the strength of the spring and is a number between 0 and 1. If this number is nearer 1, a larger proportion of the extension is used, i.e. A stronger force is applied to the particle. For more information on springs have a quick Google on Hooke’s Law.

Then we do the same calculation but with the particle to the right:

if(u<this.mcParticles.length-1){
   fExtensionY = this.mcParticles[u]._y - this.mcParticles[u+1]._y - this.mcParticleSprings[u].iLengthY;
   fForceY += this.fK * fExtensionY;
}

Finally we also add to the force that which is applied by the spring connecting the particle to its base position:

fExtensionY = this.mcParticles[u]._y - this.mcParticles[u].fBaseYPos;
fForceY += this.fK/15 * fExtensionY;

Notice here that I’ve diminished the strength of the spring by dividing it by 15 (this.fK/15). That means that it won’t settle down too fast, ruining the cascading effect. You can play with this number if you want the wave to settle down faster or slower.

OK, all my forces are calculated. Great. Now… err, what do we do with them?

Well we know that Force = mass x acceleration or, in our case acceleration = Force/mass. Remember that I set a mass when I first set up all the particles, so that’s fine. And we’ve just worked out the Force on each particle. So now we’re going to work out the acceleration:

this.mcParticles[u].fYAccel = -fForceY/this.mcParticles[u].iMass;

Easy. Just like the equation said. But acceleration doesn’t equate to a position, which is what we’re going to need if we want to position our particles correctly. If you want some further reading you can look into the relationship between acceleration, velocity and position. For this (and it may already be known and obvious to you) let’s just take it as read that:

Velocity is the first differential of Position with respect to time; Acceleration is the first differential of Velocity and is the second differential of Velocity, both with respect to time.

Yikes! What that means to us is that we can use our acceleration to change our velocity and our velocity to change our position. Think about a car. You hit the accelerator, the acceleration increases the velocity of the car which means that the position of the car changes.

this.mcParticles[u].fYVel += this.mcParticles[u].fYAccel;
this.mcParticles[u].fYPos += this.mcParticles[u].fYVel;

Great. We’ve now worked out what the position should be of our particles. That’s what we want, except that now we need to keep this change to ourselves a bit. Why? Look back up over the code. If you actually set the Y position of the particle (mcParticles[u]._y) you will affect the calculations for all the particles to the right, i.e. you will have applied this force BEFORE you’ve had chance to work out what the forces are that apply to the other particles. So this algorithm is a two-pass:

1st Pass: work out all the forces, accelerations, velocities and positions
2nd Pass: actually update all the particles to reflect the changes.

So for now we'll just set a variable in the Particle called fYPos. Sneaky in some ways and yet if you don't do it, the wave will look great moving in one direction and very weird in the other.

5: What’s left? Friction and connecting the dots

One of Sir Isaac’s other clever deductions was that a body would continue to move in the absence of other forces. So, if I were in space and was outside the Universe (hard to imagine, I know. Stick with me!) where there was no air particles and I threw a ball, it would continue on and on forever. Cool! Except I want my ball back. The fact is that back here on Earth there’s friction. Sometimes this takes the form of air resistance, but that's just friction caused by air particles hitting the object. What friction does is slow our particles back down to a zero velocity:

this.mcParticles[u].fYVel /= 1.04;

Really simple, huh? Just divide the Velocity by 1.xx (where xx is more than 00) and it will slow down of its own accord. The numbers in this example are just my choice. You should play around with them to see what I mean.

Ok, now it’s time to actually draw the wave.

First of all let’s see what it looks like if you just simply join the dots:

Ok, at least you can see that it looks roughly right. But crucially there are jagged points in the line, and it's clearly a dot to dot look. But we want pretty as well as correct, don’t we? So we need to draw it in a different way. I’m assuming here that you have an understanding of the curveTo function. If you don’t, take a look over the function’s description in Flash’s Help files.

this.clear();
this.lineStyle(1, 0x0, 100);
this.beginFill(0x0, 100);

I clear out the MovieClip of any past drawing (we don’t want the last frame’s wave to be still around), set a line style (optional) and begin filling in 100% black.

Next I go through the particles. If I am the dealing with the first one then I just move the pen but don’t draw. For every other particle I use the particle’s X and Y positions as
curveTo’s
control point and I use the halfway point between two particles as the anchor points (the destination for the curve).

Why do it like this? Well, if you don’t do this you’re going to get either a jagged wave like you got above by connecting the dots or you are going to have to connect each point in a very creative way to make the wave look natural. Here’s a picture of what the algorithm is actually doing:

So you can see it looks lovely and smooth. The code to do it looks like this:

for(var u:Number = 0; u < this.mcParticles.length; u++){ // use each particle as a control for the curve to and then use halfway points as actual anchors... took me AGES to work that out :-S
   if(u==0)
   this.moveTo(this.mcParticles[u].fXPos + (this.mcParticles[u + 1].fXPos - this.mcParticles[u].fXPos) / 2, this.mcParticles[u].fYPos + (this.mcParticles[u + 1].fYPos - this.mcParticles[u].fYPos) / 2);
   else if(u < this.mcParticles.length-1){
      this.curveTo(this.mcParticles[u].fXPos, this.mcParticles[u].fYPos, this.mcParticles[u].fXPos + (this.mcParticles[u + 1].fXPos - this.mcParticles[u].fXPos) / 2, this.mcParticles[u].fYPos + (this.mcParticles[u + 1].fYPos - this.mcParticles[u].fYPos) / 2);
   }

I’m also going to take this opportunity, as we are going through each particle to update the X and Y positions (as mentioned in the description of the 2nd Pass). This is just saving us another run through the particles if we do it here.

   this.mcParticles[u]._x = this.mcParticles[u].fXPos;
   this.mcParticles[u]._y = this.mcParticles[u].fYPos;
}

Finally we want to do a bit of house keeping and draw a box right round the edge and back to the first particle:

Personally, I like to add a bit of a buffer around the edge of the Stage just in case there were any rounding errors and what have you. It’s a personal choice, of course, but it does work!

this.lineTo(Stage.width+50, Stage.height+50);
this.lineTo(-50, Stage.height+50);
this.lineTo(-50, Stage.height/2);
this.endFill();

Now go back to the timeline and add this after our particle / wave code:

mcWave.setEnds(particles);
mcWave.activate();

And that’s it.

6: Conclusion

I hope you’ve enjoyed this tutorial and that it’s opened a door for some of you who may be overwhelmed by the physics involved. You can hopefully see that it’s actually quite easy to do, once you think about the forces you need.

If you like, you can download the files used in the tutorial here.

- discuss this tutorial -
 
©2006 Ultrashock.com - All rights reserved