仿黄金矿工HTML小游戏源码:Canvas物理与关卡设计实战
2026/9/15 4:40:55 网站建设 项目流程

简介:面向Web游戏开发初学者,这份仿《黄金矿工》的HTML5小游戏源码可直接运行,基于HTML、CSS与JavaScript编写,借助Construct 2引擎完成场景与逻辑调度,适合个人网站、导航页或游戏站插入使用,也便于授课演示与二次改造。压缩包共203个文件,大小7.66MB,其中png图片156张,涵盖游戏背景、角色、矿石道具及界面元素;m4a与ogg音频各21个,既提供背景音乐,也区分抓取、过关、爆炸等音效,双格式适配不同浏览器;另有4个js脚本和1个html入口,运行时需配合这些脚本读取图片与音频资源。目前已有547人浏览学习,可看到完整的抓钩摆动、抓取矿石、计分与过关逻辑,并包含离线与在线运行的基础配置,便于对照研究。读者拿到后既可一键部署到个人站点,也可替换素材包中的图片音频,快速生成不同主题的黄金矿工变体,是理解网页小游戏资源组织与交互实现的实用范例。

1. 仿《黄金矿工》的HTML小游戏源码,难点不在画图在物理

钩子甩出去跟不跟手、抓到大金块收绳是否变慢、倒计时和过关目标怎么配合,这三件事直接决定一款仿黄金矿工的HTML网页小游戏好不好玩。所谓完整源码,核心就是把这套抓钩物理和关卡节奏用纯前端还原出来。

做这类网页游戏的大多是两类人:练手的前端新手想搞懂游戏循环和碰撞检测怎么落地;要教学素材或自用合集的人需要一个能直接跑、参数可改的底子。这两类需求,一套基于 Canvas 的迷你框架都能覆盖。

下文按复刻街机游戏的标准流程展开:先搭运行框架,再做抓钩与抓取判定,补上关卡计分,最后收在移动端适配和调试技巧。代码可拼成单文件页面,参数均给出建议值。

2. 黄金矿工小游戏的运行框架:Canvas 选型与游戏循环

2.1 为什么黄金矿工这类 HTML 小游戏首选 Canvas 2D

黄金矿工一帧里要画的东西不多:一条会变长的绳子、一个钩子、几十个静态矿物和一个倒计时文本。用 DOM+CSS 动画理论上也能拼,但绳子是动态角度的线段,钩子要沿绳头运动,抓取判定要拿像素坐标做距离计算。DOM 方案要么频繁改 style 触发布局,要么靠绝对定位加 transform,代码绕而且容易掉帧。

Canvas 2D 的优势在于把场景当成同一张位图来画:每帧 clearRect 后按顺序 fill、drawImage、stroke,渲染路径短,碰撞检测和绘图共用同一套逻辑坐标。对小游戏来说,性能不是主要收益,代码直白才是。有一点要提醒:Canvas 的坐标系原点在左上角,y 轴向下。矿工站在顶部地面线,钩子往屏幕下方放绳,角度和位置公式要按这个坐标系写,不能照抄数学课本里的直角坐标系习惯。

既然是完整源码,常见的交付形态是单文件 HTML,CSS 和 JS 都内联进去,双击就能跑。用 React/Vue 包装游戏也没问题,但游戏逻辑最好放在不依赖框架的模块里,canvas 引用和 ctx 从外部注入,换框架、做多实例都方便。经典黄金矿工是竖屏操作,画布尺寸一般取 480×720,这个比例能让一屏装下约 500 像素的有效绳长。

2.2 最小可跑的游戏循环:requestAnimationFrame 与 delta

游戏循环用 requestAnimationFrame 而不是 setInterval。它跟显示器刷新率同步,切后台自动暂停,省电也省 CPU。但刷新率不固定,60Hz、120Hz、省电模式 30Hz 都有,所以不能按「每帧固定步长」写,否则同样速度在 120Hz 屏上会快一倍。

let lastTime = 0; let clock = 0; function gameLoop(timestamp) { // delta 单位是秒,所有速度都用 px/s,帧率高低不影响实际速度 const delta = Math.min((timestamp - lastTime) / 1000, 0.05); lastTime = timestamp; clock += delta; update(delta); render(); requestAnimationFrame(gameLoop); } requestAnimationFrame(gameLoop);

这里的Math.min(..., 0.05)是防跳帧。用户切到别的标签页再回来时,时间戳差值可能达到几秒,不截断的话钩子瞬间放到底,计时也直接扣完一关。截断到 50ms 后表现是「暂停后原地继续」,而不是「凭空消耗一关」。

2.3 用状态机管理钩子的四种行为

钩子是整个游戏唯一的动态主体,生命周期适合用状态机表达:待机摆动、放绳下落、收绳带回、空绳回收。不要用一堆 boolean 组状态,isDropping && !hasCargo这类组合很容易漏掉「触底」和「抓空」的分支。每个状态的代码量很小,单独一个函数维护即可。

状态触发时机钩子行为绳长变化
idle开局或上一钩结算完按单摆左右摆动保持 HOME_LEN
dropping玩家按键出手锁定角度,匀速放绳每帧加 DROP_SPEED×delta
retracting抓中矿物或钩子触底按货物重量决定速度每帧减少
done绳头回到矿工位置结算货物,回到 idle回到 HOME_LEN

状态切换都在 update 的边界判断里完成,不要在渲染函数里改状态。一个常见的坑是:触底瞬间要直接从 dropping 进 retracting,中间不要插入「无动作」帧,否则绳子会在最底处停一帧,视觉上明显卡顿。渲染顺序也固定下来,先铺背景再画动态层:

function drawScene() { ctx.clearRect(0, 0, W, H); // 地面以下整体铺土色,再做两道深浅纹理 ctx.fillStyle = '#8B5A2B'; ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y); // 画绳子,线宽和颜色单独设,避免继承上次的样式 ctx.beginPath(); ctx.moveTo(hook.originX, hook.originY); ctx.lineTo(hook.x, hook.y); ctx.strokeStyle = '#333333'; ctx.stroke(); }

3. 抓钩物理与抓取判定:黄金矿工玩法实现的核心

3.1 出手角度:把钩子当单摆算

idle 状态下,钩子挂在矿工正下方,绳长固定为 HOME_LEN,角度以竖直向下为 0°,按正弦规律左右摆动:

// swingCenter=0 表示竖直向下,swingRange=0.7 约 40°,swingRate=1.2 rad/s hook.angle = swingCenter + swingRange * Math.sin(clock * swingRate); hook.x = hook.originX + Math.sin(hook.angle) * hook.ropeLen; hook.y = hook.originY + Math.cos(hook.angle) * hook.ropeLen;

Math.sin(clock * swingRate)而不是逐帧累加角度,angle 就始终是时间的纯函数,不会因为掉帧或暂停累积误差。swingRange 建议 0.6~0.8 弧度:太小出手选择少,太大会让左右两端的切向速度接近零,钩子看起来「甩不出去」。绳头坐标用极坐标转直角坐标,注意这里的 cos 是正号,因为绳长向屏幕下方延伸,y 轴向下。

3.2 放绳与收绳的速度模型

玩家按键出手后,把当前角度锁定,绳长按恒定速度增加。触底就自动转收绳:

if (hook.state === 'dropping') { hook.ropeLen += DROP_SPEED * delta; // DROP_SPEED 单位 px/s if (hook.ropeLen >= level.maxRope) { hook.state = 'retracting'; } syncHookPosition(); } function syncHookPosition() { hook.x = hook.originX + Math.sin(hook.angle) * hook.ropeLen; hook.y = hook.originY + Math.cos(hook.angle) * hook.ropeLen; }

收绳分两种情况:空钩收绳接近匀速;带货时按货物重量折算速度,重量越大收得越慢。给收绳速度设一个下限,防止重量系数接近零时绳头瞬移:

function getRetractSpeed() { const base = EMPTY_RETRACT_SPEED; // 空钩 140 px/s if (!hook.cargo) return base; const w = hook.cargo.weight; // 物品重量系数 return Math.max(base / w, 30); // 最低 30 px/s }

大金块重量系数 2.0,收绳只有 70 px/s,深度 500px 要 7 秒多,玩家这段时间只能等。这是刻意的设计——大金块的高收益靠「占时间」来平衡。但石头这类低价值物品不要也这么慢,把石头重量系数设 0.7,收得比金块快,玩家才会顺手捞走挡路的石头。

3.3 抓取判定与关键参数表

抓取判定用圆心距离,比 AABB 矩形碰撞更符合「钩子夹住东西」的直觉:

function tryGrab(items) { for (const it of items) { if (it.grabbed || it.removed) continue; const dx = it.x - hook.x; const dy = it.y - hook.y; // Math.hypot 比手写开方更稳,数值很大时也不损失精度 if (Math.hypot(dx, dy) <= it.radius + GRAB_RADIUS) { hook.cargo = it; it.grabbed = true; hook.state = 'retracting'; return; } } }

GRAB_RADIUS 给 12px 左右。太小玩家要瞄得很准,很挫败;太大会出现隔着金块边缘就「吸走」的失重感。生成矿物时保证彼此间距大于两倍半径之和,避免一个钩子同时命中两个目标。

参数建议值作用调参提示
swingRange0.7 rad待机摆幅改大选择多,改小出手窗口收窄
swingRate1.2 rad/s摆动频率0.8 偏慢,适合第一关
DROP_SPEED160 px/s放绳速度配合最大绳长决定到最深处耗时
EMPTY_RETRACT_SPEED140 px/s空钩收绳比放绳略慢,制造节奏差
GRAB_RADIUS12 px抓取判定半径与矿物半径联动调整
HOME_LEN40 px待机绳长小于矿工身高即可

提示:参数不要单独调。摆幅、放绳速度、最大绳长共同决定「出手最远距离」,收绳下限和最大绳长共同决定「一收一趟的时间」。每次改完跑一遍时间预算。

4. 黄金矿工的关卡结构:计时、目标金额与矿物生成

4.1 单关时间与过关判定

经典街机规则是每关 60 秒,倒计时归零时看累计金额是否达到目标,没达到就失败。HTML 小游戏通常放宽到 60~75 秒。计时用 delta 累减,不要依赖 setInterval:

level.timeLeft -= delta; if (level.timeLeft <= 0) { if (level.money >= level.target) { levelIndex++; initLevel(levelIndex); // 过关,按配置重开一关 } else { showBanner('目标未达成,本关失败'); // 常见做法:给一次重试机会,重试后回到本关初始状态 } }

过关判定放在倒计时归零之后,不要在 money 变化那一刻立刻判,否则最后一钩把金额带上岸的结算感就被切掉了。入账时机选在钩头回到矿工位置时:cargo 从物品列表移除,money 累加,然后才继续下一钩。顺序错了会出现「金额已经到了但还在放绳」的怪体验。

4.2 矿物定价与重量系数

矿物表直接放配置对象里,按关卡调整出现概率。推荐这套组合:

物品分值重量系数生成深度出现节奏
小金块101.0浅层为主每关都有
中金块251.5全深度第 2 关起
大金块502.0深层偏多第 3 关起
钻石1001.0全深度随机点缀
石头20.7任意放在金块路径上

钻石重量系数做成 1.0 而不是更小,是为了让「钻石一定比大金块划算」成立:分值 100、收绳速度快一倍,玩家自然优先抓;如果系数给 0.5,钻石收得飞快,其他矿物全变成劣质选项。石头分值低、重量轻,是设计来占坑的,放在通向大金块的路径上,玩家要么多花一趟捞走,要么冒时间不够的风险绕路。

生成逻辑要保证两点:所有物品深度不超过最大绳长的 0.9 倍;同屏物品总值约等于目标金额的 1.2~1.5 倍。目标 500 的关卡就放总值 700 左右的矿物,低于 1 倍会出现「全抓也过不了关」的死局,高于 1.5 倍则失去取舍压力。

4.3 关卡节奏:目标金额怎么递增

目标金额不能按固定比例加,因为玩家每钩收益上限基本固定,线性递增会让后期关卡变成「必须摸到几乎全部大金块和钻石」才过关。常见做法是按关卡写显式配置,并用一个概率字段控制矿物结构:

const LEVEL_CONFIG = [ { time: 60, target: 500, bigRate: 0.1 }, // 第 1 关,教学 { time: 60, target: 800, bigRate: 0.2 }, { time: 55, target: 1200, bigRate: 0.3 }, ]; function initLevel(index) { const cfg = LEVEL_CONFIG[index]; // bigRate 决定本关大金块/钻石占矿物总数的比例 generateMinerals(cfg); }

目标金额每关递增 40%~60% 是多数玩家能接受的范围。第一关做成教学关:只放金块不放石头,让玩家先建立「摆动—出手—抓取—收绳」的操作闭环,第二关再上石头和深层矿物。显式的关卡配置比套随机公式更容易扩展,我一般会把这份配置单独抽成 JSON,方便别人改关卡。

完整一点的源码还会加两个扩展项:炸药和双倍金袋。炸药消耗次数,抓取后清除半径 60px 内的石头,用于打破大金块被石头围死的局面;双倍金袋则把下一钩收益乘 2。这两样都做成道具对象,复用抓取判定的同一套逻辑,只是结算时走不同分支,代码量很小。存档用 localStorage 就够了:

function saveProgress() { const data = { level: levelIndex, best: bestScore }; localStorage.setItem('gold-miner-save', JSON.stringify(data)); }

注意:localStorage 按 origin 隔离,同一目录下不同页面共用一份存储,改关卡配置后调试时记得先清一次存档。

5. 黄金矿工网页游戏移动端适配与性能调试的三个实用技巧

5.1 用离屏 Canvas 预渲染静态背景

地面纹理、矿工角色、界面装饰每帧都重画是浪费。初始化时把静态层画到离屏 canvas,主循环里只 drawImage 一次:

const bg = document.createElement('canvas'); bg.width = W; bg.height = H; drawBackground(bg.getContext('2d')); function render() { ctx.drawImage(bg, 0, 0); drawRope(); drawMinerals(); }

离屏 canvas 的尺寸要用物理像素而不是 CSS 尺寸,否则高分屏上背景会糊。

5.2 devicePixelRatio 与触摸事件

手机上直接放大画布会模糊。正确做法是把画布 buffer 放大到 dpr 倍,CSS 尺寸保持逻辑值:

const dpr = window.devicePixelRatio || 1; canvas.width = W * dpr; canvas.height = H * dpr; ctx.scale(dpr, dpr); canvas.style.width = W + 'px'; canvas.style.height = H + 'px';

触摸输入用 pointerdown 统一处理鼠标和手指,注意坐标换算。换算要用逻辑宽高比而不是 dpr 直接乘,因为 CSS 尺寸和 buffer 尺寸已经分离:

canvas.addEventListener('pointerdown', (e) => { const rect = canvas.getBoundingClientRect(); const y = (e.clientY - rect.top) * (H / rect.height); if (y < GROUND_Y) hook.state = 'dropping'; });

5.3 调试开关与命中框可视化

源码里留一个 debug 变量,按 D 或双击画布切换。开启后画出所有矿物的命中圆和钩子抓取半径,手感问题的根源一眼可见——是判定半径太小,还是矿物重叠:

function renderDebug() { ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 1; for (const it of minerals) { ctx.beginPath(); ctx.arc(it.x, it.y, it.radius, 0, Math.PI * 2); ctx.stroke(); } // 钩子抓取半径单独画,对比判定是否匹配视觉大小 }

最后做一次时间预算验证:把一关连续跑三遍,记录总出手次数和每钩平均耗时。单关 60 秒内出手次数若少于 5 次,先调低 EMPTY_RETRACT_SPEED 的下限;多于 8 次说明收绳太快、深度不够,玩家来不及思考选择。反向调整 DROP_SPEED 和收绳下限后重跑同一关对比手感。

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

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

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

立即咨询