HTML5捕鱼游戏源码解析:Canvas渲染、QuarkJS引擎与plist纹理集实战

📅 发布时间:2026/9/13 13:00:28
HTML5捕鱼游戏源码解析:Canvas渲染、QuarkJS引擎与plist纹理集实战
简介这是一份以HTML与JavaScript实现的经典捕鱼达人小游戏源码高度还原街机捕鱼的视觉风格、计分规则与交互手感适合前端初学者、网页游戏爱好者以及需要休闲游戏案例的开发者学习使用。资源共91个文件压缩包大小70.92MB文件类型以PNG图片素材、JS逻辑脚本为主辅以plist/tps配置文件、JSON数据、CSS样式、HTML入口及一个演示视频便于拆解画面渲染、精灵动画与碰撞计分之间的协作关系。游戏设定初始分数10000分每次出网扣1分捕获不同类型的鱼可获得对应积分且场景尺寸支持自由调整运行流畅。该资源目前已有724人学习下载。解压后可直接运行同时还附带项目文件目录和资源合集导航方便快速上手适合作为二次开发或网页游戏入门参考。1. 先跑起来的 HTML5 捕鱼达人小游戏一份完整的 Canvas 源码拿到这个包的第一反应是轻没有安装依赖没有构建链打开 index.html 就能进游戏界面。入口逻辑在 fishjoy.js渲染和事件系统由 quark.base-1.0.0.alpha.min.js 提供QuarkJS 是一个轻量 Canvas 2D 游戏框架这套源码正好把引擎用法展示完整。玩法按街机版还原初始 10000 分每发一次网扣 1 分捕到的鱼按品种加分场景尺寸可自由调整。适合两类人想改出一版自己的 html 小游戏拿去练习或接单的从业者以及想研究 Canvas 动画调度、plist 图集解析的初学者。包内还带 mp4 演示和界面截图方便对照效果。2. 目录、QuarkJS 引擎与 plist 纹理集的加载顺序2.1 脚本加载顺序引擎、资源、工具、玩法、入口包的目录结构很典型。根目录是入口 index.html 和 css/index.css脚本分两层引擎与工具类quark.base-1.0.0.alpha.min.js、R.js、Utils.js玩法类FishGroup.js、FishManager.js、fishjoy.js。另外还有一份 fishjoy2.js通常和 fishjoy.js 是同一条入口线的调整版本运行二选一具体以 readme.txt 为准。images 目录下是全部贴图、plist 和 tps 工程文件。index.html 里脚本的加载顺序基本是!-- 引擎必须先加载后续脚本里直接引用 quark 命名空间 -- script srcquark.base-1.0.0.alpha.min.js/script script srcR.js/script script srcUtils.js/script script srcFishGroup.js/script script srcFishManager.js/script !-- 若改用 fishjoy2.js替换最后一行即可 -- script srcfishjoy.js/script这里没有 defer 也没有 async原因很实际FishGroup.js、FishManager.js 的类定义里直接引用了 Quark 命名空间的对象例如 quark.Class、quark.display.*引擎没加载完脚本执行到类定义就会抛 ReferenceError。R.js 排在 Utils.js 之前是因为工具函数里很可能会调用 R 的资源查表。保持「引擎 → 资源注册 → 工具 → 玩法类 → 入口」这个顺序能避开九成以上的白屏问题。提示打开页面只看到画布没有内容时先看控制台第一行报错。Quark is not defined是引擎顺序问题R is not defined是资源注册表没先执行这两类都不用查业务逻辑直接核对 script 顺序。2.2 R.js资源注册表与 plist/tps 的资源对应关系R.js 在项目里承担资源注册中心把图片路径和纹理集统一登记视图类都通过 R 取帧而不是手写路径。写法类似R.register(fish-hd_plist, images/fish-hd.plist); R.register(fish-hd, images/fish-hd.png); // plist 对应的合图 R.register(fish1, images/fish1.png); R.register(startbg, images/startbg.jpg);注意 fish-hd.plist 和 fish-hd.png 是成对出现的plist 里的帧坐标全部相对于 fish-hd.png 这张合图。包里同时保留 fish1.png 到 fish10.png 的单张小图以及鲨鱼、炮台、子弹等独立贴图说明这套资源经历过从单图到合图的演进旧资源没有清理。改的时候不要只改 plist 不换同名 png否则坐标对不上裁出来的全是错位像素。tps 后缀是 TexturePacker 的工程文件不属于运行时资源。它记录合图时每张源图的尺寸、旋转、间距作用是让你下次能重新导出 plist 和 png机器上没装 TexturePacker 不影响游戏运行只有整体替换鱼类美术时才需要打开它重做合图。web.json、coinAni1/coinAni2.png 这类文件则是渔网动画配置和金币飘分动画素材分别被网状态机和得分动画加载。梳理一张资源对照表排错时按表找文件作用fish1.png ~ fish10.png普通鱼种贴图shark1.png / shark2.png / shark-hd.png鲨鱼贴图与合图cannon1.png ~ cannon7.png炮台升级档位bullet1.png ~ bullet8.png子弹贴图随炮台等级变化web.png / web.json / web.tps渔网贴图、动画配置、TP 工程coinAni1.png / coinAni2.png / coinText.png捕获后的金币飘分动画energy-bar.png / number_black.png能量条与分数数字game_bg_1~4.plist游戏内四张背景场景合图main_menu.plist主菜单界面合图2.3 plist 帧数据QuarkJS 怎么从大图里裁出小鱼plist 文件本体是 XML每个条目对应合图中的一张子图keyfish5.png/key dict !-- frame 前一组是起点后一组是宽高 -- keyframe/key string{{2,2},{56,48}}/string keyoffset/key string{0,0}/string keyrotated/key false/ /dictframe 里的{{2,2},{56,48}}前一组是子图在合图中的起点坐标后一组是宽高。rotated 为 true 时表示子图存储时旋转了 90°绘制前要把 canvas 上下文转回去再 drawImage很多纹理裁歪的 bug 都出在这个标志位上排查时优先看它。QuarkJS 加载完 plist 会维护一张帧表视图取帧的本质是在合图上截矩形区域var frame R.getFrame(fish5.png); ctx.drawImage(atlas, frame.x, frame.y, frame.w, frame.h, destX, destY, frame.w, frame.h);drawImage 前四个参数是源图裁剪区域也就是 plist 的 frame后四个是目标绘制区域。把目标宽高改成 frame 宽高的整数倍就能控制鱼的大小这也是场景整体放大时鱼依然清晰的底层手段之一。3. FishGroup 与 FishManager鱼群生成与对象回收3.1 FishGroup一条鱼阵就是一条泳道游戏里的鱼不是单条随机游动而是按阵型进出场。FishGroup.js 负责定义鱼群的排列方式和游动路径组成员共享同一条路径按间隔错开位置视觉上就是一支整齐的鱼队游过屏幕。这个抽象很值得抄它把「一条鱼的属性」和「一群鱼的编队」解耦了新增鱼阵只需要加配置。常见做法是用一段路径点描述泳道再给每个成员一个相位偏移var fishGroup new FishGroup({ fishType: fish6, count: 6, interval: 36, // 相邻鱼之间的像素间距 path: [ {x: -70, y: 300}, // 从屏幕左侧外进场 {x: 220, y: 280}, {x: 520, y: 260}, {x: 820, y: 290}, {x: 1100, y: 320} // 到右侧外离场 ] });count 是成员数量interval 是间距两者共同决定鱼阵密度。path 里的点越多游动曲线越平滑只给两个点就是直线游动画面明显呆板。起点 x 写成负数保证鱼从屏幕外游进来不会出现半条鱼突然出现在屏幕边缘的穿帮。调试路径时把 y 坐标改成屏幕中线附近的值最好观察鱼阵横穿屏幕时一眼能看出曲线是否合理。3.2 FishManager谁把鱼阵放出来谁负责收回去FishManager 的角色是生产者按时间间隔把预置的 FishGroup 实例化进场景同时负责回收。tick 循环的核心逻辑是var FishManager quark.Class(quark.display.DisplayObjectContainer, { tick: function(dt) { this.elapsed dt; if (this.elapsed this.spawnInterval) { this.spawnGroup(); // 时间到放出一组鱼 this.elapsed 0; } for (var i this.fishList.length - 1; i 0; i--) { var fish this.fishList[i]; fish.x fish.vx * dt; // 按帧间隔时间位移 if (fish.x this.sceneWidth 200 || fish.x -200) { this.removeFish(fish); // 完全出屏再回收 } } } });tick 是引擎每帧回调的更新入口dt 是距上一帧的时间差。用 dt 乘速度做位移是为了让鱼在不同帧率的机器上移动一致如果每帧固定步长60 帧和 30 帧的机器上鱼速会差一倍这就是很多游戏在高低刷率显示器上速度不一致的根源。spawnInterval 越大同屏鱼越少游戏越简单这个值也可以做成随关卡时间递减实现「越往后鱼越多」的难度曲线。出界阈值比场景宽 200 像素是为了让鱼完全离开画面再回收避免半身还在屏幕里就被回收产生闪烁。这里有个常见坑从数组删除鱼要倒序遍历代码里 i--因为 splice 会让索引前移正序遍历会跳掉元素基座上莫名少一条鱼多半就是这个原因。3.3 对象池为什么要回收而不是销毁鱼被击中或游出屏幕后不直接丢弃对象而是放回对象池。原因很直接Canvas 2D 下频繁 new 和销毁会触发 GCGC 停顿的表现就是画面突然卡半帧甚至一帧捕鱼这种满屏精灵的玩法对卡顿极其敏感流畅度的差距就在这里拉开。对象池实现很短收益却明显var FishPool { pool: [], get: function(fishType) { return this.pool.pop() || createFish(fishType); // 池空才新建 }, put: function(fish) { fish.visible false; // 只隐藏不销毁 this.pool.push(fish); } };get 先取池里已有的没有才新建put 只隐藏不销毁。要点是鱼身上所有引用——帧动画、速度、碰撞半径——在复用时都要重置否则会带着上一次的状态复生表现为鱼突然加速或者隐形。改这个池子时把「状态重置」抽成 createFish 里的公共初始化函数让复用和新建走同一条初始化路径是最稳妥的做法。4. 炮台、渔网、碰撞与积分核心玩法链路对照4.1 炮台瞄准鼠标坐标到炮口角度炮台跟随鼠标旋转本质是已知两点坐标求夹角var dx mouseX - cannon.x; var dy mouseY - cannon.y; cannon.rotation Math.atan2(dy, dx) * 180 / Math.PI; // 弧度转角度Math.atan2 返回弧度QuarkJS 的 rotation 按角度制所以要乘 180/Math.PI。这里不要用 Math.atan(dy/dx)dx 为 0 时结果变成 Infinity而且 atan 无法区分 180° 和 0° 两个方向鼠标移到炮台左侧时炮身会反指。4.2 子弹到渔网一次发射的状态迁移发射后子弹沿角度飞行到达射程后切换为渔网精灵网持续扩散、渐隐一段时间后消失。状态机可以这样组织if (bullet.state fly) { bullet.x Math.cos(bullet.angle) * bullet.speed * dt; bullet.y Math.sin(bullet.angle) * bullet.speed * dt; if (bullet.distance bullet.maxRange) bullet.state net; // 转网 } else if (bullet.state net) { net.radius net.expandSpeed * dt; // 网扩散半径 net.alpha - net.fadeSpeed * dt; // 网渐隐速度 if (net.alpha 0) this.removeNet(net); }用 state 字段而不是拆成两个类是因为子弹和网是同一个对象生命周期的两个阶段拆类会增加场景对象的管理成本。net.expandSpeed 控制扩散快慢扩散太快漏鱼太慢判定迟钝fadeSpeed 决定网消失时间这两个值和手感直接挂钩是调整这套源码「手感极佳」评价时最该动的参数。4.3 碰撞检测半径判定够用吗网和鱼的碰撞不需要像素级检测。捕鱼达人的鱼有明显体积感用圆半径近似完全够代码只有几行function hitTest(net, fish) { var dx net.x - fish.x; var dy net.y - fish.y; var r net.radius fish.radius * 0.6; // 瘦长鱼形打六折 return dx * dx dy * dy r * r; }对鱼的碰撞半径乘 0.6 而不是直接用整半径是因为鱼贴图大多是瘦长形全额半径会放大判定区域玩家会觉得「差很远也抓到了」。这个系数是这类射击捕鱼玩法通用的调优手段嫌难抓调到 0.75嫌太容易调到 0.5比改网扩散速度更可控。网每帧对场景里所有存活鱼做一次遍历几十条鱼的规模下 O(n) 完全没有压力不建议把检测放到鱼对象里反向遍历网同屏对象一多计算量会放大好几倍。4.4 积分规则出网扣分与鱼种分值表积分集中定义成一张表不同鱼对应不同分值。经典档位大致如下实际数值以包内 fishjoy.js 里的配置为准鱼种典型分值说明fish1 / fish212最常见小鱼刷新频率最高fish3 / fish448速度稍快命中窗口小fish5 / fish61020血量高需要多次命中shark1 / shark2100200高分鱼回报高但难抓扣分和加分在入口逻辑里通常是相邻的两段代码var totalScore 10000; // 初始分数 var NET_COST 1; // 出网扣分 function castNet() { totalScore - NET_COST; for (var i 0; i hitFishList.length; i) { totalScore fishScore[hitFishList[i].type]; // 按鱼种加分 } updateScoreUI(totalScore); }出网扣 1 分还原自街机版作用是限制无限开火分值表决定单次捕获的收益上限。调难度优先改 NET_COST 和 fishScore 的比值调完在控制台打印 totalScore连发三网不打鱼分数应精确减 3这个自检能确认改的是不是正确的那份常量。注意出网扣分与鱼种分值各自分布在「发网函数」和「击杀回调」里全局搜索 NET_COST 和 fishScore 两个关键字可以一次定位别在 UI 更新代码里浪费时间。5. 场景尺寸自适应与渲染性能排查5.1 设计分辨率逻辑尺寸与 CSS 缩放分离源码支持场景大小自由调整推荐的做法是固定逻辑分辨率用 CSS 缩放画布而不是每次 resize 都改 canvas 的 width/height。逻辑分辨率一变所有对象坐标都要跟着换算坐标换算的 bug 会成片出现休闲游戏不需要这种复杂度。var DESIGN_W 960; var DESIGN_H 640; function resize() { var scale Math.min(window.innerWidth / DESIGN_W, window.innerHeight / DESIGN_H); canvas.width DESIGN_W; canvas.height DESIGN_H; canvas.style.width DESIGN_W * scale px; // CSS 等比缩放 canvas.style.height DESIGN_H * scale px; } window.addEventListener(resize, resize);Math.min 保证画面等比缩放不拉伸变形。屏幕长宽比和设计分辨率不一致时两侧留白这就是 letterbox 方案优点是不裁切、不拉伸缺点是不铺满但对捕鱼这种背景相对静态的游戏留白比裁切划算。想铺满就得引入相机跟随和视差背景复杂度上一个量级。若想让画布居中给 canvas 外套一层容器用 flex 布局的 justify-content 和 align-items 设为 center 即可游戏逻辑完全不用动。5.2 高分屏模糊devicePixelRatio 的正确用法固定逻辑分辨率在 Retina 屏上会糊因为画布像素只有逻辑尺寸物理像素是它的整数倍。常见做法是乘 dpr 再统一缩放上下文var dpr window.devicePixelRatio || 1; canvas.width DESIGN_W * dpr; canvas.height DESIGN_H * dpr; canvas.getContext(2d).scale(dpr, dpr); // 坐标系仍按 960x640scale 之后游戏坐标仍然全部按 960×640 的逻辑坐标书写只有真实像素密度翻倍。这个改动的代价是像素填充量变成原来的 dpr² 倍手机端如果明显掉帧第一件事是关掉这个而不是去优化业务逻辑。桌面上通常没问题移动端要按机型实测取舍。5.3 帧率掉到 30 以下先查这四个位置Canvas 2D 掉帧按排查顺序查这四处ctx.save()与ctx.restore()数量是否匹配。save 会复制整个上下文状态一帧里调用上百次明显拖慢能用 transform 参数替代的就合并。阴影和模糊特效是否高频使用。ctx.shadowBlur是经典性能杀手代码里有就先注释掉对比帧率。tick 里有没有每帧 new。新数组、新对象、新闭包都会放大 GC 压力改成模块级复用对象。合图尺寸是否超过 2048×2048。TexturePacker 默认上限是 2048fish-hd.png 如果超限部分 GPU 会拒收纹理或退回软件渲染画面直接卡死。排查工具用 Chrome DevTools 的 Performance 面板记录 10 秒看 Scripting 和 Rendering 两个 block 的占比Rendering 高说明 drawImage 或状态切换多Scripting 高说明对象分配和逻辑计算多优化方向完全不同。6. 改分、调速、换鱼三个高频修改点的定位方法源码到手最快熟悉的方式不是通读而是改三个点把机制跑通。第一处是初始分数和出网扣分全文搜10000和NET_COST改完刷新即生效注意这是运行时初始化变量不是配置文件必须重新加载页面。第二处是鱼速位置在 FishManager.tick 里给 fish.vx、fish.vy 赋值的代码以及 FishGroup 的 path 点数速度改一处、路径点改一处两边对照看才不会出现鱼在原地震荡的现象。第三处是分值表在 fishjoy.js 或 fishjoy2.js 里搜fishScore直接改对象字面量里的数值改完手动调用一次发网函数验证加分是否正确。验证手段上本地改源码不建议双击 index.html 用 file:// 打开部分浏览器的本地资源安全策略会拦截图片和 plist 加载表现为画面白屏但控制台只有资源错误。在项目根目录起静态服务最省事# 进入项目根目录后执行 python3 -m http.server 8080浏览器访问http://localhost:8080控制台打印R和FishManager的实例能看到资源帧注册数量和当前存活鱼数。改完一轮手感参数把 fishjoy.js 单独复制存档这份文件就是你基于这套源码定制的完整玩法版本后续换鱼、加炮台等级都从它入手。本文还有配套的精品资源点击获取