Simulating Gravity in Flash

I’ve got some kind of compulsory disorder. Sometimes I just have to do some simulations, play with physics and get a break from normal web development. I haven’t had an “episode” in over two years now (view the last one), but a few days ago all this pent up need just had to be released and this tutorial is the uber-geeky result. Just touch the spheres with the mouse pointer, not too fast or you’re gonna break out of orbit!

So how do we accomplish the above? The answer is modified physical laws plus some equation solving and scripting, we’ll go through everything.

Let’s start with the AS in the .fla file (from top to bottom):

var oldPoint = new V2D(0, 0);
var curPoint = new V2D(0, 0);
var mouseSpeed = new V2D(0, 0);
var maxSpeed = 8;
var moveInterval = 50;

We initiate some globals to control various things:

oldPoint: The position the mouse pointer had in the previous frame in time. The object keeping track of this is a V2D from 2 Dimensional Vector. A vector is basically just an X value and an Y value which will result in a direction and length/power. We’ll get to the details of this one shortly.

curPoint: The current position of the mouse pointer.

mouseSpeed: With the help of the oldPoint and the curPoint we can calculate the speed and direction of the mouse pointer and store the result in mouseSpeed. This value will be used to calculate in which direction and with which speed a sphere shall move after being hit by the mouse pointer.

maxSpeed: With some imagination this value could be thought of as the speed of light. It’s a dampening value that will be applied to moving objects to dampen their oscillations, it would be pretty boring if they broke loose from their wells after like 2 seconds.

moveInterval: This is the amount in time frames that has to pass before the mouse can hit a particular sphere again after hitting it. Set it to 1 and you’ll see why we need it.

var objects = new Array(
	new AdvObj(circle0), 
	new AdvObj(circle1)
);

var wells = new Array();
for(i in objects){
	wells[i] = (new GravityWell(objects[i], _root["well"+i]._x, _root["well"+i]._y, 2, 1e9, 1000));
	objects[i].moving = -1;
}

We will store the objects we want to be affected by the gravity wells in an array we call objects, they will be of type AdvObj. Circle0 and 1 refers to the movieclip instances on the stage, I named them in the properties area. Next we create our wells by looping through our spheres/circles and passing them as the first argument to the GravityWell constructor (we will go through the GravityWell class shortly).

They will be mapped to the positions of two invisible movieclips on stage called well0 and 1. The final three arguments are the power we want the gravity to diminish by as a function of the distance (it’s 2 in the real universe).

The second last argument would constitute m1*m2 in the gravity formula and the final argument is a value that we manipulate the behavior of our gravity with, this last value will be involve in some logic that breaks the natural order of things. It’s all for the best though as you will see…

this.onEnterFrame = function(){
	curPoint.set(_xmouse, _ymouse);
	mouseSpeed = curPoint.subtract(oldPoint);
	handleObjects();
	oldPoint.from(curPoint);
}

The code that is being run on every frame (25 fps). All lines except handleObjects() have to do with tracking the mouse speed.

function handleObjects(){
	for(i in objects){
		var o = objects[i];
		if(o.clip.hitTest(_xmouse, _ymouse, true) && o.moving < 0){
			o.speedFrom(mouseSpeed);
			o.moving = moveInterval;
			o.started = true;
		}else if(o.started == true){
			wells[i].pull();
			o.simulate();
			o.moving--;
			if(o.speed.length() > maxSpeed){
				o.speed.lengthTo(maxSpeed - 0.1);
				o.acc.fromOne(0);
			}
		}
	}
}

As you might have suspected already; we loop through all affected objects and store them in the temporary variable o, just like above. Next we check if the mouse pointer is on top of them or not, also if the moving counter is below zero or not, if it is and we are hitting the object we proceed with giving the object the same speed as the mouse, we set the moving interval to the moveInterval global we covered before and set started to true.

If started is true we can start to simulate. Each well will pull on it’s assigned object which means that they only affect one object at a time, also very unnatural, however simulating the pull from several wells at once would quickly become very computationally intensive.

Each object is then responsible for simulating its own movement, hence o.simulate(), next we count down moving, as you can see with the value of 50 we have set the interval to it’s only possible to hit the spheres every 2 seconds.

Next it’s time to check if the current speed of each object is bigger than the max speed, if it is we lower it to a little bit lower than the maxSpeed value, we also completely reset the acceleration. It makes no sense to let the object’s acceleration stay the same as it will affect the speed which we are trying to push lower. The reason we are doing this is if we didn’t the objects would oscillate wildly and disappear out of screen pretty quickly.

During the development of this simulation it has really struck me how few objects there are in the universe that are actually in orbit around something. Most must be deflected, flying aimlessly through space, the rest have been absorbed.

Let’s take a look at the GravityWell class first and its pull method.

class GravityWell{
	var pos:V2D;
	var tgt:AdvObj;
	var power:Number;
	var factor:Number;
	var modifier:Number;
	function GravityWell(tgt:AdvObj, x, y, power:Number, factor:Number, modifier:Number){
		this.pos = new V2D(x, y);
		this.tgt = tgt;
		this.power = power;
		this.factor = factor;
		this.modifier = modifier;
	}
	
	function pull(){
		var acc:V2D = this.pos.subtract(tgt.ctrlPoint);
		var dist:Number = tgt.ctrlPoint.dist(this.pos);
		acc.divideScal(Math.pow(dist, this.power));
		acc.multi(this.factor * 6.67e-11);
		
		/* Unlawful modification start*/
		if(HSMath.alike(acc.x, tgt.acc.x) == false)
			acc.x *= modifier;
			
		if(HSMath.alike(acc.y, tgt.acc.y) == false)
			acc.y *= modifier;
		/* end */
		
		tgt.accMe(acc);
	}
}

We’ve already covered the arguments to the constructor. In pull() we will first create the current acceleration vector by subtracting the position of the affected target (tgt.ctrlPoint) from the position of the well. The result will be a vector pointing back towards the well, this is the vector that will be added to the total acceleration of the object on this frame in time. We also get the distance between the two objects.

Next we perform the gravity equation by diminishing the acceleration as a function of the distance and the passed power variable (in our case it’s 2). We also need to increase the acceleration as a function of the mass of the object and well (in this case 1e9), 6.67e-11 is the gravitational constant.

Next comes our modification of the natural laws, basically we check if we are moving towards or away from the object, if we are moving away from it we increase its attractional power (in this case by a factor of 1000). If we didn’t do this the chances of establishing any kind of orbit would be virtually zero, stuff would simply be deflected and never come back. Finally we increase/decrease the acceleration of the object by this timeframe’s acceleration.

In fact, this is not correct either, try playing around with the code by changing tgt.accMe(acc); to tgt.acc.from(acc);, if you do that you need to change the last three parameters in the call to the GravityWell constructor to something like 2, 1e12 and 1. One there will completely nullify the unlawful behavior and you will have a true simulation of Gravity (Newtonic mind you).

In the AdvObj:

function simulate(){
  this.speedMe();
  this.moveMe();
  this.drawMe();
}

function drawMe(){
  this.setVpos(this.curPoint);
}

function moveMe(){
  this.ctrlPoint.inc(this.speed);
  this.curPoint.inc(this.speed);
}

function speedMe(){
  this.speed.inc(this.acc);
}

function accMe(a:V2D){
  this.acc.inc(a);
}

The simulation begins with an increase of the speed by the acceleration, next we move the position by the speed. Disregard the fact that we are having two points here (ctrlPoint and curPoint). In some dynamics the curPoint is used as the objects true position and the ctrlPoint functions as the “system’s” center. This will allow some affectors to cause the object to have eccentricity, check the prior example for instance. There the bubbles’ curPoint oscillate around the ctrlPoint which is not affected by the wave movement.

We’re getting side tracked, back to drawMe() which finally moves the object’s movieclip representation to the position indicated in the call to moveMe().

Let’s get back to the speed limitation that is handled by lengthTo() in V2D:

function lengthTo(limit:Number){
  var l = this.length();
  var dl =  Math.abs(l - limit);
  var dx = HSMath.lengthChangeToSide(dl, l, this.x);
  var dy = HSMath.lengthChangeToSide(dl, l, this.y);
  if(limit < l){
    this.x -= HSMath.getNegPos(this.x) * dx;
    this.y -= HSMath.getNegPos(this.y) * dy;
  }else{
    this.x += HSMath.getNegPos(this.x) * dx;
    this.y += HSMath.getNegPos(this.y) * dy;
  }
}

First we get the actual length of the vector, we calculate the difference in length (dl). The crux now is to determine by which fraction to decrease the x and y-component, we can’t just decrease them with say 30% if the length of the vector is supposed to be decreased by that amount. Doing that would result in the wrong value.

To make a long story short, the dx and dy values relate to the original system in a way that gives you several equations for relating them to known variables. Since you have more than one equation you can do a variable substitution in one through the other to get an equation with only one unknown variable, for instance dx. Solving that equation was a real bitch but I barely managed within the confines of an A4 :) The solution is in HSMath.lengthChangeToSide():

static function lengthChangeToSide(dl:Number, l:Number, side:Number):Number{
  var roof = Math.pow(side, 2) * Math.pow(dl, 2);
  var cellar = Math.pow(l, 2) * (dl + 2 * Math.pow(side, 2));
  return Math.sqrt(roof / cellar);
}

Yes I’m sure I could’ve reduced it further but I want quick results. This is geeky but there are limits, I’m not going to sit around all day prettifying some equation when I’ve got something that works. So there it is in all its ugliness. The side is the original x or y-value, l the original length (hypotenuse), and dl the value that l is to be changed by.

The getNegPos function simply returns -1 if the argument is negative, 1 if it’s positive and 0 if 0. We use it to determine whether or not to use a negative value to subtract/add with. If for instance the limit is shorter than the length we have to deduct, but if we do -5 - 4 for instance, which could be the case if the vector is pointing left, we get -9 and that is not right, the call to getNegPos would in that case result in -5 - -4, much better since that would give us -1, a reduction in length.

Note that there is some unused stuff in the source, unused as not used in this tutorial, still used by other forces and various affectors in other projects though, the most important stuff with respect to gravity should have been covered above though, feel free to explore on your own.

Gravity Well Source

Isn’t physics and math fun!?

Related Posts

Tags: , , , , ,