简介:这份捕鱼达人H5游戏源码面向具备一定前端基础的开发者与游戏爱好者,提供一套可直接运行的网页捕鱼游戏基础框架,用于快速搭建游戏原型或作为二次开发的起点。压缩包共85个文件,约4.4MB,以53个png图片资源、13个js脚本、10个plist配置及json、tps等数据文件为主,涵盖鱼类与炮台美术素材、子弹与关卡配置、游戏主逻辑与工具模块,并附index.html入口页面,结构清晰便于按模块查阅。已有720人学习下载,说明其在同类资源中具备一定参考价值。读者可从中了解捕鱼游戏的鱼类管理、分组刷新、炮台发射与关卡切换等核心逻辑,并在此基础上扩展新鱼种、设计多种游戏模式、调整美术风格与音效,或加入排行榜、社交分享等玩法,适合作为学习H5游戏架构与实战练手的素材。
1. 捕鱼达人 H5 源码:一份能直接跑在浏览器里的完整小游戏工程
前阵子帮朋友看一个线下体验店的互动屏项目,对方想要一个“打开网页就能玩、不用装 App、手机横过来就能操作”的小游戏,预算和工期都不允许从零写引擎。翻了一圈,最后落到这份捕鱼达人 H5 游戏源码上——它把炮台瞄准、鱼群游动、碰撞判定、金币结算、音效播放这一整套东西都塞进了一个纯前端工程里,双击index.html或者起个静态服务就能跑。适合两类人:一是想拿一个完整可运行的小游戏当 H5 游戏源码入门样本的新手,二是想在这个骨架上换皮、换玩法、接自己后端做二次开发的熟手。下面我按“它是什么 → 怎么跑起来 → 核心怎么实现 → 怎么改”的顺序,把这份源码拆开讲。
2. 工程结构与技术选型:一个纯前端小游戏是怎么组织的
2.1 目录长什么样,每个文件管什么
这份源码是典型的“无构建、纯静态”组织方式,没有 webpack、没有 npm 依赖树,拿到就能扔进任意静态服务器。常见结构大致是这样(不同版本文件名可能略有出入,但职责划分一致):
fishing-game/ ├── index.html # 入口页,挂 canvas、引脚本、放开始/结束遮罩 ├── css/ │ └── style.css # 页面布局、横屏适配、遮罩层样式 ├── js/ │ ├── main.js # 启动入口:初始化 canvas、加载资源、拉起主循环 │ ├── game.js # 游戏主控:状态机、帧循环、分数与金币结算 │ ├── fish.js # 鱼对象:路径、速度、分值、被击中后的表现 │ ├── bullet.js # 子弹对象:发射、飞行、命中销毁 │ ├── cannon.js # 炮台:角度跟随、等级切换、发射冷却 │ └── utils.js # 碰撞检测、随机数、资源加载等工具函数 ├── images/ # 鱼、炮台、子弹、背景、按钮等贴图 └── audio/ # 背景音乐与发射、命中音效这个划分方式很朴素,但边界清楚:game.js只关心“现在是什么状态、这一帧要更新谁”,fish.js和bullet.js只管自己的数据和行为,utils.js放那些到处都要用的小函数。二次开发时最常动的是fish.js(加鱼种、改分值)和game.js(改结算规则、加关卡),基本不用碰入口。
2.2 为什么选 Canvas 2D 而不是 DOM 或游戏引擎
捕鱼这类玩法的特点是:同屏对象多(几十条鱼 + 若干子弹)、每帧都要重绘、还要做圆形碰撞检测。用 DOM 元素堆鱼,几十个节点每帧改transform,在低端安卓机上很容易掉帧;上 Phaser、Cocos 这类引擎又太重,一个体验屏项目没必要背一套构建链。Canvas 2D 正好卡在中间:绘制 API 够用,requestAnimationFrame驱动帧循环,碰撞自己写个圆与圆的距离判断就行。
代价是资源管理和对象生命周期得自己管——鱼什么时候回收、子弹飞出屏幕怎么销毁、音效并发太多会不会卡,这些引擎帮你做的事这里都要手写。这也是读这份源码最有价值的地方:它把“一个最小可用游戏循环”的每个环节都摊开给你看。
2.3 跑起来需要什么环境
严格来说不需要任何后端。本地验证有两种方式:
# 方式一:直接起一个静态服务(推荐,避免 file:// 下的资源加载限制) cd fishing-game python3 -m http.server 8080 # 浏览器打开 http://localhost:8080 # 方式二:如果装了 Node,用任意静态服务器 npx serve .提示:直接双击
index.html用file://打开时,部分浏览器会拦截音频自动播放或图片跨域加载,表现是“画面出来了但没声音”或“鱼是空白”。起个本地静态服务最省事。
移动端调试时,把手机和电脑放同一局域网,访问电脑的http://内网IP:8080即可。页面一般会做横屏提示,竖屏状态下弹一个“请横屏游玩”的遮罩,这是 H5 小游戏的常规做法。
3. 核心模块拆解:一帧游戏循环里到底发生了什么
3.1 主循环:requestAnimationFrame 驱动的更新与渲染
整个游戏的骨架就是一个帧循环。main.js里初始化完 canvas 和资源后,把控制权交给game.js的循环函数:
// game.js —— 主循环骨架 let lastTime = 0; function loop(timestamp) { // 计算两帧之间的时间差,用于做与帧率无关的位移 const delta = timestamp - lastTime; lastTime = timestamp; update(delta); // 更新所有对象状态 render(); // 清屏 + 重绘所有对象 requestAnimationFrame(loop); } function update(delta) { if (gameState !== 'playing') return; // 暂停/结束时冻结逻辑 cannon.update(delta); // 炮台角度、冷却 bullets.forEach(b => b.update(delta)); // 子弹飞行 fishes.forEach(f => f.update(delta)); // 鱼群游动 checkCollisions(); // 碰撞检测 cleanup(); // 回收飞出屏幕的对象 } requestAnimationFrame(loop);这里有两个关键点值得说。第一,delta时间差必须参与位移计算,否则在 120Hz 高刷屏上鱼会游得比 60Hz 快一倍——这是新手最容易翻车的地方,写死x += 2在测试机上好好的,换台手机就变快进。第二,gameState的判断放在update最前面,暂停时逻辑冻结但渲染继续,这样暂停遮罩下面的画面是静止的而不是黑屏。
render()里通常是先clearRect清整屏,再按“背景 → 鱼 → 子弹 → 炮台 → UI 文字”的层级顺序绘制。绘制顺序就是图层顺序,鱼画在炮台后面还是前面,全看这两行的先后。
3.2 鱼群对象:路径、速度与分值的配置化
鱼是这个游戏里数量最多、变化最频繁的对象。源码里每条鱼是一个普通对象,核心字段包括贴图、位置、速度、半径、分值和游动路径:
// fish.js —— 鱼对象的创建与更新 function createFish(config) { return { img: config.img, // 贴图 x: config.startX, // 出生点 x y: config.startY, // 出生点 y speed: config.speed, // 像素/毫秒 radius: config.radius, // 碰撞半径,用于圆形检测 score: config.score, // 击中得分 angle: config.angle, // 游动方向(弧度) alive: true }; } function updateFish(fish, delta) { // 沿方向角前进,delta 保证不同帧率下速度一致 fish.x += Math.cos(fish.angle) * fish.speed * delta; fish.y += Math.sin(fish.angle) * fish.speed * delta; // 完全游出画布后标记为可回收 if (fish.x < -100 || fish.x > canvas.width + 100 || fish.y < -100 || fish.y > canvas.height + 100) { fish.alive = false; } }参数说明:speed的单位是“像素/毫秒”,所以speed * delta得到这一帧该走的像素数;radius不是贴图的真实尺寸,而是比贴图略小的一个圆,用来让碰撞判定“手感”更合理——贴图边缘是透明的鱼鳍,按真实尺寸判定会显得“明明没碰到却算命中”。score直接决定击中后加多少金币,改分值只需要改配置,不用动逻辑。
鱼种配置一般集中放在一个数组里,比如小鱼分值低速度快、大鱼分值高速度慢,加新鱼种就是往数组里加一项、往images/里丢一张图。这种“数据驱动”的写法是这份源码里最值得学的设计,后面二次开发加 Boss 鱼、加特殊鱼全靠它。
3.3 炮台与子弹:角度跟随、冷却与发射
炮台的核心是“跟随指针角度 + 发射冷却”。鼠标或触摸移动时算出指针相对炮台底座的角度,炮台贴图绕底座旋转;点击时如果冷却已过,就从炮口位置生成一颗子弹,方向与炮台角度一致:
// cannon.js —— 角度计算与发射 function aimAt(pointerX, pointerY) { const dx = pointerX - cannon.baseX; const dy = pointerY - cannon.baseY; // atan2 返回 -PI 到 PI,直接作为炮台旋转角 cannon.angle = Math.atan2(dy, dx); } function fire() { const now = performance.now(); if (now - cannon.lastFire < cannon.cooldown) return; // 冷却未到 cannon.lastFire = now; bullets.push({ x: cannon.baseX + Math.cos(cannon.angle) * cannon.barrelLength, y: cannon.baseY + Math.sin(cannon.angle) * cannon.barrelLength, angle: cannon.angle, speed: 1.2, // 像素/毫秒 radius: 6, alive: true }); playSound('fire'); // 发射音效 deductCost(cannon.level); // 按炮台等级扣金币 }barrelLength是炮口到旋转中心的距离,子弹从炮口而不是底座出生,视觉上才不穿模。cooldown控制射速,改小射速变快、改大变慢,配合炮台等级可以做出“高级炮射速快但单发成本高”的经济模型。deductCost是发射即扣费,这是捕鱼类玩法的核心经济循环:打出去的子弹要花钱,打中鱼才回本,所以命中率和鱼的分值配置直接决定游戏难度曲线。
子弹的更新和鱼类似,沿angle前进,飞出屏幕或命中后置alive = false。注意子弹速度通常比鱼快不少,否则会出现“子弹追不上鱼”的诡异手感。
3.4 碰撞检测与结算:圆与圆的距离判定
碰撞用的是最朴素的圆与圆相交判断,两层循环遍历子弹和鱼:
// utils.js —— 圆形碰撞 function isHit(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; const dist = Math.sqrt(dx * dx + dy * dy); return dist < a.radius + b.radius; } // game.js —— 每帧检测 function checkCollisions() { bullets.forEach(bullet => { if (!bullet.alive) return; fishes.forEach(fish => { if (!fish.alive) return; if (isHit(bullet, fish)) { bullet.alive = false; fish.alive = false; addScore(fish.score); // 加分 spawnEffect(fish.x, fish.y); // 命中特效 playSound('hit'); } }); }); }这里有个性能细节:子弹和鱼都是几十个量级,双重循环每帧几百次距离计算,对现代设备毫无压力,所以没必要上四叉树之类的空间划分。但如果二次开发把鱼加到几百条、子弹加到上百颗,这个 O(n×m) 就会开始吃 CPU,那时候再考虑按屏幕分块或空间哈希。addScore里通常还会做金币上限、连击加成之类的扩展,是改玩法的主要入口之一。
4. 避坑与常见问题:跑不起来时先看这几条
4.1 画面空白或鱼是方块
现象:页面打开了,背景有,但鱼和炮台显示成空白或纯色方块。原因基本是图片路径不对或资源没加载完就开始渲染。源码里资源加载通常是异步的,如果主循环在图片onload之前就启动,drawImage拿到的是未完成的图像。解决:把主循环的启动放到资源加载完成的回调里,或者用Promise.all包一层加载器,全部resolve后再requestAnimationFrame。
4.2 高刷屏上游戏速度翻倍
现象:在 60Hz 手机上正常,换到 120Hz 手机上鱼游得飞快、子弹像瞬移。原因就是位移写成了固定值x += speed,没有乘delta。解决:所有位移、旋转、冷却计时统一改成基于时间差的计算,delta从requestAnimationFrame的时间戳里取。改完之后在浏览器 DevTools 里把帧率限制到 30fps 验证一遍,速度应该保持一致。
4.3 手机上点不动或点了没反应
现象:桌面浏览器鼠标操作正常,手机上触摸没反应。原因通常是只监听了mousedown/mousemove,没监听touchstart/touchmove,或者 canvas 的 CSS 尺寸和实际像素尺寸不一致导致坐标偏移。解决:触摸事件里用e.touches[0].clientX换算坐标,并且注意 canvas 的width/height属性(绘制分辨率)和 CSS 宽高(显示尺寸)是两回事,坐标换算要乘缩放比。
4.4 音效越玩越卡、最后没声音
现象:玩几分钟后音效开始延迟、卡顿,甚至完全消失。原因是每次发射都new Audio()创建一个新实例,实例越堆越多,浏览器对同时播放的音频数量有上限。解决:预加载少量音频实例做对象池,循环复用;或者用 Web Audio API 的AudioBufferSourceNode,播放完自动释放。背景音乐则单独一个实例循环播放,不要和音效混在一起管理。
4.5 金币变成负数或结算对不上
现象:连续快速点击后金币扣成负数,或者打中鱼加分和扣子弹成本对不上账。原因是发射扣费和命中加分不在同一个时序里,快速点击时冷却判断和扣费之间可能被多次触发。解决:把“判断冷却 → 扣费 → 生成子弹”做成一个原子操作,扣费前先检查余额是否足够,不足时直接return并给个提示音;结算逻辑集中在一个函数里,别散落在各处。
5. 二次开发改造:换皮、加鱼种与接后端排行榜
5.1 换皮:改配置不改逻辑
最快见效的改造是换主题。把images/下的鱼、炮台、背景整套替换,再改css/style.css里的主色调和遮罩文案,一个“深海捕鱼”就能变成“太空打怪”或“果园摘果”。关键是别去动fish.js和bullet.js的逻辑,只改配置数组里的img、score、speed。我一般会先建一个config.js,把所有鱼种、炮台等级、关卡参数集中进去,这样换皮和调数值都不用翻业务代码。
5.2 加鱼种和特殊鱼:数据驱动 + 行为钩子
加普通鱼只需要往配置数组里加一项。加特殊鱼(比如会拐弯的、被打中分裂的)则需要给鱼对象加一个behavior字段,在updateFish里按类型分发:
// fish.js —— 按行为类型分发更新逻辑 const behaviors = { straight: (fish, delta) => { fish.x += Math.cos(fish.angle) * fish.speed * delta; fish.y += Math.sin(fish.angle) * fish.speed * delta; }, sine: (fish, delta) => { // 正弦摆动:主方向前进,垂直方向叠加周期偏移 fish.x += Math.cos(fish.angle) * fish.speed * delta; fish.y += Math.sin(fish.angle) * fish.speed * delta + Math.sin(performance.now() / 300) * 0.5; } }; function updateFish(fish, delta) { (behaviors[fish.behavior] || behaviors.straight)(fish, delta); // ...边界回收逻辑 }这样加新行为就是往behaviors里加一个函数,不用改主循环。分裂鱼则在命中回调里spawn两条小鱼,注意给新鱼一个短暂的“无敌时间”,否则同一颗子弹的循环里可能立刻又命中新生成的鱼,出现“一枪打出一串”的 bug。
5.3 接后端:排行榜与金币持久化
纯前端版本的金币和分数存在内存里,刷新就没了。要接后端,最小改动是在addScore和游戏结束时各加一个上报:
// 游戏结束时上报本局成绩 async function submitScore(finalScore) { try { await fetch('/api/score', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ score: finalScore, duration: Math.floor((Date.now() - gameStartTime) / 1000) }) }); } catch (e) { // 上报失败不阻塞游戏,本地先存一份 localStorage.setItem('pendingScore', finalScore); } }注意两点:一是上报失败要有降级,别让网络问题卡住结算界面;二是分数校验一定要放在服务端做,前端传来的分数只能当参考,否则改个localStorage就能刷榜。金币持久化同理,登录态下的余额以服务端为准,前端只做展示。
5.4 验证改造是否成功的检查清单
改完之后别只看“能玩”,按这几条过一遍:换台高刷设备确认速度一致;断网状态下确认游戏不崩、成绩能本地暂存;连续快速点击确认金币不会变负;把鱼的数量临时调到 200 条看帧率是否还稳;手机横竖屏切换确认遮罩和坐标换算正常。这套检查我每次改完小游戏都会走一遍,血泪经验是——最容易漏的永远是触摸坐标换算和音频实例回收,这两个在桌面浏览器上根本测不出来。
从那以后我每次接这类 H5 小游戏改造,都强制先在真机上把“高刷速度、触摸坐标、音频回收”这三项跑一遍再谈功能,省下来的返工时间远比检查花的多。希望这份拆解能帮你把这份捕鱼达人源码顺利跑起来,也改得动。
本文还有配套的精品资源,点击获取