JavaScript游戏开发:⼿把⼿实现碰撞物理引擎
⽬录
基础结构
绘制⼩球
移动⼩球
重构代码
碰撞检测
边界碰撞
向量的基本操作
碰撞处理
动量守恒定律
动能守恒定律
⾮弹性碰撞
重⼒
总结
年前我看到合成⼤西⽠⼩游戏⽕了,想到之前从来没有研究过游戏⽅⾯的开发,这次就想趁着这个机会看看 JavaScript 游戏开发,从原⽣⾓度上如何实现游戏⾥的物理特性,例如运动、碰撞。虽然之前研究过物理相关的动画库,但是我打算试试不⽤框架编写⼀个简单的JavaScript 物理引擎,实现⼩球的碰撞效果。
为什么不⽤现成的游戏库呢?因为我觉得在了解底层的实现原理之后,才能更有效的理解框架上的概念和使⽤⽅法,在解决 BUG 的时候能够更有效率,同时对⾃⼰的编码技能也是⼀种提升。在对 JavaScript 物理引擎的研究过程中,发现写代码是次要的,最主要的是理解相关的物理、数学公式和概念,虽然我是理科⽣,但是数学和物理从来不是我的强项,我不是把知识还给⽼师了,⽽是压根就没掌握过 o。过年期间花了有⼩半个⽉的时间在学习物理知识,现在仍然对某些概念和推导过程没有太⼤的⾃信,不过最后还算是做出了⼀个简单的、⽐较满意的结果,见下图。
接下来看⼀下怎么实现这样的效果。
基础结构
我们这⾥使⽤ canvas 来实现 JavaScript 物理引擎。⾸先准备项⽬的基础⽂件和样式,新建⼀个 index.html、index.js 和 style.css ⽂件,分别⽤于编写 canvas 的 html 结构、引擎代码和画布样式。
在 index.html 的 标签中引⼊样式⽂件:
1 在 中,添加 canvas 元素、加载 index.js ⽂件: 这段代码定义了 id 为 gameboard 的 元素,并放在了 元素下, 元素主要是⽤来设置背景⾊和画布⼤⼩。在 元素的下⽅引⼊ 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%);
}
样式很简单,去掉所有元素的外边距、内间距,并把 元素的宽⾼设置为与浏览器可视区域相同,背景⾊为深灰⾊。
hsl(hue, saturation, brightness) 为 css 颜⾊表⽰法之⼀,参数分别为⾊相,饱和度和亮度,可通过我之前出过的视频进⾏学习。
绘制⼩球
接下来绘制⼩球,主要⽤到了 canvas 相关的 api。
在 index.js 中,编写如下代码:
const canvas = ElementById(“gameboard”);
const ctx = Context(“2d”);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let width = canvas.width;
let height = 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 中,浏览器提供了 questAnimationFrame() ⽅法,它接收⼀个回调函数作为参数,每⼀次执⾏回调函数就相当于 1 帧动画,我们需要通过递归或循环连续调⽤它,浏览器会尽可能的在 1 秒内执⾏ 60 次回调函数。那么利⽤它,我们就可以对canvas 进⾏重绘,以实现⼩球的移动效果。
由于 questAnimationFrame() 的调⽤基本是持续进⾏的,所以我们也可以把它称为游戏循环(Game loop)。
接下来我们来看如何编写动画的基础结构:
function process() {
}
这⾥的 process() 函数就是 1 秒钟要执⾏ 60 次的回调函数,每次执⾏完毕后继续调⽤ questAnimationFrame(process)进⾏下⼀次循环。如果要移动⼩球,那么就需要把绘制⼩球和修改圆⼼ x、y 坐标的代码写到 process() 函数中。
为了⽅便更新坐标,我们把⼩球的圆⼼坐标保存到变量中,以⽅便对它们进⾏修改,然后再定义两个新的变量,分别表⽰在 x 轴⽅向上的速度 vx,和 y 轴⽅向上的速度 vy,然后把 context 相关的绘图操作放到 process() 中:canvas动画
let x = 100;
let y = 100;
let vx = 12;
let vy = 25;
process() {
ctx.fillStyle = “hsl(170, 100%, 50%)”;
ctx.beginPath();
ctx.arc(x, y, 60, 0, 2 * Math.PI);
ctx.fill();
}
要计算圆⼼坐标 x、y 的移动距离,我们需要速度和时间,速度这⾥有了, 那么时间要怎么获取呢? questAnimationFrame()会把当前时间的毫秒数(即时间戳)传递给回调函数,我们可以把本次调⽤的时间戳保存起来,然后在下⼀次调⽤时计算出执⾏这 1 帧动画消耗了多少秒,然后根据这个秒数和 x、y 轴⽅向上的速度去计算移动距离,分别加到 x 和 y 上,以获得最新的位置。注意这⾥的时间是上⼀次函数调⽤和本次函数调⽤的时间间隔,并不是第 1 次函数调⽤到当前函数调⽤总共过去了多少秒,所以相当于是时间增量,需要在之前 x 和 y 的值的基础上进⾏相加,代码如下:
let startTime;
function process(now) {
if (!startTime) {
startTime = now;
}
let seconds = (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();
}
process() 现在接收当前时间戳作为参数,然后做了下⾯这些操作:
计算上次函数调⽤与本次函数调⽤的时间间隔,以秒计,记录本次调⽤的时间戳⽤于下⼀次计算。
根据 x、y ⽅向上的速度,和刚刚计算出来的时间,计算出移动距离。
调⽤ clearRect() 清除矩形区域画布,这⾥的参数,前两个是左上⾓坐标,后两个是宽⾼,把 canvas 的宽⾼传进去就会把整个画布清除。重新绘制⼩球。
现在⼩球就可以移动了:
重构代码
上边的代码适合只有⼀个⼩球的情况,如果有多个⼩球需要绘制,就得编写⼤量重复的代码,这时我们可以把⼩球抽象成⼀个类,⾥边有绘图、更新位置等操作,还有坐标、速度、半径等属性,重构后的代码如下:
class Circle {
constructor(context, x, y, r, vx, vy) {
this.x = x;
this.y = y;
this.r = r;
this.vx = vx;
this.vy = vy;
}
// 绘制⼩球
draw() {
}
/**
更新画布
@param {number} seconds
*/
update(seconds) {
this.x += this.vx * seconds;
this.y += this.vy * seconds;
}
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。