微信小游戏(WeChat Mini Game)运行在小程序基础库之上,却与普通小程序共享的是两套截然不同的运行环境。普通小程序由逻辑层(AppService)与渲染层(WebView)双线程构成,通过 setData 做跨线程通信;而小游戏只保留逻辑层,渲染层被替换为一块独占的 Canvas,开发者需要直接调用 Canvas 2D 或 WebGL 接口把画面「画」出来,没有 WXML、没有样式表、没有节点树。
这意味着小程序生态里成熟的那套「数据驱动视图」的心智模型在小游戏里完全失效。帧率、内存、DrawCall 全部要手动管理,首包体积更是决定了用户点开游戏后要等多久才能看到第一帧。本文按引擎选型、渲染性能、变现链路三条主线,把一套小游戏从立项到上线需要做的技术决策梳理清楚。如果你还在纠结要不要用引擎,也可以先看 Unity 和 Unreal 哪个更适合入门 了解原生引擎的思路差异,再回到小游戏这个受限环境做取舍。
一、小游戏与小程序的环境差异
先把边界划清楚,后面所有性能决策都建立在这些约束上。
| 维度 | 普通小程序 | 微信小游戏 |
|---|---|---|
| 视图层 | WXML + WXSS + WebView | 单个 Canvas(2D / WebGL) |
| 线程模型 | 逻辑层 + 渲染层双线程 | 逻辑层 + 渲染(Canvas 在同一线程组) |
| 通信 | setData 跨线程序列化 | 直接调用绘制 API,无序列化开销 |
| 主包上限 | 2MB(分包合计 20MB) | 4MB(分包合计 20MB) |
| 入口 | app.json 页面路由 | game.json + game.js |
| 可用 API | 全量 wx.* | 子集,无 wx.request 的 enableHttp2 等部分能力 |
主包从 2MB 放宽到 4MB 是小游戏相对宽松的地方,但引擎运行时本身就吃掉 1.5~3MB。以 Cocos Creator 3.x 为例,仅引擎核心 + 必要的 2D 模块,构建后 cocos-js 目录通常就有 1.8MB 左右,留给业务代码和首屏资源的空间非常紧张。
game.json 里的关键配置项:
{
"deviceOrientation": "portrait",
"showStatusBar": false,
"networkTimeout": {
"request": 10000,
"downloadFile": 10000
},
"subpackages": [
{ "name": "level2", "root": "subpackages/level2/" }
]
}
subpackages 是小游戏的命脉:主包只放引擎、启动场景和第一关,后续关卡、皮肤、音效全部塞进分包,配合 wx.loadSubpackage 在关卡切换的过场动画里预下载,用户几乎感知不到加载。
二、引擎选型
小游戏引擎的选择本质是「开发效率」与「运行时开销」的博弈。主流方案可以分成四类。
2.1 重型一体化引擎:Cocos Creator / LayaAir / Egret
Cocos Creator 是目前小游戏生态占有率最高的方案,编辑器、场景系统、物理引擎、UI 系统、动画状态机一应俱全,还能一键发布到抖音、快手、支付宝等平台。代价是运行时体积和启动耗时。
# Cocos Creator 3.8 构建小游戏
# 编辑器内:项目 -> 构建发布 -> 微信小游戏 -> 构建
# 命令行构建(CI 场景)
/Applications/Cocos/Creator/3.8.0/CocosCreator.app/Contents/MacOS/CocosCreator \
--project /path/to/project \
--build "platform=wechatgame;debug=false;md5Cache=true"
LayaAir 的定位接近 Cocos,但在 2D 渲染上做了更多批处理优化,适合卡牌、休闲类。Egret 生态在 2019 年后活跃度下降,新项目不建议采用,但存量项目维护仍会遇到。
选这类引擎的判断标准很简单:如果你的游戏有复杂 UI 层级、需要动画状态机、需要跨平台发布,就直接上 Cocos Creator,别自己造轮子。
2.2 轻量渲染库:原生 WebGL + Three.js / PixiJS
如果游戏逻辑简单(消除类、点击类、单场景跑酷),引入完整引擎是巨大的浪费。直接在小游戏里用 WebGL 写渲染,或者引入 PixiJS 这类 2D 渲染库,主包能压到 600KB 以内。
小游戏的 wx.createCanvas() 返回的 canvas 可以直接取 WebGL 上下文:
const canvas = wx.createCanvas()
const gl = canvas.getContext('webgl')
// 视口按设备像素比设置,否则高分屏会糊
const info = wx.getSystemInfoSync()
canvas.width = info.screenWidth * info.pixelRatio
canvas.height = info.screenHeight * info.pixelRatio
gl.viewport(0, 0, canvas.width, canvas.height)
用原生方案要自己处理纹理图集、Sprite 批处理、触摸事件分发、音频播放,工作量不小。适合团队里有图形学背景的工程师,或者游戏本身渲染极简单的情况。
2.3 引擎选型对照表
| 引擎 | 运行时体积 | 上手成本 | 3D 支持 | 跨平台 | 适用品类 |
|---|---|---|---|---|---|
| Cocos Creator 3.x | ~1.8MB | 中 | 强 | 全平台 | 中重度 2D/3D |
| LayaAir 3.x | ~1.2MB | 中 | 中 | 全平台 | 2D 卡牌/休闲 |
| PixiJS + 自研逻辑 | ~400KB | 高 | 无 | 需适配 | 轻度 2D |
| 原生 WebGL | ~0 | 极高 | 自研 | 需适配 | 极简/技术验证 |
2.4 混合方案:引擎 + 自研逻辑层
实际项目里最常见的是折中方案:用引擎负责渲染、资源加载与 UI,业务逻辑(数值、战斗结算、存档)全部抽成不依赖引擎的纯 JavaScript 模块。这样做的好处是逻辑层可以脱离引擎单独跑单元测试,未来换引擎或加平台时不用重写核心玩法。
// battle-core.js —— 纯逻辑,不 import 任何引擎 API
export function calcDamage(attacker, defender, rng) {
const base = attacker.atk * (1 - defender.def * 0.6)
const crit = rng() < attacker.critRate ? 1.5 : 1.0
return Math.max(1, Math.floor(base * crit))
}
// battle-scene.ts —— 引擎侧只负责把结果渲染出来
import { calcDamage } from './battle-core'
判断是否该抽离的信号:当一段代码需要 import { Node } from 'cc' 才能测试时,它大概率不该待在场景脚本里。
2.5 从 3D 项目借来的经验
即使做的是 2D 小游戏,大型游戏世界的 AOI 系统设计 里关于「只渲染视野内对象」的思路依然适用——小游戏里对应的是视锥剔除与对象池,把屏幕外的 Sprite 从渲染列表里摘掉,能直接省下可观的 DrawCall。
三、渲染性能
小游戏的性能瓶颈几乎全在渲染线程。和 /miniprogram-performance-optimization/ 里讨论的 setData 优化不同,小游戏没有 setData,瓶颈变成 DrawCall 数量、纹理内存和 GC 停顿。
3.1 DrawCall 与合批
每一次 gl.drawElements 都是一次 DrawCall,小游戏的 DrawCall 建议控制在 100 以内,超过 200 在中低端安卓机上会明显掉帧。合批(Batching)是核心手段:
- 同一张纹理图集(Atlas)内的 Sprite 可以合批成一次 DrawCall
- 打乱图集顺序会打断合批,所以 UI 层级要按图集归组
- 位图字体(Bitmap Font)替代系统字体渲染,避免每个字符一次 DrawCall
// Cocos Creator 中查看合批情况:开启调试模式
// 构建时勾选 debug=true,运行时按 F1 打开性能面板
// 关注 "Draw call" 与 "Instancing" 两项
3.2 纹理内存
一张 2048x2048 的 RGBA8888 纹理占 16MB 内存。小游戏在 iOS 上的内存上限大约 1GB(视机型),安卓更紧张。压缩纹理(Compressed Texture)能把它降到 1/4:
| 格式 | 压缩比 | iOS | Android | 小游戏支持 |
|---|---|---|---|---|
| PNG/JPG | 无(运行时解码) | 是 | 是 | 是 |
| ETC1/ETC2 | 6:1 | 否 | 是 | 部分 |
| ASTC | 4~8:1 | 是(A8+) | 部分 | 是 |
| PVRTC | 4~8:1 | 是 | 否 | 是 |
Cocos Creator 支持按平台自动分包纹理,构建时勾选 ASTC 与 ETC2 两套,运行时按 wx.getSystemInfoSync().platform 加载对应格式。
3.3 帧率与自适应
小游戏默认 60fps,但可以通过 wx.setPreferredFramesPerSecond 降到 30fps 来省电:
// 静态场景降到 30fps,战斗场景拉回 60fps
wx.setPreferredFramesPerSecond(30)
// 监听掉帧,动态降级
let lowFpsCount = 0
setInterval(() => {
// Cocos 中通过 director.getDeltaTime() 反推
}, 1000)
对战中一旦连续掉帧,应该主动降低特效粒子数量、关闭后处理,而不是硬扛。
3.4 内存与 GC
JavaScript 的垃圾回收会在堆增长时触发,一次 Full GC 可能停顿几十毫秒,直接表现为卡顿。核心原则是「对象池化」:子弹、敌人、飘字、粒子全部预分配,用完回收而不是 new 出来再丢弃。
class ObjectPool {
constructor(factory, size) {
this.factory = factory
this.pool = []
for (let i = 0; i < size; i++) this.pool.push(factory())
}
acquire() {
return this.pool.pop() || this.factory()
}
release(obj) {
obj.reset()
this.pool.push(obj)
}
}
配合 wx.onMemoryWarning 做兜底:收到内存警告时立刻释放非必要纹理、清空缓存关卡资源。
wx.onMemoryWarning(res => {
// level 5 表示内存压力较低,10 以上需要立即释放
if (res.level >= 10) {
resourceManager.releaseUnused()
cc.assetManager.releaseUnusedAssets()
}
})
3.5 音频与网络
音频在小游戏里有两套接口:wx.createInnerAudioContext() 适合短音效,wx.createWebAudioContext() 适合需要混音、变速的场景。音效对象必须池化复用,否则频繁创建销毁会触发音频子系统抖动。网络层则要警惕小游戏不保证 TCP 长连接稳定,弱网下应做请求重试与离线兜底。
四、变现链路
小游戏的收入主要来自广告与内购两条线,/miniprogram-monetization/ 里讨论的通用模型在小游戏里同样成立,但激励视频的权重远高于普通小程序。
4.1 激励视频广告
激励视频(Rewarded Video)是休闲小游戏最主流的变现方式,用户看完 15~30 秒视频换取复活、道具或双倍奖励。核心接口:
const videoAd = wx.createRewardedVideoAd({ adUnitId: 'adunit-xxxx' })
videoAd.onError(err => {
// 广告拉取失败,必须给用户降级方案,不能卡死流程
console.error('rewarded video error', err)
})
videoAd.onClose(res => {
// res.isEnded === true 表示完整观看
if (res && res.isEnded) {
grantReward()
} else {
wx.showToast({ title: '需完整观看才能领取', icon: 'none' })
}
})
// 预加载,避免用户点击时才开始拉取
videoAd.load().then(() => videoAd.show())
关键细节:onClose 的 isEnded 字段在低版本基础库可能为 undefined,此时应视为「已观看」以避免误伤用户;广告拉取失败率在弱网下可达 10% 以上,必须准备无广告的降级路径。
4.2 插屏与 Banner
插屏广告(Interstitial)适合关卡结算、复活失败等自然停顿点,但频率过高会被平台判罚。Banner 常驻在屏幕边缘,收益低但稳定。
const banner = wx.createBannerAd({
adUnitId: 'adunit-yyy',
style: { left: 0, top: 0, width: 320 }
})
banner.onResize(size => {
// 安卓上 banner 高度可能变化,需要重新定位避免遮挡
banner.style.top = wx.getSystemInfoSync().screenHeight - size.height
})
4.3 虚拟支付
小游戏的虚拟支付(道具、金币、皮肤)走的是「米大师」(Midas)体系,而非普通小程序的微信支付。注意 iOS 端的限制:iOS 小游戏不允许直接跳转外部支付,虚拟支付能力仅对安卓开放,iOS 用户只能通过广告变现。这是产品设计阶段就必须考虑的收入结构差异。
// 安卓端虚拟支付(需先开通并配置 offerId)
wx.requestMidasPayment({
mode: 'game',
env: 0,
offerId: 'your-offer-id',
currencyType: 'CNY',
platform: 'android',
buyQuantity: 100,
zoneId: '1',
success: () => syncBalanceToServer()
})
4.4 变现与体验的平衡
广告密度直接伤害留存。经验值:单次会话激励视频不超过 5 次,插屏间隔不小于 90 秒,且绝不在用户操作过程中弹出打断。服务端要对奖励发放做校验与幂等,防止客户端被篡改后刷奖励。
4.5 变现模型测算
上线前应把变现模型算清楚,而不是先堆广告再看数据。一个简化的测算框架:
| 指标 | 典型值 | 说明 |
|---|---|---|
| DAU | 10,000 | 日活 |
| 人均激励视频次数 | 2.5 | 受广告位设计影响 |
| 激励视频 eCPM | 60 元 | 受地域、时段影响 |
| 插屏 eCPM | 25 元 | 通常低于激励视频 |
| 广告 ARPU | ≈ 0.19 元 | 2.5 × 60 / 1000 + 插屏折算 |
| 安卓付费率 | 1.5% | iOS 无虚拟支付 |
| 付费 ARPPU | 18 元 | 首充 + 复购 |
按此模型,1 万 DAU 的日广告收入约 1,900 元,安卓付费收入约 2,700 元,其中安卓用户的付费贡献远高于广告。所以产品早期应优先做安卓端的付费点设计,iOS 端则把广告位密度做到上限。任何变现决策都要回到「是否伤害次日留存」这条红线,留存掉了,再高的 eCPM 也是负收益。
五、调试与发布
5.1 真机性能分析
开发者工具的性能面板只能作参考,真实数据必须真机抓取。iOS 上用 Safari 的 Web Inspector 连真机调试,安卓用 Chrome 的 chrome://inspect。重点关注:
- 帧率曲线是否有周期性尖刺(通常是 GC)
- 内存曲线是否单调上升(泄漏)
- DrawCall 与三角形数量
5.2 首屏启动优化
小游戏的首屏加载链路是:下载主包 → 解压 → 执行 game.js → 初始化引擎 → 加载首场景 → 渲染第一帧。每一步都要压:
- 主包体积压到 4MB 以下,且尽量小
- 引擎初始化时只加载必要模块(Cocos 的「功能裁剪」)
- 首场景资源随主包走,避免二次网络请求
- 用
wx.getLaunchOptionsSync()拿到的scene做首屏差异化,分享进来的用户直达对应关卡
5.3 分包与版本更新
小游戏的更新策略与小程序一致:主包更新后需重新下载,分包可按需拉取。wx.getUpdateManager() 用来处理版本热更:
const updateManager = wx.getUpdateManager()
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已就绪,重启后生效',
success: res => {
if (res.confirm) updateManager.applyUpdate()
}
})
})
资源层面,把小游戏的所有远程资源(关卡、皮肤、音频)挂 CDN,配合版本号做缓存失效。资源清单(manifest)随主包发布,客户端启动时比对版本,只拉变化的分片,能把一次更新的下载量从几十兆压到几百 KB。
5.4 审核与合规
小游戏审核比小程序更严,尤其对「诱导分享」「赌博玩法」「未实名」的判定。必须接入实名认证与防沉迷(wx.getUserInfo 已废弃,改用实名 + 时长限制接口),未成年人在线时长受强制限制。虚拟抽奖类玩法需要概率公示,否则会被驳回。
小结
微信小游戏的技术栈可以概括为「受限环境下的图形工程」:环境约束(无 DOM、主包 4MB、Canvas 单视图)决定了引擎选型的边界,渲染性能(DrawCall、纹理、GC)决定了下限体验,变现链路(激励视频、虚拟支付、iOS 限制)决定了商业上限。三条线中任何一条没处理好,游戏都跑不起来。
落地建议:先按品类决定引擎(重度上 Cocos,轻度自研渲染),再用分包和纹理压缩守住包体与内存红线,最后把广告与支付抽象成独立服务层,方便按平台差异做开关。上线前务必真机跑一遍中低端安卓机,开发者工具里的流畅不代表用户手机上的流畅。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。