前情提要
在上一节中,我们实现了一个可以通过键盘控制移动的角色。但是细心的你可能已经发现了一个问题:角色的移动速度似乎不太稳定。
问题在哪里
还记得第一节课中提到的吗?我们的游戏循环间隔时间其实是不固定的。虽然我们设置了 setInterval(main, 1),但实际上每次循环的间隔可能是 1 毫秒,也可能是 5 毫秒,甚至更长。
这是因为游戏逻辑的处理和画面的绘制都需要时间,而且这个时间是不固定的。
在上一节的代码中,我们是这样移动角色的:
if ('KeyA' in keysDown) {
player.x += -1; // 每次循环移动 1 像素
}
这意味着什么呢?如果循环间隔是 1 毫秒,角色 1 秒钟会移动 1000 像素。但如果循环间隔变成了 2 毫秒,角色 1 秒钟就只能移动 500 像素了。
这就是为什么角色的移动速度会不稳定的原因。
如何解决
解决方法其实很简单,还记得我们在第一节课中计算的 delta 吗?它记录的就是每次循环的实际间隔时间。
我们只需要把移动的距离和这个时间关联起来就可以了。
定义角色的速度
首先,我们需要给角色定义一个速度属性。这个速度的单位是"像素/秒"。
const player = {
image: new Image(),
x: cvWidth / 2,
y: cvHeight / 2,
speed: 200, // 新增:角色的移动速度,单位是像素/秒
};
player.image.src = './images/mario.png';
使用 delta 来计算移动距离
然后,在 update 函数中,我们就可以使用 delta 来计算角色应该移动的距离了。
function update(modifier) {
gameData.currentTimeStamp = new Date().getTime();
gameData.updateDelta = modifier;
gameData.fps = Math.floor(1 / modifier);
// modifier 就是 delta,单位是秒
// player.speed 的单位是像素/秒
// player.speed * modifier 的结果就是这一帧应该移动的像素数
if ('KeyA' in keysDown) {
player.x += -player.speed * modifier; // 向左移动
}
if ('KeyD' in keysDown) {
player.x += player.speed * modifier; // 向右移动
}
if ('KeyW' in keysDown) {
player.y += -player.speed * modifier; // 向上移动
}
if ('KeyS' in keysDown) {
player.y += player.speed * modifier; // 向下移动
}
}
原理解释
让我们来理解一下这个计算:
player.speed是 200,表示角色每秒移动 200 像素modifier是每次循环的间隔时间,单位是秒(比如 0.001 秒)player.speed * modifier就是这一帧应该移动的距离
举个例子:
- 如果循环间隔是 0.001 秒(1 毫秒),那么这一帧移动 200 * 0.001 = 0.2 像素
- 如果循环间隔是 0.002 秒(2 毫秒),那么这一帧移动 200 * 0.002 = 0.4 像素
虽然每帧移动的距离不同,但是 1 秒钟累计移动的距离都是 200 像素左右,这样就保证了移动速度的稳定性。
添加边界限制
现在角色可以稳定移动了,但是它可以移动到画布外面去。让我们添加一个边界限制,防止角色跑出画布。
function update(modifier) {
gameData.currentTimeStamp = new Date().getTime();
gameData.updateDelta = modifier;
gameData.fps = Math.floor(1 / modifier);
if ('KeyA' in keysDown) {
player.x += -player.speed * modifier;
}
if ('KeyD' in keysDown) {
player.x += player.speed * modifier;
}
if ('KeyW' in keysDown) {
player.y += -player.speed * modifier;
}
if ('KeyS' in keysDown) {
player.y += player.speed * modifier;
}
// 边界限制
if (player.x < 0) {
player.x = 0;
}
if (player.x > cvWidth) {
player.x = cvWidth;
}
if (player.y < 0) {
player.y = 0;
}
if (player.y > cvHeight) {
player.y = cvHeight;
}
}
最终效果
现在,无论游戏循环的间隔时间如何变化,角色的移动速度都会保持稳定。你可以尝试修改 player.speed 的值,来调整角色的移动速度。
比如:
player.speed = 100- 角色移动得很慢player.speed = 200- 角色移动速度适中player.speed = 400- 角色移动得很快
完整的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<div id="screen"></div>
</body>
<script>
// 创建 canvas
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// 该变量用于存储上一帧开始的时间
let then;
// Canvas 宽高
const cvWidth = 538;
const cvHeight = 430;
// 存储游戏数据的变量
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 player = {
image: new Image(),
x: cvWidth / 2,
y: cvHeight / 2,
speed: 200, // 角色的移动速度,单位是像素/秒
};
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 update(modifier) {
gameData.currentTimeStamp = new Date().getTime();
gameData.updateDelta = modifier;
gameData.fps = Math.floor(1 / modifier);
if ("KeyA" in keysDown) {
player.x += -player.speed * modifier;
}
if ("KeyD" in keysDown) {
player.x += player.speed * modifier;
}
if ("KeyW" in keysDown) {
player.y += -player.speed * modifier;
}
if ("KeyS" in keysDown) {
player.y += player.speed * modifier;
}
// 边界限制
if (player.x < 0) {
player.x = 0;
}
if (player.x > cvWidth) {
player.x = cvWidth;
}
if (player.y < 0) {
player.y = 0;
}
if (player.y > cvHeight) {
player.y = cvHeight;
}
}
function drawImg() {
ctx.clearRect(0, 0, cvWidth, cvHeight);
ctx.fillStyle = "#E6E6FA";
ctx.fillRect(0, 0, cvWidth, cvHeight);
ctx.fillStyle = "#000";
ctx.moveTo(cvWidth / 2, 0);
ctx.lineTo(cvWidth / 2, cvHeight);
ctx.stroke();
ctx.moveTo(0, cvHeight / 2);
ctx.lineTo(cvWidth, cvHeight / 2);
ctx.stroke();
ctx.font = "20px 微软雅黑";
ctx.fillStyle = "#000";
ctx.fillText("当前时间戳为: " + gameData.currentTimeStamp, 260, 50);
ctx.fillText("当前循环耗时(秒): " + gameData.updateDelta, 260, 120);
ctx.fillText("当前帧数: " + gameData.fps, 260, 180);
ctx.drawImage(
player.image,
player.x - player.image.width / 2,
player.y - player.image.height / 2
);
}
window.onload = init();
</script>
</html>