前情提要
在上一节中,我们实现了重力系统和跳跃功能,角色已经可以在地面上跳来跳去了。但是画面还是太单调了,只有一个紫色的背景和一条黑线。
这节课我们来添加一张背景图片,让游戏画面更加丰富。
准备背景图片
首先,你需要准备一张背景图片。可以从 Mari0 游戏中提取,或者自己找一张合适的图片。
图片的尺寸最好和画布的尺寸一致(600 x 500),这样绘制起来最简单。如果尺寸不一致也没关系,Canvas 的 drawImage 方法可以缩放图片。
把图片放到 images 文件夹中,命名为 bg.jpg(或者其他你喜欢的名字)。
加载背景图片
和加载角色图片一样,我们需要创建一个 Image 对象来加载背景图片:
const backgroundImg = new Image();
backgroundImg.src = './images/bg.jpg';
这段代码可以放在 player 对象定义的附近,方便管理。
绘制背景图片
在 drawImg 函数中,我们需要先绘制背景图片,然后再绘制其他内容。
重要:绘制顺序很重要!
Canvas 的绘制是有顺序的,后绘制的内容会覆盖先绘制的内容。所以背景图片应该最先绘制,然后是地面线,最后是角色。
function drawImg() {
ctx.clearRect(0, 0, cvWidth, cvHeight); // 清除画面
// 先绘制背景图片
ctx.drawImage(backgroundImg, 0, 0, cvWidth, cvHeight);
// 绘制文字信息
ctx.font = "20px 微软雅黑";
ctx.fillStyle = "#fff"; // 改成白色,在深色背景上更清晰
ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 10, 30);
ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 10, 60);
ctx.fillText("当前帧数: " + gameData.fps, 10, 90);
ctx.fillText("按空格跳跃,A/D 左右移动", 10, 120);
// 最后绘制角色
ctx.drawImage(
player.image,
player.x - player.image.width / 2,
player.y - player.image.height / 2
);
}
drawImage 的不同用法
你可能注意到了,绘制背景图片时我们使用了 4 个参数:
ctx.drawImage(backgroundImg, 0, 0, cvWidth, cvHeight);
而绘制角色时只用了 3 个参数:
ctx.drawImage(player.image, x, y);
这是因为 drawImage 有多种用法:
三参数用法
ctx.drawImage(image, x, y);
image- 要绘制的图片x- 图片左上角的 X 坐标y- 图片左上角的 Y 坐标
这种用法会按照图片的原始尺寸绘制。
五参数用法
ctx.drawImage(image, x, y, width, height);
image- 要绘制的图片x- 图片左上角的 X 坐标y- 图片左上角的 Y 坐标width- 绘制出的图片宽度height- 绘制出的图片高度
这种用法可以缩放图片。我们用这种方法来确保背景图片填满整个画布。
九参数用法
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
这种用法可以裁剪图片的一部分来绘制,我们会在后面讲动画时用到。
优化:添加半透明遮罩
如果背景图片太亮,文字可能看不清楚。我们可以在背景图片上添加一个半透明的黑色遮罩:
function drawImg() {
ctx.clearRect(0, 0, cvWidth, cvHeight);
// 绘制背景图片
ctx.drawImage(backgroundImg, 0, 0, cvWidth, cvHeight);
// 绘制文字信息
ctx.font = "20px 微软雅黑";
ctx.fillStyle = "#fff";
ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 10, 30);
ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 10, 60);
ctx.fillText("当前帧数: " + gameData.fps, 10, 90);
ctx.fillText("按空格跳跃,A/D 左右移动", 10, 120);
// 绘制角色
ctx.drawImage(
player.image,
player.x - player.image.width / 2,
player.y - player.image.height / 2
);
}
rgba(0, 0, 0, 0.3) 表示黑色,透明度为 30%。你可以调整最后一个参数来改变遮罩的深浅。
关于图片加载
和角色图片一样,背景图片也是异步加载的。但是因为我们有一个无限循环在不断尝试绘制,所以不需要特别处理图片加载完成的事件。
如果你想确保图片加载完成后再开始游戏,可以使用 onload 事件:
const backgroundImg = new Image();
backgroundImg.onload = function() {
console.log('背景图片加载完成');
};
backgroundImg.src = './images/bg.jpg';
但在我们的游戏中,这不是必需的。
最终效果
现在,游戏画面应该有了一个漂亮的背景图片。角色在背景前跳跃,看起来就像一个真正的游戏了。
完整的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<div id="screen"></div>
</body>
<script>
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
let then;
const cvWidth = 538;
const cvHeight = 430;
const floorY = 344;
const g = 980;
const gameData = {
currentTimeStamp: new Date().getTime(),
};
const keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.code] = true;
});
addEventListener("keyup", function (e) {
delete keysDown[e.code];
});
// 加载背景图片
const backgroundImg = new Image();
backgroundImg.src = "./images/bg.jpg";
const player = {
image: new Image(),
x: cvWidth / 2,
y: floorY,
speed: 200,
ySpeed: 0,
airCount: 0,
maxAirCount: 28,
};
player.image.src = "./images/mario.png";
function init() {
then = Date.now();
createCanvas(cvWidth, cvHeight);
setInterval(main, 1);
}
function createCanvas(width, height) {
canvas.width = width;
canvas.height = height;
canvas.style.margin = "0 auto";
let screen = document.getElementById("screen");
screen.appendChild(canvas);
}
function main() {
const now = Date.now();
const delta = now - then;
update(delta / 1000);
drawImg();
then = now;
}
function gravityController(modifier) {
player.y -= player.ySpeed * modifier;
if (player.y < floorY) {
player.ySpeed -= g * modifier;
} else {
player.ySpeed = 0;
player.y = floorY;
player.airCount = 0;
}
}
function update(modifier) {
gameData.currentTimeStamp = new Date().getTime();
gameData.updateDelta = modifier;
gameData.fps = Math.floor(1 / modifier);
if ("Space" in keysDown) {
if (player.airCount < player.maxAirCount) {
player.ySpeed += 20;
player.airCount++;
}
}
if ("KeyA" in keysDown) {
player.x += -player.speed * modifier;
}
if ("KeyD" in keysDown) {
player.x += player.speed * modifier;
}
if (player.x < 0) {
player.x = 0;
}
if (player.x > cvWidth) {
player.x = cvWidth;
}
gravityController(modifier);
}
function drawImg() {
ctx.clearRect(0, 0, cvWidth, cvHeight);
// 绘制背景图片
ctx.drawImage(backgroundImg, 0, 0, cvWidth, cvHeight);
// 绘制文字信息
ctx.font = "20px 微软雅黑";
ctx.fillStyle = "#fff";
ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 10, 30);
ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 10, 60);
ctx.fillText("当前帧数: " + gameData.fps, 10, 90);
ctx.fillText("按空格跳跃,A/D 左右移动", 10, 120);
// 绘制角色
ctx.drawImage(
player.image,
player.x - player.image.width / 2,
player.y - player.image.height / 2
);
}
window.onload = init();
</script>
</html>