微信小游戏适配
把《野花笔记》已经实测过的素材与物理参数换算成微信小游戏的包体、内存、填充率预算, 再逐项对照平台能力给出「能 / 不能 / 需要改」。 玩法本身不动(见 玩法、关卡与解谜、小游戏), 这里只回答一个问题:这套东西搬到微信小游戏里,要改哪几处、改到什么程度。
一、一句话结论#
适合,而且比大多数休闲小游戏更适合——包体和内存根本不是约束,只需要改三处工程细节。 真正决定生死的第四件事不在工程里,在留存里。
判断依据三条,每条都是算出来的,不是感觉:
| # | 判断 | 依据 | 结论 |
|---|---|---|---|
| 1 | 包体毫无压力 | 首包(主包)1.79 MB,占 4 MB 硬上限的 45%;主包 + 全部分包 4.13 MB,占 30 MB 总上限的 14% | 不需要为包体做任何素材妥协 |
| 2 | 内存毫无压力 | 常驻纹理 3.0 MiB,加画布后备缓冲 8.3 MiB,水位约 11.3 MiB,是 iOS 低端机 1 GB 上限的 1.1% | 内存不是瓶颈 |
| 3 | 真正的性能风险只有一个 | 竖屏横版 = 满屏重绘。按"视差 4 层 + 世界条 + 土壤 + 手账"算,每帧要填 4.94 M 像素,60 fps 就是 296 M px/s(见 4.0) | 填充率是唯一要认真治的东西 |
必须改的三件事(都是工程,工作量可控):
- HUD 避让刘海与胶囊按钮——现在的 HUD 在原生 y 0~110,会被状态栏和右上角胶囊直接盖住(见 2.1);
- WebP 双轨——包一张 PNG 兜底图集,防止老 iOS 解不出 WebP 时首帧是空白兔子(见 2.4);
wx.onHide落盘 +wx.onShow无弹窗续跑——"随时能停"必须落成机制,否则它只是一句宣传语(见五、六)。
第四件事不在工程里:图鉴 30 种植物 + 10 件遗物,5~8 天收齐,收齐之后没有回来的理由。
微信小游戏是"用完即走"的生态,它比任何平台都更需要一个"回来看看"的理由。
工程适配全部做完也只是"能上",第 ④ 层循环做不出来就是"上了也没人留"。
这一条的判断标准和取舍已经写在 循环与商业化 第二节,这里只把它列为上线门槛(见八)。
二、适配可行性核对表#
总表先给结论,逐条展开在后面。
| # | 能力 / 限制 | 微信小游戏的实际约束 | 本作怎么做 | 判定 |
|---|---|---|---|---|
| 1 | 竖屏 9:16 屏幕适配 | 没有"逻辑分辨率"概念,全屏画布自己算;宽高比跨度 0.43~0.625;刘海屏有 safeArea |
宽恒 1080 虚拟单位、高按比例伸缩;地面线距屏底恒定 538 原生 px | 需要改(HUD 避让) |
| 2 | 触摸操作 | wx.onTouchStart / Move / End,坐标是 CSS px |
只用单指;点击 / 按住 / 滑动三种,无摇杆无虚拟键 | ✅ 能 |
| 3 | 音频 | mp3 / aac / wav 双端支持;Android 同时最多播 10 个;来电会中断 | 1 条 BGM + 8 个音效实例池 | ✅ 能(要处理中断恢复) |
| 4 | 帧动画 / WebP | Canvas 2D、WebGL 都支持;WebP 解码依赖系统(iOS 14+);高性能模式的压缩纹理只有 ASTC / ETC1 | 保留 WebP,另出一张 PNG 兜底图集 + 启动自检 | 需要改(格式双轨) |
| 5 | 本地存储 | 单个 key ≤ 1 MB,单账号总上限 10 MB;用户文件目录 200 MB(可申请 1 GB) | 存档 < 1 KB(收集态用 bitmask) | ✅ 能 |
| 6 | 云开发 | 小游戏支持云开发(数据库 / 存储 / 云函数),按量计费有免费额度 | 每账号一条文档,约 0.5 KB | ✅ 能 |
| 7 | 分包加载 | 主包 ≤ 4 MB;主包+分包 ≤ 30 MB;单个普通分包不限大小;单个独立分包 ≤ 4 MB | 主包 1.79 MB + 3 个分包 2.34 MB | ✅ 能(余量巨大) |
| 8 | 广告 | 激励视频 / 插屏 / Banner 都能接;流量主开通需 累计 UV > 500(或游戏圈官方帖 5 篇,或同主体已有变现满 1 季度的小程序) | 只接激励视频,只放结算页 | ✅ 能(难的是克制) |
| 9 | 分享与好友 | onShareAppMessage 可用;关系链 API(getFriendCloudStorage 等)对个人主体/部分类目已受限 |
分享"发现" + 带参卡片进好友花园 | 需要改(不依赖关系链 API) |
| 10 | 登录 | wx.login() 静默换 openid,没有授权弹窗;getUserInfo 已不返回真实昵称头像 |
全程没有"登录"这个动作 | ✅ 能(天然契合) |
| 11 | 支付 | 虚拟支付需企业主体 + 版号 | P1~P3 不做内购 | 需要改(资质先行) |
| 12 | 合规 | 需 ICP 备案 + 实名 + 防沉迷;未成年人限 周五/六/日及法定节假日 20:00–21:00 | 照接,无内购无社交攀比风险极低 | 需要改(接实名) |
外部口径来源:分包加载、 本地存储、 文件系统、 音频、 高性能模式(内存限制)、 广告开通条件。
2.1 竖屏 9:16 与屏幕适配(需要改)#
小游戏没有"设计分辨率"这回事,画布就是屏幕。所以第一步是自建一个虚拟坐标系,规则必须是可算的,不能"差不多"。
规则(三条,缺一条就会在某些机型上错位):
- 宽度恒为 1080 虚拟单位,高度按屏幕实际比例伸缩:
H = 1080 × screenHeight / screenWidth - 兔子地面线距屏幕底部恒为 538 虚拟 px,不按比例算。
- 多出来的高度全给天空带(水彩大留白正好吃这个),少掉的高度从天空带顶部裁。
第 2 条是硬要求,理由是物理参数是绝对像素:重力 1800 px/s²、小跳初速 380(跳高 40 px)、 大跳初速 520(跳高 75 px)、台阶一级 22 px。这些数字一旦被缩放,跳跃余量就变了 (大跳过 72 px 的石头只剩 3 px 余量,再缩 5% 就会撞上)。 所以世界条必须是绝对尺寸,只有天空带是可伸缩的。
实测各机型换算(基准 1080×1920):
| 机型 / 比例 | CSS 分辨率 | 宽高比 | 虚拟高 H | H − 1920 | 天空带高度 | 地面线占屏高 |
|---|---|---|---|---|---|---|
| iPhone 8 / SE2(16:9) | 375×667 | 0.5625 | 1921 | +1 | 884 | 72.0% |
| 安卓 720×1280(16:9) | — | 0.5625 | 1920 | 0 | 884 | 72.0% |
| iPhone 15(19.5:9) | 393×852 | 0.4612 | 2341 | +421 | 1305 | 77.0% |
| iPhone 15 Pro Max(19.5:9) | 430×932 | 0.4614 | 2340 | +420 | 1304 | 77.0% |
| 安卓 1080×2340(19.5:9) | — | 0.4615 | 2340 | +420 | 1304 | 77.0% |
| 安卓平板(16:10) | — | 0.625 | 1728 | −192 | 692 | 68.9% |
| 超长屏(21:9) | — | 0.4286 | 2520 | +600 | 1484 | 78.7% |
看最后两列就能得出结论:地面线的屏幕百分比在 68.9% ~ 78.7% 之间浮动, 但距屏底永远是 538。所以:
- ✅ 地面线、地形剖面、手账条:距底锚定,绝对尺寸(538 / 153 / 269)
- ✅ 天空带:吸收全部差额,高度区间 692 ~ 1484 px
- ⚠️ 天空带的内容必须能在 692 px 内成立(16:10 是最坏情况):云、远山、树线、灌木 从下往上排——树线贴世界条上沿,远山在其上,云在最上面(可被裁)。 云的铺法按密度而不是固定数量,否则窄天空里会挤成一堆。
刘海屏与胶囊按钮(这是"需要改"的具体所在):
| 障碍 | 实测位置(iPhone 15,CSS 393×852) | 换算到 1080 虚拟坐标 | 影响 |
|---|---|---|---|
| 状态栏 / 刘海 | safeArea.top ≈ 59 CSS px |
≈ 162 虚拟 px | HUD 原定 y 0~110,整条被盖住 |
| 底部指示条 | safeArea.bottom ≈ 34 CSS px |
≈ 93 虚拟 px | 手账条最下 93 px 不能放文字 |
| 右上角胶囊按钮 | wx.getMenuButtonBoundingClientRect(),约 top 50 / 高 32 CSS px |
y ≈ 137 |
HUD 右上的速度档正好压在胶囊上 |
- HUD 顶边距:
top = max(110, safeArea.top × 1080 / screenWidth + 24)→ iPhone 15 上从 0 变成 186; - 速度档从右上角挪到左上或 HUD 中部——右上角永远让给胶囊按钮,这是小游戏里唯一不能碰的死区;
- 手账条文字底边距增加 93 虚拟 px(
safeArea.bottom换算值),别让收集格名压在 home 指示条上。
safeArea 从基础库 2.7.0 才有,且"部分机型没有安全区域概念、不返回该字段",
代码里必须有默认值兜底(用 screenHeight − 84 之类的保守估计 + wx.getMenuButtonBoundingClientRect() 交叉校验)。
2.2 触摸操作(✅ 能)#
本项目只有三种手势,没有摇杆、没有虚拟按键、没有多点,是触摸适配里最省事的一档。
| 手势 | 用在哪 | 实现 | 判据 |
|---|---|---|---|
| 单击 | 岔路口选出口 | onTouchStart 记点 → onTouchEnd 结算 |
位移 < 10 CSS px 且时长 < 300 ms |
| 按住 | 小游戏蓄力(跳铃铛等 12 种) | onTouchStart 记 t0,onTouchMove 更新蓄力条,onTouchEnd 结算 |
蓄力 = clamp((t1−t0)/800ms, 0, 1) |
| 滑动 | 手账收集格横滑 | onTouchMove 的 dx 驱动内容偏移,松手做惯性 |
惯性每帧 ×0.94,< 30 px/s 停 |
实现要点四条:
- 坐标一律换算到 1080 虚拟系:
vx = clientX × 1080 / screenWidth。 触摸事件给的是 CSS px,不要拿它和虚拟坐标直接比,否则高 DPR 机型上点不准。 - 热区最小 88×88 虚拟 px(≈ 44×44 CSS px,微信/苹果的最小可点尺寸)。 岔路出口牌给 240×240 虚拟 px——它要能被拇指随便点中,因为这是玩家唯一的操作。
- 触摸按 30 Hz 读取(每 2 帧一次)就够。蓄力条不需要 60 Hz,省下来的 CPU 留给渲染。
- 不做"滑动返回"拦截。小游戏里从左边缘右滑不会触发系统返回(没有页面栈), 但 Android 物理返回键会直接退出小游戏——这不是要拦的东西,因为本项目 任何一秒退出都不丢东西(见五)。
2.3 音频(✅ 能)#
| 项 | 平台约束(官方口径) | 本作 |
|---|---|---|
| 格式 | mp3 / aac / wav iOS 与 Android 都支持;flac / amr / wma / mp4 只有 Android 支持 | 统一 mp3,96 kbps 单声道(不用 wav,不用 flac) |
| 并发 | Android 上最多同时播放 10 个音频,超出部分做有损处理,开发者不感知 | 音效实例池固定 8 个(留 2 个余量),BGM 独立 1 个 |
| 中断 | 来电 / 闹钟 / 切后台会中断播放 | 必须监听 wx.onAudioInterruptionBegin / onAudioInterruptionEnd + wx.onShow 重建实例 |
| 静音键 | iOS 静音键是否影响播放需真机核对(InnerAudioContext.obeyMuteSwitch) |
首屏给可见的声音开关,默认跟随系统;同时满足合规上"提供关闭声音的能力" |
体积账(按 96 kbps 单声道 = 12 KB/秒):
| 音频 | 时长 | 体积 | 放哪 |
|---|---|---|---|
| 音效 12 个(跳跃 / 落地 / 摘花 / 翻页 / 铃铛 / 结算 …) | 平均 1.0 秒 | 144 KB | 主包 |
| BGM 1 首环境循环 | 45 秒 | 540 KB | 主包 |
| 季节 BGM 2 首 | 各 45 秒 | 1 080 KB | 分包 C |
三件事一定会遇到,都要提前写:① 用户第一次进入时 iOS 不允许自动播(要等第一次触摸)——
本作的第一次触摸是岔路口选出口,所以 BGM 从第 30 秒才开始,这个延迟要接受,别做成"卡住不出声";
② 切后台再回来 BGM 静音且状态错乱 → onShow 时销毁重建而不是 play();
③ 音效不要每次 createInnerAudioContext()——池化 8 个实例反复 stop() + play(),
否则 Android 上很快超过 10 个并发上限。
2.4 帧动画与 WebP(需要改)#
三个必须分清的概念,混起来会做出错误决策:
| 概念 | WebP 有优势吗 | 数字 |
|---|---|---|
| 包体 / 下载量 | ✅ 优势巨大 | 5 张精灵表 42 帧 = 243 KB;同样内容 PNG 预估 0.5~0.8 MB(水彩半透明边缘 PNG 压缩很差) |
| 运行时纹理内存 | ❌ 完全没优势 | WebP 解码后是 RGBA8888,42 帧 × 128×128 × 4 B = 2.63 MiB,和 PNG 一模一样 |
| GPU 压缩纹理 | ❌ 不支持 | iOS 高性能模式只支持 ASTC / ETC1,WebP 不是 GPU 纹理格式 |
换 PNG 只会把包体从 243 KB 涨到 0.5~0.8 MB,一个字节的纹理内存都省不下来。 真正要做的是"格式双轨":
- 主轨:WebP 图集(243 KB)。iOS 14+ / Android 全部支持;
- 兜底轨:一张 PNG 版图集(预估 1.4 MB),放分包 A、按需下载, 只有自检失败时才去拉。不放进主包,避免给 99% 的用户多下 800 KB;
- 启动自检:用 1 个 4 KB 的 1×1 WebP 做解码探测,失败 → 切 PNG 轨。 自检成本 4 KB + 一次 1 帧异步解码,可以忽略。
WebP 还有两个本项目特有的坑:
- 水彩半透明边缘的"灰边":有损 WebP 的 RGB 与 alpha 是分开压的,透明区域的 RGB 值会被邻近像素污染, 合成后边缘出现一圈脏色。本项目整套风格都是半透明水彩边缘,这个问题一定看得见。 做法:导出前做颜色外扩(alpha bleeding / dilate)——把边缘像素的 RGB 往透明区扩 2~4 px 再压缩;
- 不要为了"顺手"把 5 组动作合进一张大图。理由见 4.5(兔子同一时刻只播一组动作,合图集基本零收益, 反而多占 14% 纹理)。
2.5 本地存储与云开发(✅ 能)#
| 项 | 上限 | 本作用量 | 余量 |
|---|---|---|---|
| storage 单 key | 1 MB | < 1 KB | 1000 倍 |
| storage 单账号总量 | 10 MB | < 1 KB | 1 万倍 |
用户文件目录(wx.env.USER_DATA_PATH) |
200 MB(可申请 1 GB) | 季节素材按需下载,约 2 MB | 100 倍 |
存档小是因为收集态用 bitmask:30 种植物 + 10 件遗物 = 40 格 = 5 字节, base64 之后 8 个字符。整个存档(收集位图 + 花园状态 + 版本号 + 时间戳)约 0.5 KB。
这个尺寸带来一个重要结论:每次收集都同步落盘是可行的。
一次 < 1 KB 的 setStorageSync 耗时 < 1 ms,一局最多 40 次收集,累积开销可以忽略。
不需要做"攒起来定时写",也就不会出现"闪退丢最后几格"。
单账号 storage 是设备维度的(同一微信账号换手机读不到),云开发是账号维度的。
本作没有"账号"概念(wx.login 静默拿 openid),所以云端存档天然跟着微信号走,
玩家不需要做任何"同步"动作——这正好和零操作一致。
2.6 分包加载(✅ 能,且不需要妥协)#
| 限制 | 官方数额 | 本作 | 占用 |
|---|---|---|---|
| 主包 | ≤ 4 MB | 1.79 MB | 45% |
| 主包 + 全部分包 | ≤ 30 MB | 4.13 MB | 14% |
| 单个独立分包 | ≤ 4 MB | 最大 1.34 MB | 34% |
早期任务书给的口径是总包 20 MB,那个数字是错的(属于旧口径)。 从 官方现行文档 抓到的原文是:
整个小游戏所有主包+分包大小不超过 30M;主包不超过 4M; 单个普通分包不限制大小;单个独立分包不超过 4M。
本文统一按 30 MB 编预算(此前文档里一处按 30 MB、一处按 20 MB,自相矛盾,已统一)。 顺带说:按哪个口径都不影响任何决策——我们总共用 4.51 MB, 占 30 MB 的 15%、占 20 MB 的 23%,余量都在五倍以上。
另外"单个普通分包不限制大小"这条比总包上限更有用: 它意味着季节内容(分包 C,1.34 MB)将来还能继续长,不必为了分摊而硬拆。
详细的分包方案见第三节。
2.7 广告接入点(✅ 能,难在克制)#
| 广告形式 | 接不接 | 为什么 |
|---|---|---|
激励视频(wx.createRewardedVideoAd) |
✅ 只接这一种,只在关卡结算页 | 结算页本来就是停顿点,插在这里不打断节奏;而且玩家是主动点的 |
插屏(InterstitialAd) |
❌ 不接 | 会打断"看风景"这个核心动作。这是最伤的一种 |
Banner(BannerAd) |
❌ 不接 | 竖屏底部就是手账条(拇指自然落点),Banner 会永久压住画面 1/9 |
本作唯一成立的激励视频位(详细论证见 循环与商业化 3.2):
| 位置 | 玩家换到什么 | 频次上限 |
|---|---|---|
| 关卡结算界面 | "这一关有 3 种花没摘到,重跑这一段" | 每关 1 次(一局 3 |
技术要点:激励视频实例必须复用单例(createRewardedVideoAd 返回单例),
onClose 里用 res.isEnded 判断是否完整观看;
show() 失败要能静默退回(不弹错误、不阻塞流程),这是零失败原则在广告上的延伸。
广告加载失败时不提示、不降级成别的广告,直接当"这个按钮今天不在"。
"不看这个广告,我会不会觉得亏?" 会 → 这是强迫,迟早把玩家赶走。 不会 → 可以加。「重跑这一段」属于后者(不看就是不重跑,本来也没这功能); 「双倍收获」属于前者。这条标准比"能不能接"更重要。
2.8 分享与好友(需要改)#
本项目不做排行榜、不做数值攀比,社交只做两件 (这条不只是设计偏好,也是合规上的低风险选择,见七):
| 玩法 | 怎么做 | 技术落点 |
|---|---|---|
| 分享一朵花 | 手账里每种花生成一张卡片(一张花 + 一句观察,如"苔藓长在北面") | wx.shareAppMessage({ imageUrl, title }),分享图不要带"点击领取"这类诱导文案 |
| 花园互访 | 好友点分享卡进来看"他的花园里有什么花、住了哪只小动物" | ⚠️ 不能依赖关系链 API,改走带参分享卡片 |
| 赠一株花 | 送好友一株(自己手账里那株不会消失) | 云端写一条 gift 记录,好友下次进来自动收到,不要求回赠 |
wx.getFriendCloudStorage / getGroupCloudStorage 这类关系链开放数据域能力,
对个人主体和部分类目已经不可用。如果"花园互访"设计成"打开好友列表挨个看",
这个功能做不出来。
可行做法:把"互访"降级为"看到朋友分享的花园"—— 分享卡片带一个花园快照 token,好友点进来渲染的就是分享那一刻的花园状态。 单向、无需授权、无需关系链,而且恰好更符合本作"不攀比"的气质: 你看到的是一张明信片,不是一个排行榜条目。
三、包体预算表#
3.1 素材清单与实测体积#
| 素材 | 数量 | 单件规格 | 磁盘体积 | 数据来源 |
|---|---|---|---|---|
| 兔子序列帧 | 5 组 / 42 帧 | 128×128 / 帧 | 243 KB | 实测(stand 8 / hop 10 / leap 12 / land 6 / look 6) |
| 场景物件 | 17 个 | 54×38 ~ 150×210 | 111 KB | 实测(草 3 / 花 5 / 树 4 / 石 3 / 灌木 2) |
| 植物 | 30 种 | 128×128 | 179 KB | 实测(进分包,不进主包) |
| 自包含演示页 | 1 | 内联全部素材 | 498 KB | 实测(仅开发期用,不进包) |
原来的 604 KB / 671 KB 是旧色板时期的实测值。这一轮按新参考图重做了整套色板 (见 美术规范 第二节),全部素材重新生成,体积跟着变了:
| 素材 | 旧值 | 现值 | 变化 |
|---|---|---|---|
| 兔子序列帧 5 张表 | 604 KB | 243 KB | −60% |
| 场景物件 17 个 | 145 KB | 111 KB | −23% |
| 自包含演示页 | 671 KB | 498 KB | −26% |
变小是合理的:新色板的暗部有颜色(色度 12~22), 而旧色板的暗部是一片中性灰、相邻像素差更小 —— 相邻像素差越小,WebP 越压不动。 换句话说,"颜色更准"顺带把包体压小了 60%。
结论没变(包体依然远不是约束,主包反而从 54% 降到 45%), 但表里的数必须是现在的数 —— 拿旧实测值去规划预算,是"文档和实物对不上"的典型。
3.2 主包预算(首包)#
主包 = 首次启动必须有、且不能等 的东西。
| # | 模块 | 内容 | 大小 | 依据 |
|---|---|---|---|---|
| 1 | 逻辑代码 | 原生 Canvas 2D 自研运行时(不引第三方引擎)+ 自动跑跳 + 岔路解谜 + 手账 + 花园 | 250 KB | 演示页同构逻辑的上限估计 |
| 2 | 兔子序列帧 | 5 组 42 帧,128×128,WebP | 243 KB | 实测(色板重做后重算) |
| 3 | 常驻场景物件 | 17 个 WebP | 111 KB | 实测(色板重做后重算) |
| 4 | UI 图集 | 手账条 / 线索卡 / HUD / 结算 / 按钮,合 1 张 1024×1024 WebP | 320 KB | 按 UI 面积估算 |
| 5 | 音效 | 12 个 × 12 KB(mp3 96 kbps 单声道,平均 1.0 秒) | 144 KB | 12 KB/秒 |
| 6 | BGM | 1 首环境循环 45 秒 | 540 KB | 12 KB/秒 |
| 7 | 配表与文本 | levels.json + 30 种植物 / 10 件遗物的中文文案 |
40 KB | 约 12 万字节 UTF-8 的一半 |
| 8 | 启动图 / 图标 / 分享图 | 3 张 | 180 KB | 按分辨率估算 |
| 主包合计 | 1 828 KB ≈ 1.79 MB | 占 4 MB 上限的 45% |
不放进主包的东西:30 种花的素材、10 件遗物、12 种小游戏、季节内容。 理由:第一次启动时玩家只看得到 5 种花和 1 只兔子,其余全部可以边跑边下。
冷启动下载时间估算(主包必须启动前下完):
| 网络 | 下行速度 | 1.79 MB 耗时 |
|---|---|---|
| 4G 良好 | 2 MB/s | ≈ 1.1 秒 |
| 4G 一般 | 1 MB/s | ≈ 2.2 秒 |
| 弱网 / 地铁 | 0.5 MB/s | ≈ 4.4 秒 |
对比"把主包塞满 4 MB"的情况,弱网下省掉约 4.4 秒。这是"不引引擎"带来的直接收益。
3.3 分包方案#
| 分包 | 装什么 | 什么时候加载 | 大小 |
|---|---|---|---|
| 主包 | 见 3.2 | 启动前 | 1 828 KB |
| 分包 A · 花园与图鉴 | 30 种植物世界图(60×90,≈150 KB)+ 30 张手账图标 96×96(≈90 KB)+ 10 件遗物(≈50 KB)+ 花园背景 1080×800(≈120 KB)+ 1 套花园装饰(≈150 KB) | 第一关结算后(进花园前)后台预下载 | 560 KB |
| 分包 B · 小游戏 12 种 | 12 种小游戏的专属素材(铃铛 / 水车 / 风车 …,12 × 30 KB = 360 KB)+ 12 段逻辑代码(≈140 KB) | 跑第一关时后台预下载(第一关的小游戏是基础版,已在主包) | 500 KB |
| 分包 C · 季节内容 | 换季场景图集(2 个季节 × 400 KB = 800 KB)+ 季节 BGM 2 首(1 080 KB) | 首次换季时(第 8 天以后) | 1 340 KB |
| PNG 兜底图集(条件分包) | 序列帧 + 物件的 PNG 版 | 仅 WebP 自检失败时 | ≈1 400 KB |
| 分包合计(常用) | 2 400 KB ≈ 2.34 MB |
总计(主包 + 常用分包)= 4 623 KB ≈ 4.51 MB,占 30 MB 上限的 15%。 加上 PNG 兜底轨也只有 5.88 MB。
12 种小游戏每种只玩 10~20 秒、每关出现 0~1 次—— 它们是"插曲",不是"必经之路"。全塞进主包会让每个玩家为了可能不做的小游戏 多下 500 KB。分包之后只有真的玩到才加载,而加载发生在"兔子在跑"的 60~90 秒里, 玩家完全感知不到。
不再需要 CDN 的理由:季节内容已经进了分包 C。剩下的边角(未来的第 3 季、限定外观)
再考虑走 wx.downloadFile 落到 wx.env.USER_DATA_PATH(200 MB 空间),
那是上线半年后的事,现在不需要为它设计。
3.4 纹理内存估算#
这是包体之外的另一本账:磁盘体积 ≠ 内存占用。图片进内存后统一是 RGBA8888(4 字节/像素)。
| 项 | 像素数 | 纹理内存(RGBA8888) |
|---|---|---|
| 兔子序列帧 5 张精灵表(1024×128 / 1280×128 / 1536×128 / 768×128 / 768×128) | 688 128 px | 2.63 MiB |
| 常驻场景物件 17 张 | 159 012 px | 0.61 MiB |
| UI 图集 1024×1024 | 1 048 576 px | 4.00 MiB |
| 常驻纹理小计 | 7.24 MiB | |
| 画布后备缓冲(1080×1920 × 4 B,60 fps 满屏重绘) | 2 073 600 px | 7.91 MiB |
| 内存水位合计 | ≈ 15.2 MiB |
对照官方口径:iOS 高性能模式下,2 GB RAM 机型(6s / 7 / 8)内存限制 1 GB,3 GB 以上机型 1.4 GB。
本作水位 ≈ 15.2 MiB / 1000 MiB = 1.5%
15.2 MiB 是低端机上限的 1.5%。序列帧 42 帧全驻留只要 2.63 MiB,这个交换太划算了 (用 2.63 MiB 换掉每帧的贴图上传),不要为了省内存去做异步流式加载序列帧。
唯一的红线是画布尺寸:UI 图集开 1024×1024 = 4 MiB,开 2048×2048 = 16 MiB, 开 4096×4096 = 64 MiB。永远不要创建超过 2048×2048 的 RGBA 画布—— 一张 4096² 就能吃掉低端机预算的 6%。"图集开大一点更整齐"是本项目最可能踩的内存坑。
四、性能优化清单(按收益排序)#
4.0 先把成本算出来(不然排序是瞎排)#
竖屏横版是满屏重绘:每一帧整个屏幕都要重画。按 1080×1920 的虚拟坐标算每帧填充量:
| 区域 | 面积(虚拟 px) | 层数 | 每帧填充 |
|---|---|---|---|
| 天空 + 远景(云 / 远山 / 树线 / 灌木) | 1080 × 884 | ×4 | 3 818 880 |
| 世界条(地形 + 物件 + 兔子) | 1080 × 614 | ×1 | 663 120 |
| 地形剖面(土壤) | 1080 × 153 | ×1 | 165 240 |
| 手账条 | 1080 × 269 | ×1 | 290 520 |
| 合计 | 4 937 760 ≈ 4.94 M px/帧 |
60 fps → 296 M px/s。而且水彩画风是大面积半透明混合(alpha blending), GPU 实际成本还要再乘一个系数。这就是本作唯一真实的性能风险所在—— 不是对象多,不是帧动画,是"一个屏幕上叠了太多层"。
同一项目此前在无 GPU 的测试机上量出的成本模型是
耗时(ms) ≈ 0.0221 × 实例数 + 14.8 × 百万像素——像素项的权重远大于实例项。
(那台机器是软件光栅化,绝对值不能直接用,但"往像素上使劲"这个方向是对的。)
所以下面按减少填充量排序,而不是按"看起来优雅"排序。
① 背景视差从 4 层降到 2 层,预烘焙成可平铺位图(收益最大)#
做法:把「云 + 远山」预渲染成一张 2160×884 的离屏位图(2160 = 两屏宽,保证无缝平移),
「树线 + 灌木」再一张。每帧只做 drawImage 平移 blit,不再每帧走矢量路径画云和山。
收益:
| 方案 | 天空带填充 | 全屏填充 | 相对现状 |
|---|---|---|---|
| 现状(4 层逐层绘制) | 3 818 880 | 4.94 M px/帧(296 M px/s) | — |
| 合成 2 层(保视差) | 1 909 440 | 3.03 M px/帧(182 M px/s) | −39% |
| 合成 1 层(弃视差) | 954 720 | 2.07 M px/帧(124 M px/s) | −58% |
推荐 2 层:保住视差("风景即线索"需要纵深线索),拿到 −39%; 低端机档位再降到 1 层拿满 −58%。
额外收益:矢量绘制 → 位图 blit,省掉每帧的路径计算与渐变重算。 如果现在的云和远山是代码画的(本项目灰蓝水彩天空极可能是渐变 + 路径), 这一项的收益可能比 39% 还大,因为它省的不只是填充,还有 CPU 侧的绘制命令构建。
注意:预烘焙位图要按虚拟高度分段(884 / 1304 / 1484 三档),
为每档出一张,而不是用一张拉伸——拉伸会把水彩的纸纹和线条拉变形,破坏 美术规范 的验收标准。
② 低端机三档降级 + 自适应(性价比第二)#
三个旋钮的收益都是乘性的,叠加起来非常猛:
| 旋钮 | 动作 | 收益 |
|---|---|---|
| 渲染分辨率 | DPR 2 → 1(1080×1920 → 540×960) | 填充率 −75%(像素数 ∝ DPR²) |
| 视差层数 | 4 层 → 2 层 → 1 层 | −39% / −58% |
| 帧率 | 60 → 30 | 每帧预算不变,但每秒总量 −50% |
| 序列帧烘焙尺寸 | 128×128 → 96×96 → 64×64 | 纹理内存 −44% / −75%,WebP −40% / −75% |
三项叠加(DPR 1 + 1 层 + 30 fps)= 高端的 1/16。
分档规则:
| 档 | 触发条件 | DPR | 帧率 | 视差层 | 序列帧 | 粒子 |
|---|---|---|---|---|---|---|
| 高 | iOS A12+ ;或 benchmarkLevel ≥ 20 |
2 | 60 | 4 | 128×128 全帧率 | 全开 |
| 中 | 10 ≤ benchmarkLevel < 20 |
1.5 | 60 | 3 | 128×128 | 减半 |
| 低 | benchmarkLevel < 10;或首 60 帧滑动均值 > 22 ms |
1 | 30 | 1 | 64×64 重烘焙 | 关闭 |
检测:wx.getSystemInfoSync().benchmarkLevel 只在 Android 有效
(取值 -2 / 0 = 该设备无法运行小游戏,-1 = 未知,≥1 越大越好)。
iOS 上恒为 -1,所以 iOS 必须靠首 60 帧的帧时间滑动平均自适应:
若 滑动均值 > 22 ms → 降一档(有 10 秒冷却,避免抖动)
只降不升(升档会来回横跳,体验比一直低画质更差)
顺带一提:低端机降 DPR 到 1 时,画布变成 540×960, 这时候 64×64 的序列帧和 128×128 在屏幕上是一样清楚的—— 所以"降 DPR"和"降烘焙尺寸"要同时降,这是本作最省的一刀(一次改动,填充率和纹理内存双降)。
③ 对象池:横向滚动不要每帧 new(收益在 p99,不在均值)#
要池化的东西:世界条里循环滚动的所有物件。
池容量怎么算(不要拍脑袋):
最密情况:世界条宽 1080 虚拟 px ÷ 最小物件间距 60 px = 18 个/层
地形有 6 个高度层(−2 ~ 3),但同屏不重复铺满 → 有效层数取 4
物件池 = 18 × 4 = 72 → 取 80
粒子池(摘花飞出、落地尘土) = 32
兔子 + 阴影 = 2
────────────────────────────
池总容量 = 114 个实例,启动时一次分配,之后永不 new / 永不 delete
回收策略(关键):物件移出屏幕左侧时不是销毁,而是"重新绑定":
if (obj.x < -80) { obj.x += 世界条总长; 重新绑定物种/尺寸/随机相位; }
收益:GC 触发次数从"每帧若干次"降到 0 次。 这项的收益不体现在平均帧时间上,而在 p99 抖动: 16.7 ms 的预算下,一次 5 ms 的 GC 就是一次可见掉帧。 验收方式不是看平均 fps,而是看帧时间 p99 ≤ 20 ms。
顺带:粒子、飘字、收集格高亮这些反馈全部用代码画,零素材 (这是 玩法 已经写死的约定),所以它们的池化成本只是几个浮点数组。
④ 地形高度图的渲染优化#
数据结构:地形高度层 −2~3 共 6 层,一级 22 px,总落差 66 − (−44) = 110 px。
跳跃水平距离是 127 px(小跳)/ 173 px(大跳),所以高度采样间距取 8 px 足够——
一屏世界条宽 1080 → 135 个采样点。
三条优化:
- 等高段合并(RLE):渲染前把高度数组压成"连续等高的段", 典型一屏只有 5~15 段,地形绘制调用从 135 次降到 ≤15 次(−89%)。
- 草皮顶边预渲染成 6 张切片位图(每层一张,宽 128 可横向平铺), 拼接绘制。不再每帧重画草叶的锯齿边——这些细节每帧都在变的话,肉眼看是"抖"的。
- 土壤剖面(153 px 高)用一张可平铺的纹理位图,每帧只 blit 一次。
收益:地形绘制耗时预计 −60~75%,且顺带把画面稳定性提上来。
渲染必须从物理读的那一份高度数组派生,不许各算一遍。 渲染侧和物理侧各维护一份高度图,迟早会出现"看起来踩在地面上、其实悬空 4 px", 而这种 bug 在自动跳跃的游戏里极其难查(因为跳跃是全自动的,玩家不会触发异常操作)。 一份数据,两个消费者。小跳只跳 40 px、大跳过 72 px 的石头只剩 3 px 余量—— 4 px 的渲染偏差就足以让"看起来跳过了"变成"其实撞上了"。
⑤ 帧动画图集合并(收益小,但顺手就做)#
这一条要如实说,不能为了显得"优化了"就把它排到前面:
| 事实 | 数字 |
|---|---|
| 兔子同一时刻只播一组动作 | 纹理切换只发生在动作切换的那一帧,不是每帧 |
| 5 组动作合成一张 1536×640 的图集 | 纹理 2.63 MiB → 3.00 MiB(+14%),因为有 294 912 px 的空白 |
| 省下的 | 每帧最多 4 次纹理绑定,约 20~80 μs,占 16.7 ms 的 0.5% 以下 |
结论:兔子不要合图集,合了反而多占 14% 纹理内存。
真正值得合的是常驻场景物件的 17 张——它们是同屏同时出现的:
| 分开 | 合 1 张 512×512 | |
|---|---|---|
| 纹理内存 | 0.61 MiB | 1.00 MiB(+64%) |
| 每帧纹理绑定 | 17 次 | 1 次 |
| 省下 | — | 16 次绑定,约 80 μs/帧 |
该不该做?做。 理由是它不增加包体(合并后 WebP 通常更小,因为共享调色板),
纹理 +0.39 MiB 在 15.2 MiB 的水位里毫无感知,而 bindTexture 的次数从 17 降到 1
对低端机的驱动开销是有意义的(移动 GPU 的纹理切换比桌面贵得多)。
但不要为了这一项把图集开到 2048×2048(16 MiB)——用 512×512 紧排, 浪费 19% 空间换取一整档内存,比"图集整齐"重要得多。
⑥ 首帧与冷启动#
| 阶段 | 目标 | 怎么做 |
|---|---|---|
| 冷启动到首帧 | ≤ 100 ms | 首帧只画静态层:预烘焙的天空位图 + 手账条 + 地形,全部是位图 blit,同步可完成 |
| 兔子出场 | ≤ 1.5 s(中端机) | 序列帧图集异步解码完成后再放兔子。宁可先给一个空风景,也不要给一只白兔子 |
| 进游戏(可交互) | ≤ 2.5 s | 主包 1.79 MB + 首帧渲染 |
本作开局就是零操作——玩家在头 3 秒做的事就是"看"。 如果这 3 秒看到的是资源没加载完的空白,他对这个游戏的第一印象就是"卡"。 而如果先给一幅完整的静态风景(天空 + 远山 + 草地 + 手账条), 兔子晚 1 秒从左边跑进来,反而像"它从远处跑过来了",是加分不是减分。
⑦ 音频池(顺带)#
固定 8 个 InnerAudioContext 实例复用(stop() + play(),不重建),
BGM 独立 1 个。原因见 2.3:Android 同时播放上限是 10 个,
每次新建实例的写法在连续收集时会迅速撞上限,表现是"音效时有时无",非常难查。
优化优先级排序(一页版)#
| 序 | 做什么 | 预计收益 | 工作量 |
|---|---|---|---|
| 1 | 背景视差 4 层 → 2 层预烘焙位图 | 填充 −39%(≈ 296 → 182 M px/s) | 小 |
| 2 | 低端机三档降级(DPR / 层数 / 帧率 / 烘焙尺寸) | 低端机填充最高 −94% | 中 |
| 3 | 世界条对象池(80 物件 + 32 粒子 + 2 角色) | GC p99 抖动归零(均值不变) | 小 |
| 4 | 地形 RLE 合并 + 顶边切片位图 | 地形绘制 −60~75% | 中 |
| 5 | 常驻物件 17 张 → 1 张 512×512 图集 | 纹理绑定 17 → 1(≈80 μs/帧) | 小 |
| — | 不做(+14% 纹理,收益 < 0.5%) | — |
五、操作与交互适配#
5.1 三种手势的落地#
本作没有摇杆、没有虚拟按键、没有多点,三种手势各管一件事,互不重叠:
| 手势 | 唯一用途 | 实现细节 |
|---|---|---|
| 单击 | 岔路口选出口(每关 2~3 次) | 出口牌热区 240×240 虚拟 px;判据:位移 < 10 CSS px 且 < 300 ms |
| 按住 | 小游戏蓄力起跳(每关 0~1 次,12 种小游戏共用这一种操作) | 蓄力值 = clamp((t1−t0)/800ms, 0, 1);松手必然命中(零失败) |
| 滑动 | 手账收集格横滑(看已经收了多少) | onTouchMove 的 dx 驱动偏移,松手惯性每帧 ×0.94 |
三条实现约束:
- 所有触摸坐标换算到 1080 虚拟系(
vx = clientX × 1080 / screenWidth); - 最小热区 88×88 虚拟 px,出口牌 240×240——因为这是玩家唯一的手部操作, 点不中会让整个游戏变成"我在跟 UI 较劲";
- 触摸按 30 Hz 读取,不需要 60 Hz。
5.2 "随时能停"怎么从特性变成优势#
微信的核心场景是**"用完即走":秒开、随时放下、下次再来。 大多数游戏和这个场景是对抗**的(体力、冷却、连续登录、限时活动,都是在阻止你走), 本作是唯一不需要对抗的——它把"停下来"的成本做到了 0:
| 本作的设计 | 直接消灭的成本 |
|---|---|
| 零操作(跑跳全自动) | 任何一秒放下都不会"输"——没有正在进行的操作被打断 |
| 岔路口主动停下且不计时(每 30~45 秒一次) | 天然暂停点。玩家不用找"安全的地方"停下来 |
| 零失败(选错只是折返,还给一样东西) | 回来后没有要补救的东西——没有心理债 |
| 无体力 / 无门票 / 无冷却 | 不会因为"今天玩过了"被拒之门外 |
| 不做每日签到 | 不会因为"断签了"而不敢回来 |
这四条合起来的效果:打开这个游戏的心理成本 ≈ 0,所以启动次数会高。 而微信生态里,"一天打开 3 次 × 4 分钟"比"一周打开 1 次 × 12 分钟"健康得多。
技术上必须落成机制,否则以上全是宣传语:
| 时机 | 做什么 | 硬指标 |
|---|---|---|
wx.onHide(切后台 / 下拉 / 锁屏) |
立即同步落盘:兔子世界坐标 x、当前动作与帧号、当前段落种子、手账增量、花园时间戳 | 存档 < 1 KB,同步写入 < 1 ms |
wx.onShow(回来) |
什么都不弹,直接从存档续跑。玩家看到的是"兔子还在那儿" | 禁止"欢迎回来"弹窗、禁止"点击继续"——那都是一次操作 |
wx.onAudioInterruptionEnd |
销毁重建 BGM 实例(不是 play()) |
BGM 恢复率 100% |
| 关卡内进度 | 离线不推进(回来还是原来那一步) | 避免"离线跑完半关"这种失控感 |
| 花园进度 | 按绝对时间戳推进(见六) | 与是否在线无关 |
不要在 HUD 上加"暂停"按钮——按钮 = 一次操作,而且它会暗示"你需要暂停"。 让兔子在岔路口自己停下来看牌子这个动作承担暂停语义就够了: 画面还在动、BGM 还在放、兔子有待机小动作(look 6 帧),但它在等你。 这是本作最优雅的一处:"停下来"看起来是剧情,不是功能。
5.3 上线后指标要按"碎片化"来定#
| 指标 | 目标 | 为什么是这个数 |
|---|---|---|
| 平均单次时长 | 3~5 分钟 | 一局 4~6 分钟且可中途停。不追求 10 分钟——追求长时长会诱导我们去做"让人停不下来"的设计,那是反向的 |
| 日启动次数 | ≥ 2.5 次 | "随时能停"的真实度量。启动次数上不去,说明"打开成本"还是太高 |
| 首关完成率 | ≥ 85% | 零操作应该接近 100%。低于 85% 说明第一次进入有卡点(大概率是加载或 HUD 遮挡) |
| 岔路口停留时长 | 中位数 8~20 秒 | 太短(< 5 秒)说明线索太直白、解谜是假的;太长(> 40 秒)说明线索看不懂 |
六、登录与存档#
6.1 登录:本作没有"登录"这个动作#
| 项 | 做法 |
|---|---|
| 身份 | wx.login() 静默拿 code → 云函数换 openid。没有授权弹窗(小游戏的 login 本来就是静默的) |
| 昵称 / 头像 | 不要。getUserInfo 早已不返回真实昵称头像,而本作也不需要——兔子才是主角 |
| 游客模式 | 首启动没有账号概念。openid 静默拿,本地和云端是同一份档案;没网就纯本地 |
| 玩家感知 | 全程零登录动作。这是"零操作"在账号层的直接体现 |
主流小游戏的第一步是"授权头像昵称",那一次点击就是一次流失。 本作不需要昵称的原因是世界观里根本没有"玩家"这个角色—— 你是一只兔子的旁观者,旁观者不需要报名。
6.2 存档结构(约 0.5 KB)#
| 字段 | 类型 | 大小 | 说明 |
|---|---|---|---|
collectMask |
uint64 → base64 | 8 B → 12 字符 | 40 格(30 植物 + 10 遗物)bitmask |
gardenStage |
uint8 | 1 B | 花园成长阶段(0~5,对应离线时段) |
lastVisitTs |
uint32 | 4 B | 上次访问的绝对时间戳(秒) |
decoId / petId |
uint8 × 2 | 2 B | 花园装饰 / 小动物 |
seed |
uint32 | 4 B | 本季地形与谜题种子 |
ver |
uint8 | 1 B | 存档版本号(必须留,改结构时靠它迁移) |
| ≈ 30 B | 序列化后连同 JSON 外壳 < 500 B |
6.3 三级存档与同步策略#
| 级 | 存储 | 时机 | 失败时 |
|---|---|---|---|
| 1 | 内存 | 每一帧 | — |
| 2 | 本地 storage | 每次收集立即同步写(< 1 ms) | 不可能失败(写失败也继续玩) |
| 3 | 云开发数据库 | 每次关卡结算异步写(一局 3~4 次) | 静默重试,不提示、不阻塞 |
冲突合并:取并集。
collectMask = localMask | cloudMask,gardenStage = max(local, cloud),
lastVisitTs = max(local, cloud)。
因为本作的数据没有任何一项会减少(收集只增不减,花园只长不退), 所以两边存档合并永远不会丢东西,不需要时间戳覆盖、不需要"哪边更新"的判断、 更不需要弹"存档冲突"对话框。
对比之下,一个数值有涨有跌的游戏必须处理冲突覆盖,而任何覆盖策略都可能让玩家丢东西。 本作靠"只增不减"这条设计铁律,直接消掉了整个冲突处理模块。
6.4 "7 天不登录花园不会枯萎"怎么落地#
做法:绝对时间戳 + 分段函数,不模拟。
elapsed = now − lastVisitTs
stage = f( min(elapsed, 7 天) ) ← 封顶在 7 天
| 离线时长 | 花园发生什么 | 玩家看到 |
|---|---|---|
| 2 小时 | 已收进手账的植物从灰线轮廓变成上色 | 图鉴里灰的变彩的 |
| 8 小时 | 长出一格 "?"新芽 | 一个新格子,点开才知道是什么 |
| 1 天 | 来一只小动物住下(兔子 / 鸟 / 刺猬) | 花园里多一个会动的东西 |
| 3 天 | 换一个景(石头长苔、溪水改道) | 看得出"它自己过了几天" |
| 7 天 | 封顶,之后不再变化 | — |
为什么封顶在 7 天,两条理由都成立:
- 不封顶 → 离线 30 天回来一口气跳过全部内容,内容被浪费,玩家也没看到过程;
- 不封顶 → 反过来就得做"退化"(太久不来就枯萎),那就直接违反零失败铁律。
封顶函数是单调不减的:f(7天) = f(30天) = f(365天)。
离线一年回来,花园停在第 5 阶段,不会更差,也不会更满。
三条实现细节:
- 不需要"领取"。离线长出来的东西直接就在那里,玩家打开就能看见。 一旦需要点一下领,它就从"发现"变成"打卡"——那就变回每日签到了;
- 离线不推进关卡。花园按时间走,关卡进度冻结(回来还是原来那一步);
- 不做每日签到(设计铁律,见 循环与商业化 第五节)。 花园自己长已经提供了"回来"的理由,而且不需要玩家做任何事。
"花园离线成长"和"每日签到"只差一步:要不要玩家点一下。 判据写死在这里:如果实现里出现"收取""领取""确认"这三个词中的任何一个,就是做错了。
七、发布与合规清单#
7.1 主体与类目#
| 项 | 建议 | 理由 |
|---|---|---|
| 主体 | 个人主体先上(无内购版本),企业主体留给后续 | 个人主体可上线无虚拟支付的小游戏;开通虚拟支付必须企业主体 + 版号 |
| 类目 | 休闲 / 益智解谜 | 避开"角色扮演 / 卡牌"——那些类目会额外触及抽卡概率公示、虚拟货币等要求,而本作本来就没有这些东西 |
| 分级说明 | 全年龄、无暴力无恐怖 | 灰蓝水彩、无敌人无战斗(玩法 第七节已明确"敌人/战斗/BOSS"不许加) |
7.2 资质与门槛清单#
| 事项 | 要求 | 本作状态 | 周期 |
|---|---|---|---|
| ICP 备案 | 小程序 / 小游戏上线前必须完成工信部备案 | 需要,提前 2~4 周启动 | 2~4 周 |
| 版号 | 内购(虚拟支付)必须;纯免费 + 广告变现按官方流量主开通条件执行 | P1~P3 不做内购 → 不阻塞上线 | 数月(要内购才启动) |
| 流量主(广告) | 三条满足其一即可:累计独立访客 UV > 500 / 开通游戏圈并发官方帖 5 篇 / 同主体已有小程序变现满 1 季度 | 上线后 1~2 周可达 500 UV | 1~2 周(资格次日刷新) |
| 实名 + 防沉迷 | 游戏类必须接入;未成年人仅可在周五、周六、周日及法定节假日 20:00–21:00 游玩 | 必须接,没有内购也要接 | 接入 1 周 |
| 隐私协议 | 声明收集的信息。本作只取 openid | 一份短协议,上线前必须挂 | 1 天 |
- "没有内购就不用版号"≠"什么都不用":ICP 备案是硬门槛,没备案不能上架, 而备案要 2~4 周。这件事要第一周就去办,不能等游戏做完。
- "广告变现是否要版号"要按官方口径确认:公开的流量主开通条件列的是 UV / 游戏圈 / 同主体三条, 没有把版号列为前置。但这个口径会变,上线前必须以后台实际提示为准, 不要拿本文当法务依据。
7.3 内容合规注意点#
| 项 | 本作的情况 | 风险 |
|---|---|---|
| 内购 / 抽卡 / 赌博 | 全无。无抽卡、无概率获得、无限时折扣 | 极低 |
| 社交攀比 | 不做排行榜、不做分数榜、不做战力对比 | 极低 |
| UGC | 无玩家文本输入 | 极低(无内容审核压力) |
| 分享文案 | 只放"我发现了一种花"这类观察,不放"点击领取""集齐 X 个领奖励" | 低,但要卡死——诱导分享是审核高发区 |
| 广告 | 只做激励视频、只在结算页;拒绝插屏与 Banner | 低 |
| 未成年人 | 零失败、无内购、无社交、随时能停 | 极低,且"随时能停"应写进审核说明作为加分项 |
"随时能停"是可以写进审核材料的。 一个明确不做体力、不做签到、不做排行榜、不诱导连续在线的游戏, 在"未成年人保护"这一维度上是主动合规的,不需要靠补丁去救。
7.4 分享技术的合规边界(和 2.8 呼应)#
| 能做 | 不能做 |
|---|---|
onShareAppMessage 自定义分享图与标题 |
分享图里放"点击领取""拉好友得奖励" |
| 分享卡片带花园快照 token,好友点进来看到一张"明信片" | 读取好友列表 / 关系链(个人主体已受限,且涉及隐私) |
| 单向赠花(自己那份不消失) | 互赠 / 互帮 / 组队(会产生"欠人情"的社交压力,且触碰更多合规面) |
八、上线节奏建议#
8.1 五个阶段#
| 阶段 | 版本 | 做什么 | 周期 | 准入门槛 |
|---|---|---|---|---|
| P0 | 内部体验版 | 已有 30 项自动化验收(无头 Chrome CDP)全部保留;另加 6 台真机验证 | 1 周 | 真机 60 fps 达标率 ≥ 80%;零崩溃;帧时间 p99 ≤ 20 ms |
| P1 | 纯体验版(无广告、无内购) | 上线,不推广。只为拿真实数据 | 2~4 周 | 次留 ≥ 30%、7 日留存 ≥ 12%;平均单次 3~5 分钟 |
| P2 | 加第 ④ 层循环 | 花园离线成长(6.4 那张表全部实现) | 3~4 周 | 7 日留存 ≥ 15%;日启动 ≥ 2.5 次 |
| P3 | 加变现 | 达 500 UV 后开流量主;结算页上激励视频;试 2 套外观 | 4 周 | 激励视频渗透率 15~30%;留存不低于 P2 |
| P4 | 增长 | 分享卡片 + 花园"明信片"互访 | 持续 | 分享率 ≥ 8%(分享 UV / DAU) |
8.2 什么叫"成功了"(每个数字都要能埋点算出来)#
| 指标 | 目标值 | 不达标说明什么 |
|---|---|---|
| 次日留存 | ≥ 35% | < 25% → 第一次进入的体验有问题(加载、HUD 遮挡、第一关不够好看) |
| 7 日留存 | ≥ 15% | < 10% → 第 ④ 层循环没做对。此时绝对不要加商业化,加了只是把一个留不住的游戏更快地消耗掉 |
| 平均单次时长 | 3~5 分钟 | > 10 分钟说明玩家在"硬玩",要重新审视节奏;< 2 分钟说明没看到值得看的东西 |
| 日启动次数 | ≥ 2.5 次 | < 1.5 次说明"随时能停"没变成"随时能打开" |
| 首关完成率 | ≥ 85% | 零操作的游戏低于这个数一定是技术问题,不是玩法问题 |
| 分享率 | ≥ 8% | 分享是这套设计里唯一的自然增长手段(没有排行榜可炫耀)。< 5% 说明分享卡片做得不像"一封明信片" |
| 收集进度 | 第 3 天中位数 ≥ 12/40 格 | 太快(3 天就 30 格)说明内容太薄;太慢说明流程太长 |
收齐后的次日留存(单独埋点 collection_complete) |
≥ 25% | 这是全项目最该盯的一个数。它直接度量"第 ④ 层循环到底有没有用" |
8.3 30 项自动化验收怎么搬过来#
现有的 30 项验收跑在无头 Chrome CDP 上,小游戏里没有等价的成熟工具链。不要赌,拆成两层:
| 层 | 验收什么 | 怎么跑 | 数量 |
|---|---|---|---|
| 纯逻辑层(保留无头) | 自动跳跃的三档判定、起跳点、缺口贴边、岔路判对判错、折返、收集、存档序列化 / 并集合并 | 把物理与规则抽成不依赖渲染的纯函数模块,在 Node + 无头 Chrome 里跑现有 30 项 | 30 项原样保留,加约 10 项新增(存档合并、bitmask) |
| 渲染与输入层(真机) | 首帧时间、帧率、帧时间 p99、内存峰值、触摸坐标换算、HUD 避让、音频中断恢复 | 微信开发者工具 + 6 台真机,逐台跑截图对比 + 帧时间断言 | 6 台 × 5 项 = 30 项 |
本作最容易出错的逻辑全是像素级的绝对量(小跳 40 px / 大跳 75 px / 台阶 22 px / 余量 3~23 px), 这些不需要 GPU 就能验,而且必须在 CI 里每次提交都验。 渲染层的验收需要真机、无法高频跑,所以只验"是不是真的画出来了、有没有掉帧"。 把两者的频次分开,比强行统一到一套工具上更可靠。
九、风险清单#
| # | 风险 | 具体后果 | 缓解 |
|---|---|---|---|
| 1 | 真机性能未知 | 开发机没有 GPU(软件光栅化),此前所有性能数字只能看趋势。竖屏横版满屏重绘、每帧 4.94 M px,低端机上极可能掉到 30 fps 以下 | ① 优化 ①②④ 全做(目标把每帧填充压到 2.07 M px 以下);② 6 台真机(含 2 台 2 GB RAM 老安卓)跑每帧强制同步的 profiler;③ 首 60 帧自适应降档兜底 |
| 2 | WebP 在部分 iOS 上解不出来 | WebP 依赖系统解码器(iOS 14+),老机型上首帧是空白兔子,而这是玩家看到的第一件事 | ① PNG 兜底图集放条件分包(1.4 MB,只对失败机型下载);② 启动时用 4 KB 的 1×1 WebP 做解码自检;③ 自检失败静默切轨,不弹任何提示 |
| 3 | 第 ④ 层循环是空的(最致命,且不是工程问题) | 图鉴 40 格 5~8 天收齐,之后没有理由再打开。微信是"用完即走"生态,没有"回来的理由"就等于没有留存 | ① P2 必须把"花园离线成长"做出来(6.4);② 把 7 日留存 ≥ 15% 设为 P3 商业化的准入条件,不达标就不加广告;③ 单独埋点 collection_complete 后的次日留存,这是全项目最该盯的指标 |
| 4 | 好友关系链 API 不可用 | getFriendCloudStorage 等能力对个人主体 / 部分类目已受限,"花园互访"若依赖它会直接做不出来 |
① 改走带参分享卡片(花园快照 token),好友看到的是"明信片";② 把"互访"定义为"看到朋友分享的花园",不读关系链;③ P0 阶段就先验证分享链路能不能跑通,不要等做到 P4 |
| 5 | 备案与版号的周期撞车 | ICP 备案 2~4 周;版号数月。若 P3 的外观直购要动虚拟支付,整条变现路线要往后推一个季度 | ① 第一周就启动备案,不等游戏做完;② P1~P3 全部走"无内购"版本;③ 外观直购若被资质卡住,降级为"看激励视频解锁 24 小时试用"(不违反零操作,也不违反无内购) |
| 6 | 音频中断后不恢复(经典坑,一定会遇到) | 来电 / 闹钟 / 切后台之后 BGM 静音且状态错乱;连续收集时音效时有时无(Android 并发上限 10 个) | ① 8 实例音效池,不重建;② onAudioInterruptionEnd + onShow 时销毁重建 BGM 实例;③ 首屏给可见声音开关;④ P0 阶段专门测"来一次电话再回来" |
| 7 | 被误判为"低交互 / 不是游戏" | 零操作 + 零失败,审核方或首批玩家可能觉得"这不算游戏",评价翻车 | ① 前 30 秒必须发生三件事:兔子自己跳过一块石头、花自动飞进手账、手账格子亮起来——把"看"的价值前置;② 第一关第 40 秒就放一次小游戏(按住蓄力),让"手"尽早参与一次;③ 审核说明里主动写清"零操作"是设计选择,并附"随时能停 / 无内购 / 无排行榜"三条 |
| 8 | HUD 被刘海与胶囊按钮盖住 | 右上角速度档正好压在胶囊上(x ≈ 880 |
① 顶边距 max(110, safeArea.top + 24);② 速度档挪出右上角,右上永远让给胶囊;③ safeArea 可能不返回,必须有默认兜底;④ 6 台真机逐台截图核对(已列入 8.3 的 30 项) |
十、下一步工单(按依赖排序)#
| 序 | 工单 | 依赖 | 预估 |
|---|---|---|---|
| 1 | 启动 ICP 备案(周期最长,不阻塞开发) | — | 2~4 周(等待期) |
| 2 | 抽离纯逻辑模块(物理 / 规则 / 存档),把现有 30 项验收迁过来 | — | 3 天 |
| 3 | 屏幕适配层:虚拟坐标系 + 地面线距底 538 + safeArea / 胶囊避让 | — | 2 天 |
| 4 | 背景视差 4 层 → 2 层预烘焙(收益第 1 的优化) | 3 | 2 天 |
| 5 | 对象池(80 物件 + 32 粒子)+ 地形 RLE 合并 | 3 | 3 天 |
| 6 | 低端机三档降级 + 自适应(含 64×64 序列帧重烘焙) | 4、5 | 3 天 |
| 7 | 存档层:bitmask + 本地即时落盘 + 云端并集合并 | 2 | 2 天 |
| 8 | WebP 自检 + PNG 兜底轨(构建期出双份图集) | 4 | 1 天 |
| 9 | 6 台真机验收(帧时间 p99、内存峰值、首帧、HUD、音频中断) | 6、8 | 3 天 |
| 10 | 接实名 + 防沉迷、挂隐私协议 | 1 | 1 周 |
| 11 | 上线 P1 纯体验版,只埋点不推广 | 9、10 | — |
这份文档里所有"必须改"的东西加起来大约 3 周。 真正需要提前动手的只有一件:ICP 备案——它不占开发人力,但占日历时间, 而它不到手,前面所有工作都上不了线。