前言
自上一篇文章业务代码写累了,写个小游戏玩玩, 开发了一款射击类的小游戏之后,感觉玩起来还是蛮有意思的,开发游戏的动力空前比较高涨,于是还想再开发一个同类的小游戏。上次是使用Three.js开发的,这次换个技术,采用原生的Canvas开发一款射击类的小游戏。
效果展示
游戏的玩法规则是:不断有射击目标从上方落下,当射击目标触碰到射击者时,游戏结束。射击者可以按下左右方向键进行左右方向的移动,按空格键开火射击。每击中一个目标,加10分。
开发步骤
整体思路是先绘制场景元素,接着给每种场景元素加动效,最后处理场景元素之间的联动关系。
绘制场景元素
先绘制画布和声明公共变量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: 5 和 height: 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游戏好玩。如果你能看到这里,说明你对这个游戏比较感兴趣,如果你想详细了解这个游戏代码实现细节的话,可以点击这里下载学习,探讨与交流。