使用canvas来实现一个弹跳小球小游戏
本文通过编写一个弹球 demo 来展示 JavaScript 中对象的重要性。我们的小球会在屏幕上弹跳,当它们碰到彼此时会变色。最终会像这样:
这个实例将会利用 Canvas API 来在屏幕上画小球,还会用到 requestAnimationFrame API 来使整个画面动起来,这个过程中我们会用到一些技巧,比如小球从墙上反弹,检查它们是否撞到了对方(也就是碰撞检测)。
项目结构
包含以下三个文件:index.html、style.css 和 main.js。它们分别包含以下内容:
一个非常简单的 HTML 文档,包括一个 <h1>
元素、一个<canvas>
元素来画小球,还有一些元素将 CSS 和 JavaScript 运用到我们的 HTML 中。 一些非常简单的样式,主要是 <h1>
元素的样式和定位,另外还能使画面填充整个页面从而摆脱滚动条和边缘的空白(这样看起来非常简洁) 一些 JavaScript 用来设置 <canvas>
元素,并提供我们要用到的基本函数。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>弹跳彩球</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>弹跳彩球</h1>
<canvas></canvas>
<script src="main.js"></script>
</body>
</html>
css
html, body {
margin: 0;
}
html {
font-family: sans-serif;
height: 100%;
}
body {
overflow: hidden;
height: inherit;
}
h1 {
font-size: 2rem;
letter-spacing: -1px;
position: absolute;
margin: 0;
top: -4px;
right: 5px;
color: transparent;
text-shadow: 0 0 4px white;
}
JavaScript代码
接下来我们来分析JavaScript代码
准备部分
脚本的第一部分是:
JS
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const width = (canvas.width = window.innerWidth);
const height = (canvas.height = window.innerHeight);
用canvas
变量代指<canvas>
元素,然后对其调用了getContext()
,来获得了一个开始画画的环境,赋给了ctx
,ctx
是一个对象,直接代指画布上的一块允许我们绘制 2D 图形的区域。
接下来,我们设置 width
和 height
变量,并且让画布元素的宽和高(分别使用 canvas.width
和 canvas.height
表示)等于浏览器的宽和高(也就是网页显示的区域 --- 可以从 Window.innerWidth
和 Window.innerHeight
参数获得)。
这里的小技巧是串联了多个赋值表达式在一起,这样能更快地设置变量。
JS
// 生成随机数的函数
function random(min, max) {
const num = Math.floor(Math.random() * (max - min)) + min;
return num;
}
// 生成随机颜色值的函数
function randomColor() {
const color = 'rgb(' +
random(0, 255) + ',' +
random(0, 255) + ',' +
random(0, 255) + ')';
return color;
}
第一个函数为我们生成一个 min 至 max 之间的随机整数,第二个函数为我们生成一个随机的颜色值。事实上第二个函数返回的是一个格式为'rgb(1,2,3)'这样的字符串,这是css颜色属性的格式
为程序中的小球建立模型
我们的项目中会有很多小球在屏幕上跳来跳去。因此这些小球会以相同的方式运作,从而我们可以通过一个对象实例化它们。首先,我们将下面的构造器加入到代码的底部。
JS
function Ball(x, y, velX, velY, color, size) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.color = color;
this.size = size;
}
这个构造器中定义了每个小球需要的参数:
- x 和 y 坐标 ------ 小球在屏幕上最开始时候的坐标。坐标的范围从 0(左上角)到浏览器视口的宽和高(右下角)。
- 水平和竖直速度(velX 和 velY)------ 我们会给每个小球一个水平和竖直速度。实际上,当我们让这些球开始运动时候,每过一帧都会给小球的 x 和 y 坐标加一次这些值。
- color ------ 每一个小球会有自己的颜色。
- size ------ 每一个小球会有自己的大小 --- 也就是小球的半径,以像素为单位。
为小球的原型加上方法
画小球
首先给小球的原型加上 draw() 方法:
JS
Ball.prototype.draw = function () {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
};
通过使用这个函数,通过使用我们之前定义的 ctx对象 的方法,我们就可以让在屏幕上画出小球了。ctx 的内容区域就像是一张纸,现在我们就可以命令我们的笔画一点东西。
- 首先,我们使用 beginPath() 来声明我们现在要开始在纸上画一个图形了。
- 然后,我们使用 fillStyle 来定义这个图形的颜色 --- 这个值正是小球的颜色属性。
- 接下来,我们使用 arc() 方法来在纸上画出一段圆弧。有这些参数:
- x 和 y 是圆弧的中心的坐标 ------ 也就是小球的中心坐标。
- 圆弧的半径 ------ 小球的半径。
- 最后两个参数是开始和结束,也就是圆弧对应的夹角,单位以弧度表示。这里我们用的是 0 和 2 * PI,也就是 360 度(如果你设置成 0 和 1 * PI,则只会出现一个半圆,也就是 180 度)
- 最后,我们使用 fill() 方法,也就是声明我们结束了以 beginPath() 开始的绘画,并且使用我们之前设置的颜色进行填充。
我们可以先测试一下小球能否被画出来,通过一下的代码
JS
let testBall = new Ball(50, 100, 4, 4, "blue", 10);
testBall.draw();
这样你应该会在你的画布上看到一个小球被画出来了。
更新小球的数据
我们需要一个函数来更新小球的状态,所以给小球的原型添加一个update()
方法
JS
Ball.prototype.update = function () {
if (this.x + this.size >= width) {
this.velX = -this.velX;
}
if (this.x - this.size <= 0) {
this.velX = -this.velX;
}
if (this.y + this.size >= height) {
this.velY = -this.velY;
}
if (this.y - this.size <= 0) {
this.velY = -this.velY;
}
this.x += this.velX;
this.y += this.velY;
};
代码的前四个if
部分是用来检查小球是否碰到画布的边缘。如果碰到我们就反转小球的速度方向。同时我们都会加上小球的半径,因为 x
或者y
坐标是小球中心的坐标,我们希望小球在其边界接触浏览器窗口的边界时反弹,而不是小球的一部分都不见了再返回。 最后两行,我们将 velX
的值加到 x
的坐标上,将 velY
的值加到 y
坐标上 ------ 每次调用这个方法的时候小球就移动这么多。
制作小球的动画
- 首先我们需要一个地方储存小球,下面的数组会干这件事
JS
let balls = [];
while (balls.length <= 25) {
let size = random(10, 20);
let ball = new Ball(
// 为避免绘制错误,球至少离画布边缘球本身一倍宽度的距离
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
randomColor(),
size,
);
balls.push(ball);
}
- 给小球的原型添加collisionDetect()方法
JS
Ball.prototype.collisionDetect = function () {
for (let j = 0; j < balls.length; j++) {
if (this !== balls[j]) {
const dx = this.x - balls[j].x;
const dy = this.y - balls[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.size + balls[j].size) {
balls[j].color = this.color = randomColor();
}
}
}
};
- 对于每个小球,我们都要检查其他的小球是否和当前这个小球相撞了。为了达到此目的,我们构造另外一个 for 循环来遍历 balls[] 数组中的小球。
- 在循环里面,我们使用一个 if 语句来检查遍历的小球是否是当前的小球。我们不希望检测到一个小球撞到了自己!为了达到这个目的,我们需要检查当前小球 (即正在调用 collisionDetect 方法的球) 是否和被循环到的小球 (for 循环检测中的当前遍历所引用的球) 是不是同一个。我们使用 ! 来否定判断,因此只有两个小球不是同一个时,条件判断中的代码才会运行。
- 我们使用了一个常见的算法来检测两个小球是否相撞了,两个小球中心的距离是否小于两个小球的半径之和。
- 如果检测到了碰撞,会运行 if 语句中的代码。我们会将两个小球的颜色都设置成随机的一种。
- 几乎所有的动画效果都会用到一个运动循环,也就是每一帧都自动更新视图。这是大多数游戏或者其他类似项目的基础。
JS
function loop() {
ctx.fillStyle = "rgba(0, 0, 0, 0.25)";
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < balls.length; i++) {
balls[i].draw();
balls[i].update();
balls[i].collisionDetect();
}
requestAnimationFrame(loop);
}
loop() 函数做了下面的事情:
- 将整个画布的颜色设置成半透明的黑色。然后使用 fillRect()(这四个参数分别是起始的坐标、绘制的矩形的宽和高)画出一个填充满整个画布的矩形。这是在下一个视图画出来时用来遮住之前的视图的。如果不这样做得话,你就会在屏幕上看到一条蛇的形状而不是小球的运动了。用来填充的颜色设置成半透明的rgba(0,0,0,0.25),也就是让之前的视图留下来一点点,从而你可以看到小球运动时的轨迹。如果你将 0.25 设置成 1 时,你就完全看不到了。试着改变其中的值查看造成的影响。
- 当且仅当小球数量小于等于 25 时,将 random() 函数产生的数字传入新的小球实例从而创建一个新的小球,并且加入到数组中。
- 遍历数组中的所有小球,并且让每个小球都调用 draw() 和 update() 函数来将自己画出来,并且再接下来的每一帧都按照其速度进行位置的更新。
- 使用 requestAnimationFrame() 方法再运行一次函数 ------ 当一个函数正在运行时传递相同的函数名,从而每隔一小段时间都会运行一次这个函数,这样我们可以得到一个平滑的动画效果。这主要是通过递归完成的 ------ 也就是说函数每次运行的时候都会调用自己,从而可以一遍又一遍得运行。
记得调用loop函数
JS
loop();
这样就完成了弹跳彩球的小游戏,运行一下index.html
看一下吧。