这两天闲的无事,看到一个博客中提到了canvas实现的小球碰撞,想来挺有意思,也能熟练熟练canvas接口,就花了点时间实现了,整体过程还是比较简单的,就是对球处理的时候花了点心思。最后的思路是用向量转换实现碰撞之后的速度计算。
一、原理分析
碰撞有弹性和非弹性碰撞两种,其中弹性碰撞没有能量损失,非弹性碰撞在碰撞过程中会有能量(速度)损失。在这里我们只研究刚性球的弹性碰撞,毕竟非弹性碰撞还有多考虑一些东西,甚是麻烦。
刚性小球在碰撞过程中满足动量定理和动能定理。这两个定理我们在高中物理就已经学过了,在此不多做说明。
因为小球在碰撞过程中的速度我们可以分解成沿球心连线和垂直球心连线两个方向的速度(向量分解),其中垂直球心连线的速度是不会变化的,满足动量定理和动能定理的只是沿球心连线方向上的分速度。
但对于这个沿球心连线的分速度,我们直接计算是不太容易的,在这里,我们应用一个旋转矩阵,将球原有的坐标系旋转到沿球心连线方向上,这样一来,球的速度坐标系就和球心联系一致了,大大便于计算。
二、代码实现
上面思路有了,代码实现起来就简单多了,下面直接上核心代码:
for (var i = 0; i < balls.length; i++) {
var ball_1 = balls[i];
// 移动球
ball_1.x += ball_1.vx;
ball_1.y += ball_1.vy;
// 碰撞检查
// 知识不够,只能循环迭代检测
for (var j = i + 1; j < balls.length; j++) {
var ball_2 = balls[j];
// 重叠检测
if (isOverLapping(ball_1, ball_2)) {
// 球心连线与 x 轴正方向夹角
var line = [ball_2.x - ball_1.x, ball_2.y - ball_1.y];
var radian_line = Math.atan2(line[1], line[0]);
// 将球的速度坐标系的 x 轴旋转到与球心连线重合
var vx_1 = ball_1.vx * Math.cos(radian_line) + ball_1.vy * Math.sin(radian_line),
vy_1 = ball_1.vy * Math.cos(radian_line) - ball_1.vx * Math.sin(radian_line);
var vx_2 = ball_2.vx * Math.cos(radian_line) + ball_2.vy * Math.sin(radian_line),
vy_2 = ball_2.vy * Math.cos(radian_line) - ball_2.vx * Math.sin(radian_line);
// 排除同向远离情况
if (vx_1 - vx_2 <= 0) break;
// 根据动量定理及动能定理求解 x 方向上的速度变化
var nvx_1 = (ball_1.m - ball_2.m) / (ball_1.m + ball_2.m) * (vx_1 - vx_2) + vx_2;
var nvx_2 = 2 * ball_1.m / (ball_1.m + ball_2.m) * (vx_1 - vx_2) + vx_2;
// 复原球的坐标系
ball_1.vx = nvx_1 * Math.cos(-radian_line) + vy_1 * Math.sin(-radian_line);
ball_1.vy = vy_1 * Math.cos(-radian_line) - nvx_1 * Math.sin(-radian_line);
ball_2.vx = nvx_2 * Math.cos(-radian_line) + vy_2 * Math.sin(-radian_line);
ball_2.vy = vy_2 * Math.cos(-radian_line) - nvx_2 * Math.sin(-radian_line);
}
}
// 边界检查
if (ball_1.x - ball_1.r < 0) {
ball_1.x = ball_1.r;
ball_1.vx = Math.abs(ball_1.vx);
}
if (ball_1.x + ball_1.r > screenWidth) {
ball_1.x = screenWidth - ball_1.r;
ball_1.vx = -Math.abs(ball_1.vx);
}
if (ball_1.y - ball_1.r < 0) {
ball_1.y = ball_1.r;
ball_1.vy = Math.abs(ball_1.vy);
}
if (ball_1.y + ball_1.r > screenHeight) {
ball_1.y = screenHeight - ball_1.r;
ball_1.vy = -Math.abs(ball_1.vy);
}
// 绘制球
context.fillStyle = "red";
context.beginPath();
context.arc(ball_1.x, ball_1.y, ball_1.r, 0, 2 * Math.PI);
context.fill();
}
这样基本就实现了刚性小球的弹性碰撞,具体可以看这边的 DEMO。