ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Canvas和JavaScript实现猫咪狂奔记:2D跑酷小游戏开发实战

2026/9/2 4:51:53 拓冰建站 浏览量
用Canvas和JavaScript实现猫咪狂奔记:2D跑酷小游戏开发实战 做小游戏练手的人最容易卡在哪一步不是算法不是框架而是角色动起来之后“假”。很多初学者写的 Canvas 小游戏里面的猫咪要么在地上平移要么四只脚像电动玩具一样同步摆动怎么看都别扭。问题不在于创意也不在于美术能力而在于没有理解“跑动循环”这件事。这篇文章想用“猫咪狂奔记”这个项目把游戏角色动画的最小可行路径讲清楚。我们会参考 Simons Cat 那种黑白简笔画风格用纯 HTML5 Canvas 加原生 JavaScript从零实现一个会跑、会跳、能躲避障碍物的横向跑酷小游戏。项目不依赖任何第三方库一个浏览器加一个文本编辑器就能跑通。Simons Cat 是英国动画师 Simon Tofield 创作的系列动画短片主角是一只黑白简笔画风格的猫咪。据公开资料这个系列的核心特色是线条极简动态极丰富。这两种特质放到技术项目里非常讨喜——不需要准备美术资源不需要引入骨骼动画工具只需要理解几个关键帧和一点游戏状态机的设计。我的判断是这个项目的技术含量不在“画一只猫”而在跑动循环、状态切换和碰撞检测这三件事。把这三件事想清楚你不仅能做出猫咪狂奔记还能迁移到任何 2D 跑酷游戏上。下面从问题开始一步一步把整个项目拆开。1. 这篇文章真正要解决的问题1.1 新手练手项目的三个坑很多初学者第一次做小游戏通常会踩三个坑。第一个坑是选题太大。一上来就想做“横版动作游戏”结果角色、地图、敌人、战斗系统全堆在一起代码写到一半就失去了方向。第二个坑是素材卡住。试图先找一只可爱的猫咪图片再找背景、找音效、找动画序列帧最后发现素材的时间比写代码还多而且图片风格还不统一。第三个坑是只做“静态展示”。角色明明能动了但只是简单的 translate 平移没有跳跃、没有障碍、没有碰撞、没有游戏状态玩起来没有反馈也就失去了“游戏”的意义。猫咪狂奔记可以同时绕开这三个坑。它的玩法足够小一个角色一条跑道一组障碍物。它的美术可以完全用 Canvas 绘制不需要外部图片。它又具备一个完整游戏的核心要素动画循环、物理跳跃、碰撞判定、分数增长、重新开始。1.2 为什么是“猫咪狂奔记”而不是别的跑酷类小游戏是练习 2D 游戏开发最好的入门题目之一。它看似简单却包含了几乎所有游戏都会遇到的基础问题如何用 requestAnimationFrame 驱动游戏主循环如何让角色的动画帧随时间切换如何处理跳跃重力和落地判定如何生成障碍物并基于 AABB 做碰撞检测如何管理 READY、RUNNING、OVER 这三个游戏状态如何在游戏结束后重置所有数据。这些能力是通用的。今天做的是猫咪狂奔记明天换个角色改成小狗、太空人、火箭逻辑几乎不用动。这也是我推荐大家用这个题材练手的原因它把“游戏”这个词拆成了一个个可以独立理解的小问题。1.3 读完这篇文章能做到什么读完这篇文章你会得到一份完整的可运行代码并且能说清楚每一行背后的设计意图。具体来说你能在浏览器中打开一个自绘简笔画风格的跑酷小游戏你能理解跑动循环中四帧姿态的基本原理你能掌握 Canvas 2D 游戏中最常用的状态机模式你会在遇到常见错误时知道先看哪行代码、打印什么信息你会得到一份针对“小游戏项目”的工程化建议而不是只学会复制粘贴。2. Simons Cat 画风的技术拆解2.1 简笔画风格的三要素Simons Cat 的视觉风格可以拆成三个技术要素线条、留白、节奏。线条方面角色由黑色轮廓线构成几乎不使用复杂的渐变和阴影线条粗细变化也不大。这种风格对应到 Canvas 里就是一条strokeStyle #222和lineWidth 3的设置。留白方面背景和角色之间保持高对比度角色不会淹没在复杂场景里。节奏方面动画短片常用“安静铺垫 突然爆发”的剪辑方式小游戏里对应的就是“待机状态 突然开始奔跑跳跃”。这三个要素对我们做游戏有一个直接启发美术成本可以压到极低但动态表现必须足够清晰。这非常符合独立开发者和学习者的需求。2.2 从画风到 Canvas 实现策略在 Canvas 中实现这种风格不需要精灵图也不需要使用图片资源。一只简笔画猫咪可以用几个基础图形组合出来ellipse画身体arc画头部两条直线加折线画耳朵quadraticCurveTo画尾巴四条直线画腿部。用这种方式绘制的角色体积小、加载快、缩放不模糊而且代码完全可控。如果你觉得这只猫画得不够好看可以随时调整坐标和线条参数不需要重新准备素材。这种“程序化绘制”的思路是 Canvas 小游戏相比图片素材方案的一个重要优势。2.3 版权边界与学习姿态需要提醒的是Simons Cat 是一个受版权保护的动画 IP。我们参考的是“黑白简笔画猫咪”这种风格方向并自己用 Canvas 绘制角色这属于学习和技术演示的范畴。但如果把原片的截图、官方素材、角色精确造型直接拿进游戏发布就会涉及版权问题。做技术练习时保留敬意做正式产品时替换为原创素材这是比较稳妥的姿态。3. 技术选型与整体架构3.1 为什么用纯 Canvas 而不是游戏引擎目前做小游戏有很多选择Phaser、Unity、Cocos Creator甚至还有各种微信小游戏框架。那为什么这个项目坚持用纯 HTML5 Canvas 加原生 JavaScript核心原因是降低认知负担。用游戏引擎时你同时在学习引擎的组件系统、资源管理、场景编辑器和脚本生命周期这些知识当然有用但它们会掩盖 2D 游戏最底层的运行机制。而用原生 Canvas游戏主循环、状态管理、碰撞检测全都要亲手写理解反而更扎实。当你已经清楚一个游戏是怎么从零转起来之后再去用 Phaser 或 Unity速度和深度都会不一样。3.2 游戏的核心模块整个游戏可以拆成四个模块模块职责对应文件入口页面创建 Canvas加载脚本index.html猫咪精灵类绘制角色、跑动动画、跳跃物理js/cat.js障碍物类绘制障碍物、移动、碰撞盒js/obstacle.js游戏主逻辑主循环、状态机、障碍物生成、分数js/game.js这种拆分方式并不复杂但已经具备小项目的模块化雏形。把“绘制角色的代码”和“控制游戏状态的代码”分开排查问题时会轻松很多。3.3 项目目录结构项目的文件结构非常简洁适合直接对照着建simons-cat-run/ ├── index.html └── js/ ├── cat.js ├── obstacle.js └── game.js4. 环境准备与前置条件这个项目对环境的要求低到几乎可以忽略但这恰恰是它的优点你没有理由因为环境问题卡住。4.1 运行环境你需要准备的内容如下一个现代浏览器推荐 Chrome 或 Edge版本没有硬性要求需要支持 Canvas 2D 和 ES6 class一个文本编辑器VS Code、WebStorm、记事本都可以不需要 Node.js不需要 npm不需要任何构建工具。代码中使用的是 ES6 的class语法这一点在 2017 年之后的浏览器中都得到支持。如果你在比较旧的浏览器中看到“class is not defined”之类的错误替换一个新浏览器即可。4.2 可选启动本地静态服务器直接双击index.html一般可以正常运行。但如果你在浏览器控制台看到跨域或加载错误最简单的办法是启动一个本地静态服务器。使用 Python 的写法如下# 在项目根目录执行启动一个 8000 端口的静态服务 python -m http.server 8000然后访问http://localhost:8000如果你用 VS Code安装 Live Server 插件后在index.html上右键选择“Open with Live Server”也行。本地服务器不是必须的但养成这个习惯对后续调试大有好处。4.3 画布尺寸约定为了后续所有坐标和碰撞逻辑能对上我们约定整个游戏在一个 800 x 450 的画布中运行地面高度定位在 y 380。地面以上的空间是角色和障碍物的活动区域地面以下不绘制任何对象。这个约定会在多个文件中反复出现统一放到 game.js 头部定义即可。5. 核心流程拆解在贴完整代码之前先把核心流程拆清楚。这些概念是后面理解代码的关键。5.1 游戏主循环requestAnimationFrame 与 dt游戏和普通网页的最大区别是游戏需要持续不断地更新和重绘。requestAnimationFrame是浏览器提供的定时重绘机制它会在每一帧刷新前调用回调函数频率通常在 60 次每秒左右。主循环中一定要计算dt也就是“距上一帧过去了多少秒”。如果不使用dt而是每帧固定移动 5 个像素那么 60Hz 屏幕和 120Hz 屏幕上的游戏速度会完全不同。使用dt后每帧移动距离是速度 * dt游戏在不同刷新率下表现才一致。5.2 游戏状态机READY / RUNNING / OVER小游戏虽然玩法简单也必须有状态机。这个项目只定义三个状态状态含义触发方式READY待开始页面加载后RUNNING游戏运行中玩家按下空格或点击屏幕OVER游戏结束猫咪与障碍物碰撞状态机决定了哪些逻辑能执行。比如在 READY 状态下障碍物不生成猫不移动在 RUNNING 状态下分数正常增长在 OVER 状态下猫咪停止动作玩家可以重新开始。如果不做状态机逻辑会纠缠在一起按下空格时可能同时触发“开始”和“跳跃”很难控制。5.3 猫咪跑动动画帧切换角色动画最基础的形式是逐帧切换。我们把猫咪的跑步拆分成了四个姿态每 80 毫秒切换一帧四帧循环。帧切换的核心就是一个计时器this.frameTimer dt * 1000; if (this.frameTimer this.frameDuration) { this.frameTimer - this.frameDuration; this.frame (this.frame 1) % 4; }关键是最后一行(this.frame 1) % 4当帧号到达 3 后会回到 0形成循环。四个姿态的腿部位置不同视觉上就产生了奔跑感。5.4 跳跃与重力跳跃的实现比很多人想象中简单本质是“给角色一个向上的初速度然后每帧叠加重力”。每一帧的位移公式为this.y this.vy * dt; this.vy 980 * dt;当this.y回到地面高度时把角色放回地面并清零垂直速度。这里的 980 是重力加速度的简化值单位是像素/秒²可以调节手感。数值越大跳跃下落越快数值越小角色飘在空中越久。5.5 障碍物生成与碰撞检测障碍物从画布右侧生成以恒定速度向左移动。生成时机由一个计时器控制间隔随机避免玩家预判。碰撞检测使用的是 AABB轴对齐包围盒方式。也就是把角色和障碍物都看成矩形检查两个矩形是否重叠。四条件判断听起来繁琐但实际上就是把“右边界在对方左边界的右侧”和“左边界在对方右边界的左侧”等条件组合起来return ( this.x catBox.x catBox.width this.x this.width catBox.x this.y catBox.y catBox.height this.y this.height catBox.y );只要四个条件同时成立说明两个矩形重叠判断碰撞发生。6. 完整示例代码实现下面给出完整的可运行代码。建议按文件路径逐个创建保持代码结构一致。6.1 入口页面index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title猫咪狂奔记/title style body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #faf7f2; font-family: sans-serif; } canvas { border: 2px solid #222; max-width: 90vw; background: #f5efe3; } /style /head body canvas idgame width800 height450/canvas !-- 注意加载顺序cat.js 和 obstacle.js 必须在 game.js 之前 -- script srcjs/cat.js/script script srcjs/obstacle.js/script script srcjs/game.js/script /body /html这段 HTML 的关键点只有一个脚本加载顺序。game.js中使用了Cat和Obstacle两个类所以这两个文件必须先加载。如果顺序写反浏览器会在创建对象时报错Cat is not defined。6.2 猫咪精灵类js/cat.js// 文件路径js/cat.js class Cat { constructor(x, y) { this.x x; this.y y; this.width 70; this.height 60; this.frame 0; this.frameTimer 0; this.frameDuration 80; // 每帧间隔单位毫秒 this.isJumping false; this.vy 0; this.groundY y; } jump() { // 只有在地面时才能起跳避免空中二段跳 if (!this.isJumping) { this.isJumping true; this.vy -420; // 初速度向上为负 } } update(dt) { // 1. 更新跑动动画帧 this.frameTimer dt * 1000; if (this.frameTimer this.frameDuration) { this.frameTimer - this.frameDuration; this.frame (this.frame 1) % 4; } // 2. 更新跳跃物理 if (this.isJumping) { this.y this.vy * dt; this.vy 980 * dt; // 重力 if (this.y this.groundY) { this.y this.groundY; this.isJumping false; this.vy 0; } } } draw(ctx) { ctx.save(); ctx.strokeStyle #222; ctx.fillStyle #222; ctx.lineWidth 3; ctx.lineCap round; // 说明这里的坐标值是在 800x450 画布下调试出来的 // 如果你调整了画布尺寸需要重新调身体、头、腿的位置。 const bodyX this.x; const bodyY this.y - 25; // 尾巴 ctx.beginPath(); ctx.moveTo(bodyX - 10, bodyY - 8); ctx.quadraticCurveTo(bodyX - 30, bodyY - 35, bodyX - 45, bodyY - 10); ctx.stroke(); // 身体 ctx.beginPath(); ctx.ellipse(bodyX, bodyY - 15, 28, 18, 0, 0, Math.PI * 2); ctx.stroke(); // 头 ctx.beginPath(); ctx.arc(bodyX 40, bodyY - 35, 16, 0, Math.PI * 2); ctx.stroke(); // 耳朵 ctx.beginPath(); ctx.moveTo(bodyX 31, bodyY - 48); ctx.lineTo(bodyX 37, bodyY - 64); ctx.lineTo(bodyX 48, bodyY - 48); ctx.stroke(); // 眼睛 ctx.beginPath(); ctx.arc(bodyX 36, bodyY - 38, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(bodyX 45, bodyY - 38, 2.5, 0, Math.PI * 2); ctx.fill(); // 四种跑动姿态分别控制四条腿的抬起/落下 const legPoses [ { frontL: 6, frontR: -8, backL: -8, backR: 6 }, { frontL: 10, frontR: -4, backL: -4, backR: 10 }, { frontL: -8, frontR: 6, backL: 6, backR: -8 }, { frontL: -2, frontR: 2, backL: 2, backR: -2 } ]; // 跳跃时腿收起不播放跑步姿态 const pose this.isJumping ? { frontL: -14, frontR: -14, backL: -14, backR: -14 } : legPoses[this.frame]; // 前腿 ctx.beginPath(); ctx.moveTo(bodyX 10, bodyY - 6); ctx.lineTo(bodyX 10, bodyY 26 pose.frontL); ctx.moveTo(bodyX 22, body