手把手教你用HTML5 Canvas打造火柴人跑酷游戏

文章目录

手把手教你用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行代码就完成了一个可玩的游戏。

如果想进一步扩展,可以尝试:

  1. 添加更多障碍物类型(如移动障碍物、高低组合障碍)
  2. 实现二段跳或冲刺等特殊技能
  3. 增加音效和背景音乐提升沉浸感
  4. 加入角色皮肤和道具系统
  5. 优化移动端体验(添加触摸控制)

这个游戏的核心价值在于展示了"简单即美"——不需要复杂的引擎和资源,仅用HTML5 Canvas和基础数学知识,就能创造出有趣的交互体验。希望本文能帮助你理解游戏开发的基本逻辑,开启你的创意开发之旅!

源码下载:https://download.youkuaiyun.com/download/dengjianbin/92179208

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值