简介:这份捕鱼达人H5游戏源码面向具备一定前端基础的网页游戏开发者与学习者,提供可直接运行的捕鱼游戏基础框架,帮助快速搭建游戏原型并在此基础上做二次开发。压缩包共85个文件,约4.4MB,以53个png图片资源、13个js脚本、10个plist配置及json、tps等为主,涵盖鱼类与炮台美术素材、子弹与关卡配置、游戏主逻辑与工具模块,index.html为入口页面,结构清晰便于按模块定位。已有720人学习下载,可作为H5游戏入门的实战参考。读者能从中获得完整的捕鱼玩法逻辑与资源组织方式,理解鱼类分组管理、炮台发射、金币动画等核心机制的代码实现,并据此调整美术风格、扩充鱼种、设计关卡模式,或加入排行榜、社交分享等扩展功能,是研究HTML5跨平台游戏开发的实用素材。
1. 捕鱼达人 H5 源码:一份能直接跑在浏览器里的完整小游戏工程
第一次拿到这份捕鱼达人 H5 源码时,我本以为是个只有几行 Canvas 绘制的玩具 Demo,解压后却发现它是一套结构完整的浏览器小游戏工程:有独立的资源目录、场景管理、炮台与鱼群的碰撞判定、金币结算逻辑,还有一套能直接双击打开就跑的入口页面。它适合两类人:一类是想找一个「能跑起来、能改数值、能换素材」的 H5 游戏练手项目的开发者;另一类是想把捕鱼玩法拆开看看,搞清楚帧循环、对象池、碰撞检测这些基础件在真实小游戏里怎么落地的人。整份源码不依赖后端,纯前端运行,改起来门槛低,翻车成本也低。
2. 工程结构与技术选型:从入口页到帧循环的完整链路
2.1 目录骨架与文件职责
这份源码的目录组织方式很典型,属于「按职责分层、按资源类型分目录」的做法。拿到手先别急着改代码,把目录过一遍,后面定位问题会快很多。常见结构大致如下:
fishing-h5/ ├── index.html # 入口页,挂载 canvas 与加载脚本 ├── css/ │ └── style.css # 页面重置、canvas 居中、加载层样式 ├── js/ │ ├── main.js # 启动入口,初始化游戏实例 │ ├── game.js # 游戏主循环、场景切换、状态管理 │ ├── fish.js # 鱼群对象:移动、路径、生命值 │ ├── bullet.js # 子弹对象:发射、飞行、命中判定 │ ├── cannon.js # 炮台:角度、等级、发射冷却 │ ├── pool.js # 对象池,复用鱼与子弹实例 │ └── utils.js # 随机数、碰撞检测、资源加载封装 ├── images/ # 鱼、炮台、背景、按钮等贴图 ├── audio/ # 发射、命中、结算音效 └── lib/ # 第三方库(如轻量动画/加载库)这个骨架里最关键的是game.js和pool.js。前者是整局游戏的大脑,负责把「帧循环 → 更新对象 → 检测碰撞 → 渲染」串起来;后者决定了游戏在鱼群密集时会不会卡。新手最容易忽略pool.js,觉得鱼和子弹直接new出来就行,结果鱼一多帧率就掉,这是后面避坑章节要重点说的。
2.2 技术栈:原生 Canvas 2D 为主,不绑框架
从入口页和脚本组织看,这套源码走的是原生 JavaScript + Canvas 2D 的路线,没有引入 React、Vue 这类框架,也没有用游戏引擎。这个选择对小游戏来说是合理的:捕鱼玩法的渲染对象数量可控,逻辑以 2D 精灵移动和圆形碰撞为主,用引擎反而增加包体和学习成本。
核心依赖通常只有一两个轻量库,比如资源预加载和补间动画。判断方式很简单,打开index.html看<script>标签引了哪些文件,lib/目录里有什么。如果只有一两个文件,说明作者刻意控制了依赖,这对二次开发是好事——你不需要先理解一套引擎的生命周期,直接读game.js就能上手。
渲染层面用的是requestAnimationFrame驱动的帧循环,配合canvas.getContext('2d')做逐帧清屏重绘。这种「全量重绘」模式在捕鱼这种背景固定、对象数量几十到上百的场景里完全够用,不需要上离屏 Canvas 或 WebGL。如果你后续想加粒子特效、水面波纹,再考虑分层 Canvas 也不迟。
2.3 启动流程:一个请求从进入到返回的链路
把启动链路走一遍,你就知道改哪里能改到点子上。入口页加载完成后,脚本按「预加载资源 → 初始化游戏实例 → 进入主循环」三步走。下面这段是启动逻辑的典型写法,我按源码风格整理了一版,关键位置加了注释:
// main.js —— 启动入口 window.addEventListener('load', function () { // 1. 预加载图片与音频,全部就绪后再启动,避免首帧白屏 preloadAssets(ASSET_LIST, function (progress) { updateLoadingBar(progress); // 更新加载进度条 }, function () { // 2. 资源就绪,创建游戏实例 var game = new Game({ canvas: document.getElementById('gameCanvas'), width: 960, height: 640 }); // 3. 启动主循环 game.start(); }); });这段代码的逻辑很直白:preloadAssets接收资源清单、进度回调和完成回调三个参数,资源没加载完就不进游戏,这是避免「鱼贴图还没到、先画了个白框」的标准做法。new Game(...)把 canvas 和逻辑分辨率传进去,逻辑分辨率固定为 960×640,实际显示尺寸由 CSS 缩放,这样不同屏幕下游戏内坐标不用改。game.start()里才是真正的帧循环,下一节展开。
参数上要注意width和height:这两个值是游戏世界的逻辑尺寸,不是 canvas 的 CSS 尺寸。改这两个值会连带影响鱼群生成坐标、炮台位置和碰撞半径,所以除非你要整体改分辨率,否则别动。
2.4 主循环与状态机:帧循环里到底做了什么
game.js的主循环是整份源码的心脏。捕鱼游戏的帧循环不是简单地「画鱼」,而是按固定顺序处理输入、更新、碰撞、渲染。典型实现如下:
// game.js —— 主循环 Game.prototype.loop = function (timestamp) { var dt = timestamp - this.lastTime; // 两帧时间差,用于匀速运动 this.lastTime = timestamp; this.update(dt); // 更新鱼、子弹、炮台状态 this.checkCollision(); // 子弹与鱼的命中判定 this.render(); // 清屏并重绘所有对象 requestAnimationFrame(this.loop.bind(this)); }; Game.prototype.update = function (dt) { // 鱼群按各自路径移动,超出边界则回收进对象池 this.fishList.forEach(function (fish) { fish.update(dt); }); // 子弹飞行,命中或出界后回收 this.bulletList.forEach(function (bullet) { bullet.update(dt); }); // 炮台冷却计时 this.cannon.update(dt); };这里有几个关键点值得说清楚。第一,dt的引入是为了让运动与帧率解耦,鱼的速度按「像素/毫秒」算,而不是「像素/帧」,否则高刷屏上鱼会游得飞快。第二,update和checkCollision分开,是为了让碰撞判定基于更新后的最新位置,避免「看起来打中了却没算命中」的玄学问题。第三,requestAnimationFrame递归调用时用bind(this)保住上下文,这是原生 JS 写游戏最常见的翻车点之一,丢了this就会报undefined is not a function。
状态机方面,游戏通常有「加载中 / 游戏中 / 结算中」几个状态,切换靠一个state字段控制。改玩法时,比如加一个「Boss 出现」的阶段,就是在这个状态机里加分支,而不是到处写if。
3. 核心模块拆解:鱼群、子弹、炮台与对象池
3.1 鱼群模块:路径、速度与生命值
鱼是捕鱼游戏的主角,fish.js里一条鱼通常包含这些字段:贴图帧、当前位置、移动速度、移动路径、生命值、金币价值。鱼群不是随机乱游,而是沿预设路径移动,常见做法是用一组控制点做插值,或者用正弦曲线做上下摆动。下面是一段典型的鱼移动逻辑:
// fish.js —— 单条鱼的更新 Fish.prototype.update = function (dt) { // 沿路径前进,pathIndex 指向当前目标控制点 var target = this.path[this.pathIndex]; var dx = target.x - this.x; var dy = target.y - this.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < 4) { // 到达控制点,切换到下一个;走到尽头则标记为可回收 this.pathIndex++; if (this.pathIndex >= this.path.length) { this.alive = false; return; } } else { // 按速度向目标点移动,speed 单位为像素/毫秒 this.x += (dx / dist) * this.speed * dt; this.y += (dy / dist) * this.speed * dt; } // 根据移动方向翻转贴图,避免鱼倒着游 this.facing = dx >= 0 ? 1 : -1; };逻辑说明:鱼沿path数组里的控制点逐段移动,到达一个点就切下一个,走完就标记alive = false交给对象池回收。speed的单位是像素/毫秒,所以乘dt之后才是本帧位移,这个单位约定要在整个工程里统一,否则鱼和子弹速度会差出数量级。facing控制贴图翻转,是个小细节但很影响观感。
参数上,speed和hp是最值得调的:speed决定鱼游得快慢,hp决定打几枪才死。想做出「小鱼快而脆、大鱼慢而肉」的层次,就是在这两个值上做梯度。路径path一般由关卡配置生成,改路径就能改鱼的游动轨迹,比如让鱼群从屏幕左侧斜穿到右下角。
3.2 子弹与碰撞:圆形判定为什么够用
子弹模块相对简单,核心是「发射位置 + 方向 + 速度 + 存活标记」。真正需要琢磨的是碰撞判定。捕鱼游戏里鱼和子弹都是近似圆形,所以用圆心距离判定就够,不需要矩形包围盒或像素级检测。典型写法:
// utils.js —— 圆形碰撞检测 function hitTest(a, b) { var dx = a.x - b.x; var dy = a.y - b.y; var r = a.radius + b.radius; // 距离平方与半径和平方比较,省一次开方 return dx * dx + dy * dy <= r * r; } // game.js —— 碰撞检测阶段 Game.prototype.checkCollision = function () { var self = this; this.bulletList.forEach(function (bullet) { if (!bullet.alive) return; self.fishList.forEach(function (fish) { if (!fish.alive) return; if (hitTest(bullet, fish)) { fish.hp -= bullet.damage; // 扣血 bullet.alive = false; // 子弹命中即消失 if (fish.hp <= 0) { self.onFishKilled(fish); // 结算金币、播放音效 } } }); }); };这里用距离平方比较省掉开方,是性能上的小习惯,鱼和子弹数量上百时能省一点算力。bullet.damage和fish.hp的配合决定了「几炮打死一条鱼」,炮台等级越高damage越大,这就是捕鱼游戏数值体系的核心。onFishKilled里做金币结算和音效播放,把「命中」和「结算」拆开,是为了后面加连击、暴击这类玩法时不用改碰撞逻辑。
要注意的是,这段是双重循环,子弹数 × 鱼数,数量大了会成瓶颈。常见优化是给鱼群做空间网格划分,或者先按屏幕区域粗筛。小规模下不用管,但如果你把鱼加到几百条,就得考虑这层。
3.3 炮台与发射:角度、等级与冷却
炮台是玩家唯一能操作的对象,cannon.js负责三件事:跟随鼠标或触摸点旋转、按等级切换贴图和伤害、控制发射冷却。发射逻辑典型如下:
// cannon.js —— 发射子弹 Cannon.prototype.fire = function () { if (this.cooldown > 0) return; // 冷却未结束,忽略本次点击 this.cooldown = this.fireInterval; // 根据炮台角度计算子弹初始速度分量 var rad = this.angle * Math.PI / 180; var bullet = this.pool.get(); // 从对象池取一个空闲子弹 bullet.init({ x: this.x, y: this.y, vx: Math.cos(rad) * this.bulletSpeed, vy: Math.sin(rad) * this.bulletSpeed, damage: this.level * 10 // 等级越高伤害越大 }); this.game.bulletList.push(bullet); };angle是炮台朝向的角度值,用Math.cos/sin转成速度分量,这是 2D 游戏里最基础的向量运算。cooldown和fireInterval控制射速,防止玩家疯狂点击导致子弹刷屏。damage直接跟level挂钩,改数值就能调平衡。this.pool.get()是从对象池拿实例,下一节细说。
炮台等级切换通常还伴随贴图切换和金币消耗,这部分逻辑一般在 UI 层,改的时候注意别把「扣金币」和「换贴图」的顺序写反,否则会出现金币不够却已经升级的漏洞。
3.4 对象池:鱼群密集时不卡的关键
对象池是这份源码里最容易被新手跳过、但最影响体验的模块。捕鱼游戏里鱼和子弹会频繁创建销毁,如果每次都new,垃圾回收会周期性触发,表现为「每隔几秒卡一下」。对象池的做法是预先创建一批实例,用完不销毁,标记为空闲放回池子,下次直接复用。
// pool.js —— 通用对象池 function Pool(factory, size) { this.factory = factory; // 创建新实例的工厂函数 this.free = []; // 空闲实例列表 for (var i = 0; i < size; i++) { this.free.push(factory()); // 预热,避免运行时集中创建 } } Pool.prototype.get = function () { // 池子空了就临时新建一个,保证不会拿不到实例 return this.free.length > 0 ? this.free.pop() : this.factory(); }; Pool.prototype.recycle = function (obj) { obj.alive = false; this.free.push(obj); // 回收,等待下次复用 };factory是创建实例的函数,size是预热数量。get优先从空闲列表取,取不到才新建,这样既保证可用性又控制峰值。recycle把对象标记为不存活并放回池子。使用时要注意:回收前必须把对象的所有状态重置干净,否则复用时会出现「上一条鱼的残血带到新鱼身上」这种黑匣子问题。我一般会在init里统一重置所有字段,而不是在recycle里逐个清。
4. 跑起来与改起来:本地运行、参数调整与素材替换
4.1 本地运行:三种方式与各自适用场景
这份源码是纯前端工程,运行方式很灵活。最省事的是直接双击index.html,但要注意,如果代码里用了fetch或模块化import,file://协议下会被浏览器拦截,表现为「页面空白、控制台报跨域」。所以稳妥做法是起一个本地静态服务:
# 方式一:Python 自带静态服务器(Python 3) python -m http.server 8080 # 方式二:Node 环境用 npx npx serve . # 启动后浏览器访问 http://localhost:8080起服务后访问对应端口即可。如果你只是改改数值、换换贴图,不起服务直接双击也能跑;但只要涉及资源异步加载,就老老实实起服务,这是血泪经验。另外,移动端调试时把手机和电脑放同一局域网,用电脑 IP 加端口访问,能直接在真机上验证触摸发射的手感。
4.2 数值调整:改哪几个参数最见效
想让游戏「手感」变个样,不用动逻辑,改几个数值就行。下面这张表是我整理的高频调参点:
| 参数 | 所在文件 | 作用 | 调整建议 |
|---|---|---|---|
fish.speed | fish.js | 鱼游动速度 | 调大更刺激,调小更休闲 |
fish.hp | fish.js | 鱼的生命值 | 与炮台伤害配合,控制击杀成本 |
cannon.fireInterval | cannon.js | 发射冷却 | 调小射速快,注意别刷屏 |
cannon.bulletSpeed | cannon.js | 子弹飞行速度 | 太快看不清轨迹,太慢打不中 |
pool.size | main.js | 对象池预热数量 | 按同屏最大鱼数估算 |
调参时建议一次只改一个,改完刷新对比,否则几个参数一起动,出了问题根本不知道是哪个引起的。这是我在多个小游戏项目里踩出来的习惯。
4.3 素材替换:贴图规格与命名约定
换素材是二次开发最常见的需求。替换前先确认三件事:贴图尺寸是否与代码里的radius匹配、命名是否与资源清单一致、透明通道是否保留。鱼和子弹的碰撞半径通常写死在配置里,如果你把鱼贴图放大一倍却没改radius,就会出现「看着打中了却没判定」的诡异现象。
资源清单一般在main.js或单独的assets.js里,形如{ name: 'fish_01', src: 'images/fish_01.png' }。替换时保持name不变、只换文件,是最安全的做法;要新增鱼种,就在清单里加一项,再在关卡配置里引用。音频同理,注意格式兼容,mp3在多数浏览器没问题,但个别环境对ogg支持更好,可以两个都放。
4.4 移动端适配:触摸事件与分辨率缩放
源码如果默认按桌面鼠标写的,搬到手机上要补触摸事件。常见做法是把mousedown/mousemove换成同时监听touchstart/touchmove,并在触摸时用e.touches[0]取坐标。分辨率适配则靠 CSS 把 canvas 等比缩放,逻辑坐标不变:
/* style.css —— canvas 等比缩放 */ #gameCanvas { width: 100%; max-width: 960px; height: auto; display: block; margin: 0 auto; }这样在窄屏上 canvas 会等比缩小,游戏内坐标仍是 960×640,不用改任何逻辑。要注意触摸坐标要换算回逻辑坐标,否则点击位置会偏,换算比例就是「逻辑宽度 / 实际显示宽度」。
5. 避坑与排查:五个真实会遇到的翻车点
5.1 页面空白,控制台报跨域
现象:双击index.html打开,页面一片白,控制台出现CORS或Failed to fetch字样。原因:代码用了fetch或 ES Module 加载资源,file://协议下浏览器出于安全限制拒绝。解决:起本地静态服务(见 4.1),用http://localhost访问。这个问题新手十有八九会撞上,记住「异步加载就别双击」就行。
5.2 鱼游得飞快或慢如蜗牛
现象:换了一台高刷屏,鱼的速度明显不对。原因:移动逻辑按「像素/帧」写,没乘dt,帧率越高跑得越快。解决:把所有位移改成「速度 × dt」,并统一速度单位为像素/毫秒。改完在 60Hz 和 120Hz 屏上各跑一遍对比,确认一致。
5.3 打中了鱼却不掉血
现象:视觉上子弹穿过鱼身,但没有命中反馈。原因通常有两个:一是碰撞半径radius与贴图尺寸不匹配,判定圈比看到的鱼小;二是碰撞检测在update之前执行,用的是上一帧位置。解决:先核对radius配置,再把checkCollision放到update之后调用。
5.4 玩几分钟后越来越卡
现象:开局流畅,鱼群多起来后帧率持续下降。原因:鱼和子弹频繁new,垃圾回收压力大;或者已死亡的鱼没从列表移除,越积越多。解决:接入对象池,死亡对象及时recycle;同时在每帧更新时过滤掉alive === false的对象,别让死鱼留在渲染列表里。
5.5 复用的鱼带着上一条的残血
现象:新刷出来的鱼一枪就死,或者血量明显不对。原因:对象池复用时没重置状态,上一条鱼的hp、pathIndex残留。解决:在init方法里把所有字段显式重置一遍,别依赖「新建时默认值」。这是对象池最隐蔽的坑,排查时可以在init里打日志确认每次初始化都干净。
6. 二次开发:从改数值到加玩法的落地路径
二次开发建议按「先跑通、再改数值、后加功能」的顺序推进,别一上来就动架构。第一步是把工程跑起来,确认加载、发射、命中、结算整条链路正常;第二步是调数值,把鱼的速度、血量、炮台伤害调到你满意的手感;第三步才是加玩法。
加玩法里性价比最高的是「连击与暴击」。实现思路是在onFishKilled里维护一个连击计数,短时间内连续击杀就累加,达到阈值触发暴击伤害加成。核心改动集中在一个函数里,不用碰渲染和碰撞:
// game.js —— 连击结算(二次开发示例) Game.prototype.onFishKilled = function (fish) { var now = Date.now(); // 距上次击杀在 1.5 秒内,连击数累加,否则重置 this.combo = (now - this.lastKillTime < 1500) ? this.combo + 1 : 1; this.lastKillTime = now; var reward = fish.value; if (this.combo >= 5) { reward = Math.floor(reward * 1.5); // 五连击起金币加成 } this.coins += reward; this.pool.recycle(fish); // 击杀后回收进池 };这段改动只依赖已有的fish.value和coins字段,风险低、见效快。验证方法是连续快速击杀五条鱼,看金币增量是否出现 1.5 倍,同时确认连击中断后计数归零。
再往深一层是加「Boss 鱼」和「关卡切换」。Boss 鱼本质是一条hp很高、value很高的鱼,配独立贴图和血条;关卡切换则是在鱼群全部游出屏幕后,加载下一组路径配置。这两块会牵动状态机,建议先把state字段的切换逻辑理清楚再动手,别在渲染层里塞判断。
还有一个容易被忽略的点是存档。捕鱼游戏的金币如果只存在内存里,刷新就归零,体验很差。加一个localStorage存档,在金币变动时写入、启动时读取,十几行代码就能解决:
// 存档读写(二次开发示例) function saveCoins(coins) { localStorage.setItem('fishing_coins', String(coins)); } function loadCoins() { var v = localStorage.getItem('fishing_coins'); return v ? parseInt(v, 10) : 0; // 无存档时从 0 开始 }改完记得清一次浏览器存储再测,否则会拿旧存档干扰判断。我一般会在开发阶段加一个「重置存档」的隐藏按钮,省得每次手动清。
最后说个习惯:每次动核心逻辑前,先把当前能跑的版本复制一份备份,改完对比着测。捕鱼这类小游戏看着简单,但帧循环、对象池、碰撞三块耦合在一起,一处改错很容易连锁翻车。从那以后我每次改game.js的主循环都强制先备份再动手,跑通一版再往下加。希望这份拆解能帮你少走几步弯路,把这份源码真正改成自己想要的样子。
本文还有配套的精品资源,点击获取