
简介《黄金矿工》H5小游戏源码包面向Web前端学习者与游戏开发入门者是一份可直接运行的HTML5游戏参考实现适合研究Canvas绘图、JavaScript交互逻辑与CSS3动效的结合方式。压缩包共64个文件大小仅1.14MB含38个PNG图片素材、15张JPG背景图、8个GIF动效及HTML入口、JS核心逻辑与TXT说明文件图片素材覆盖角色、金币、钩子等元素代码结构适合逐模块拆解。已有437人学习下载。阅读源码可重点掌握游戏主循环、对象类定义、碰撞检测、用户输入处理、渲染流程、音效管理和分数系统等关键模块理解从抛出钩子到判定抓取、计算得分的完整链路为后续独立开发同类H5游戏打下基础。1. 一个 zip 包里的黄金矿工能拆出多少东西拿到“H5小游戏源码 黄金矿工.zip”这种压缩包第一反应不是解压而是先想清楚里面大概率装了什么一个用 Canvas 或 DOM 实现的黄金矿工玩法页面可能带几个关卡配置、金币钻石的分数表、钩爪摆动的物理参数以及一套勉强能跑的移动端适配。这类源码散落在各个源码站、GitHub 仓库和个人博客里质量参差不齐但共同点是都能在浏览器里直接跑改一改就能嵌进微信公众号、App 内嵌 H5 页面甚至用 uniapp 打包成小程序或 App。这篇文章不打算带着你逐行读某个特定作者的源码——那既不可能也不负责任。我会按一线工程师接手这类项目的思路来展开先拆 zip 里该有什么文件、每个文件管什么再讲黄金矿工最核心的钩爪摆动和抓取判定怎么写然后给出一套可复现的参数调优与碰撞检测方案最后落在真机调试、缓存清除和打包嵌入时一定会遇到的坑。如果你正要拿这份源码做二次开发或者被领导要求“把这个小游戏塞进公众号里”这篇文章能帮你少走两三天弯路。2. 解压之后先别跑读懂 H5 小游戏源码的目录结构与启动入口2.1 zip 压缩包里的常见文件清单与各自职责几乎所有的 H5 小游戏源码 zip 包解压后都逃不出下面这几类文件。先花五分钟把目录捋一遍比直接双击 index.html 有用得多。gold-miner/ ├── index.html # 入口页面承载 Canvas 画布 ├── css/ │ └── style.css # 页面布局、移动端适配样式 ├── js/ │ ├── main.js # 游戏主循环requestAnimationFrame 入口 │ ├── player.js # 玩家数据金钱、关卡、工具等级 │ ├── hook.js # 钩爪的摆动与伸缩逻辑 │ ├── gold.js # 金块、钻石、石头等物体的数据定义 │ ├── collision.js # 碰撞检测与抓取判定 │ └── ui.js # 分数面板、倒计时、弹窗控制 ├── assets/ │ ├── images/ # 背景图、钩爪、金块、钻石的精灵图 │ ├── audio/ # 抓取音效、钩爪发射音效、背景音乐 │ └── levels/ │ └── level1.json # 关卡配置物体位置、尺寸、分数 └── libs/ └── jquery.min.js # 如果用了老式 DOM 操作可能会有这个这份清单来自多数同类源码的通用结构不是某个具体包的标准答案但足以帮你建立预期。main.js是整个游戏的骨架它负责调用requestAnimationFrame驱动每一帧的更新与绘制hook.js决定钩爪的摆动角度和伸缩速度gold.js里只是纯数据对象通常长这样// gold.js 片段定义一种金块的属性 const GOLD_NUGGET { type: gold, size: 40, // 碰撞半径单位像素 score: 200, // 勾中后加的分 weight: 15, // 重量影响拽回速度 color: #FFD700 };size是碰撞检测用的圆形半径weight这玩意很关键——它直接决定钩爪抓住金块后往回拉的速度。重量越大回收越慢给玩家的操作反馈就越“沉”。这部分参数在level1.json里按关卡配也可以写死在代码里取决于原作者的习惯。2.2 本地运行的启动方式与常见失败点拿到源码第一步直接在浏览器里双击index.html大概率能看到游戏画面但注意两个问题一是音频文件可能因本地文件协议被浏览器拦截尤其是 Chrome二是如果源码里有模块化写法import/export双击打开会直接报 CORS 错误。最稳妥的办法是起一个本地静态服务器。# 在源码根目录下执行任选一种方式 python3 -m http.server 8080 # 或者用 Node.js 的方式 npx serve -l 8080启动后访问http://localhost:8080确认页面能正常加载。这里有个容易踩的坑如果代码里有fetch(assets/levels/level1.json)这类读取本地 JSON 的语句用file://协议打开时会因跨域被拒绝报错信息是 “Access to fetch at file:///... from origin null has been blocked by CORS policy”。所以任何情况下都别直接双击 index.html养成起本地服务的习惯。如果起服务后发现页面空白打开 DevTools 的 Console 面板看报错最常见的三类错误是xxx is not definedJS 文件加载顺序错了检查 index.html 里script标签的顺序通常先hook.js再main.js因为后者依赖前者。Cannot read property draw of undefined图片资源没加载完就开始绘制需要在window.onload或图片的onload回调里初始化游戏。AudioContext was not allowed to start移动端浏览器要求用户先有交互才能播放音频需要在首屏加一个“开始游戏”按钮点击后再初始化音频上下文。提示鼠标右键点击页面并选择“查看页面源代码”你能看到实际加载的 JS 文件顺序这对排查加载顺序问题非常直接。2.3 把游戏跑在手机上的两种路径对比黄金矿工这类操作简单的 H5 小游戏目标平台必然是移动端。常见的两条路直接做响应式 H5 页面嵌入微信公众号或 App WebView或者用 uniapp 把现有 H5 代码封装成 App。前者改动小、上线快后者能拿到更多原生能力但需要摸熟 uniapp 的web-view组件。如果是嵌入微信公众号需要处理的核心需求是获取地理位置因为有些游戏逻辑会根据地理位置做排行榜或区域化配置——虽然黄金矿工本身不太需要但如果你打算做联机排行榜这是绕不开的一步。常见做法是在页面加载后调用微信 JS-SDK// 在微信内嵌浏览器中获取定位 wx.config({ debug: false, appId: 你的公众号appId, timestamp: 后端生成的时间戳, nonceStr: 后端生成的随机串, signature: 后端生成的签名, jsApiList: [getLocation] }); wx.ready(() { wx.getLocation({ type: wgs84, success(res) { console.log(经度, res.longitude, 纬度, res.latitude); // 在这里把坐标返回给游戏逻辑层 }, error(err) { console.error(定位失败, err.errMsg); } }); });这段代码里signature必须由后端用appId、timestamp、nonceStr和公众号的secret做 SHA1 加密生成前端只能拿到成品。如果你只是自己调试没有后端可以用微信官方提供的测试号工具临时绕开签名校验但正式环境必须走完整流程。嵌入式开发中另一个高频问题是定位失败后的降级策略。用户拒绝授权、手机定位关闭、或者网络请求超时都会导致getLocation失败这时候游戏不能直接卡死在加载页。我一般的处理方案是定位失败后默认取城市级数据可以通过 IP 反查然后在画面上非侵入式提示“定位失败已使用默认区域”。对于黄金矿工这种弱地域关联的游戏这个降级策略足够实用。3. 黄金矿工的钩爪摆动与抓取判定从物理模型到可调代码3.1 钩爪摆动的数学基础与参数设计黄金矿工最核心的操作手感来自钩爪的左右摆动。这个摆动本质上是一个简谐运动——钩爪从某个初始角度出发以固定角速度向一侧摆动碰到边界后反向。大多数源码里实现的是等速摆动即角度随时间线性递增/递减在两端反弹。但如果你想让手感更好可以改成变角速度摆动让钩爪在两端停留时间更短、中间速度更快这样玩家更容易预判发射时机。// hook.js 中的核心摆动逻辑 class Hook { constructor(canvasWidth, canvasHeight) { this.angle -Math.PI / 2; // 初始角度垂直向下 this.angleSpeed 0.02; // 摆动角速度单位弧度/帧 this.minAngle -Math.PI / 2 - Math.PI / 4; // 左边界-135度 this.maxAngle -Math.PI / 2 Math.PI / 4; // 右边界-45度 this.state swinging; // swinging: 摆动, extending: 伸出, retracting: 回收 this.length 0; // 当前钩爪长度 this.maxLength 400; // 最大伸长距离 } update() { if (this.state swinging) { this.angle this.angleSpeed; // 碰到边界反弹 if (this.angle this.maxAngle || this.angle this.minAngle) { this.angleSpeed -this.angleSpeed; } } else if (this.state extending) { // 伸出钩爪长度递增 this.length 4; // 伸出速度 if (this.length this.maxLength) { this.state retracting; // 伸到最长还什么都没抓强制收回 } } else if (this.state retracting) { this.length - 3; // 回收速度可以比伸出慢一点 if (this.length 0) { this.state swinging; // 完全收回恢复摆动 } } } }angleSpeed决定摆动快慢0.02弧度/帧在 60fps 下意味着每秒摆动约 19 度一次完整的左到右摆动大概需要 5 秒。这个参数如果你觉得太快或太慢可以在 0.01 到 0.03 之间调整。maxLength是关键平衡参数设太短玩家够不到边缘的金块设太长一次发射要等很久才能看到结果节奏拖沓。这里要指出一个多数老源码的共性问题所有速度参数都是直接写在主循环里没有考虑帧率变化。如果你的游戏跑在 120Hz 高刷手机上钩爪摆动速度会比 60Hz 设备快一倍游戏难度直线上升。正确做法是把速度乘以时间增量deltaTimeupdate(deltaTime) { // deltaTime 是上一帧到当前帧的间隔单位为秒 this.angle this.angleSpeed * deltaTime * 60; // 乘以60确保在60fps时行为不变 // 其余逻辑同理 }这样无论屏幕刷新率是多少物理表现都是一致的。改造这个需要把主循环里的requestAnimationFrame回调加上时间戳参数具体写法在下一小节讲。3.2 碰撞检测的两种实现圆形碰撞与像素级判定钩爪伸出去之后要判断是否勾到了金块核心是碰撞检测。大多数源码采用圆与圆的碰撞检测因为金块和钻石被抽象成不同半径的圆计算简单且性能好。// collision.js圆与圆的碰撞检测 function isCollision(ax, ay, aRadius, bx, by, bRadius) { const dx ax - bx; const dy ay - by; const distance Math.sqrt(dx * dx dy * dy); return distance aRadius bRadius; } // 在 main.js 主循环中调用 const hookTipX player.hook.x Math.cos(player.hook.angle) * player.hook.length; const hookTipY player.hook.y Math.sin(player.hook.angle) * player.hook.length; for (const obj of currentLevel.objects) { if (isCollision(hookTipX, hookTipY, HOOK_TIP_RADIUS, obj.x, obj.y, obj.radius)) { // 碰撞成功把物体挂到钩爪上 player.hook.state retracting; player.hook.attachedObject obj; break; } }HOOK_TIP_RADIUS通常设 35 像素太小会导致玩家明明感觉勾到了、实际却没判定造成强烈的挫败感。我见过不少源码把这个值设成 810手感会宽松很多——代价是偶尔会出现“看起来没碰到但被勾住了”的情况。对于休闲游戏来说宽容的判定比精准的判定更受欢迎。另一种方案是按像素检测把钩爪尖端画到一个离屏 Canvas 上再把金块画到另一个离屏 Canvas 上比对像素的 alpha 通道。这种方案更精准但性能消耗大在低端 Android WebView 上容易掉帧一般不太推荐。圆形碰撞对黄金矿工这个玩法已经是精度与性能的最佳平衡。3.3 物体重量对拉回速度的影响与动态表现黄金矿工的“手感”很大程度来自不同重量的物体对钩爪回收速度的影响。小金子勾住后嗖嗖地飞回来大石头勾住了半天才拉上来一格——这就是重量在起作用。// main.js 中处理回收速度的部分 if (hook.state retracting hook.attachedObject) { const weight hook.attachedObject.weight; // 基础回收速度15减去重量影响但保底3 const pullSpeed Math.max(3, 15 - weight * 0.6); hook.length - pullSpeed; // 如果太重且拉了很久有概率挣脱有些源码有这个设定 if (weight 50 Math.random() 0.001) { hook.attachedObject null; hook.state retracting; // 空钩继续回收 playSound(tear_sound); } }pullSpeed的计算公式里15 - weight * 0.6是一个线性衰减模型。换成百分比减速或非线性衰减会有完全不同的手感。石头重量通常设为 6080金块 1030钻石 510。以下是这类源码中常见的物体配表可以直接写进level1.json物体类型半径(px)重量分数出现频率小金块1510100高中金块2520250中大金块4035500低钻石188800极低石头30700中一个常见误用是把分数和重量线性绑定——金块越大分数越高看似合理但实际上会导致玩家永远只抓大金块小金额直接忽略游戏变得无聊。好的设计是小金块有存在的意义它出现在抓大金块必经的路线上让玩家顺手抓取保持操作的连续感。这和 flappy bird 里的管道间距一样属于需要手工调整的 game feel 问题没有公式能一步到位。提示如果你调完参数发现游戏“手感不对”先别动代码逻辑用表格列一遍物体的重量和回收速度比值。检查一个中等金块的拉起时间是否在 3 秒左右——太短没有重量感太长玩家会焦虑。4. 主循环重构与性能优化把老代码改造成 60fps 不卡顿4.1 requestAnimationFrame 的时间戳驱动改造老一点的 H5 小游戏源码主循环往往长这样function gameLoop() { update(); render(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);这在 PC 浏览器上没问题但在手机上问题非常大不同设备的刷新率不同跑起来的速度完全不一样。更合理的做法是使用requestAnimationFrame回调里的时间戳参数来计算时间增量// main.js改造后的主循环 let lastTime 0; let elapsed 0; function gameLoop(timestamp) { // 第一次进入时 timestamp 可能略大于0做个保护 if (lastTime 0) { lastTime timestamp; } // deltaTime 秒 const deltaTime (timestamp - lastTime) / 1000; lastTime timestamp; // 累积时间用于倒计时等逻辑 elapsed deltaTime; // 核心更新逻辑所有物理行为都乘以 deltaTime update(deltaTime); render(); // 继续下一帧 requestAnimationFrame(gameLoop); } // 开始游戏循环 requestAnimationFrame(gameLoop);改造update函数时注意只需要把涉及速度的位移计算乘以deltaTime。前面 hook 的angleSpeed、length增减还有金币的漂浮动画都是重点改造对象。UI 上的倒计时则直接用elapsed变量配合初始时间做减法。4.2 Canvas 绘制优化离屏缓存与避免频繁重绘黄金矿工的背景是不动的画面元素土壤纹理、地下的石块、顶部的操作台。每一帧重新绘制这些静态元素是对 GPU 资源的极大浪费。最直接的优化是把静态背景绘制到一个离屏 Canvas 上每一帧只用drawImage把缓存贴上去// 初始化时生成背景缓存 const bgCanvas document.createElement(canvas); bgCanvas.width canvas.width; bgCanvas.height canvas.height; const bgCtx bgCanvas.getContext(2d); // ... 绘制地面、背景图、装饰元素 ... // 游戏循环中只需要一行 ctx.drawImage(bgCanvas, 0, 0);这样一来原来一帧需要做 50 次绘制操作现在只剩 1 次drawImage加上动态物体的绘制。对于中低端 Android 设备这个优化能直接把帧率从 35fps 提升到稳定的 60fps。另一个陷阱是阴影和模糊滤镜的使用。如果你在代码里看到了ctx.shadowBlur 10这类语句在移动端它会导致严重的性能开销。我的建议是直接用图片阴影素材代替实时阴影计算或者干脆去掉。黄金矿工这种卡通画风扁平化阴影放在视觉上几乎无差别但性能提升立竿见影。4.3 资源预加载与音频播放实践H5 游戏最致命的体验问题是游戏开始后图片和音频还在加载画面掉帧甚至白屏。解决方案是做一个资源加载器提前加载所有图片和音频加载完成后再初始化游戏// assets.js简单的资源预加载器 const assets { images: {}, audio: {} }; function loadAssets(callback) { const imageUrls [ assets/images/hook.png, assets/images/gold-small.png, assets/images/gold-big.png, assets/images/diamond.png, assets/images/stone.png ]; let loadedCount 0; const totalCount imageUrls.length audioUrls.length; imageUrls.forEach(url { const img new Image(); img.onload () { loadedCount; assets.images[url] img; if (loadedCount totalCount) { callback(); } }; // 注意onerror 也要加否则一个图片加载失败会卡死整个游戏 img.onerror () { console.error(图片加载失败, url); loadedCount; if (loadedCount totalCount) { callback(); } }; img.src url; }); } // 页面加载后调用 window.addEventListener(load, () { loadAssets(() { initGame(); // 初始化游戏逻辑 }); });音频方面要避免直接使用new Audio()后立刻播放——浏览器策略要求用户交互后才能播放。正确的模式是在用户点击“开始游戏”按钮时先创建AudioContext并调用resume()之后所有音效都通过这个上下文播放。提示如果你发现某些机型上游戏声音忽大忽小或者首音延迟明显多半是音频格式问题。微信内置浏览器对 WAV 支持较差尽量使用 MP3 或 M4A 格式并保持单文件在 100KB 以下。5. 真机调试、Uniapp 嵌入与缓存清除的落地技巧5.1 H5 页面嵌入微信公众号的定位与分享配置把黄金矿工嵌进公众号文章或自定义菜单主要有三种方式直接配置网页授权域名、使用公众号文章内嵌web-view、或通过菜单跳转。这三种方式都需要在公众号后台配置 JS 接口安全域名否则调用微信 SDK 时会报invalid signature。// 微信分享配置在游戏结束后提示玩家炫耀分数 wx.ready(() { wx.updateAppMessageShareData({ title: 我在黄金矿工挖到了 5000 分你能超过我吗, desc: 经典挖金玩法重新上线试试你的手气, link: https://your-domain.com/gold-miner/index.html, imgUrl: https://your-domain.com/gold-miner/share-cover.png, success() { console.log(分享配置成功); } }); });share-cover.png建议尺寸 300×300使用 jpg 或 png 格式大小控制在 100KB 以内。如果分享链接指向的页面没有配置 JS 接口安全域名分享卡片会只显示链接地址失去视觉吸引力。在微信公众号内调试 H5 时最烦人的是缓存问题修改 JS 后刷新页面还是旧代码在跑。这是因为微信 WebView 的缓存策略比普通浏览器激进很多。解决方法是给 JS 文件加上版本号或时间戳参数。!-- index.html 中引入 JS 时加版本号 -- script srcjs/main.js?v20240615/script?v后面的版本号每次更新代码时改一下WebView 就会把它当作新文件请求。如果你是在 App 内嵌 H5 页面还要处理 Android 上 WebView 缓存不失效的问题更极端的做法是在服务器端对 JS 文件设置Cache-Control: no-cache响应头。5.2 Uniapp 打包 App 时 web-view 的使用与通信机制如果你打算把纯 HTML 的黄金矿工源码通过 uniapp 封装成一个 App核心就是web-view组件。在 uniapp 项目里新建一个页面写法如下template view stylewidth: 100%; height: 100%; web-view :srcgameUrl messagehandleMessage/web-view /view /template script export default { data() { return { // 本地打包时需要把 H5 游戏放到 uniapp 项目的 static 目录下 gameUrl: /static/gold-miner/index.html }; }, methods: { handleMessage(event) { // 接收 H5 页面通过 postMessage 传来的数据 const data event.detail.data; console.log(来自游戏的数据, data); // 可以在这里调用 uni.setStorage 存储游戏分数 } } }; /script注意一个常见坑本地 web-view 加载页面时H5 页面内不能使用window.location.href跳转。在 App 的 web-view 里这个跳转会直接打开浏览器而不是留在 web-view 内。如果需要页面内跳转必须用a标签的target_self或者修改location.hash。H5 向 App 传数据需要在 H5 页面里用uni.webview.js提供的 API// 在 H5 游戏页面中引入 // script srchttps://unpkg.com/dcloudio/uni-webview-js/script // 游戏结束时把分数传给 App 外壳 if (window.uni) { uni.postMessage({ data: { score: player.score, level: player.level } }); }uni.postMessage只在 App 的 web-view 环境里有意义普通浏览器里会报错所以要做环境判断。另外uniapp 开发 h5 嵌入微信公众号中获取定位这件事有一个天然的区别uniapp 的uni.getLocation在 App 里走的是原生定位在 H5 里走的是浏览器定位接口后者需要用户授权且必须通过 HTTPS 访问。5.3 Android 内嵌 H5 页面缓存清除的几种姿势最后说一个所有 H5 游戏嵌 App 都会遇到的问题线下测试时改了代码手机上还是旧的。Android WebView 的缓存策略和浏览器相比更像“死缓存”尤其是在混合开发项目里。最直接的办法是在 H5 页面加载时强制不走缓存// 在 index.html 的 head 里加 meta http-equivCache-Control contentno-cache, no-store, must-revalidate / meta http-equivPragma contentno-cache / meta http-equivExpires content0 /这只是第一步。更根本的办法是在 App 外壳里主动清 WebView 缓存// Android 原生代码中在打开 web-view 前调用 WebView webView new WebView(context); webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); webView.clearCache(true); webView.clearHistory();LOAD_NO_CACHE会让 WebView 每次都从服务器拉取资源彻底绕过缓存。但这会影响加载速度所以只在开发调试阶段用或者做成“设置页面里的一个清除缓存按钮”。生产环境建议改成LOAD_DEFAULT配合 HTTP 头部的Cache-Control: max-age3600让静态资源有 1 小时缓存入口 HTML 不缓存。5.4 游戏结束后的分享卡片与成绩校验一个实用技巧最后给一个进阶技巧静态 H5 游戏的分数很容易被玩家用网页调试工具直接篡改。如果要做排行榜必须在后端校验分数——最简单的方式是提交分数时附带一个用会话密钥生成的哈希值游戏逻辑在本地维护一个“可信的分数增量”每次加分的动作同时更新哈希斜率而不是等到游戏结束再给总分做加密。这个技巧具体实现起来约一两百行代码但对于真正要上线的项目能挡住九成以上的简单作弊。如果你只是把黄金矿工源码当练习项目那在localStorage里存个最高分就够了。压缩包里的代码可以当作起点但真正让它变成一款能上线的游戏核心逻辑、性能调优和外壳通信才是你花时间的重心。从修改第一个速度参数开始到真机跑通分享和排行榜这套路径才算完整走了一遍。本文还有配套的精品资源点击获取