站内有一组 SaaS 小游戏平台 的文章,那是做平台的口径:怎么设计一套存档、排行榜、联机 API 卖给别人用。本节反过来——作为一款游戏的开发者,去消费这些 API,并且要在九个宿主上消费同一套能力。另有一组小程序工程口径 的内容,讲的是单平台的小程序工程;本节讲的是多平台的能力差异与降级。2.1 解决了广告怎么放、2.2 解决了产品怎么上,这一节解决「上线之后,平台能给你什么、给不了什么」。
平台给的不只是广告位
小游戏上架后,平台往往会附赠一批专属能力:云存档(把进度存在平台侧)、排行榜(全服榜、好友榜)、实时多人(帧同步或房间)、分享(拉新)、云配置(不发版改参数)。这些能力不是每个平台都有:国内头部平台通常给得很全,海外 H5 平台则往往只有「分享」这一项,其余要你自己补。
这就带出一个工程决策:要不要用平台能力? 用了,能力缺失的平台就得写一套降级实现;不用,等于放弃平台侧的社交与留存红利。本节的答案是——用,但把差异关进能力矩阵,把缺失关进降级链。这和卷二「存储与云存档抽象」 是同一个思路,只是把范围从「存档」扩到六种能力。
六个能力位
本节登记六个能力位:云存档(cloudSave)、排行榜(leaderboard)、好友榜(friendRank)、实时多人(realtimeMultiplayer)、分享(share)、云配置(cloudConfig)。每个平台对每一位打 0/1,就能算出能力覆盖率与降级成本:
satisfied = Σ features[k] for k in featureKeys # 六个能力位登记值之和
missing = len(featureKeys) - satisfied
coverage‰ = satisfied * 1000 / len(featureKeys)
fallbackCost = Σ fallbackCost[k] for k in missing features # 缺失能力的降级实现成本
fallbackCost 是缺失能力时要自己补一套降级实现的工程成本(相对分):云存档缺了要补本地存档 + 导出码(2 分),实时多人缺了要补异步对战(3 分)。它不是「损失了多少钱」,而是「你要多写多少代码」。
降级链:能力缺失怎么办
能力缺失不是「关掉功能」就完事,而是要设计一条降级链——能力在,走平台侧;能力不在,走自建兜底,且两条路对业务层透明:
| 能力 | 缺失时的降级策略 |
|---|---|
| 云存档 | 本地存档 + 导出码 |
| 排行榜 | 本地最高分 + 分享截图 |
| 好友榜 | 关闭好友榜,仅保留全服榜 |
| 实时多人 | 异步对战(幽灵回放) |
| 分享 | 复制口令 / 截图分享 |
| 云配置 | 打包内置配置,改配置走发版 |
降级链的价值在于:业务代码只问「这个能力有没有」,不问「这是哪个平台」。能力探测在适配层完成,业务层拿到的是一个布尔与一个兜底实现。
S 层:能力矩阵仿真
下面这段是 S 层仿真脚本 sim/g02_feature_matrix.py。它读 config/g02_platform_features.json,算出九个平台各自的能力覆盖率、缺失位数与降级实现成本,并额外算出「每个能力位有多少平台提供」,产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/g02_feature_matrix.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g02_feature_matrix.py --config config/ --out out/
"""平台专属能力矩阵仿真:算出每个平台缺哪些能力、要补多少降级实现的成本。
读 config/g02_platform_features.json:
satisfied = Σ features[k] for k in featureKeys # 六个能力位登记值之和
missing = len(featureKeys) - satisfied
coverage‰ = satisfied * 1000 // len(featureKeys)
fallbackCost = Σ fallbackCost[k] for k in missing features # 缺失能力的降级实现成本
再按能力位给出「多少平台提供该能力」的覆盖率。产出 out/g02_feature_matrix.json + .csv + .svg。
全程整数、地板除,无浮点。能力位是「公开可观察机制」口径的建模假设;真实平台能力与配额、
真实多人联机服务需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def evaluate(p: dict, feat_keys: list[str], cost: dict) -> dict:
have = [k for k in feat_keys if p["features"].get(k, 0) == 1]
missing = [k for k in feat_keys if p["features"].get(k, 0) == 0]
satisfied = len(have)
fallback = sum(cost.get(k, 0) for k in missing)
return {
"id": p["id"], "name": p["name"], "region": p["region"],
"have": have, "missing": missing, "satisfied": satisfied,
"missingCount": len(missing),
"coveragePermille": satisfied * 1000 // len(feat_keys),
"fallbackCostSum": fallback,
}
def feature_support(cfg: dict) -> list[dict]:
"""按能力位统计:九个平台里有多少个提供该能力。"""
out = []
for k in cfg["featureKeys"]:
n = sum(1 for p in cfg["platforms"] if p["features"].get(k, 0) == 1)
out.append({"feature": k, "platforms": n})
out.sort(key=lambda x: (-x["platforms"], x["feature"]))
return out
def draw_svg(path: str, rows: list[dict]) -> int:
"""纯 Python SVG:每个平台的能力覆盖率(千分比)横条 + 降级成本标注。"""
W, H, PAD, ROW = 800, 92 + 40 * len(rows), 190, 40
root = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(root, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(root, "text", {"x": "16", "y": "28", "font-size": "15", "fill": "#111827"}).text = (
"《合成小岛》九平台专属能力覆盖率(千分比)与降级实现成本")
span = W - PAD - 24
for i, r in enumerate(rows):
y = 56 + i * ROW
ET.SubElement(root, "text", {"x": "16", "y": str(y + 18), "font-size": "12",
"fill": "#374151"}).text = f'{r["name"]}({r["region"]})'
w = span * r["coveragePermille"] // 1000
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 4), "width": str(w),
"height": "20", "rx": "3", "fill": "#7c3aed"})
ET.SubElement(root, "text", {"x": str(PAD + w + 6), "y": str(y + 18), "font-size": "11",
"fill": "#6b7280"}).text = (
f'{r["coveragePermille"]}‰ · 缺 {r["missingCount"]} 位 · 降级成本 {r["fallbackCostSum"]}')
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(root).write(path, encoding="utf-8", xml_declaration=True)
return len(ET.tostring(root))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
with open(os.path.join(args.config, "g02_platform_features.json"), encoding="utf-8") as f:
cfg = json.load(f)
rows = [evaluate(p, cfg["featureKeys"], cfg["fallbackCost"]) for p in cfg["platforms"]]
support = feature_support(cfg)
most_equipped = max(rows, key=lambda r: r["satisfied"])
most_degraded = max(rows, key=lambda r: r["fallbackCostSum"])
digest = ",".join(f'{r["id"]}:{r["satisfied"]}:{r["missingCount"]}:'
f'{r["fallbackCostSum"]}:{r["coveragePermille"]}' for r in rows)
os.makedirs(args.out, exist_ok=True)
svg_bytes = draw_svg(os.path.join(args.out, "g02_feature_matrix.svg"), rows)
report = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"featureKeys": cfg["featureKeys"], "fallbackCost": cfg["fallbackCost"],
"platforms": rows, "featureSupport": support,
"mostEquipped": most_equipped["id"], "mostDegraded": most_degraded["id"],
"digest": digest, "svg": os.path.join(args.out, "g02_feature_matrix.svg"), "svgBytes": svg_bytes}
with open(os.path.join(args.out, "g02_feature_matrix.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "g02_feature_matrix.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
cols = ("id", "name", "region", "satisfied", "missingCount", "coveragePermille", "fallbackCostSum")
w.writerow(cols)
for r in rows:
w.writerow([r[k] for k in cols])
print(json.dumps({"digest": digest, "mostEquipped": most_equipped["id"],
"mostDegraded": most_degraded["id"], "featureSupport": support,
"platforms": rows}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
参数放在 config/g02_platform_features.json(D 层配置)。能力位是「公开可观察机制」口径的建模假设,不是任何平台的官方能力清单;真实平台能力与配额属于 X 层。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "九平台专属能力(存档 / 排行榜 / 多人)登记表。口径是「公开可观察机制」的建模假设(观察时点 2026-10,非实测):features 是六个能力位的 0/1 登记值;fallbackCost 是「该能力缺失时要自己补一套降级实现」的工程成本(相对分)。satisfied/missing/coveragePermille/fallbackCostSum 由登记值推导,仅用于工程排期与降级设计,不代表任何平台官方能力清单。真实平台能力与配额、真实多人联机服务需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"featureKeys": ["cloudSave", "leaderboard", "friendRank", "realtimeMultiplayer", "share", "cloudConfig"],
"fallbackCost": { "cloudSave": 2, "leaderboard": 1, "friendRank": 2, "realtimeMultiplayer": 3, "share": 1, "cloudConfig": 1 },
"platforms": [
{ "id": "wechat", "name": "微信小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 1, "realtimeMultiplayer": 1, "share": 1, "cloudConfig": 1 } },
{ "id": "douyin", "name": "抖音小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 1, "realtimeMultiplayer": 1, "share": 1, "cloudConfig": 1 } },
{ "id": "kuaishou", "name": "快手小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 0, "realtimeMultiplayer": 0, "share": 1, "cloudConfig": 1 } },
{ "id": "qq", "name": "QQ 小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 1, "realtimeMultiplayer": 1, "share": 1, "cloudConfig": 1 } },
{ "id": "baidu", "name": "百度小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 0, "realtimeMultiplayer": 0, "share": 1, "cloudConfig": 0 } },
{ "id": "alipay", "name": "支付宝小游戏", "region": "cn", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 0, "realtimeMultiplayer": 0, "share": 1, "cloudConfig": 1 } },
{ "id": "poki", "name": "Poki", "region": "global", "features": { "cloudSave": 0, "leaderboard": 0, "friendRank": 0, "realtimeMultiplayer": 0, "share": 1, "cloudConfig": 0 } },
{ "id": "crazygames", "name": "CrazyGames", "region": "global", "features": { "cloudSave": 1, "leaderboard": 1, "friendRank": 0, "realtimeMultiplayer": 0, "share": 1, "cloudConfig": 0 } },
{ "id": "gamedistribution", "name": "GameDistribution", "region": "global", "features": { "cloudSave": 0, "leaderboard": 0, "friendRank": 0, "realtimeMultiplayer": 0, "share": 0, "cloudConfig": 0 } }
]
}
在本机跑 /opt/homebrew/bin/python3 sim/g02_feature_matrix.py --config config/ --out out/,结果如下(同机相对量级,绝对数值不可移植):
| 平台 | 满足位数 | 缺失位数 | 覆盖率‰ | 降级成本 |
|---|---|---|---|---|
| 微信小游戏 | 6 | 0 | 1000 | 0 |
| 抖音小游戏 | 6 | 0 | 1000 | 0 |
| 快手小游戏 | 4 | 2 | 666 | 5 |
| QQ 小游戏 | 6 | 0 | 1000 | 0 |
| 百度小游戏 | 3 | 3 | 500 | 6 |
| 支付宝小游戏 | 4 | 2 | 666 | 5 |
| Poki | 1 | 5 | 166 | 9 |
| CrazyGames | 3 | 3 | 500 | 6 |
| GameDistribution | 0 | 6 | 0 | 10 |
按能力位再看一层,脚本还给出每个能力位的支持度:分享最普及(8 个平台都有),云存档与排行榜各 7 个,云配置 5 个,而好友榜与实时多人只有 3 个。这两层合起来给出清晰结论:能力越「社交」和越「实时」,跨平台越稀缺。微信 / 抖音 / QQ 六位全有(覆盖率 1000‰、零降级成本),可以放心用满;Poki 只有分享(166‰、降级成本 9),GameDistribution 六位全无(0‰、降级成本 10)——在这两个平台上,你要么自己补一套后端,要么就把社交与联机功能整体砍掉。
P 层:能力探测与降级
S 层算出了矩阵,但落地时业务层要的是一个能力探测入口:问一句「这个平台有没有云存档」,拿到布尔;没有就走降级。下面这段是 P 层 TypeScript 内核 core/src/g02/features.ts,它读同一份 config/g02_platform_features.json,复算同一套覆盖率与降级成本——和 Python 逐字符一致。
// file: core/src/g02/features.ts
// tier: P
// verify: node --test 'core/test/g02_features.test.ts'
//
// 平台专属能力内核:探测某平台提供哪些能力(云存档 / 排行榜 / 多人等),
// 算出能力覆盖率与降级实现成本,并给出缺失能力对应的降级策略——一份逻辑跑九平台。
// 与 sim/g02_feature_matrix.py 逐位等价;读同一份 config/g02_platform_features.json。
// 全整数、地板除、无浮点;只用可擦除语法(禁 enum / namespace)。
export interface RawPlatformFeature {
readonly id: string; readonly name: string; readonly region: string;
readonly features: Readonly<Record<string, number>>;
}
export interface FeatureConfig {
readonly version: number; readonly game: string; readonly observedAt: string;
readonly featureKeys: readonly string[];
readonly fallbackCost: Readonly<Record<string, number>>;
readonly platforms: readonly RawPlatformFeature[];
}
export interface FeatureResult {
readonly id: string; readonly name: string; readonly region: string;
readonly have: readonly string[]; readonly missing: readonly string[];
readonly satisfied: number; readonly missingCount: number;
readonly coveragePermille: number; readonly fallbackCostSum: number;
}
/** 缺失能力 → 降级策略(工程侧的可执行映射)。 */
export const DEGRADATION: Readonly<Record<string, string>> = {
cloudSave: "本地存档 + 导出码",
leaderboard: "本地最高分 + 分享截图",
friendRank: "关闭好友榜,仅保留全服榜",
realtimeMultiplayer: "异步对战(幽灵回放)",
share: "复制口令 / 截图分享",
cloudConfig: "打包内置配置,改配置走发版",
};
export function evaluate(p: RawPlatformFeature, featKeys: readonly string[],
cost: Readonly<Record<string, number>>): FeatureResult {
const have = featKeys.filter((k) => (p.features[k] ?? 0) === 1);
const missing = featKeys.filter((k) => (p.features[k] ?? 0) === 0);
let fallback = 0;
for (const k of missing) fallback += cost[k] ?? 0;
return { id: p.id, name: p.name, region: p.region, have, missing,
satisfied: have.length, missingCount: missing.length,
coveragePermille: Math.floor((have.length * 1000) / featKeys.length),
fallbackCostSum: fallback };
}
export function evaluateAll(cfg: FeatureConfig): FeatureResult[] {
return cfg.platforms.map((p) => evaluate(p, cfg.featureKeys, cfg.fallbackCost));
}
/** 跨语言对拍摘要:与 sim/g02_feature_matrix.py 的 digest 逐字符一致。 */
export function digest(rows: readonly FeatureResult[]): string {
return rows.map((r) => `${r.id}:${r.satisfied}:${r.missingCount}:${r.fallbackCostSum}:${r.coveragePermille}`)
.join(",");
}
/** 某个能力位在九个平台里的支持度(从高到低)。 */
export function featureSupport(cfg: FeatureConfig): { feature: string; platforms: number }[] {
const out = cfg.featureKeys.map((k) => ({
feature: k,
platforms: cfg.platforms.filter((p) => (p.features[k] ?? 0) === 1).length,
}));
return out.sort((a, b) => (b.platforms - a.platforms) || (a.feature < b.feature ? -1 : 1));
}
/** 某平台的能力是否可用(业务层探测入口,缺失即走降级)。 */
export function hasFeature(cfg: FeatureConfig, platformId: string, feature: string): boolean {
const p = cfg.platforms.find((x) => x.id === platformId);
return !!p && (p.features[feature] ?? 0) === 1;
}
/** 缺失能力对应的降级策略。 */
export function degradationFor(feature: string): string {
return DEGRADATION[feature] ?? "人工评估";
}
配套测试 core/test/g02_features.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 复算结果逐字符相等,并验证能力探测与降级策略。在本机跑 node --test 'core/test/g02_features.test.ts',7 个用例全过;两套独立实现给出同一个摘要:
digest = wechat:6:0:0:1000,douyin:6:0:0:1000,kuaishou:4:2:5:666,qq:6:0:0:1000,baidu:3:3:6:500,alipay:4:2:5:666,poki:1:5:9:166,crazygames:3:3:6:500,gamedistribution:0:6:10:0
hasFeature 与 degradationFor 是这份内核的对外接口:业务层写 if (hasFeature(cfg, platform, "cloudSave")) { 用平台存档 } else { 用本地存档 },同一份逻辑在九个平台都能跑。这正是本卷的核心主张——一份逻辑、多种宿主——在「平台专属能力」这一层的具体落地,也为第 3 章的「平台抽象层」铺好了地基。
自检表
| 检查项 | 合格标准 |
|---|---|
| 口径清晰 | 能力位标「公开可观察机制」+ 观察时点,不写「实测」 |
| 覆盖率可复算 | coverage 由登记值唯一确定,Python / TS 结果一致 |
| 降级有策略 | 每个能力缺失都有明确兜底,不只「关掉功能」 |
| 探测统一 | 业务层只调 hasFeature,不判断具体平台 |
| 成本可排序 | 降级成本可直接比较,用于排期 |
| 边界清晰 | 真实能力与配额标 X,不写成「实测」 |
到此,第 2 章的三件事都齐了:广告怎么接(2.1)、产品怎么上(2.2)、平台能力怎么用(2.3)。但你会发现,三节都在讲差异——差异越多,适配层越厚。第 3 章就来收口:把这些差异收敛成一套平台抽象层与渠道配置,用构建矩阵把「一份代码」自动产出九个平台的产物。
阅读导航:上一节:2.2 上架资料、审核与驳回 · 下一节:3.1 平台抽象层与渠道配置 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。