实现生命值相当直接。让我们首先在声明其他变量的地方添加一个变量来存储生命的数量。
绘制生命计数器看起来与绘制得分计数器几乎相同——在代码中的 drawScore() 函数下方添加以下函数。
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}
与其立即结束游戏,不如在生命值用完之前减少生命的数量。当玩家开始下一条生命时,我们还可以重置球和挡板的位置。因此,在 draw() 函数中,用以下三行替换
alert("GAME OVER");
document.location.reload();
clearInterval(interval); // Needed for Chrome to end game
通过这一点,我们可以为其添加稍微复杂一些的逻辑,如下所示:
lives--;
if (!lives) {
alert("GAME OVER");
document.location.reload();
clearInterval(interval); // Needed for Chrome to end game
} else {
x = canvas.width / 2;
y = canvas.height - 30;
dx = 2;
dy = -2;
paddleX = (canvas.width - paddleWidth) / 2;
}
现在,当球击中屏幕底部边缘时,我们会从 lives 变量中减去一条生命。如果没有剩余生命,游戏就失败了;如果还有剩余生命,则重置球和挡板的位置,并随之重置球的移动。
现在您需要在 draw() 函数中调用 drawLives(),并将其放在 drawScore() 调用下方。
现在让我们处理一些与游戏机制无关,但与游戏渲染方式相关的内容。 requestAnimationFrame() 帮助浏览器比我们目前使用 setInterval() 实现的固定帧率更好地渲染游戏。替换以下行
interval = setInterval(draw, 10);
with
并删除所有出现的
clearInterval(interval); // Needed for Chrome to end game
然后,在 draw() 函数的最底部(就在闭合花括号之前),添加以下行,这会导致 draw() 函数一遍又一遍地调用自身:
requestAnimationFrame(draw);
draw() 函数现在在一个 requestAnimationFrame() 循环中一遍又一遍地执行,但我们不再使用固定的 10 毫秒帧率,而是将帧率控制权交还给浏览器。它将相应地同步帧率,并仅在需要时渲染形状。这比旧的 setInterval() 方法产生了更高效、更流畅的动画循环。
就是这样——游戏的最终版本已经准备就绪,可以开始使用了!
<canvas id="myCanvas" width="480" height="320"></canvas>
<button id="runButton">Start game</button>
canvas {
background: #eeeeee;
}
button {
display: block;
}
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");
const ballRadius = 10;
let x = canvas.width / 2;
let y = canvas.height - 30;
let dx = 2;
let dy = -2;
const paddleHeight = 10;
const paddleWidth = 75;
let paddleX = (canvas.width - paddleWidth) / 2;
let rightPressed = false;
let leftPressed = false;
const brickRowCount = 5;
const brickColumnCount = 3;
const brickWidth = 75;
const brickHeight = 20;
const brickPadding = 10;
const brickOffsetTop = 30;
const brickOffsetLeft = 30;
let score = 0;
let lives = 3;
let bricks = [];
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
document.addEventListener("keydown", keyDownHandler);
document.addEventListener("keyup", keyUpHandler);
document.addEventListener("mousemove", mouseMoveHandler);
function keyDownHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = true;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = true;
}
}
function keyUpHandler(e) {
if (e.key === "Right" || e.key === "ArrowRight") {
rightPressed = false;
} else if (e.key === "Left" || e.key === "ArrowLeft") {
leftPressed = false;
}
}
function mouseMoveHandler(e) {
let relativeX = e.clientX - canvas.offsetLeft;
if (relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
}
}
function collisionDetection() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
let b = bricks[c][r];
if (b.status === 1) {
if (
x > b.x &&
x < b.x + brickWidth &&
y > b.y &&
y < b.y + brickHeight
) {
dy = -dy;
b.status = 0;
score++;
if (score === brickRowCount * brickColumnCount) {
alert("YOU WIN, CONGRATS!");
document.location.reload();
}
}
}
}
}
}
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height - paddleHeight, paddleWidth, paddleHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
function drawBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const brickX = r * (brickWidth + brickPadding) + brickOffsetLeft;
const brickY = c * (brickHeight + brickPadding) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
}
}
function drawScore() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Score: ${score}`, 8, 20);
}
function drawLives() {
ctx.font = "16px Arial";
ctx.fillStyle = "#0095DD";
ctx.fillText(`Lives: ${lives}`, canvas.width - 65, 20);
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
drawScore();
drawLives();
collisionDetection();
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius) {
if (x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else {
lives--;
if (!lives) {
alert("GAME OVER");
document.location.reload();
} else {
x = canvas.width / 2;
y = canvas.height - 30;
dx = 3;
dy = -3;
paddleX = (canvas.width - paddleWidth) / 2;
}
}
}
if (rightPressed && paddleX < canvas.width - paddleWidth) {
paddleX += 7;
} else if (leftPressed && paddleX > 0) {
paddleX -= 7;
}
x += dx;
y += dy;
requestAnimationFrame(draw);
}
const runButton = document.getElementById("runButton");
runButton.addEventListener("click", () => {
draw();
runButton.disabled = true;
});
您已经完成了所有课程——恭喜!至此,您应该已经掌握了 Canvas 操作的基础知识和 2D 游戏的逻辑。现在是学习一些框架并继续游戏开发的绝佳时机。您可以查看本系列配套的 使用 Phaser 进行 2D 弹球游戏 或 使用 Phaser 构建的 Cyber Orb 教程。您还可以查看 MDN 上的 游戏部分 以获取灵感和更多知识。
您也可以返回 本教程系列的索引页。祝您编码愉快!