使用canvas来实现一个弹跳小球小游戏

本文通过编写一个弹球 demo 来展示 JavaScript 中对象的重要性。我们的小球会在屏幕上弹跳,当它们碰到彼此时会变色。最终会像这样:

ball

这个实例将会利用 Canvas API 来在屏幕上画小球,还会用到 requestAnimationFrame API 来使整个画面动起来,这个过程中我们会用到一些技巧,比如小球从墙上反弹,检查它们是否撞到了对方(也就是碰撞检测)。

项目结构

包含以下三个文件:index.html、style.css 和 main.js。它们分别包含以下内容:

一个非常简单的 HTML 文档,包括一个 <h1> 元素、一个<canvas> 元素来画小球,还有一些元素将 CSS 和 JavaScript 运用到我们的 HTML 中。
一些非常简单的样式,主要是 <h1> 元素的样式和定位,另外还能使画面填充整个页面从而摆脱滚动条和边缘的空白(这样看起来非常简洁)
一些 JavaScript 用来设置 <canvas> 元素,并提供我们要用到的基本函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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代码

准备部分

脚本的第一部分是:

1
2
3
4
5
6
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(),来获得了一个开始画画的环境,赋给了ctxctx是一个对象,直接代指画布上的一块允许我们绘制 2D 图形的区域。

接下来,我们设置 widthheight 变量,并且让画布元素的宽和高(分别使用 canvas.widthcanvas.height 表示)等于浏览器的宽和高(也就是网页显示的区域 — 可以从 Window.innerWidthWindow.innerHeight参数获得)。

这里的小技巧是串联了多个赋值表达式在一起,这样能更快地设置变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成随机数的函数

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颜色属性的格式

为程序中的小球建立模型

我们的项目中会有很多小球在屏幕上跳来跳去。因此这些小球会以相同的方式运作,从而我们可以通过一个对象实例化它们。首先,我们将下面的构造器加入到代码的底部。

1
2
3
4
5
6
7
8
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() 方法:

1
2
3
4
5
6
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() 开始的绘画,并且使用我们之前设置的颜色进行填充。

我们可以先测试一下小球能否被画出来,通过一下的代码

1
2
let testBall = new Ball(50, 100, 4, 4, "blue", 10);
testBall.draw();

这样你应该会在你的画布上看到一个小球被画出来了。

更新小球的数据

我们需要一个函数来更新小球的状态,所以给小球的原型添加一个update()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 坐标上 —— 每次调用这个方法的时候小球就移动这么多。

制作小球的动画

  1. 首先我们需要一个地方储存小球,下面的数组会干这件事
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
  1. 给小球的原型添加collisionDetect()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
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 语句中的代码。我们会将两个小球的颜色都设置成随机的一种。
  1. 几乎所有的动画效果都会用到一个运动循环,也就是每一帧都自动更新视图。这是大多数游戏或者其他类似项目的基础。
1
2
3
4
5
6
7
8
9
10
11
12
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函数

1
loop();

这样就完成了弹跳彩球的小游戏,运行一下index.html看一下吧。