☰
PixiJS捕鱼游戏源码:生产级H5游戏骨架,开箱即用
2026/10/31 21:34:29 网站建设 项目流程

简介:这是一份面向前端开发者与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游戏常见翻车点:鱼游得快,子弹帧率低,视觉上明明打中了,却没触发伤害。本项目用「时间轴回溯法」解决:

  1. 子弹每帧记录自身位置prevPos和currPos
  2. 每帧遍历所有鱼,对每条鱼的包围盒(AABB)做线段-矩形相交检测
  3. 若相交,再用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档1x300px/s0.2shoot_low.mp31.0x
5档5x800px/s0.8shoot_high.mp31.5x

切换档位时,CannonManager.setPowerLevel(level)会同步更新:

  • cannon.speed(影响子弹初速度)
  • cannon.recoilStrength(影响炮管抖动幅度)
  • cannon.scale(CSS transform scale,视觉上更粗壮)
  • 播放对应音效
  • 更新HUD上显示的「当前倍率」数字

这种强绑定让玩家每一次换档都有明确的视听反馈,形成操作肌肉记忆。我见过太多项目只改倍率数值,UI毫无变化,导致玩家根本意识不到自己已经切到高倍炮。


4. 运行与调试:从零启动只需3步,本地联调支持多端实时同步

4.1 快速启动三步走:Node.js + npm + 浏览器,无额外依赖

  1. 安装依赖(确保已安装 Node.js 16+ 和 npm)

    git clone https://github.com/xxx/fishing-h5.git cd fishing-h5 npm install
  2. 启动后端服务(默认端口3000)

    cd server npm install npm start # 控制台输出 "Server running on http://localhost:3000" 即成功
  3. 启动前端开发服(默认端口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%的跨域、上下文、生命周期类玄学问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询