用Canvas开发一款小游戏玩玩

830 阅读5分钟

前言

自上一篇文章业务代码写累了,写个小游戏玩玩, 开发了一款射击类的小游戏之后,感觉玩起来还是蛮有意思的,开发游戏的动力空前比较高涨,于是还想再开发一个同类的小游戏。上次是使用Three.js开发的,这次换个技术,采用原生的Canvas开发一款射击类的小游戏。

效果展示

游戏的玩法规则是:不断有射击目标从上方落下,当射击目标触碰到射击者时,游戏结束。射击者可以按下左右方向键进行左右方向的移动,按空格键开火射击。每击中一个目标,加10分。

轮播5.gif

开发步骤

整体思路是先绘制场景元素,接着给每种场景元素加动效,最后处理场景元素之间的联动关系。

绘制场景元素

先绘制画布和声明公共变量player(玩家),bullets(子弹), targets(射击目标), score(游戏分值), gameOver(游戏结束标志位)。

const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");

canvas.width = window.innerWidth
canvas.height = window.innerHeight;

let player, bullets, targets, score, gameOver;

在每个渲染帧,要清除画布内容,重新绘制玩家,子弹,射击目标,游戏分值等UI元素。

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ctx.fillStyle = "white";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  bullets.forEach((bullet) => {
    ctx.fillStyle = "red";
    ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
  });

  targets.forEach((target) => {
    ctx.fillStyle = "green";
    ctx.fillRect(target.x, target.y, target.width, target.height);
  });

  ctx.fillStyle = "white";
  ctx.font = "24px Arial";
  ctx.fillText(`Score: ${score}`, 10, 30);

  if (gameOver) {
    ctx.fillStyle = "red";
    ctx.font = "48px Arial";
    ctx.fillText("Game Over", canvas.width / 2 - 100, canvas.height / 2);
  }
}

创建射击目标

每秒生成一个射击目标,射击目标具有固定的大小和速度,随机生成其在画布顶部的x坐标。

// 创建射击目标
function createTargets() {
  setInterval(() => {
    if (!gameOver) {
      let x = Math.random() * (canvas.width - 50);
      let y = -50;
      targets.push({ x, y, width: 50, height: 50, speed: 2 });
    }
  }, 1000);
}

移动玩家+开火功能

监听左右方向键和空格键的按下弹起事件。根据按键移动玩家位置,横向移动玩家的位置时,要判断左移和右移是否超过画布边界。并在按下空格键时发射子弹。

let keys = {};
window.addEventListener("keydown", (e) => {
  keys[e.code] = true;
});

window.addEventListener("keyup", (e) => {
  keys[e.code] = false;
});

// 移动玩家
function movePlayer() {
  if (keys["ArrowLeft"] && player.x > 0) player.x -= player.speed;
  if (keys["ArrowRight"] && player.x + player.width < canvas.width) player.x += player.speed;
  if (keys["Space"]) shoot();
}

shoot函数用于创建子弹对象,子弹对象的几个参数的初始设置值为:

  • 子弹的水平位置 x

player.x + player.width / 2 计算子弹的水平起始位置,确保子弹从玩家的中心位置发射。 player.x 是玩家的左上角x坐标,player.width / 2 是玩家宽度的一半,因此 player.x + player.width / 2 是玩家的水平中点。

  • 子弹的垂直位置 y

player.y 是玩家的顶部y坐标,设定子弹的初始y坐标与玩家相同,使子弹从玩家顶部发射。

  • 子弹的大小

width: 5height: 10 指定子弹的宽度和高度,分别为5像素和10像素。

  • 子弹的速度speed: 7 指定子弹的速度,每帧向上移动7像素。
// 射击
function shoot() {
  bullets.push({ x: player.x + player.width / 2, y: player.y, width: 5, height: 10, speed: 7 });
}

击中目标检测

  • 子弹与射击目标碰撞:删除子弹和射击目标,增加得分。

遍历所有子弹和目标,检查每个子弹是否击中每个目标。 击中检测条件:子弹和射击目标的边界是否重叠。

bullet.x < target.x + target.width:子弹的左边界在目标的右边界左侧
bullet.x + bullet.width > target.x:子弹的右边界在目标的左边界右侧
bullet.y < target.y + target.height:子弹的上边界在目标的下边界上侧
bullet.y + bullet.height > target.y:子弹的下边界在目标的上边界下侧

如果检测到碰撞:子弹和射击目标都要被销毁。 使用splice方法从bullets数组中删除被击中的子弹; 使用splice方法从targets数组中删除被击中的目标; 增加得分score

  • 射击目标与玩家碰撞:游戏结束。

遍历所有目标,检查每个目标是否撞到玩家。 撞到检测条件:射击目标和玩家的边界是否重叠。

target.x < player.x + player.width:目标的左边界在玩家的右边界左侧
target.x + target.width > player.x:目标的右边界在玩家的左边界右侧
target.y < player.y + player.height:目标的上边界在玩家的下边界上侧
target.y + target.height > player.y:目标的下边界在玩家的上边界下侧

如果检测到碰撞,设置游戏结束标志gameOver

// 检查是否击中目标
function checkHit() {
  bullets.forEach((bullet, bIndex) => {
    targets.forEach((target, eIndex) => {
      if (
        bullet.x < target.x + target.width &&
        bullet.x + bullet.width > target.x &&
        bullet.y < target.y + target.height &&
        bullet.y + bullet.height > target.y
      ) {
        bullets.splice(bIndex, 1);
        targets.splice(eIndex, 1);
        score += 10;
      }
    });
  });

  targets.forEach((target) => {
    if (
      target.x < player.x + player.width &&
      target.x + target.width > player.x &&
      target.y < player.y + player.height &&
      target.y + target.height > player.y
    ) {
      gameOver = true;
    }
  });
} 

游戏初始化

在网页加载完成时调用init函数。初始化玩家位置、大小和速度;初始化子弹、敌人、得分和游戏结束标志;开始生成射击目标并启动游戏主循环。

window.onload = init;
// 初始化游戏变量
function init() {
  player = { x: canvas.width / 2, y: canvas.height - 50, width: 50, height: 50, speed: 5 };
  bullets = [];
  targets = [];
  score = 0;
  gameOver = false;

  createTargets();
  requestAnimationFrame(gameLoop);
}

在游戏主循环中,移动玩家,射击目标和子弹,删除超出画布的射击目标和子弹。并检测是否击中目标。

// 游戏主循环
function gameLoop() {
  update();
  draw();
  if (!gameOver) requestAnimationFrame(gameLoop);
}
// 更新游戏画面
function update() {
  movePlayer();

  bullets = bullets.filter((bullet) => bullet.y > 0);
  bullets.forEach((bullet) => (bullet.y -= bullet.speed));

  targets = targets.filter((target) => target.y < canvas.height);
  targets.forEach((target) => (target.y += target.speed));

  checkHit();
}

主页面的内容如下, 定义一个canvas元素,并加载game.js,执行其中的逻辑。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Canvas射击小游戏</title>
    <style>
      body {
        margin: 0;
        overflow: hidden;
      }
      canvas {
        background: #000;
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas id="gameCanvas"></canvas>
    <script src="game.js"></script>
  </body>
</html>

以上就是实现文中开头演示效果的全部代码了。

最后

笔者在项目中使用canvas的场景一般是对图片进行裁剪,以及将多张图片合成为一张图片,将图片转化为Base64编码等,用canvas开发完这个游戏,才感觉使用canvas开发动画才是canvas的真谛与经典使用场景。另外感觉这个游戏虽然是个2D游戏,可是移动效果比之前的3D移动效果更好一些。所以不能片面地以为3D游戏就一定比2D游戏好玩。如果你能看到这里,说明你对这个游戏比较感兴趣,如果你想详细了解这个游戏代码实现细节的话,可以点击这里下载学习,探讨与交流。