前情提要
在上一节中,我们修正了角色的移动速度,让它变得稳定可控。现在角色可以在画布上自由移动了,但是看起来还是有点奇怪——它可以在空中飞来飞去。
一个真正的游戏角色应该受到重力的影响,能够跳跃,也会下落。这节课我们就来实现这个功能。
什么是重力系统
在现实世界中,所有物体都会受到重力的影响。在游戏中,我们也需要模拟这个效果。
简单来说,重力系统就是:
- 角色会不断向下加速
- 当角色碰到地面时,停止下落
- 当角色跳跃时,给它一个向上的速度
- 向上的速度会逐渐被重力抵消,最终角色会开始下落
定义地面和重力
首先,我们需要定义一些新的变量:
// 游戏基本变量
const floorY = 344; // 地面的 Y 坐标
const g = 980; // 重力加速度,单位是像素/秒²
为什么重力加速度是 980 呢?因为现实世界中的重力加速度是 9.8 米/秒²,我们这里把 1 米对应 100 像素,所以就是 980 像素/秒²。当然,你也可以根据游戏的感觉来调整这个值。
给角色添加垂直速度
角色需要一个垂直方向的速度属性:
const player = {
image: new Image(),
x: cvWidth / 2,
y: floorY, // 初始位置改为地面上
speed: 200,
ySpeed: 0, // 新增:垂直方向的速度,单位是像素/秒
};
player.image.src = './images/mario.png';
实现重力控制器
现在我们来写一个函数,专门处理重力相关的逻辑:
function gravityController(modifier) {
// 更新角色的 Y 坐标
player.y -= player.ySpeed * modifier;
// 如果角色在空中,就施加重力
if (player.y < floorY) {
player.ySpeed -= g * modifier; // 重力让垂直速度不断减小(向下加速)
} else {
// 如果角色在地面上或者穿过了地面
player.ySpeed = 0; // 垂直速度归零
player.y = floorY; // 把角色固定在地面上
}
}
让我们理解一下这段代码:
-
player.y -= player.ySpeed * modifier- 根据垂直速度更新 Y 坐标。注意这里是减号,因为 Canvas 的 Y 轴是向下的,而我们希望正的 ySpeed 表示向上的速度。 -
if (player.y < floorY)- 判断角色是否在空中(Y 坐标小于地面坐标) -
player.ySpeed -= g * modifier- 如果在空中,重力会让垂直速度不断减小。正的 ySpeed 会逐渐变小,变成 0,然后变成负数(开始下落)。 -
如果角色到达或穿过地面,就把它固定在地面上,并且把垂直速度归零。
实现跳跃
现在我们来添加跳跃功能。当玩家按下空格键时,给角色一个向上的速度:
function update(modifier) {
gameData.currentTimeStamp = new Date().getTime();
gameData.updateDelta = modifier;
gameData.fps = Math.floor(1 / modifier);
// 跳跃控制
if ('Space' in keysDown) {
if (player.y >= floorY) { // 只有在地面上才能跳跃
player.ySpeed = 400; // 给一个向上的初速度
}
}
// 左右移动
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);
}
优化:二段跳
现在的跳跃已经可以工作了,但是只能在地面上跳。我们可以添加一个"二段跳"的功能,让游戏更有趣。
首先,给角色添加一个"滞空次数"的属性:
const player = {
image: new Image(),
x: cvWidth / 2,
y: floorY,
speed: 200,
ySpeed: 0,
airCount: 0, // 新增:当前滞空次数
maxAirCount: 28, // 新增:最大滞空次数(可以理解为可以在空中按多少次跳跃键)
};
player.image.src = './images/mario.png';
然后修改跳跃逻辑:
// 跳跃控制
if ('Space' in keysDown) {
if (player.airCount < player.maxAirCount) {
player.ySpeed += 20; // 每次按空格增加一点向上的速度
player.airCount++; // 滞空次数加 1
}
}
最后,在重力控制器中,当角色落地时重置滞空次数:
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; // 落地时重置滞空次数
}
}
这样,玩家可以在空中连续按空格键来延长滞空时间,甚至实现二段跳的效果。
移除上下移动
现在我们有了重力系统,就不需要 W 和 S 键来控制上下移动了。可以把这部分代码删除:
// 删除这些代码
if ('KeyW' in keysDown) {
player.y += -player.speed * modifier;
}
if ('KeyS' in keysDown) {
player.y += player.speed * modifier;
}
if (player.y < 0) {
player.y = 0;
}
if (player.y > cvHeight) {
player.y = cvHeight;
}
最终效果
现在,角色会受到重力的影响,可以跳跃,也会下落。按住空格键可以跳得更高,松开后角色会自然下落。
你可以尝试调整这些参数来改变游戏的手感:
g- 重力加速度,越大角色下落越快player.ySpeed = 400- 跳跃初速度,越大跳得越高player.maxAirCount- 最大滞空次数,越大可以在空中停留越久
完整的代码
<!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; // 地面的 Y 坐标
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 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.fillStyle = "#E6E6FA";
ctx.fillRect(0, 0, cvWidth, cvHeight);
ctx.font = "20px 微软雅黑";
ctx.fillStyle = "#000";
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>