The study of forces is of central interest in dynamics, the study of causes of motion and changes in motion. Gravitational force is one example; it is this which causes satellites to revolve around planets and us to stay on the ground.
In this tutorial, we will build a simulation of such phenomenon and be able to observe, experiment and play with particles on the scene.
Among all particles generated, one main particle will attract others. As these particles move towards the main one, users can click on this main particle to drag it around, causing these particles to redirect their course. These particles will stop moving as they collide with the edge of main ball, but they won't overlap each other.
The structure of this tutorial is arranged in a manner where a brief theory in Physics in delivered before introducing the implementation of the simulation. Enjoy!
Final Result Preview
Let's take a look at the final result we will be working towards:
Click and drag the big green circle to move it around, and watch how the little blue circles react.
Step 1: Gravitational Force, Formula
First, a Physics preface. The attractive gravitational force between any two objects is expressed through the following formula:
F: attractive force being exerted onto object of interest (p2) by
an arbitrary particle (p1).
G: Gravitational constant
m1: mass of p1
m2: mass of p2
r: distance between p1 and p2
Take special note of the following:
- The relationship between gravity and distance: F is inversely proportional to the square of the distance separating the two particles. This means the closer A and B are to each other, the higher the attractive force between them and vice versa. If you double the distance, the force goes down to a quarter of its original value.
- Value of gravitational constant, G, is scientifically 6.67259 x 10-11 N * m2 / kg2. However, 500 will substitute this value in Flash environment.
- We can relate particle’s width to their mass. For this example, I’ve defined a particle’s mass to be half of its radius.
Step 2: Newton's 2nd Law, Equation
In order to translate force into kinematics, we need to calculate particle’s acceleration. The famous equation by Sir Isaac Newton is shown below:
F: gravitational force being exerted onto object of interest (p2)
m: mass of object of interest (p2)
a: acceleration of object of interest (p2) under influence of F
Here, the implication is that a higher force applied onto particle A results in a higher acceleration (assuming its mass stays the same). This acceleration will change the particle’s velocity.
Step 3: Starting Project
Implementation will be done in FlashDevelop IDE. Build your project file.
- Start a new Project, PROJECT > NEW PROJECT…
- Select from pop up window, AS3 PROJECT
- Name your project. In my case, Attractor
- Select your project location
See this guide for an introduction to FlashDevelop.
Step 4: Classes You Need
There are 4 classes to create in folder \src\: Main.as, Vector2D.as, Ball.as & Math2.as. It is advisable that you download all these files from the source package and try to map them against steps to come to get a general understanding of the mechanism before modifying them. Their roles are expressed as below:
Class Name | Purpose of Organisation |
Main.as | Class to create the balls visually and to attach animation to events. |
Vector2D | Class that holds all vector manipulation functions. |
Ball | Class that contains functions to visually generate a ball, implements dynamics and kinematics of a ball. |
Math2 | Static class that holds a function to facilitate randomizing the initial location of balls. |
Step 5: Randomising Values
Lets talk about the Math2 class first. The function below will help to generate a random number within the specified range. Accepts two inputs, minimum limit and maximum limit in range.
public static function randomiseBetween(range_min:int, range_max:int):int { var range:int = range_max - range_min; var randomised:int = Math.random() * range + range_min; return randomised; }
Step 6: Vector2D, Getters and Setters
The bulk of Math used is located in Vector2D. This tutorial assumes a level of familiarity in vector analysis in the users. The functions below are generally used to get and set vector components, plus a method to reset all components to zero. In any case, if you are uncomfortable with Vectors, visit a great post on Euclidean Vectors by Daniel Sidhion.
public function Vector2D(valueX:Number, valueY:Number) { this._x = valueX; this._y = valueY; } public function set vecX(valueX:Number):void { this._x = valueX; } public function get vecX():Number { return this._x } public function set vecY(valueY:Number):void { this._y = valueY; } public function get vecY():Number { return this._y } public function setVector(valueX:Number, valueY:Number):void { this._x = valueX; this._y = valueY; } public function reset():void { this._x = 0; this._y = 0; }
Step 7: Vector2D, Operations
The major uses of Vector2D lie in the following functions, which:
- obtain the magnitude of vector
- obtain the angle made by vector in relation to the origin
- obtain the vector direction of vector
- perform simple vector operations of addition, subtraction and scalar
multiplication
public function getMagnitude():Number { var lengthX:Number = this._x; var lengthY:Number = this._y; return Math.sqrt(lengthX * lengthX +lengthY * lengthY); } public function getAngle():Number { var lengthX:Number = this._x; var lengthY:Number = this._y; return Math.atan2(lengthY, lengthX); } public function getVectorDirection():Vector2D { var vectorDirection:Vector2D = new Vector2D(this._x / this.getMagnitude(), this._y / this.getMagnitude()); return Vector2D(vectorDirection); } public function minusVector(vector2:Vector2D):void { this._x -= vector2.vecX; this._y -= vector2.vecY; } public function addVector(vector2:Vector2D):void { this._x += vector2.vecX; this._y += vector2.vecY; } public function multiply (scalar:Number):void { this._x *= scalar; this._y *= scalar; }
Step 8: Ball.as Drawing
The Ball
class is where all the interesting operations take place. To begin our animation, we need to draw a ball and set several kinematics- and dynamics-related variables. The function to draw a ball is as below:
private function draw(radius:Number, color:uint) :void { graphics.beginFill(color, 1); graphics.drawCircle(0, 0, radius); graphics.endFill(); }
Step 9: Ball.as Private Variables
The mentioned several kinematics and dynamics related variables are stated as below:
private var _disp:Vector2D; //displacement vector, relative to the origin private var _velo:Vector2D; //velocity vector private var _acc:Vector2D; //acceleration vector private var _attractive_coeff:Number = 500; private var _mass:Number;
Step 10: Ball.as Initiation
As the constructor of Ball class is called, graphics is drawn. Once drawn, the ball will be placed on the stage randomly. We will also set the private variables. All vector quantities will also be initialized at 0, except for the displacement which is measured relative to origin.
public function Ball(radius:Number = 20, color:uint = 0x0000FF) { this.draw(radius, color); this._mass = radius / 2; //assuming mass is half of radius this.x = Math2.randomiseBetween(0, 550); this.y = Math2.randomiseBetween(0, 400); this._disp = new Vector2D(this.x, this.y); //set initial displacement this._velo = new Vector2D(0, 0); this._acc = new Vector2D(0, 0); }
Step 11: Ball.as Calculate Attractive Force
We need to calculate the underlying force that causes our particles to animate. Guess what, it's the gravitational force. The function below will help in calculating this force. Note that a cap is applied on the acceleration at 5
. The horizontal and vertical components of force are derived using trigonometry; the animation above may help in understanding the mathematics of this.
public function get mass():Number { return _mass; } private function getForceAttract (m1:Number, m2:Number, vec2Center:Vector2D):Vector2D { /* calculate attractive force based on the following formula: * F = K * m1 * m2 / r * r */ var numerator:Number = this._attractive_coeff * m1 * m2; var denominator:Number = vec2Center.getMagnitude() * vec2Center.getMagnitude(); var forceMagnitude:Number = numerator / denominator; var forceDirection:Number = vec2Center.getAngle(); //setting a cap if (forceMagnitude > 0) forceMagnitude = Math.min(forceMagnitude, 5); //deriving force component, horizontal, vertical var forceX:Number = forceMagnitude * Math.cos(forceDirection); var forceY:Number = forceMagnitude * Math.sin(forceDirection); var force:Vector2D = new Vector2D(forceX, forceY); return force; }
Step 12: Ball.as Calculate Acceleration
Once force vector has been obtained, we can calculate the resulting acceleration. Remember, F = ma
, so a = F/m
.
public function getAcc(vecForce:Vector2D):Vector2D { //setting acceleration due to force var vecAcc:Vector2D = vecForce.multiply(1 / this._mass); return veccAcc; }
Step 13: Ball.as Calculate Displacement
With acceleration calculated, we can effectively calculate the resulting displacement.
Remember that force calculated is actually based upon the displacement between the center of the balls.
private function getDispTo(ball:Ball):Vector2D { var currentVector:Vector2D = new Vector2D(ball.x, ball.y); currentVector.minusVector(this._disp); return currentVector; } public function attractedTo(ball:Ball) :void { var toCenter:Vector2D = this.getDispTo(ball); var currentForceAttract:Vector2D = this.getForceAttract(ball.mass, this._mass, toCenter); this._acc = this.getAcc(currentForceAttract); this._velo.addVector(this._acc); this._disp.addVector(this._velo); }
Step 14: Ball.as Implement Displacement
Then, we are able to move our ball to its new location, through the function below. Note that displacement calculated is never implemented onto the ball's current location straightaway. Such design is to allow checking be done: collision detection between balls.
public function setPosition(vecDisp:Vector2D):void { this.x = Math.round(vecDisp.vecX); this.y = Math.round(vecDisp.vecY); }
Remember, the force is based upon distance between centers. Therefore, the balls will penetrate and continue moving due to attractive force being higher when they are closer. We need to reset acceleration and velocity to 0 when the balls touch one another’s edge. However, we need to obtain a means of detecting the collision between two balls.
Step 15: Ball.as Collision Detection
Collision can be easily checked. Separation between any two balls should not be less than the sum of their radii. Here’s the collision detection function:
public function collisionInto (ball:Ball):Boolean { var hit:Boolean = false; var minDist:Number = (ball.width + this.width) / 2; if (this.getDispTo(ball).getMagnitude() < minDist) { hit = true; } return hit; }
Step 16: Ball.as Calculate Displace to Repel
Usually when collision has been detected between two balls, their state is overlapping each other. We need to make sure that they will just sit nicely on the edge and not overlap. How? We can displace one of the balls away from the other, but we need to calculate the right displacement to adjust first. Here’s the displacement calculation:
public function getRepel (ball:Ball): Vector2D { var minDist:Number = (ball.width + this.width) / 2; //calculate distance to repel var toBall:Vector2D = this.getDispTo(ball); var directToBall:Vector2D = toBall.getVectorDirection(); directToBall.multiply(minDist); directToBall.minusVector(toBall); directToBall.multiply( -1); return directToBall; }
Step 17: Ball.as Implement Displacement to Repel
After we have calculated the right displacement, we need to implement it. The action is like repelling one of the balls. In addition, we need to do another two extra commands. Remember, we are dealing with a dynamic environment. Even after we have set the displacement one of the ball to the edge, acceleration due to force and the resulting velocity will animate it, causing an undesirable movement of jerking in and out. We need to reset these values of acceleration and velocity to zero.
public function repelledBy(ball:Ball):void { this._acc.reset(); this._velo.reset(); var repelDisp:Vector2D = getRepel(ball); this._disp.addVector(repelDisp); }
Step 18: Ball.as Animate
Finally, we can animate (render) our ball as if it were being attracted by another. When collision is detected, displacement will be adjusted so that it will not penetrate the edge. This will happen first for the balls when they collide with center, and then for the balls when they collide with one another.
public function animate(center:Ball, all:Array):void { this.attractedTo(center); if (collisionInto(center)) this.repelledBy(center); for (var i:int = 0; i < all.length; i++) { var current_ball:Ball = all[i] as Ball; if (collisionInto(current_ball) && current_ball.name != this.name) this.repelledBy(current_ball); } this.setPosition(this._disp); }
Step 19: Main.as Private Variables
Moving on to our last class, Main
. Main class is generated at the start of the project. Private variables will include the one ball that attracts all the others and the number of balls in our Flash presentation.
private var mainBall:Ball; private var totalBalls:int = 10;
Step 20: Main.as Draw Balls
First of all, we should initialize balls. There will be one main ball that attract all the others. The others are named so that referencing can be easily done later.
private function createBalls ():void { mainBall = new Ball(50, 0x00FF00); this.addChild(mainBall); for (var i:int = 0; i < this.totalBalls; i++) { var currentBall:Ball = new Ball(); currentBall.name = "ball" + i; this.addChild(currentBall); } }
Step 21: Main.as Implement Ball Interaction
Then, assign events to the main ball to make it draggable when clicked and stop when released.
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point createBalls(); mainBall.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); animateAll(); } private function onMouseUp(e:MouseEvent):void { stopDrag(); } private function onMouseDown(e:MouseEvent):void { e.target.startDrag(); }
Step 22: Main.as Animate Balls
Animating balls that are being attracted by the main. An EnterFrame event is assigned to each ball.
private function animateAll():void { for (var i:uint = 0; i < totalBalls; i++) { //each ball is pulled by main_ball var current_ball:Ball = this.getChildByName("ball" + i) as Ball; current_ball.addEventListener(Event.ENTER_FRAME, enterFrame); } } private function enterFrame(e:Event):void { var allObj:Array = new Array(); for (var i:int = 0; i <= totalBalls; i++) { var current_ball:Ball = this.getChildAt(i) as Ball; allObj.push(current_ball); } e.target.animate(mainBall, allObj); }
Step 23: Test Movie
Finally, press Ctrl + Enter to preview the animation.
Conclusion
To bring this tutorial one step further, readers may extend this project by implementing other linear forces.
In any case, simulations serve as a great tool in delivering ideas difficult to explain by plain text and image in a Physics classroom environment, especially when the state changes over time.
I hope this little tutorial helps you in some way. Terima kasih (that is "thank you" in Malaysia) for taking time to read and looking forward to hearing comments from fellow readers.
Comments