
简介这是一份名为“数钱数到手抽筋”的HTML5微信小游戏源码包适合前端初学者、微信小游戏开发入门者以及需要快速搭建休闲游戏Demo的开发者参考。游戏核心以“数钱”为互动主题可帮助理解H5游戏中的事件响应、计分逻辑与音画配合整体既可作为练手项目也能作为功能模块移植到其他H5案例中。压缩包为rar格式共22个文件包含14个png图片素材、6个js脚本、1个mp3音频和1个html入口文件资源包仅236KB。png文件主要承担游戏背景、钱币图标和按钮等界面元素js脚本实现游戏逻辑、动画调度与事件交互mp3提供音效反馈html则作为启动页面方便本地直接打开调试。目前已有307人浏览学习可见该源码对同类自制游戏或微信小游戏开发具有实际参考价值。下载后可获得完整可运行的项目结构能够直接体验“数钱”手感也可进一步修改图片素材、调整计分规则或适配微信小游戏环境用于课程设计、个人作品集或商业项目前期的原型验证。1. “数钱数到手抽筋”是 HTML5 微信小游戏里最典型的点击数值样板把“数钱数到手抽筋”拆开看本质就是一枚大金币、一个不断跳动的数字、一次比一次高的收益加成。这类项目在源码站上常年挂着“HTML5 微信小游戏源码”的标签原因很简单它不依赖复杂美术几百行代码就能跑通 Canvas 渲染、触摸事件、数值膨胀、存档恢复这一整条链路是理解微信小游戏运行机制最省成本的入口。对刚接触微信小游戏的前端来说它能回答“为什么浏览器能跑的页面到微信里白屏”对拿到类似资源包想改数值、接分享、做排行榜的开发者来说它又是一份可以直接动刀的基础骨架。接下来按“运行原理 → 最小实现 → 性能避坑 → 上线收尾”的顺序把这类源码从能跑到能发拆开讲透。2. HTML5 代码在微信小游戏里的运行环境与触摸坐标适配2.1 为什么浏览器跑通的 HTML5 页面到微信小游戏里直接白屏微信小游戏的运行环境是 JavaScript 引擎加 Canvas 2D / WebGL 渲染接口没有 DOM、没有window.document、没有click事件。你在网页里写的document.getElementById(coin)、element.addEventListener(click)到这里全部不可用。这不是某个接口缺失而是整个页面模型不存在。常见的适配做法是引入官方适配库比如 weapp-adapter 这类垫片由它把window、document、XMLHttpRequest等对象在内存里模拟出来。但你要清楚垫片解决的是“不报错”不是“性能好”。很多白屏问题恰恰是因为代码里混用了setTimeout做动画同时又依赖 DOM 节点做点击判定结果适配层兜不住渲染和事件全部静默失败。我一般建议在写点击类小游戏时从一开始就只用四个东西wx.createCanvas()创建画布、wx.onTouchStart/onTouchMove/onTouchEnd绑定触摸、CanvasRenderingContext2D做绘制、wx.setStorageSync/getStorageSync做存档。这四样是微信小游戏原生能力不经过任何模拟层行为稳定出了问题也好查。2.2 触摸事件与坐标体系clientX 直接映射到画布逻辑坐标微信小游戏的触摸事件对象里自带clientX、clientY它们是相对于屏幕左上角的逻辑像素坐标。和浏览器里还要算getBoundingClientRect不同这里的坐标已经可以直接当作画布上的绘制坐标使用。wx.onTouchStart((e) { const touch e.touches[0]; const x touch.clientX; const y touch.clientY; // 判断是否落在金币圆内使用距离平方避免开根号 const dx x - this.coinX; const dy y - this.coinY; if (dx * dx dy * dy this.coinRadius * this.coinRadius) { this.addMoney(this.perTap); } });这里的this.coinX/Y是金币中心点坐标this.coinRadius是金币半径。判断用距离平方而不是Math.sqrt因为平方计算更快在高频点击场景下能省掉不必要的浮点运算。注意clientX/clientY的单位是逻辑像素不是物理像素。真机上逻辑像素和物理像素之间存在pixelRatio倍数这个问题放到第 4 章专门处理。2.3 渲染循环微信环境里没有原生 requestAnimationFrame 时的替代方案浏览器里有requestAnimationFrame微信小游戏基础库原生环境不提供完整的这个接口。最常见的做法是让适配层在全局挂一个或者自己用setTimeout做 16ms 循环。不管是哪种方式动画循环里都要自己算增量时间驱动数值变化和重绘。const raf (callback) { setTimeout(() { const now Date.now(); callback(now); }, 1000 / 60); }; // 游戏主循环 tick(now) { const delta (now - this.lastTime) / 1000; // 单位秒 this.lastTime now; this.autoMoney this.perSec * delta; // 自动收益按秒累积 this.render(); // 每帧重绘全部画面 raf((t) this.tick(t)); }delta是距离上一帧的时间差用秒做单位。自动收益必须乘delta否则不同帧率的设备上收益速度会不一致。用 60fps 做基准时一帧大约 0.016 秒如果机器掉到 30fpsdelta会变大每秒收益总量仍然维持稳定。这是点击数值类游戏最基础的帧率无关写法不要用“每帧加固定值”。2.4 技术路线对照原生 Canvas 渲染与引擎打包方案做这类小游戏其实有两条路一是直接用原生 JavaScript 写 Canvas另一条是用 Unity 引擎做 unity 微信小游戏打包。两者不是替代关系而是工作量不同的选择。对比项原生 JS CanvasUnity 引擎打包包体大小几十 KB 到几百 KB起步几 MB上手门槛需要自己拼 UI 和动画需要懂引擎与资源管线触摸事件wx.onTouchStart全局监听后自己命中判定引擎内部做 GUI 事件派发适合场景金币点击、合成类、轻量休闲3D 模型、角色动画、物理效果如果目标是做“数钱”这种核心循环只有点击和数字变化的游戏原生 Canvas 足够而且更容易针对微信环境做优化。如果你要做的版本里有大量角色动画和特效才值得走引擎路线但那时要处理的就不再是本文的坐标换算问题而是 WebGL 模板配置和资源分包了。3. 数钱游戏最小源码从 Game 类、点击判定到自动收益3.1 项目入口 game.js 与 game.json 的初始化骨架微信小游戏的项目根目录只需要两个必需文件game.js是入口脚本game.json是全局配置。game.js执行时创建画布并启动游戏。{ deviceOrientation: portrait, showStatusBar: false, networkTimeout: { request: 5000 } }deviceOrientation锁定竖屏对点击类游戏是合理的默认选择。showStatusBar控制是否显示系统状态栏数钱类游戏通常全屏沉浸关掉体验更好。networkTimeout是网络请求超时时间如果你后续接排行榜、分享回调这个值会影响请求失败的速度。game.js入口只需创建游戏实例并启动// game.js - 微信小游戏入口 const Game require(./game-core.js); const game new Game(); game.start();这里把核心逻辑放在game-core.js里入口文件保持极薄方便后续通过小游戏分包或插件机制拆分代码。实测经验是改动频率高的数值公式和资源加载逻辑放在单独文件game.js只在开发者工具重新编译时变化能减少很多重复构建。3.2 金币绘制与点击命中游戏主体的核心类包含画布、上下文、窗口尺寸和游戏状态。金币用 Canvas 2D 画出来不依赖任何图片资源这也是这类源码能保持轻量的原因。class Game { constructor() { this.canvas wx.createCanvas(); this.ctx this.canvas.getContext(2d); const info wx.getSystemInfoSync(); this.width info.windowWidth; this.height info.windowHeight; this.coinX this.width / 2; this.coinY this.height / 2; this.coinRadius 90; this.money 0; this.perTap 1; this.perSec 0; this.tapLevel 0; this.autoLevel 0; } drawCoin() { const ctx this.ctx; const scale this.coinScale || 1; ctx.save(); ctx.translate(this.coinX, this.coinY); ctx.scale(scale, scale); // 外圈金色 ctx.beginPath(); ctx.arc(0, 0, this.coinRadius, 0, Math.PI * 2); ctx.fillStyle #f5a623; ctx.fill(); // 内圈浅色 ctx.beginPath(); ctx.arc(0, 0, this.coinRadius * 0.72, 0, Math.PI * 2); ctx.fillStyle #ffe29a; ctx.fill(); // 符号 ctx.fillStyle #b8860b; ctx.font bold 52px sans-serif; ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(¥, 0, 4); ctx.restore(); } }ctx.save()和ctx.restore()用于隔离缩放变换避免金币的缩放影响后续 UI 绘制。coinScale是点击瞬间的缩放值点击时设为 1.15每帧向 1 回退形成按下弹起的效果。font里的sans-serif在微信环境可用但如果你需要特定字体得在game.json里配置并加载字体文件不能用 CSSfont-face。3.3 点击事件与升级系统的数值膨胀公式触摸绑定在构造函数里完成升级按钮也用坐标矩形做命中检测bindEvents() { wx.onTouchStart((e) { const touch e.touches[0]; const x touch.clientX; const y touch.clientY; // 金币命中 const dist2 (x - this.coinX) ** 2 (y - this.coinY) ** 2; if (dist2 this.coinRadius ** 2) { this.money this.perTap; this.coinScale 1.15; this.spawnFloatText(${this.perTap}, x, y); return; } // 升级按钮升级点击收益 const tapBtn this.tapBtnRect; if (x tapBtn.x x tapBtn.x tapBtn.w y tapBtn.y y tapBtn.y tapBtn.h) { this.buyUpgrade(tap); return; } // 升级按钮升级自动收益 const autoBtn this.autoBtnRect; if (x autoBtn.x x autoBtn.x autoBtn.w y autoBtn.y y autoBtn.y autoBtn.h) { this.buyUpgrade(auto); } }); } buyUpgrade(type) { const cost this.getCost(type); if (this.money cost) return; this.money - cost; if (type tap) { this.perTap * 2; this.tapLevel; } else { this.perSec * 1.5; this.autoLevel; } } getCost(type) { const base type tap ? 50 : 100; const growth type tap ? 1.8 : 1.6; const level type tap ? this.tapLevel : this.autoLevel; return Math.floor(base * Math.pow(growth, level)); }按钮矩形区域tapBtnRect在构造函数里根据this.width/height计算改为wx.getSystemInfoSync()得到的值在横竖屏切换场景不可靠所以我在初始化时锁定一次之后不再查询。升级公式用指数增长前几级花几秒攒钱就能升十几级之后单次升级要按分钟计。这个节奏符合点击放置类游戏的验收标准也是“数钱数到手抽筋”里那个“抽筋”感的来源。3.4 自动收益循环与飘字特效的源码级实现自动收益不需要额外定时器直接挂在主循环的delta上即可。飘字特效用一个数组管理每帧更新位置和透明度spawnFloatText(text, x, y) { const item this.floatList.find((f) !f.alive) || this.createFloatItem(); item.text text; item.x x; item.y y; item.life 1.0; item.alive true; } createFloatItem() { const item { text: , x: 0, y: 0, life: 0, alive: false }; this.floatList.push(item); return item; } updateFloat(delta) { for (const f of this.floatList) { if (!f.alive) continue; f.y - 60 * delta; // 向上飘 f.life - delta * 0.8; // 生命周期 1.25 秒 if (f.life 0) f.alive false; } }find优先复用已经死亡的对象避免频繁创建新对象造成 GC 压力。createFloatItem只在对象池不够时调用一次后续飘字全部复用。飘字位置用点击时的触摸坐标这样玩家能明确看到“点哪里出收益”。如果你在源码包里看到类似逻辑但有细微差别比如用数组filter清理死亡对象注意那种写法每次点击都在扫全数组点击频率高时会有性能隐患。4. 数钱游戏的避坑指南高清屏、连点判定与渲染性能4.1 高清屏发虚pixelRatio 三行代码解决真机像素密度不一样直接把canvas.width设为windowWidth在高分屏上会明显模糊。微信官方推荐的思路是画布用物理像素做分辨率绘图上下文按pixelRatio缩放绘制坐标仍然用逻辑像素。const { windowWidth, windowHeight, pixelRatio } wx.getSystemInfoSync(); this.canvas.width windowWidth * pixelRatio; this.canvas.height windowHeight * pixelRatio; this.ctx.scale(pixelRatio, pixelRatio); this.width windowWidth; this.height windowHeight;这样设置之后后续所有ctx.arc、fillRect、fillText都按windowWidth和windowHeight的逻辑坐标写不用管硬件分辨率。注意wx.getSystemInfoSync的返回值里windowWidth和windowHeight单位是逻辑像素pixelRatio是设备像素比。两者相乘才是画布实际占用的物理像素。如果你漏掉这步在 iPhone 等高分屏上金币边缘会发虚看起来像低清资源放大。4.2 连点与误触点击冷却与热区热圈控制点击类游戏最容易出现的问题不是点不到而是点太快导致收益计算错乱或动画抖动。触摸事件回调本身就是高频的没必要禁止连点但要在代码层面过滤“同一帧内多次触发”和“点击瞬间同时命中两个按钮”。常见做法是设置最小触发间隔也叫冷却。const MIN_TAP_INTERVAL 50; // 毫秒 onTouchStart(e) { const now Date.now(); if (now - this.lastTapTime MIN_TAP_INTERVAL) return; this.lastTapTime now; // 后面的命中检测逻辑 }50 毫秒的间隔允许每秒最多 20 次有效点击正常人手指极限在每秒 10 次左右这个阈值不会误伤真手速又能挡住同帧重复回调。同时金币触控热区不止圆形内部我会把热区半径设为coinRadius * 1.2比视觉略大避免玩家点金币边缘没反应产生“这游戏坏了”的感觉。热区扩大之后视觉上金币弹起的缩放动画也要同步做得更明显让玩家有明确的操作反馈。4.3 粒子对象池与渲染性能控制飘字、特效、金币光晕都属于高频创建销毁的对象。真机上new对象越多垃圾回收越频繁表现就是点击几十次之后开始掉帧。对象池是这类小游戏最有效的优化手段原则只有一个内存里放固定数量的对象用状态标记区分存活与死亡。this.floatList []; for (let i 0; i 20; i) { this.floatList.push({ text: , x: 0, y: 0, life: 0, alive: false }); }固定 20 个飘字对象够用。如果所有对象都处于存活状态spawnFloatText里find不到可用项最简单的处理是复用列表里生命值最低的那一个放弃最老的特效而不是继续扩容。这种策略保证内存占用有上限点击再快也不会无限制创建对象。4.4 渲染开销清单哪些绘制能省就省把高频绘制函数的开销列成检查表逐项排查检查项问题建议ctx.fillStyle每帧改颜色会打断渲染状态颜色不变的对象分组绘制ctx.font每次设字符串都会重新解析只在字号变化时设置ctx.save/restore高频调用有开销尽量用一次变换加绘制阴影与渐变shadowBlur在 Canvas 上极贵用预绘制的贴图代替离屏 Canvas适合静态元素金币可以先画到离屏画布再drawImage金币这种静态形状尤其适合离屏 Canvas创建一张不显示的wx.createCanvas()在上面把金币画一次之后每帧drawImage贴到主画布。这样主循环里只有一次纹理拷贝不再有arc、渐变、fillText这些矢量绘制操作。注意离屏画布和主画布共享 2D 上下文数量真机上过多离屏画布会占用显存控制在 2 到 3 张以内。5. 从源码跑通到可上线的收尾分享卡片、本地存档与排错5.1 分享卡片把“数钱”变成社交钩子点击类游戏的传播核心是炫耀数值。onShareAppMessage在game.js的入口脚本里定义或者在使用wx.shareAppMessage前先声明wx.onShareAppMessage(() ({ title: 我数了 ${Math.floor(this.money)} 块你能超过我吗, imageUrl: this.shareImageUrl, }));shareImageUrl需要是本地路径或者已上传到微信后台的图片路径不能是网络 URL 直链。如果你没准备分享图微信会自动截取当前游戏画面但截图时机不稳定最好还是准备一张固定尺寸的分享图。试玩过的项目里头像和金币数字截图混排的分享卡片点击率明显高于纯文字。5.2 本地存档与自动收益的恢复微信小游戏的存档接口是同步的wx.setStorageSync和浏览器localStorage用法几乎一样。存档时机放在两个位置每次点击收益变更时节流写入以及wx.onHide时强制写入。wx.onHide(() { const save { money: this.money, perTap: this.perTap, perSec: this.perSec, tapLevel: this.tapLevel, autoLevel: this.autoLevel, updatedAt: Date.now() }; wx.setStorageSync(coinGameSave, save); });回读的细节在updatedAt字段玩家上次退出时间和本次进入时间差多少秒自动收益要补算这段时间的产出。这就是点击放置类游戏的“离线收益”。补算逻辑不能直接在构造函数同步执行太久如果离线了一天收益数字可能很大同时要注意玩家本地时间被手动修改的边界问题稳妥的做法是按 8 小时为上限截断离线收益避免一次性把成长曲线拉爆。5.3 在线排错三个检查点定位 90% 问题真机调试时打开 vConsole重点看三块。第一wx.createCanvas是否在game.js顶层调用成功返回的 Canvas 对象是否有getContext方法这一项报错基本可以断定基础库版本过低。第二触摸事件里打印touch.clientX/clientY和 UI 上金币中心的坐标对比差一个固定偏移就是pixelRatio没处理差半个屏幕就是横竖屏配置错误。第三wx.setStorageSync偶发失败多发生在存储空间满的情况写入后马上wx.getStorageSync读回校验不一致就在回调里提示玩家清理空间。这三个检查点基本能覆盖白屏、坐标漂移、存档丢失三类最常见问题。把它们作为源码注释贴在game-core.js顶部后续维护时一眼能定位排查顺序。本文还有配套的精品资源点击获取