JavaScript怎么实现碰撞物理引擎
本文小编为大家详细介绍“JavaScript怎么实现碰撞物理引擎”,内容详细,步骤清晰,细节处理妥当,希望这篇“JavaScript怎么实现碰撞物理引擎”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。
效果图:
接下来看一下怎么实现这样的效果。
基础结构
我们这里使用canvas来实现JavaScript物理引擎。首先准备项目的基础文件和样式,新建一个index.html、index.js和style.css文件,分别用于编写canvas的html结构、引擎代码和画布样式。
在index.html的<head/>
标签中引入样式文件:
<linkrel="stylesheet"href="./style.css"/>
在<body />
中,添加 canvas 元素、加载 index.js 文件:
<main><canvasid="gameboard"></canvas></main><scriptsrc="./index.js"></script>
这段代码定义了id
为gameboard
的<canvas/>
元素,并放在了<main/>
元素下,<main/>
元素主要是用来设置背景色和画布大小。在<main/>
元素的下方引入index.js文件,这样可以在DOM加载完成之后再执行JS中的代码。
style.css中的代码如下:
*{box-sizing:border-box;padding:0;margin:0;font-family:sans-serif;}main{width:100vw;height:100vh;background:hsl(0deg,0%,10%);}
样式很简单,去掉所有元素的外边距、内间距,并把<main/>
元素的宽高设置为与浏览器可视区域相同,背景色为深灰色。
绘制小球hsl(hue,saturation,brightness)为css颜色表示法之一,参数分别为色相,饱和度和亮度。
接下来绘制小球,主要用到了canvas相关的api。
在index.js中,编写如下代码:
constcanvas=document.getElementById("gameboard");constctx=canvas.getContext("2d");canvas.width=window.innerWidth;canvas.height=window.innerHeight;letwidth=canvas.width;letheight=canvas.height;ctx.fillStyle="hsl(170,100%,50%)";ctx.beginPath();ctx.arc(100,100,60,0,2*Math.PI);ctx.fill();
代码中主要利用了二维context进行绘图操作:
通过canvas的id获取canvas元素对象。
通过canvas元素对象获取绘图context,getContext()
需要一个参数,用于表明是绘制2d图像,还是使用webgl绘制3d图象,这里选择2d。context就类似是一支画笔,可以改变它的颜色和绘制基本的形状。
给canvas的宽高设置为浏览器可视区域的宽高,并保存到width
和height
变量中方便后续使用。
给context设置颜色,然后调用beginPath()
开始绘图。
使用arc()
方法绘制圆形,它接收5个参数,前两个为圆心的x、y坐标,第3个为半径长度,第4个和第5个分别是起始角度和结束角度,因为arc()
其实是用来绘制一段圆弧,这里让它画一段0到360度的圆弧,就形成了一个圆形。这里的角度是使用radian形式表示的,0到360度可以用0到2*Math.PI来表示。
最后使用ctx.fill()
给圆形填上颜色。
这样就成功的绘制了一个圆形,我们在这把它当作一个小球:
不过,这个时候的小球还是静止的,如果想让它移动,那么得修改它的圆心坐标,具体修改的数值则与运动速度有关。在移动小球之前,先看一下canvas进行动画的原理:
Canvas进行动画的原理与传统的电影胶片类似,在一段时间内,绘制图像、更新图像位置或形状、清除画布,重新绘制图像,当在1秒内连续执行60次或以上这样的操作时,即以60帧的速度,就可以产生连续的画面。
那么在JavaScript中,浏览器提供了window.requestAnimationFrame()
方法,它接收一个回调函数作为参数,每一次执行回调函数就相当于1帧动画,我们需要通过递归或循环连续调用它,浏览器会尽可能的在1秒内执行60次回调函数。那么利用它,我们就可以对canvas进行重绘,以实现小球的移动效果。
由于
window.requestAnimationFrame()
的调用基本是持续进行的,所以我们也可以把它称为游戏循环(Gameloop)。
接下来我们来看如何编写动画的基础结构:
functionprocess(){window.requestAnimationFrame(process);}window.requestAnimationFrame(process);
这里的process()
函数就是1秒钟要执行60次的回调函数,每次执行完毕后继续调用window.requestAnimationFrame(process)
进行下一次循环。如果要移动小球,那么就需要把绘制小球和修改圆心x、y坐标的代码写到process()
函数中。
为了方便更新坐标,我们把小球的圆心坐标保存到变量中,以方便对它们进行修改,然后再定义两个新的变量,分别表示在x轴方向上的速度vx
,和y轴方向上的速度vy
,然后把context相关的绘图操作放到process()
中:
letx=100;lety=100;letvx=12;letvy=25;functionprocess(){ctx.fillStyle="hsl(170,100%,50%)";ctx.beginPath();ctx.arc(x,y,60,0,2*Math.PI);ctx.fill();window.requestAnimationFrame(process);}window.requestAnimationFrame(process);
要计算圆心坐标x、y的移动距离,我们需要速度和时间,速度这里有了,那么时间要怎么获取呢?window.requestAnimationFrame()
会把当前时间的毫秒数(即时间戳)传递给回调函数,我们可以把本次调用的时间戳保存起来,然后在下一次调用时计算出执行这1帧动画消耗了多少秒,然后根据这个秒数和x、y轴方向上的速度去计算移动距离,分别加到x和y上,以获得最新的位置。注意这里的时间是上一次函数调用和本次函数调用的时间间隔,并不是第1次函数调用到当前函数调用总共过去了多少秒,所以相当于是时间增量,需要在之前x和y的值的基础上进行相加,代码如下:
letstartTime;functionprocess(now){if(!startTime){startTime=now;}letseconds=(now-startTime)/1000;startTime=now;//更新位置x+=vx*seconds;y+=vy*seconds;//清除画布ctx.clearRect(0,0,width,height);//绘制小球ctx.fillStyle="hsl(170,100%,50%)";ctx.beginPath();ctx.arc(x,y,60,0,2*Math.PI);ctx.fill();window.requestAnimationFrame(process);}
process()
现在接收当前时间戳作为参数,然后做了下面这些操作:
计算上次函数调用与本次函数调用的时间间隔,以秒计,记录本次调用的时间戳用于下一次计算。
根据x、y方向上的速度,和刚刚计算出来的时间,计算出移动距离。
调用clearRect()
清除矩形区域画布,这里的参数,前两个是左上角坐标,后两个是宽高,把canvas的宽高传进去就会把整个画布清除。
重新绘制小球。
现在小球就可以移动了:
上边的代码适合只有一个小球的情况,如果有多个小球需要绘制,就得编写大量重复的代码,这时我们可以把小球抽象成一个类,里边有绘图、更新位置等操作,还有坐标、速度、半径等属性,重构后的代码如下:
classCircle{constructor(context,x,y,r,vx,vy){this.context=context;this.x=x;this.y=y;this.r=r;this.vx=vx;this.vy=vy;}//绘制小球draw(){this.context.fillStyle="hsl(170,100%,50%)";this.context.beginPath();this.context.arc(this.x,this.y,this.r,0,2*Math.PI);this.context.fill();}/***更新画布*@param{number}seconds*/update(seconds){this.x+=this.vx*seconds;this.y+=this.vy*seconds;}}
里边的代码跟之前的一样,这里就不再赘述了,需要注意的是,Circle类的context画笔属性是通过构造函数传递进来的,更新位置的代码放到了update()
方法中。
对于整个canvas的绘制过程,也可以抽象成一个类,当作是游戏或引擎控制器,例如把它放到一个叫Gameboard
的类中:
classGameboard{constructor(){this.startTime;this.init();}init(){this.circles=[newCircle(ctx,100,100,60,12,25),newCircle(ctx,180,180,30,70,45),];window.requestAnimationFrame(this.process.bind(this));}process(now){if(!this.startTime){this.startTime=now;}letseconds=(now-this.startTime)/1000;this.startTime=now;for(leti=0;i<this.circles.length;i++){this.circles[i].update(seconds);}ctx.clearRect(0,0,width,height);for(leti=0;i<this.circles.length;i++){this.circles[i].draw(ctx);}window.requestAnimationFrame(this.process.bind(this));}}newGameboard();
在Gameboard类中:
startTime
保存了上次函数执行的时间戳的属性,放到了构造函数中。
init()
方法创建了一个circles
数组,里边放了两个示例的小球,这里先不涉及碰撞问题。然后调用window.requestAnimationFrame()
开启动画。注意这里使用了bind()
来把Gameboard
的this绑定到回调函数中,以便于访问Gameboard
中的方法和属性。
process()
方法也写到了这里边,每次执行时会遍历小球数组,对每个小球进行位置更新,然后清除画布,再重新绘制每个小球。
最后初始化Gameboard
对象就可以开始执行动画了。
这个时候有两个小球在移动了。
为了实现仿真的物理特性,多个物体间碰撞会有相应的反应,第一步就是要先检测碰撞。我们先再多加几个小球,以便于碰撞的发生,在 Gameboard 类的init()
方法中再添加几个小球:
this.circles=[newCircle(ctx,30,50,30,-100,390),newCircle(ctx,60,180,20,180,-275),newCircle(ctx,120,100,60,120,262),newCircle(ctx,150,180,10,-130,138),newCircle(ctx,190,210,10,138,-280),newCircle(ctx,220,240,10,142,350),newCircle(ctx,100,260,10,135,-460),newCircle(ctx,120,285,10,-165,370),newCircle(ctx,140,290,10,125,230),newCircle(ctx,160,380,10,-175,-180),newCircle(ctx,180,310,10,115,440),newCircle(ctx,100,310,10,-195,-325),newCircle(ctx,60,150,10,-138,420),newCircle(ctx,70,430,45,135,-230),newCircle(ctx,250,290,40,-140,335),];
然后给小球添加一个碰撞状态,在碰撞时,给两个小球设置为不同的颜色:
classCircle{constructor(context,x,y,r,vx,vy){//其它代码this.colliding=false;}draw(){this.context.fillStyle=this.colliding?"hsl(300,100%,70%)":"hsl(170,100%,50%)";//其它代码}}
现在来判断小球之间是否发生了碰撞,这个条件很简单,判断两个小球圆心的距离是否小于两个小球的半径之和就可以了,如果小于等于则发生了碰撞,大于则没有发生碰撞。
x1、y1 和 x2、y2 分别两个小球的圆心坐标。在比较时,可以对半径和进行平方运算,进而省略对距离的开方运算
r1 和 r2 为两球的半径。
在 Circle 类中,先添加一个isCircleCollided(other)
方法,接收另一个小球对象作为参数,返回比较结果:
isCircleCollided(other){letsquareDistance=(this.x-other.x)*(this.x-other.x)+(this.y-other.y)*(this.y-other.y);letsquareRadius=(this.r+other.r)*(this.r+other.r);returnsquareDistance<=squareRadius;}
再添加checkCollideWith(other)
方法,调用isCircleCollided(other)
判断碰撞后,把两球的碰撞状态设置为 true:
checkCollideWith(other){if(this.isCircleCollided(other)){this.colliding=true;other.colliding=true;}}
接着我们需要使用双循环两两比对小球是否发生了碰撞,由于小球数组存放在 Gameboard 对象中,我们给它添加一个checkCollision()
方法来检测碰撞:
checkCollision(){//重置碰撞状态this.circles.forEach((circle)=>(circle.colliding=false));for(leti=0;i<this.circles.length;i++){for(letj=i+1;j<this.circles.length;j++){this.circles[i].checkCollideWith(this.circles[j]);}}}
因为小球在碰撞后就应立即弹开,所以我们一开始要把所有小球的碰撞状态设置为false,之后在循环中,对每个小球进行检测。这里注意到内层循环是从i+1开始的,这是因为在判断1球和2球是否碰撞后,就无须再判断2球和1球了。
之后在process()
方法中,执行检测,注意检测应该发生在使用for循环更新小球位置的后边才准确:
for(leti=0;i<this.circles.length;i++){this.circles[i].update(seconds);}this.checkCollision();
现在,可以看到小球在碰撞时,会改变颜色了。
上边的代码在执行之后,小球都会穿过边界跑到外边去,那么我们先处理一下边界碰撞的问题。检测边界碰撞需要把四个面全部都处理到,根据圆心坐标和半径来判断是否和边界发生了碰撞。例如跟左边界发生碰撞时,圆心的x坐标是小于或等于半径长度的,而跟右边界发生碰撞时,圆心x坐标应该大于或等于画布最右侧坐标(即宽度值)减去半径的长度。上边界和下边界类似,只是使用圆心y坐标和画布的高度值。在水平方向上(即左右边界)发生碰撞时,小球的运动方向发生改变,只需要把垂直方向上的速度vy值取反即可,在垂直方向上碰撞则把vx取反。
现在看一下代码的实现,在 Gameboard 类中添加一个checkEdgeCollision()
方法,根据上边描述的规则编写如下代码:
checkEdgeCollision(){this.circles.forEach((circle)=>{//左右墙壁碰撞if(circle.x<circle.r){circle.vx=-circle.vx;circle.x=circle.r;}elseif(circle.x>width-circle.r){circle.vx=-circle.vx;circle.x=width-circle.r;}//上下墙壁碰撞if(circle.y<circle.r){circle.vy=-circle.vy;circle.y=circle.r;}elseif(circle.y>height-circle.r){circle.vy=-circle.vy;circle.y=height-circle.r;}});}
在代码中,碰撞时,除了对速度进行取反操作之外,还把小球的坐标修改为紧临边界,防止超出。接下来在process()
中添加对边界碰撞的检测:
this.checkEdgeCollision();this.checkCollision();
这时候可以看到小球在碰到边界时,可以反弹了:
但是小球间的碰撞还没有处理,在处理之前,先复习一下向量的基本操作,数学好的同学可以直接跳过,只看相关的代码。
向量的基本操作
由于在碰撞时,需要对速度向量(或称为矢量)进行操作,向量是使用类似坐标的形式表示的,例如<3,5>(这里用<>表示向量),它有长度和方向,对于它的运算有一定的规则,本教程中需要用到向量的加法、减法、乘法、点乘和标准化操作。
向量相加只需要把两个向量的x坐标和y坐标相加即可,例如:<3,5>+<1,2>=<4,7><3,5>+<1,2>=<4,7><3,5>+<1,2>=<4,7>
减法与加法类似,把x坐标和y坐标相减,例如:<3,5>−<1,2>=<2,3><3,5>-<1,2>=<2,3><3,5>−<1,2>=<2,3>
乘法,这里指的是向量和标量的乘法,标量指的就是普通的数字,结果是把x和y分别和标量相乘,例如:3×<3,5>=<9,15>3\times<3,5>=<9,15>3×<3,5>=<9,15>。
点乘是两个向量相乘的一种方式,类似的还有叉乘,但是在本示例中用不到,点乘其实计算的是一个向量在另一个向量上的投影,它的计算方式为两个向量的x的积加上y的积,它返回的是一个标量,即第1个向量在第2个向量上投影的长度,例如:<3,5>⋅<1,2>=3×1+5×2=13<3,5>\cdot<1,2>=3\times1+5\times2=13<3,5>⋅<1,2>=3×1+5×2=13
标准化是除掉向量的长度,只剩下方向,这样的向量它的长度为1,称为单位向量,标准化的过程是让x和y分别除以向量的长度,因为向量表示的是和原点(0,0)的距离,所以可以直接使用计算长度,例如<3,4>标准化后的结果为:<3,5>⋅<1,2>=3×1+5×2=13<3,5>\cdot<1,2>=3\times1+5\times2=13<3,5>⋅<1,2>=3×1+5×2=13。
了解了向量的基本运算后,我们来创建一个Vector工具类,来方便我们进行向量的运算,它的代码就是实现了这些运算规则:
classVector{constructor(x,y){this.x=x;this.y=y;}/***向量加法*@param{Vector}v*/add(v){returnnewVector(this.x+v.x,this.y+v.y);}/***向量减法*@param{Vector}v*/substract(v){returnnewVector(this.x-v.x,this.y-v.y);}/***向量与标量乘法*@param{Vector}s*/multiply(s){returnnewVector(this.x*s,this.y*s);}/***向量与向量点乘(投影)*@param{Vector}v*/dot(v){returnthis.x*v.x+this.y*v.y;}/***向量标准化(除去长度)*@param{number}distance*/normalize(){letdistance=Math.sqrt(this.x*this.x+this.y*this.y);returnnewVector(this.x/distance,this.y/distance);}}
代码中没有什么特殊的语法和操作,这里就不再赘述了,接下来我们看一下小球的碰撞问题。
碰撞处理碰撞处理最主要的部分就是计算碰撞后的速度和方向。通常最简单的碰撞问题是在同一个水平面上的两个物体的碰撞,称为一维碰撞,因为此时只需要计算同一方向上的速度,而我们现在的程序小球是在一个二维平面内运动的,小球之间发生正面相碰(即在同一运动方向)的概率很小,大部分是斜碰(在不同运动方向上擦肩相碰),需要同时计算水平和垂直方向上的速度和方向,这就属于是二维碰撞问题。不过,其实小球之间的碰撞,只有在连心线(两个圆心的连线)上有作用力,而在碰撞接触的切线方向上没有作用力,那么我们只需要知道连心线方向的速度变化就可以了,这样就转换成了一维碰撞。
计算碰撞后的速度时,遵守动量守恒定律和动能守恒定律,公式分别为:
动量守恒定律m1、m2 分别为两小球的质量,v1 和 v2 为两小球碰撞前的速度向量,v1' 和 v2' 为碰撞后的速度向量。根据这两个公式可以推导出两小球碰撞后的速度公式:
如果不考虑小球的质量,或质量相同,其实就是两小球速度互换,即:
这里我们给小球加上质量,然后套用公式来计算小球碰撞后速度,先在 Circle 类中给小球加上质量 mass 属性:
classCircle{constructor(context,x,y,r,vx,vy,mass=1){//其它代码this.mass=mass;}}
然后在 Gameboard 类的初始化小球处,给每个小球添加质量:
this.circles=[newCircle(ctx,30,50,30,-100,390,30),newCircle(ctx,60,180,20,180,-275,20),newCircle(ctx,120,100,60,120,262,100),newCircle(ctx,150,180,10,-130,138,10),newCircle(ctx,190,210,10,138,-280,10),newCircle(ctx,220,240,10,142,350,10),newCircle(ctx,100,260,10,135,-460,10),newCircle(ctx,120,285,10,-165,370,10),newCircle(ctx,140,290,10,125,230,10),newCircle(ctx,160,380,10,-175,-180,10),newCircle(ctx,180,310,10,115,440,10),newCircle(ctx,100,310,10,-195,-325,10),newCircle(ctx,60,150,10,-138,420,10),newCircle(ctx,70,430,45,135,-230,45),newCircle(ctx,250,290,40,-140,335,40),];
在Circle类中加上changeVelocityAndDirection(other)
方法来计算碰撞后的速度,它接收另一个小球对象作为参数,同时计算这两个小球碰撞厚的速度和方向,这个是整个引擎的核心,我们一点一点的来看它是如何实现的。首先把两个小球的速度使用Vector向量来表示:
changeVelocityAndDirection(other){//创建两小球的速度向量letvelocity1=newVector(this.vx,this.vy);letvelocity2=newVector(other.vx,other.vy);}
因为我们本身就已经使用 vx 和 vy 来表示水平和垂直方向上的速度向量了,所以直接把它们传给 Vector 的构造函数就可以了。velocity1
和velocity2
分别代表当前小球和碰撞小球的速度向量。
接下来获取连心线方向的向量,也就是两个圆心坐标的差:
letvNorm=newVector(this.x-other.x,this.y-other.y);
接下来获取连心线方向的单位向量和切线方向上的单位向量,这些单位向量代表的是连心线和切线的方向:
letunitVNorm=vNorm.normalize();letunitVTan=newVector(-unitVNorm.y,unitVNorm.x);
unitVNorm
是连心线方向单位向量,unitVTan
是切线方向单位向量,切线方向其实就是把连心线向量的 x、y 坐标互换,并把 y 坐标取反。根据这两个单位向量,使用点乘计算小球速度在这两个方向上的投影:
letv1n=velocity1.dot(unitVNorm);letv1t=velocity1.dot(unitVTan);letv2n=velocity2.dot(unitVNorm);letv2t=velocity2.dot(unitVTan);
计算结果是一个标量,也就是没有方向的速度值。v1n
和v1t
表示当前小球在连心线和切线方向的速度值,v2n
和v2t
则表示的是碰撞小球 的速度值。在计算出两小球的速度值之后,我们就有了碰撞后的速度公式所需要的变量值了,直接用代码把公式套用进去:
letv1nAfter=(v1n*(this.mass-other.mass)+2*other.mass*v2n)/(this.mass+other.mass);letv2nAfter=(v2n*(other.mass-this.mass)+2*this.mass*v1n)/(this.mass+other.mass);
v1nAfter
和v2nAfter
分别是两小球碰撞后的速度,现在可以先判断一下,如果v1nAfter
小于v2nAfter
,那么第 1 个小球和第 2 个小球会越来越远,此时不用处理碰撞:
if(v1nAfter<v2nAfter){return;}
然后再给碰撞后的速度加上方向,计算在连心线方向和切线方向上的速度,只需要让速度标量跟连心线单位向量和切线单位向量相乘:
letv1VectorNorm=unitVNorm.multiply(v1nAfter);letv1VectorTan=unitVTan.multiply(v1t);letv2VectorNorm=unitVNorm.multiply(v2nAfter);letv2VectorTan=unitVTan.multiply(v2t);
这样有了两个小球连心线上的新速度向量和切线方向上的新速度向量,最后把连心线上的速度向量和切线方向的速度向量进行加法操作,就能获得碰撞后小球的速度向量:
letvelocity1After=v1VectorNorm.add(v1VectorTan);letvelocity2After=v2VectorNorm.add(v2VectorTan);
之后我们把向量中的 x 和 y 分别还原到小球的 vx 和 vy 属性中:
this.vx=velocity1After.x;this.vy=velocity1After.y;other.vx=velocity2After.x;other.vy=velocity2After.y;
最后在checkCollideWith()
方法的 if 语句中调用此方法,即在检测到碰撞时:
checkCollideWith(other){if(this.isCircleCollided(other)){this.colliding=true;other.colliding=true;this.changeVelocityAndDirection(other);//在这里调用}}
这时,小球的碰撞效果就实现了。
现在小球之间的碰撞属于完全弹性碰撞,即碰撞之后不会有能量损失,这样小球永远不会停止运动,我们可以让小球在碰撞之后损失一点能量,来模拟更真实的物理效果。要让小球碰撞后有能量损失,可以使用恢复系数,它是一个取值范围为0到1的数值,每次碰撞后,乘以它就可以减慢速度,如果恢复系数为1则为完全弹性碰撞,为0则是完全非弹性碰撞,之间的数值为非弹性碰撞,现实生活中的碰撞都是非弹性碰撞。
先看一下边界碰撞,这个比较简单,假设边界的恢复系数为0.8,然后在每次对速度取反的时候乘以它就可以了,把GameboardcheckEdgeCollision()
方法作如下改动:
checkEdgeCollision(){constcor=0.8;//设置恢复系统this.circles.forEach((circle)=>{//左右墙壁碰撞if(circle.x<circle.r){circle.vx=-circle.vx*cor;//加上恢复系数circle.x=circle.r;}elseif(circle.x>width-circle.r){circle.vx=-circle.vx*cor;//加上恢复系数circle.x=width-circle.r;}//上下墙壁碰撞if(circle.y<circle.r){circle.vy=-circle.vy*cor;//加上恢复系数circle.y=circle.r;}elseif(circle.y>height-circle.r){circle.vy=-circle.vy*cor;//加上恢复系数circle.y=height-circle.r;}});}
接下来设置小球的恢复系数,给 Circle 类再加上一个恢复系数 cor 属性,每个小球可以设置不同的数值,来让它们有不同的弹性,然后在初始化小球时设置随意的恢复系数:
classCircle{constructor(context,x,y,r,vx,vy,mass=1,cor=1){//其它代码this.cor=cor;}}classGameboard{init(){this.circles=[newCircle(ctx,30,50,30,-100,390,30,0.7),newCircle(ctx,60,180,20,180,-275,20,0.7),newCircle(ctx,120,100,60,120,262,100,0.3),newCircle(ctx,150,180,10,-130,138,10,0.7),newCircle(ctx,190,210,10,138,-280,10,0.7),newCircle(ctx,220,240,10,142,350,10,0.7),newCircle(ctx,100,260,10,135,-460,10,0.7),newCircle(ctx,120,285,10,-165,370,10,0.7),newCircle(ctx,140,290,10,125,230,10,0.7),newCircle(ctx,160,380,10,-175,-180,10,0.7),newCircle(ctx,180,310,10,115,440,10,0.7),newCircle(ctx,100,310,10,-195,-325,10,0.7),newCircle(ctx,60,150,10,-138,420,10,0.7),newCircle(ctx,70,430,45,135,-230,45,0.7),newCircle(ctx,250,290,40,-140,335,40,0.7),];}}
加上恢复系数之后,小球碰撞后的速度计算也需要改变一下,可以简单的让v1nAfter
和v2nAfter
乘以小球的恢复系数,也可以使用带有恢复系数的速度公式
接着把公式转换为代码,在 Circle 类的changeVelocityAndDirection()
方法中,替换掉v1nAfter
和v2nAfter
的计算公式:
letcor=Math.min(this.cor,other.cor);letv1nAfter=(this.mass*v1n+other.mass*v2n+cor*other.mass*(v2n-v1n))/(this.mass+other.mass);letv2nAfter=(this.mass*v1n+other.mass*v2n+cor*this.mass*(v1n-v2n))/(this.mass+other.mass);
这里要注意的是两小球碰撞时的恢复系数应取两者的最小值,按照常识,弹性小的无论是去撞别人还是别人撞它,都会有同样的效果。现在小球碰撞后速度会有所减慢,不过还差一点,我们可以加上重力来让小球自然下落。
添加重力比较简单,先在全局定义重力加速度常量,然后在小球更新垂直方向上的速度时,累计重力加速度就可以了:
constgravity=980;classCircle{update(seconds){this.vy+=gravity*seconds;//重力加速度this.x+=this.vx*seconds;this.y+=this.vy*seconds;}}
重力加速度大约是,但是由于我们的画布是以象素为单位的,所以使用9.8看起来会像是没有重力,或者像是从很远的地方观察小球,这时候可以把重力加速度放大一定倍数来达到更逼真的效果。
读到这里,这篇“JavaScript怎么实现碰撞物理引擎”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。