研究物理动力学中的力学,是很有趣且蛮重要的,这节课主要研究力学中的运动以及变化。万有引力就是个例子:它能使卫星围绕着地球运动,并且能让我们站立在地球上。
在这节教程中,我们将要创造一个模拟自然现象,并且让其遵守自然现象的规律,试着在屏幕上玩粒子游戏。
在所有生成出来的粒子中间,有一个主要的大粒子来吸引其他的小粒子。由于这些小粒子朝着大粒子移动,用户可以单击大粒子并来回拖动它,这会使这些小粒子不断改变他们的运动轨迹。
当这些小粒子撞到大粒子的边缘线时,它们就停止运动,并且它们不会和其他粒子重叠在一起。
这篇教材的所写的构架内容,介绍了如何执行模拟,都是利用简单的物理原理来编写实现的
步骤1:万有引力公式
首先,看一看物理公式。两个物体之间相互吸引产生的吸引力,由下面的公式表述:
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:牛顿第二定律公式
为了把力转化为动力,我们需要计算出粒子的加速度。牛顿的著名公式是这么写的:
F: 作用于相互物体间的引力(p2)
m: 运动对象的质量(p1);
a: 受力(F)影响的运动对象(p2)的加速度;
从这看出,一个更大的力作用在粒子A上会导致一个更快的加速度(假设质量保持不变)。这个加速度会改变粒子的速率。
步骤3: 开始工程
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;
步骤20:Main.as绘制小球
首先,我们应该把球初始化。这里有一个主要的大球来吸引其他的小球。把其他小球命好名称来方便于引用。
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);
}
}
步骤21: Main.as执行小球相互作用
然后,给主要的大球分配事件侦听,使它单击鼠标的时候拖曳当释放鼠标的时候停止。
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();
}
步骤22:Main.as 小球的动画
使小球产生动画,让它们被大球吸引。把一个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);
}
步骤23:测试影片
最终,按下Ctrl + Enter观看动画效果吧。
结论
进一步来讲一下这个教程,读者也许通过运用线性力学来扩展了这个工程。
无论如何,模拟提供了一个传达思想的很好的手段,通过简单的图形和文本来解释这样一个复杂的物理学环境课程,特别是问题花费太多时间的时候。
我希望这小教程能给你带来一定的帮助。Terima Kasih审阅了这篇文章,也期待各位读者的评论。