字节青训营前端实践01

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

接下来,我们设置 widthheight 变量,并且让画布元素的宽和高(分别使用 canvas.widthcanvas.height 表示)等于浏览器的宽和高(也就是网页显示的区域 --- 可以从 Window.innerWidthWindow.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 坐标上 ------ 每次调用这个方法的时候小球就移动这么多。

制作小球的动画

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

相关推荐
Find1 个月前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子1 个月前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵1 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六1 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz1 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5651 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml1 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932421 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记