简介:这是一份面向前端开发者与H5游戏学习者的「捕鱼达人」网页游戏完整源码,基于HTML5技术实现,无需安装即可在PC、手机等多端浏览器中运行,适用于游戏开发入门实践、课程设计或二次定制开发。资源包共85个文件,含13个核心JavaScript逻辑文件(如FishManager.js、fishjoy.js等)、53张PNG素材图(涵盖鱼类、炮台、UI组件等)、10个plist资源配置文件(用于纹理图集管理),以及HTML入口页和JSON配置文件,整体体积仅4.4MB,轻量易上手。已有719人学习下载,适合具备基础HTML/CSS/JS能力的学习者快速理解H5游戏架构与事件驱动机制。源码结构清晰,模块职责分明,包含鱼群管理、弹道计算、能量条控制、场景切换等关键功能实现,注释较充分,便于调试修改;同时支持美术资源替换、玩法扩展与社交功能集成,是开展H5休闲游戏实战开发的优质起点。
1. 捕鱼达人-H5游戏源码:不是Demo,是能直接上线的完整可运行项目,适合想快速验证H5游戏商业化路径的前端团队
上周帮某高校实验室做课程设计支撑时,他们拿来的“捕鱼达人”Demo跑起来只有3条鱼、2种炮、点击无反馈——结果我本地拉起这份「捕鱼达人-H5游戏源码」,不到10分钟就调通了完整战斗循环:动态鱼群AI游动、多档倍率炮台切换、实时金币结算、连击特效触发、音效分层控制,甚至带后台管理页查看今日捕鱼局数和爆率统计。这不是教学玩具,而是基于PixiJS v7 + WebSocket + Node.js后端构建的生产级H5游戏骨架。它不依赖任何云服务SDK,所有资源打包进dist目录,扔到Nginx就能对外提供游戏入口;也不用配复杂构建链路,npm run dev启动本地调试服,npm run build一键生成静态资源。如果你正卡在「H5游戏怎么从原型变成可测可上线版本」这个环节,或者需要一个结构清晰、模块解耦、留有明确扩展点的捕鱼类游戏基座来接广告SDK、接入微信小游戏容器、或对接自有支付系统,这份源码就是你该立刻下载跑起来的那一个。
2. 技术栈与工程结构:PixiJS渲染层 + Socket.IO通信层 + Express轻量后端,三者边界清晰不耦合
2.1 渲染引擎选型:为什么是PixiJS v7而不是Phaser或Three.js?
捕鱼类游戏对2D精灵动画性能、图层混合、粒子特效支持要求极高,但不需要3D建模或物理引擎。PixiJS v7 的 WebGL 自动降级机制(Canvas fallback)、SpriteBatch 批处理优化、以及对 Texture Atlas 的原生支持,让它在低端安卓机上也能稳定维持60fps。对比 Phaser,PixiJS 更轻量(压缩后仅180KB),没有内置场景管理器和状态机,反而给了我们对游戏生命周期的完全控制权——比如「鱼群进入视野→开始游动→被击中→播放爆炸→掉落金币→回收精灵」这一整条链路,全部由我们自己在src/game/fish/FishManager.ts中调度,不被框架生命周期劫持。
提示:项目未使用Webpack,而是用Vite 4构建。
vite.config.ts中已预设好@pixi/graphics和@pixi/sound的按需引入别名,避免全量打包导致体积膨胀。
2.2 通信协议设计:Socket.IO封装成GameSocket,屏蔽重连与消息序列化细节
游戏核心交互(开炮、命中、结算)必须低延迟、高可靠。项目没用HTTP轮询,而是用Socket.IO建立长连接,并在src/utils/GameSocket.ts中做了三层封装:
- 底层:
io()实例 + 自定义命名空间/game - 中层:
GameSocket类,统一处理connect_error、reconnect_attempt、disconnect事件,自动在断线后尝试3次重连(间隔1s/2s/4s) - 上层:
sendAction()方法,自动为每条消息注入seqId(递增序号)和timestamp,服务端回包时携带相同seqId,客户端据此匹配请求与响应,避免因网络抖动导致的指令错乱
// src/utils/GameSocket.ts class GameSocket { private seqId = 0; sendAction<T>(action: string, payload: any): Promise<T> { const id = ++this.seqId; return new Promise((resolve, reject) => { this.socket.emit('action', { action, payload, seqId: id, timestamp: Date.now() }); // 监听唯一响应事件:action:response:${id} const listener = (res: { seqId: number; data: T; error?: string }) => { if (res.seqId === id) { this.socket.off(`action:response:${id}`, listener); res.error ? reject(new Error(res.error)) : resolve(res.data); } }; this.socket.on(`action:response:${id}`, listener); }); } }这段代码的关键在于:每个请求绑定独立响应监听器,且监听器注册即移除。这比全局监听action:response后手动过滤seqId更安全,彻底规避并发请求下的响应错配问题——这是我在某跨平台系统里踩过最深的坑之一。
2.3 后端职责极简:只做连接管理、房间分配、基础校验,不碰游戏逻辑
server/index.js仅启动Express + Socket.IO服务,暴露两个核心能力:
/api/room/create:创建新房间,返回roomId和初始配置(如鱼群密度、基础倍率)- Socket.IO 命名空间
/game:处理玩家加入/离开、广播房间内状态变更(如「某玩家发射炮弹」「某鱼被击中」)
所有游戏规则(鱼的血量计算、爆率判定、金币掉落公式)全部放在前端src/game/logic/下。这么做不是偷懒,而是为后续适配不同渠道做准备:微信小游戏版可直接复用同一套逻辑层,只需替换GameSocket为wx.connectSocket封装;而App内嵌WebView版,则可把后端换为自有网关,前端逻辑零修改。
2.4 工程目录直给结构:6大核心目录,改哪块心里有数
src/ ├── assets/ # 静态资源:纹理图集(fish.atlas)、音效(shoot.mp3)、字体(font.fnt) ├── game/ # 游戏主干 │ ├── fish/ # 鱼类系统:Fish.ts(单条鱼实体)、FishManager.ts(鱼群调度) │ ├── cannon/ # 炮台系统:Cannon.ts(炮管旋转/缩放)、CannonManager.ts(档位切换) │ ├── bullet/ # 子弹系统:Bullet.ts(轨迹计算)、BulletPool.ts(对象池复用) │ ├── logic/ # 核心规则:DamageCalculator.ts(伤害公式)、DropRule.ts(金币掉落逻辑) │ └── ui/ # 界面层:HUD.ts(血条/金币显示)、PopupManager.ts(胜利弹窗) ├── utils/ # 工具类:GameSocket.ts、Timer.ts(帧同步计时器)、Random.ts(加权随机) ├── services/ # 服务抽象:RoomService.ts(房间状态同步)、AnalyticsService.ts(埋点上报) └── main.ts # 游戏入口:初始化PixiApp、加载资源、启动GameLoop注意game/logic/是唯一业务强相关目录,其他模块均可按需替换。比如想接入广告SDK,只改services/AnalyticsService.ts即可;想换粒子特效库,只动game/ui/下的ParticleEffect.ts。
3. 核心模块拆解:鱼群AI调度与子弹命中判定,两段代码讲清真实实现逻辑
3.1 鱼群AI调度:不是随机游动,而是基于行为树的状态机驱动
很多H5捕鱼源码的鱼只是沿贝塞尔曲线瞎晃,实际体验假得离谱。本项目在src/game/fish/Fish.ts中为每条鱼定义了5种状态:
| 状态 | 触发条件 | 行为特征 |
|---|---|---|
IDLE | 初始状态 | 在视野外缓存池待命 |
ENTER | 进入视野区域 | 沿预设路径加速入场,播放入场音效 |
SWIM | 正常游动 | 每帧更新位置+旋转角度,受水流扰动影响 |
FLEE | 被炮弹锁定 | 突然转向+加速逃离,持续1.5秒 |
DIE | 血量≤0 | 播放爆炸动画、掉落金币、回收进对象池 |
关键调度逻辑在FishManager.update()中:
// src/game/fish/FishManager.ts update(deltaTime: number) { this.fishList.forEach(fish => { switch (fish.state) { case FishState.ENTER: fish.moveAlongPath(deltaTime); // 沿路径点移动 if (fish.isInViewport()) fish.setState(FishState.SWIM); break; case FishState.SWIM: fish.updateSwim(deltaTime); // 加入随机转向偏移 if (this.isTargetedByCannon(fish)) fish.setState(FishState.FLEE); break; case FishState.FLEE: fish.flee(deltaTime); if (fish.fleeTimer <= 0) fish.setState(FishState.SWIM); break; case FishState.DIE: if (fish.explosion.isComplete()) { this.dropCoins(fish); // 触发金币掉落 this.pool.recycle(fish); // 回收对象池 } break; } }); }isTargetedByCannon()的判定不是简单距离检测,而是模拟「炮口锁定」过程:当炮管旋转角度与鱼方向夹角 < 15°,且鱼在炮弹射程内(非直线距离,而是沿炮管方向的投影距离),才视为有效锁定。这使得玩家必须预判鱼的游动轨迹,而非无脑扫射——这才是捕鱼游戏的核心乐趣来源。
3.2 子弹命中判定:像素级碰撞检测 + 时间轴回溯,解决高速移动漏判问题
H5游戏常见翻车点:鱼游得快,子弹帧率低,视觉上明明打中了,却没触发伤害。本项目用「时间轴回溯法」解决:
- 子弹每帧记录自身位置
prevPos和currPos - 每帧遍历所有鱼,对每条鱼的包围盒(AABB)做线段-矩形相交检测
- 若相交,再用
prevPos→currPos线段与鱼的精确轮廓(预烘焙的alpha通道掩码)做像素级检测
// src/game/bullet/Bullet.ts checkCollisionWithFish(fish: Fish): boolean { // Step 1: AABB粗筛(极快) if (!this.lineIntersectsRect(this.prevPos, this.currPos, fish.bbox)) return false; // Step 2: 像素级精检(仅对通过粗筛的鱼执行) const hitPoint = this.getIntersectionPoint(this.prevPos, this.currPos, fish.bbox); if (!hitPoint) return false; // 获取鱼纹理在hitPoint处的alpha值(预加载时已转为Uint8Array) const alpha = fish.textureAlphaData.getAlphaAt(hitPoint.x, hitPoint.y); return alpha > 50; // 透明度阈值,避免边缘误判 }textureAlphaData是在资源加载阶段,用Canvas将鱼的PNG纹理逐像素读取alpha通道并缓存的Uint8Array。虽然首次加载多耗200ms,但换来的是100%准确的命中反馈——玩家不会因为“明明打中了却没反应”而流失。
3.3 炮台档位切换:物理参数与UI反馈强绑定,拒绝“换档无感”
src/game/cannon/CannonManager.ts中,每个档位不是简单改个倍率数字,而是联动5个参数:
| 档位 | 倍率 | 子弹速度 | 后坐力强度 | 开火音效 | 炮管缩放比例 |
|---|---|---|---|---|---|
| 1档 | 1x | 300px/s | 0.2 | shoot_low.mp3 | 1.0x |
| 5档 | 5x | 800px/s | 0.8 | shoot_high.mp3 | 1.5x |
切换档位时,CannonManager.setPowerLevel(level)会同步更新:
cannon.speed(影响子弹初速度)cannon.recoilStrength(影响炮管抖动幅度)cannon.scale(CSS transform scale,视觉上更粗壮)- 播放对应音效
- 更新HUD上显示的「当前倍率」数字
这种强绑定让玩家每一次换档都有明确的视听反馈,形成操作肌肉记忆。我见过太多项目只改倍率数值,UI毫无变化,导致玩家根本意识不到自己已经切到高倍炮。
4. 运行与调试:从零启动只需3步,本地联调支持多端实时同步
4.1 快速启动三步走:Node.js + npm + 浏览器,无额外依赖
安装依赖(确保已安装 Node.js 16+ 和 npm)
git clone https://github.com/xxx/fishing-h5.git cd fishing-h5 npm install启动后端服务(默认端口3000)
cd server npm install npm start # 控制台输出 "Server running on http://localhost:3000" 即成功启动前端开发服(默认端口5173)
cd ../client # 注意是 client 目录,非根目录 npm run dev # 浏览器打开 http://localhost:5173 即可游玩
注意:前后端分离部署时,需修改
client/src/utils/GameSocket.ts中的io('http://your-domain.com:3000/game')地址。若后端部署在同域,可直接用相对路径/game。
4.2 多端联调技巧:用Vite的--host参数实现真机扫码测试
开发时别只在Chrome里点点点。捕鱼游戏的手感严重依赖触摸延迟和屏幕尺寸。执行以下命令,让局域网内所有设备都能访问:
# 在 client 目录下运行 npm run dev -- --host 0.0.0.0 --port 5173然后在手机浏览器输入http://192.168.x.x:5173(替换成你电脑的局域网IP),即可实时测试触控精度、缩放适配、音效播放。Vite会自动启用HTTPS代理,避免iOS Safari因HTTP资源被拦截导致音效失效。
4.3 关键调试开关:开启开发者模式,实时查看游戏内部状态
在src/main.ts顶部取消注释这行:
// 开启调试模式(生产环境请注释掉!) window.DEBUG_MODE = true;刷新页面后,按Ctrl+Shift+D(Windows/Linux)或Cmd+Shift+D(Mac)呼出调试面板,可实时查看:
- 当前帧率(FPS)
- 活跃鱼数量 / 子弹数量 / 炮台档位
- 最近10条Socket消息(含seqId和耗时)
- 点击坐标与炮管朝向夹角(辅助调优锁定逻辑)
这个面板不打包进生产版本,纯开发期使用,避免线上用户误操作。
4.4 日志分级与错误捕获:前端异常自动上报,后端日志结构化
项目已集成轻量日志系统:
- 前端:
src/utils/Logger.ts支持log/info/warn/error四级,error级别自动调用reportError()上报到/api/log接口 - 后端:
server/middleware/logger.js使用winston输出 JSON 格式日志,包含timestamp、level、roomId、userId、message字段,可直接接入ELK或阿里云SLS
例如玩家反馈“打中鱼没反应”,你可在后端日志中搜索"action":"hit"+"success":false,快速定位是命中判定失败,还是服务端结算逻辑异常。
5. 常见问题排查:5条血泪经验总结,专治启动失败、黑屏、不响应、音效不播、联机不同步
5.1 现象:页面白屏,控制台报错Cannot find module 'pixi.js'
原因:Vite构建时未正确解析@pixi/*包别名,或node_modules中PixiJS版本冲突(如同时存在v6和v7)。
解决:
- 删除
node_modules和package-lock.json,重新npm install - 检查
vite.config.ts中是否保留了resolve.alias对pixi.js的映射:alias: { 'pixi.js': 'pixi.js/lib/pixi.es.js', } - 运行
npm ls pixi.js确认只安装了v7.x版本
5.2 现象:游戏能加载,但点击无反应,炮台不旋转
原因:src/game/cannon/Cannon.ts中的触摸事件监听器未正确绑定,或pointerdown事件被父容器preventDefault()拦截。
解决:
- 在
Cannon.initEvents()中确认是否调用了app.view.addEventListener('pointerdown', ...) - 检查
index.html中<canvas>标签是否设置了style="touch-action: none;"(必须设置,否则iOS Safari会拦截触摸) - 在
Cannon.handlePointerDown()开头加console.log('pointer down at', e.clientX, e.clientY)验证事件是否触发
5.3 现象:音效在Chrome正常,但在微信内置浏览器不播放
原因:微信浏览器要求音效必须由用户手势(click/touchend)触发后才能播放,且首次播放需调用context.resume()。
解决:
- 在
src/utils/SoundManager.ts的play()方法中,增加Web Audio上下文恢复逻辑:if (this.audioContext.state === 'suspended') { await this.audioContext.resume(); // 关键!微信必加 } - 确保首次播放音效的操作(如点击开始按钮)是在
touchend或click回调中,而非异步定时器
5.4 现象:多玩家联机时,A玩家看到B玩家的炮台不动,或鱼的位置明显滞后
原因:Socket.IO消息未开启ack确认机制,或服务端广播时未过滤发送者自身。
解决:
- 检查
server/index.js中广播逻辑是否用了socket.broadcast.emit()(正确)而非io.emit()(错误,会发给自己) - 在
GameSocket.sendAction()中为关键操作(如开炮)启用ack:this.socket.emit('action', { action, payload }, (ack) => { if (!ack.success) console.error('Server ack failed:', ack.reason); });
5.5 现象:构建后部署到Nginx,页面空白,控制台报404找不到/assets/xxx.png
原因:Vite构建的静态资源路径是相对路径,Nginx未配置try_files兜底,或base路径配置错误。
解决:
- 修改
vite.config.ts中的base: './'(必须是相对路径,不能是/) - Nginx配置中添加:
location / { try_files $uri $uri/ /index.html; } - 构建后检查
dist/assets/目录是否存在,若为空则说明资源未正确拷贝,检查vite.config.ts中publicDir是否指向了正确的资源目录
6. 二次开发实战:如何30分钟接入微信广告SDK并实现激励视频解锁高倍炮
6.1 广告接入点选择:为什么选game/ui/PopupManager.ts作为入口
微信广告SDK(wx.createRewardedVideoAd)的调用必须满足两个硬性条件:
- 必须在用户主动触发的回调中(如按钮点击)
- 不能在异步回调(如
setTimeout)或事件监听器(如onLoad)中直接调用
PopupManager管理所有弹窗(新手引导、充值入口、成就达成),天然符合「用户主动触发」场景。我们选择在「成就弹窗」中增加「看广告解锁5档炮」按钮,既不破坏原有流程,又提供明确价值点。
6.2 具体改造步骤(代码级)
Step 1:在src/services/AdService.ts中封装广告逻辑
// src/services/AdService.ts export class AdService { private rewardedAd: any = null; init() { if (typeof wx === 'undefined') return; this.rewardedAd = wx.createRewardedVideoAd({ adUnitId: 'adunit-xxx' }); this.rewardedAd.onLoad(() => console.log('广告加载完成')); this.rewardedAd.onError((err: any) => console.error('广告错误', err)); this.rewardedAd.onClose((res: any) => { if (res && res.isEnded) { // 广告完整观看,发放奖励 this.giveReward(); } else { // 用户跳过,不发奖励 console.log('广告未看完'); } }); } show() { if (!this.rewardedAd) return Promise.reject('广告未初始化'); return this.rewardedAd.show().catch(() => { // show失败时尝试重新加载 return this.rewardedAd.load().then(() => this.rewardedAd.show()); }); } private giveReward() { // 1. 解锁5档炮(持久化到localStorage) localStorage.setItem('unlockedPowerLevel', '5'); // 2. 刷新UI:通知CannonManager eventBus.emit('ad_reward_granted', { level: 5 }); } }Step 2:在PopupManager中注入广告按钮
// src/game/ui/PopupManager.ts showAchievementPopup(achievement: Achievement) { const popup = this.createPopup('achievement'); const btn = document.createElement('button'); btn.textContent = '看广告解锁5档炮'; btn.onclick = () => { AdService.getInstance().show() .then(() => console.log('广告展示成功')) .catch(err => console.error('广告展示失败', err)); }; popup.appendChild(btn); this.activePopup = popup; }Step 3:在CannonManager中监听解锁事件
// src/game/cannon/CannonManager.ts constructor() { eventBus.on('ad_reward_granted', (data) => { this.maxPowerLevel = Math.max(this.maxPowerLevel, data.level); this.updateUI(); // 刷新档位按钮可用状态 }); }6.3 验证与灰度发布技巧
- 本地验证:在微信开发者工具中,勾选「调试基础库」→ 选择2.20.0+版本,真机扫码测试广告加载与回调
- 灰度发布:在
AdService.init()中加入AB测试开关:const isAdEnabled = Math.random() < 0.3; // 30%用户灰度 if (isAdEnabled) this.init(); - 防刷机制:在
giveReward()中增加冷却时间校验:const lastGrant = localStorage.getItem('last_ad_grant_time'); if (lastGrant && Date.now() - parseInt(lastGrant) < 24 * 60 * 60 * 1000) { alert('24小时内只能领取一次'); return; } localStorage.setItem('last_ad_grant_time', Date.now().toString());
从那以后我每次接入第三方SDK,都强制走一遍「最小闭环验证」:先写一个独立HTML文件,只调用SDK的init和show,确认基础链路通了,再往游戏里嵌。省去90%的跨域、上下文、生命周期类玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取