文章目录
手把手教你用HTML5 Canvas打造火柴人跑酷游戏
跑酷类游戏凭借简单的操作和不断提升的难度,一直是开发者入门游戏开发的经典案例。本文将带大家从零开始,解析如何用HTML5 Canvas实现一个完整的火柴人跑酷游戏,包括角色动画、物理跳跃、障碍物系统、碰撞检测等核心功能,让你不仅能玩到游戏,更能理解每一行代码的作用。

一、游戏基础架构:搭建画布与核心参数
任何Canvas游戏的第一步都是搭建基础框架,我们需要创建画布元素、设置游戏参数,并初始化核心对象。这部分就像盖房子的地基,决定了游戏的基本运行环境。
1. 画布与DOM结构
首先在HTML中定义游戏所需的DOM元素,包括画布、分数显示、游戏结束界面等:
<div class="game-container">
<!-- 游戏主画布 -->
<canvas id="gameCanvas" width="800" height="300"></canvas>
<!-- 游戏信息显示 -->
<div class="game-info">
<div>分数: <span id="score">0</span></div>
<div>速度: <span id="speed">5</span></div>
</div>
<!-- 游戏结束界面 -->
<div class="game-over" id="gameOver">
<h2>游戏结束!</h2>
<p>你的分数: <span id="finalScore">0</span></p>
<button id="restartBtn">再来一次</button>
</div>
</div>
画布的宽高设置为800×300像素,这个尺寸既能保证游戏视野合适,又不会给性能带来太大压力。容器采用相对定位,方便后续在画布上叠加信息面板和结束界面。
2. 核心参数与对象初始化
在JavaScript中,我们需要获取画布上下文并定义游戏核心参数。这些参数就像游戏的"规则手册",控制着角色、障碍物和游戏进度的行为:
// 获取画布和上下文
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// 游戏状态参数
let score = 0; // 当前分数
let gameSpeed = 5; // 游戏速度(障碍物移动速度)
let isGameOver = false; // 游戏是否结束
// 火柴人属性
const stickman = {
x: 100, // 初始X坐标
y: 200, // 初始Y坐标
width: 30, // 宽度
height: 60, // 高度
jumping: false, // 是否正在跳跃
jumpStrength: 0, // 跳跃力度
originalY: 200, // 初始地面Y坐标(用于落地检测)
runCycle: 0 // 奔跑动画周期(控制四肢摆动)
};
// 障碍物数组与生成计时器
let obstacles = [];
let obstacleTimer = 0;
const OBSTACLE_INTERVAL = 150; // 障碍物生成间隔
特别注意stickman对象中的runCycle属性,它是实现奔跑动画的关键,通过不断递增的值控制四肢摆动角度;而gameSpeed不仅控制障碍物移动速度,还会随分数增加,实现难度递增。
二、火柴人动画:用数学实现流畅的奔跑效果
让火柴人"活"起来是游戏的核心吸引力之一。我们不需要复杂的精灵图,仅用Canvas的基本绘图API和三角函数,就能实现逼真的奔跑动画。
1. 火柴人绘制原理
火柴人由头部、身体、手臂和腿部组成,每个部分通过简单的线条和圆形绘制。关键是让四肢随时间周期性摆动,模拟跑步动作:
function drawStickman() {
ctx.save();
// 将坐标原点移动到火柴人中心位置,方便旋转计算
ctx.translate(stickman.x, stickman.y);
// 1. 绘制头部(圆形)
ctx.beginPath();
ctx.arc(0, -stickman.height/2, 10, 0, Math.PI * 2);
ctx.fillStyle = '#333';
ctx.fill();
// 2. 绘制身体(直线)
ctx.beginPath();
ctx.moveTo(0, -stickman.height/2 + 10); // 头部下方
ctx.lineTo(0, 0); // 身体底部(腰部)
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.stroke();
// 3. 绘制手臂(随奔跑周期摆动)
const armAngle = Math.sin(stickman.runCycle) * 0.5; // 手臂角度(-0.5到0.5弧度)
// 左臂
ctx.beginPath();
ctx.moveTo(0, -stickman.height/4); // 肩部
ctx.lineTo(
Math.cos(armAngle) * 20,
-stickman.height/4 + Math.sin(armAngle) * 10
);
ctx.stroke();
// 右臂(与左臂对称)
ctx.beginPath();
ctx.moveTo(0, -stickman.height/4);
ctx.lineTo(
Math.cos(armAngle + Math.PI) * 20,
-stickman.height/4 + Math.sin(armAngle + Math.PI) * 10
);
ctx.stroke();
// 4. 绘制腿部(随奔跑周期摆动,与手臂反向)
const legAngle = Math.sin(stickman.runCycle + Math.PI) * 0.5; // 腿部角度(与手臂相反)
// 左腿
ctx.beginPath();
ctx.moveTo(0, 0); // 腰部
ctx.lineTo(
Math.cos(legAngle) * 15,
Math.sin(legAngle) * 25
);
ctx.stroke();
// 右腿(与左腿对称)
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(
Math.cos(legAngle + Math.PI) * 15,
Math.sin(legAngle + Math.PI) * 25
);
ctx.stroke();
ctx.restore();
// 更新奔跑周期(让动画持续进行)
stickman.runCycle += 0.2;
}
2. 动画核心:三角函数的妙用
奔跑动画的关键是Math.sin()函数——它能生成周期性变化的角度值(-1到1之间),完美模拟四肢的摆动规律:
- 手臂和腿部的角度通过
stickman.runCycle计算,确保动作连贯 - 手臂与腿部的角度相差
Math.PI(180度),实现"手臂前摆时腿部后摆"的自然效果 - 每次游戏循环中
runCycle增加0.2,控制动画速度(值越大动作越快)
这种纯代码实现的动画无需任何图片资源,不仅轻量化,还能通过调整参数轻松改变动作风格。
三、物理系统:跳跃与重力的实现
让火柴人能通过空格键跳跃,并模拟真实的重力效果,是提升游戏体验的关键。这部分需要用简单的物理公式实现跳跃轨迹。
1. 跳跃机制与重力模拟
跳跃的核心是"跳跃力"与"重力"的对抗:按下空格键时给予向上的力,随后重力逐渐抵消这个力,使角色先上升后下落:
// 处理跳跃逻辑
function handleJump() {
if (stickman.jumping) {
// 应用重力(每次循环减少跳跃力)
stickman.jumpStrength -= 0.7;
// 根据跳跃力更新Y坐标(正值上升,负值下降)
stickman.y -= stickman.jumpStrength;
// 落地检测:当角色回到初始高度时,结束跳跃
if (stickman.y > stickman.originalY) {
stickman.y = stickman.originalY; // 固定在地面
stickman.jumping = false;
stickman.jumpStrength = 0;
}
}
}
// 空格键控制跳跃
document.addEventListener('keydown', (e) => {
// 只有在未跳跃且游戏未结束时才能起跳
if (e.code === 'Space' && !stickman.jumping && !isGameOver) {
stickman.jumping = true;
stickman.jumpStrength = 15; // 初始跳跃力(决定跳多高)
}
});
2. 物理参数调优
jumpStrength初始值为15:这个值决定跳跃高度,越大跳得越高(可根据需要调整)- 重力加速度为0.7:每次循环减少0.7的跳跃力,值越大下落越快,更接近真实物理效果
- 落地检测确保角色不会穿过地面,始终保持在
originalY高度
通过调整这两个参数,可以轻松改变游戏的手感——比如降低重力让跳跃更"轻盈",增加跳跃力让角色能越过更高的障碍物。
四、障碍物系统:随机生成与难度递增
障碍物是跑酷游戏的核心挑战来源。我们需要实现随机生成不同类型的障碍物,并随游戏进度提高难度。
1. 障碍物类型与生成逻辑
设计三种不同类型的障碍物(方块、坑洞、杆子),每种有不同的尺寸和外观,增加游戏多样性:
// 生成障碍物
function spawnObstacle() {
obstacleTimer++;
// 随游戏速度提高,障碍物生成频率增加
if (obstacleTimer > OBSTACLE_INTERVAL / (gameSpeed / 5)) {
// 随机选择障碍物类型
const types = ['block', 'pit', 'pole'];
const type = types[Math.floor(Math.random() * types.length)];
let width, height, y;
// 根据类型设置尺寸和位置(确保都在地面上)
if (type === 'block') {
width = 30 + Math.random() * 40; // 宽度随机(30-70px)
height = 30 + Math.random() * 60; // 高度随机(30-90px)
y = groundY - height; // 底部与地面对齐
} else if (type === 'pit') {
width = 40 + Math.random() * 60; // 宽度随机(40-100px)
height = 20; // 固定高度(坑洞深度)
y = groundY - height + 5; // 略微陷入地面
} else if (type === 'pole') {
width = 10; // 固定宽度(细杆)
height = 40 + Math.random() * 80; // 高度随机(40-120px)
y = groundY - height; // 底部与地面对齐
}
// 添加到障碍物数组
obstacles.push({
x: canvas.width, // 从画布右侧出现
y: y,
width: width,
height: height,
type: type
});
obstacleTimer = 0; // 重置计时器
}
}
2. 障碍物移动与难度控制
障碍物需要向左移动(模拟角色向前跑),并在移出画布后移除,同时随分数增加提高速度:
// 更新障碍物位置
function updateObstacles() {
obstacles.forEach((obstacle, index) => {
// 向左移动(速度=游戏速度)
obstacle.x -= gameSpeed;
// 移除超出画布的障碍物
if (obstacle.x + obstacle.width < 0) {
obstacles.splice(index, 1);
// 每避开一个障碍物加10分
score += 10;
scoreElement.textContent = score;
// 每得100分提高速度(难度递增)
if (score % 100 === 0) {
gameSpeed += 0.5;
speedElement.textContent = gameSpeed.toFixed(1);
}
}
});
}
这种设计让游戏难度随玩家技能自然提升——初期轻松适应,后期需要更快的反应速度,保持游戏的挑战性和趣味性。
五、碰撞检测:判断游戏生死的关键
碰撞检测是决定游戏何时结束的核心逻辑,需要精确判断火柴人与障碍物是否接触。
1. 碰撞检测原理
采用"轴对齐 bounding box"(AABB)碰撞检测算法,通过比较两个矩形的位置关系判断是否碰撞:
function checkCollision() {
// 地面碰撞检测(防止角色掉出地面)
if (stickman.y + stickman.height/2 > groundY) {
stickman.y = groundY - stickman.height/2;
stickman.jumping = false;
stickman.jumpStrength = 0;
}
// 与障碍物碰撞检测
for (let i = 0; i < obstacles.length; i++) {
const obstacle = obstacles[i];
// 定义火柴人的碰撞盒(简化为矩形)
const stickmanHitbox = {
x: stickman.x - 15, // 碰撞盒左边界
y: stickman.y - 30, // 碰撞盒上边界
width: 30, // 碰撞盒宽度
height: 60 // 碰撞盒高度
};
// AABB碰撞检测:两个矩形是否重叠
if (
stickmanHitbox.x < obstacle.x + obstacle.width &&
stickmanHitbox.x + stickmanHitbox.width > obstacle.x &&
stickmanHitbox.y < obstacle.y + obstacle.height &&
stickmanHitbox.y + stickmanHitbox.height > obstacle.y
) {
// 碰撞发生,游戏结束
gameOver();
return;
}
}
}
2. 碰撞盒设计
为了平衡检测精度和性能,我们将火柴人简化为一个30×60的矩形碰撞盒:
- 碰撞盒比视觉上的火柴人略大,确保玩家感受到的"碰撞"与视觉一致
- 地面碰撞检测确保角色始终在地面上,不会因为跳跃计算误差掉出地图
这种简化的碰撞检测既高效又直观,适合2D跑酷游戏的需求。
六、游戏主循环:驱动一切的"心脏"
游戏的所有元素需要按顺序更新和绘制,这就是主循环的作用。它就像游戏的"心脏",每秒跳动约60次(与屏幕刷新率同步)。
function gameLoop() {
if (isGameOver) return; // 游戏结束则停止循环
// 1. 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 2. 绘制背景和地面
ctx.fillStyle = '#ecf0f1';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGround(); // 绘制绿色地面
// 3. 绘制并更新火柴人
drawStickman();
// 4. 生成、更新并绘制障碍物
spawnObstacle();
updateObstacles();
drawObstacles();
// 5. 处理跳跃物理
handleJump();
// 6. 检测碰撞
checkCollision();
// 7. 循环调用(与浏览器刷新率同步)
animationId = requestAnimationFrame(gameLoop);
}
主循环的执行顺序非常重要:先清空画布,再绘制背景,最后绘制角色和障碍物,确保视觉层次正确。requestAnimationFrame方法确保循环与屏幕刷新率同步(通常60fps),避免不必要的性能消耗。
七、游戏状态管理:开始、结束与重启
一个完整的游戏需要清晰的状态管理,包括初始化、结束和重启功能:
// 初始化游戏
function initGame() {
// 重置游戏状态
score = 0;
gameSpeed = 5;
isGameOver = false;
obstacles = [];
// 重置火柴人位置和状态
stickman.x = 100;
stickman.y = stickman.originalY;
stickman.jumping = false;
stickman.jumpStrength = 0;
// 更新UI显示
scoreElement.textContent = score;
speedElement.textContent = gameSpeed.toFixed(1);
gameOverElement.style.display = 'none';
// 启动主循环
gameLoop();
}
// 游戏结束处理
function gameOver() {
isGameOver = true;
cancelAnimationFrame(animationId); // 停止主循环
finalScoreElement.textContent = score; // 显示最终分数
gameOverElement.style.display = 'flex'; // 显示结束界面
}
// 绑定重启按钮事件
restartBtn.addEventListener('click', initGame);
// 启动游戏
initGame();
通过分离初始化和结束逻辑,确保每次重启游戏都能完全重置所有状态,避免残留数据影响新游戏。
总结与扩展方向
本文实现的火柴人跑酷游戏包含了2D游戏的核心要素:角色动画、物理系统、障碍物生成、碰撞检测和状态管理。通过Canvas的基本API,我们用不到500行代码就完成了一个可玩的游戏。
如果想进一步扩展,可以尝试:
- 添加更多障碍物类型(如移动障碍物、高低组合障碍)
- 实现二段跳或冲刺等特殊技能
- 增加音效和背景音乐提升沉浸感
- 加入角色皮肤和道具系统
- 优化移动端体验(添加触摸控制)
这个游戏的核心价值在于展示了"简单即美"——不需要复杂的引擎和资源,仅用HTML5 Canvas和基础数学知识,就能创造出有趣的交互体验。希望本文能帮助你理解游戏开发的基本逻辑,开启你的创意开发之旅!
源码下载:https://download.youkuaiyun.com/download/dengjianbin/92179208

1万+

被折叠的 条评论
为什么被折叠?



