在這節教程中,我們將要創造一個模擬自然現象,並且讓其遵守自然現象的規律,試着在屏幕上玩粒子游戲。
在所有生成出來的粒子中間,有一個主要的大粒子來吸引其他的小粒子。由於這些小粒子朝着大粒子移動,用戶可以單擊大粒子並來回拖動它,這會使這些小粒子不斷改變他們的運動軌跡。
當這些小粒子撞到大粒子的邊緣線時,它們就停止運動,並且它們不會和其他粒子重疊在一起。
這篇教材的所寫的構架內容,介紹瞭如何執行模擬,都是利用簡單的物理原理來編寫實現的
首先,看一看物理公式。兩個物體之間相互吸引產生的吸引力,由下面的公式表述:
F:物體(p2)作用給任意一個粒子(p1)的吸引力
G:引力常量
m1: p1的質量;
m2: p2的質量;
r: p1到p2間的距離
特別在下面做些筆記:
- 引力與間距之間的關係: F 是和分開粒子之間距離的平方成反比的。這意味着A和B的距離越近,那麼吸引力就越大,反之亦然。如果你把距離的值乘以一倍,那麼力的值就會降爲它本來的值的四分之一。
- 引力常數的值——G,科學數值是6.67259 x 10-11 N * m2 / kg2。然而,在Flash環境下,它的值會被500所替換。
- 我們能把粒子的寬度和它們的質量扯上聯繫。在這個例子中,我已經定義了粒子的質量是它半徑的一半。
步驟2:牛頓第二定律公式
爲了把力轉化爲動力,我們需要計算出粒子的加速度。牛頓的著名公式是這麼寫的:
m: 運動對象的質量(p1);
a: 受力(F)影響的運動對象(p2)的加速度;
從這看出,一個更大的力作用在粒子A上會導致一個更快的加速度(假設質量保持不變)。這個加速度會改變粒子的速率。
步驟3: 開始工程
在FlashDevelop IDE來執行工程會更好。構建你的工程文件。
1.新建工程項目 > 新工程
2.選擇窗口上方的,AS3工程
3.命名工程文件,按照我的,給它命名爲Attrator
4.選擇你的工程文件位置。
步驟4:你需要的類
有四個類需要創建在\src\文件夾中:Main.as,Vector2D.as,Ball.as還有Math2.as.你也可以從資源包裏下載所有這些類,並且試着一步一步去了解這些類的原理,爲了以後好修改這些類。
它們的作用如下:
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. |
步驟5:隨機設值
讓我們先說說Math2類吧。下面這個函數會生成一個隨機數,在指定的範圍之內。接受兩個輸入值,minimun和maximun來限制範圍。
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;
}
步驟6:Vector2D,獲取與設定
大多數要用的數學都位於Vector2D中。對讀者而言,這篇寫向量分析的文章會比較熟悉。下面的函數是用來設定和給予向量的分量值的,增加了一個使所有分量值重新爲0的方法。
無論如何,如果你對Vector感到彆扭,請看看一個很好的帖子: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;
}
步驟7: Vector2D的操作
Vector2D的主要用法,在於以下功能:
* 獲取向量的大小值
* 獲取向量原點位置的角度
* 獲取向量的向量方向
* 執行向量的加法運算,減法運算,以及乘法運算。
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;
}
步驟8:Ball.as 繪製
Ball類是所有的有趣的運算產生的地方。爲了我們動畫的開始,我們需要畫一個小球並且設置幾個與動力學(還有力學)有關的變量。繪製小球的方法如下:
private function draw(radius:Number, color:uint) :void
{
graphics.beginFill(color, 1);
graphics.drawCircle(0, 0, radius);
graphics.endFill();
}
步驟9: Ball.as 局部變量
所提及的與動力學和力學有關的變量,可以開始定義了,如下:
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
步驟10:Ball.as 初始化
因爲Ball類的構造函數被調用了,因此圖形也畫好了。一旦畫好,小球就會被隨機擺放在舞臺上。我們也能設置局部變量。所有向量的數量也會初始化設置爲0,除了相對位移測量的起點。
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);
}
步驟11:Ball.as計算吸引力
我們需要計算出使得粒子運動的潛在的力。猜猜,爲什麼它是萬有引力。下面的函數有助於計算力。注意那個帽子,應用了加速度的值爲5.
力的水平和垂直分量,利用三角構形,得到力的大小;通過上面的動畫會幫你理解這些數學知識。
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;
}
步驟12:Ball.as 計算加速度
一旦得到向量的力,我們能計算出加速度的結果。記住,F = ma, 所以 a = F/m.
public function getAcc(vecForce:Vector2D):Vector2D
{
//setting acceleration due to force
var vecAcc:Vector2D = vecForce.multiply(1 / this._mass);
return veccAcc;
}
步驟13:Ball.as 計算位移
利用計算出的加速度,我們可以有效地計算出位移的結果
記住,那些計算出的力,實際上是建立在小球的各自中心點間的位移間距上的。
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);
}
步驟14: Ball.as 執行位移
然後,通過以下的函數,我們能把小球移動到新的位置上。注意,位移絕不會在小球當前位置上立刻執行。這樣設計是爲了更好處理:球之間的碰撞檢測。
public function setPosition(vecDisp:Vector2D):void
{
this.x = Math.round(vecDisp.vecX);
this.y = Math.round(vecDisp.vecY);
}
記住,力是建立在物體各自的中心點之間的距離上的。因此,當小球間距離越來越近,引力會越來越大,它們會繼續運動且滲透在一起的。
當小球邊緣撞擊到另一個小球邊緣的時候,我們需要重新設置它們加速度和速率爲0。我們需要得到一個檢測兩個球撞擊的方法。
步驟15:Ball.as撞擊檢測
撞擊能容易檢測。任意兩個單獨小球之間距離不能比他們的半徑的總和小。下面是撞擊檢測函數:
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;
}
步驟16:Ball.as 計算位移至排斥
通常兩個球之間的撞擊被檢測到的時候,它們的狀態是重疊在一起了。我們需要確保它們坐落在邊緣上並且不會相互覆蓋。
怎樣做呢?我們可以把球從另一個球之間距離位移一段,但是我們首先需要計算正確的位移。這裏是位移的計算:
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;
}
步驟17:Ball.as 執行位移到排斥
當我們計算好正確的位移之後,我們需要執行它。程序要讓小球互斥,我們需要額外做兩個指令。記住,我們要處理一個動力學的環境。即便我們把小球位移到小球邊緣的時候,由於作用力以及碰撞造成的速率,會改變加速度,造成
不理想的裏外顛簸。我們需要重新設置加速度的和速率的值爲0.
public function repelledBy(ball:Ball):void
{
this._acc.reset();
this._velo.reset();
var repelDisp:Vector2D = getRepel(ball);
this._disp.addVector(repelDisp);
}
步驟18: Ball.as 賦予動畫
最後,要想讓它們互相吸引,我們要把我們的小球賦予動畫(渲染)。當碰撞檢測時,位移會調節到以至不會穿透邊界。這會在他們用的中心點間距撞擊時發生,每一個小球之間的碰撞都這樣。
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);
}
步驟19:Main.as局部變量
來到我們最後一個類:Main.Main類在工程一開始就產生。局部變量包含了一個吸引其他小球的主球,以及所有球的數量,一切都在Flash展示畫面中。
private var mainBall:Ball;
private var totalBalls:int = 10;
首先,我們應該把球初始化。這裏有一個主要的大球來吸引其他的小球。把其他小球命好名稱來方便於引用。
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);
}
}
然後,給主要的大球分配事件偵聽,使它單擊鼠標的時候拖曳當釋放鼠標的時候停止。
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();
}
使小球產生動畫,讓它們被大球吸引。把一個EnterFrame事件分配在每個小球當中。
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);
}
最終,按下Ctrl + Enter觀看動畫效果吧。
進一步來講一下這個教程,讀者也許通過運用線性力學來擴展了這個工程。
無論如何,模擬提供了一個傳達思想的很好的手段,通過簡單的圖形和文本來解釋這樣一個複雜的物理學環境課程,特別是問題花費太多時間的時候。
我希望這小教程能給你帶來一定的幫助。Terima Kasih審閱了這篇文章,也期待各位讀者的評論。