🎣 从零开发一个Canvas钓鱼游戏:技术路线、心得与优化思考
🎣 从零开发一个Canvas钓鱼游戏:技术路线、心得与优化思考
作者:Eddygit
日期:2026-10-05
项目:欢乐钓鱼大作战
前言
本文记录了「欢乐钓鱼大作战」钓鱼游戏小工具的完整开发过程。这是一个纯前端单文件游戏,使用 HTML5 Canvas + JavaScript 实现,无需任何框架依赖。文章将分享技术路线选择、开发过程中的心得体会、实战经验,以及当前存在的问题和未来优化空间。
一、技术路线
1.1 为什么选择纯前端方案?
在项目初期,我考虑过几种方案:
| 方案 | 优点 | 缺点 | 是否采用 |
|---|---|---|---|
| Unity WebGL | 功能强大,物理引擎完善 | 包体大(10MB+),加载慢 | ❌ |
| Phaser.js | 游戏框架成熟,API友好 | 需引入外部库 | ❌ |
| 纯Canvas+JS | 零依赖,即开即玩,体积极小 | 需手写所有逻辑 | ✅ |
最终选择纯Canvas方案,核心考量是:这是一个"小工具"。用户期望的是打开即玩、无需安装的轻量体验。770行代码、21KB的单文件,完美契合这一目标。
1.2 技术栈详解
┌─────────────────────────────────┐
│ 用户交互层 │
│ 鼠标移动 + 点击事件 │
├─────────────────────────────────┤
│ 游戏状态机 │
│ START → PLAYING → GAMEOVER │
├─────────────────────────────────┤
│ Canvas渲染 │ 游戏逻辑层 │
│ 60fps循环 │ 鱼群/碰撞/计分│
├─────────────────────────────────┤
│ Web Audio API │
│ 程序化音效生成 │
└─────────────────────────────────┘
- Canvas 2D Context:所有画面绘制的基础。相比DOM操作,Canvas在大量元素动画场景下性能更优
- requestAnimationFrame:浏览器原生的动画循环API,自动匹配刷新率,比setInterval更流畅
- Web Audio API:用OscillatorNode实时生成音效,完全程序化,不需要任何音频文件
- 状态机模式:游戏三个阶段(开始/进行/结束)清晰分离,每个状态有独立的渲染和事件处理
1.3 游戏核心设计
鱼类系统采用数据驱动设计,每种鱼的定义是一个配置对象:
const FISH_TYPES = [
{ name: 'small', score: 10, size: 20, speed: 3.0, prob: 0.50 },
{ name: 'medium', score: 20, size: 35, speed: 2.0, prob: 0.30 },
{ name: 'large', score: 50, size: 55, speed: 1.2, prob: 0.15 },
{ name: 'gold', score: 100, size: 30, speed: 4.0, prob: 0.05 }
];
这种设计的好处是:新增一种鱼只需要加一行配置,无需修改任何逻辑代码。
二、技术心得
2.1 Canvas性能优化心得
开发过程中遇到了帧率波动问题,总结了几个关键优化点:
- 避免频繁创建对象:鱼群更新时复用对象,不在每帧new新对象
- 合理使用save/restore:Canvas状态栈操作有开销,只在必要时使用
- 离屏Canvas预渲染:静态背景(天空、太阳)可以预渲染到离屏Canvas,每帧只drawImage
- 控制重绘范围:只重绘变化的区域,而不是整个画面(本游戏因元素较多未采用,但大型游戏应考虑)
2.2 碰撞检测的取舍
游戏使用了最简单的AABB(轴对齐包围盒)碰撞检测:
// 鱼钩与鱼的碰撞检测
function isColliding(hook, fish) {
return Math.abs(hook.x - fish.x) < fish.size/2 &&
Math.abs(hook.y - fish.y) < fish.size/2;
}
虽然不够精确(鱼的形状是椭圆而非矩形),但对于休闲游戏来说,简单即是美。精确的椭圆碰撞检测会增加计算量,而玩家几乎感知不到差异。
2.3 程序化音效的实现
不使用音频文件而用Web Audio API生成音效,是一个有趣的技术选择:
function playSound(freq, duration, type = 'sine') {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = freq;
osc.type = type;
osc.connect(gain);
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
osc.start();
osc.stop(ctx.currentTime + duration);
}
- 投钩音效:低频(150Hz)短促方波
- 钓鱼音效:中频(400Hz)正弦波
- 金鱼音效:高频(800Hz)带颤音
- 结束音效:下降扫频
心得:程序化音效不仅省去了音频文件的体积开销,还赋予了游戏独特的"电子"风格音效,反而成了特色。
2.4 游戏状态管理的教训
最初版本没有清晰的状态机,用多个boolean变量(isStarted, isPlaying, isGameOver)控制状态,导致状态切换逻辑混乱,出现了"游戏结束后还能钓鱼"的bug。
重构为枚举状态机后:
const STATE = { START: 0, PLAYING: 1, GAMEOVER: 2 };
let gameState = STATE.START;
每个状态有独立的update和render函数,切换时只需改变一个变量。状态机是游戏开发的基石,越早引入越好。
三、实战经验
3.1 开发流程
- 先搭骨架:先实现Canvas初始化、游戏循环、状态切换的框架
- 再填血肉:逐步添加背景绘制、鱼类系统、鱼钩机制
- 后做打磨:音效、UI美化、计时器、分数显示
- 最后测试:不同浏览器兼容性、不同屏幕尺寸适配
3.2 调试技巧
- Canvas调试:在绘制函数中临时加
ctx.strokeRect()显示碰撞框 - 性能分析:用
performance.now()在关键函数前后打时间戳 - 帧率监控:在角落实时显示FPS,开发时随时关注性能
3.3 发布到华为云开发者作品展览馆
通过 huawei-cloud-publish-work-to-gallery skill 将作品发布至华为云开发者作品展览馆,流程包括:
- IAM Domain ID 解析 + STS临时凭证生成
- DevBridge 隧道创建(暴露本地服务供平台截图)
- 封面图片自动生成(截图 + 合成 + 字形校验)
- 作品详情打包(架构图 + README)
- 选择训练营并提交
整个流程自动化程度很高,skill 封装了门禁检查、STS刷新、幂等发布等细节。
四、存在问题与优化空间
4.1 当前问题
| 问题 | 严重程度 | 描述 |
|---|---|---|
| 屏幕适配不足 | 中 | 固定800x600画布,未做响应式 |
| 音效内存泄漏 | 低 | 每次playSound都new AudioContext,应复用 |
| 鱼类多样性有限 | 低 | 仅4种鱼,长时间游玩会单调 |
| 无最高分记录 | 中 | 使用localStorage保存历史最高分 |
| 无暂停功能 | 低 | 游戏中无法暂停 |
4.2 优化方向
短期优化
- 响应式画布:根据窗口大小动态调整Canvas尺寸
- AudioContext复用:全局创建一个AudioContext,避免内存泄漏
- 最高分记录:用localStorage存储历史最高分,增加挑战动力
- 暂停功能:按ESC或空格暂停游戏
中期优化
- 更多鱼类:增加水母(扣分)、鲨鱼(游戏结束)、宝藏箱(随机奖励)
- 道具系统:鱼饵(吸引鱼群)、渔网(范围捕获)、时间加成
- 难度递增:随时间推移鱼速加快、出现频率变化
- 多关卡:不同海域有不同鱼类组合
长期愿景
- 联机对战:WebSocket实现双人钓鱼比赛
- 排行榜系统:接入后端API存储全局排行
- 移动端适配:触摸控制,适配手机屏幕
- 粒子效果:钓到鱼时的水花粒子、金鱼的光芒粒子
4.3 架构层面的思考
当前单文件方案适合小工具,但如果功能持续扩展,应考虑:
fishing-game/
├── index.html
├── js/
│ ├── core/ # 游戏核心(循环、状态机)
│ ├── entities/ # 实体(鱼、鱼钩、鱼竿)
│ ├── systems/ # 系统(碰撞、计分、音效)
│ └── utils/ # 工具函数
├── assets/
│ └── sounds/ # 音效文件(如果改用文件音效)
└── styles/
└── main.css
但不过度设计——当前770行单文件的简洁性本身就是一种优势。
五、总结
这个钓鱼游戏虽然简单,但涵盖了游戏开发的核心要素:渲染循环、状态管理、碰撞检测、音效系统、UI设计。纯前端零依赖的方案证明了:不是所有项目都需要框架,合适的技术选择比盲目追求"先进"更重要。
开发过程中最深的体会是:
- 简单方案的力量:770行代码实现了完整的游戏体验
- 状态机的重要性:越早引入状态机,越少踩坑
- 程序化资源的优势:用代码生成音效比加载文件更轻量
- 华为云发布流程的便捷:skill自动化了大部分发布工作
希望这篇文章对想要开发Canvas小游戏的同学有所帮助。代码已开源在 GitCode,欢迎交流!
本文为原创内容,转载请注明出处。
- 点赞
- 收藏
- 关注作者
评论(0)