前两节把「小游戏是什么」和「平台怎么分发」说清了,这一节换尺度:从一款产品的加载闸门,放大到整个小游戏市场的规模、增长与结构。
先分工。1.1 提到的那篇 H5游戏产业全景 是产业综述,给的是定性全景(技术栈、生态、政策、未来五年),覆盖面广;本节给的是同一件事的可量化版本——用一套全整数的复合增长模型,把「小游戏市场有多大、长多快、机会在哪」算成可以自己复算的数字。和它一样,本节所有基数都来自公开行业报告的估算口径(观察时点 2026-10),不是实测,各机构口径不同,请只把它当建模起点。
口径:先分清「谁的市场」
在报任何一个市场规模数字之前,必须先回答「这是谁的市场」。「小游戏市场」至少有三种常见口径,混用就会得出互相矛盾的结论:
- 玩家支出口径:所有小游戏用户为游戏花的钱(含广告变现折算与内购),是产品方视角的市场。
- 平台流水口径:平台从中分成的部分,是平台方视角的市场,通常小于玩家支出。
- 开发工具 / 服务口径:围绕小游戏开发的引擎、编辑器、云服务,是工具方视角的市场,量级小得多。
本节用的是玩家支出口径(也是最常被引用的那个)。记住这一点,才不会把「某平台流水 300 亿」和「小游戏市场 400 亿」当成同一件事。口径不同,数字没有可比性——这本身就是本节最想传达的一课,后面的 B 层会把它量化到极致。
三情景复合增长测算(S 层)
下面这段是 D 层配置:基数、三情景增长率、终年结构拆分与平台占比。基数是公开行业报告的估算口径(非实测),增长率与占比是情景假设;全整数百分比、无浮点。
{
"version": 1,
"note": "小游戏市场规模与增长情景。基数来自公开行业报告估算口径,观察时点 2026-10,非实测;各机构口径不同,仅作建模起点(不可复现处标 X)。增长率与结构占比为情景假设,全整数百分比,无浮点。",
"observedAt": "2026-10",
"unit": "亿元(人民币)",
"baseYear": 2024,
"baseSizeYi": 398,
"baseSourceNote": "2024 年中国小游戏市场实际销售收入口径(公开行业报告估算),非实测。",
"years": [2024, 2025, 2026, 2027, 2028, 2029, 2030],
"scenarios": [
{ "id": "bear", "name": "保守", "growthPct": 8 },
{ "id": "base", "name": "基准", "growthPct": 15 },
{ "id": "bull", "name": "乐观", "growthPct": 22 }
],
"segments": [
{ "id": "iaa", "name": "广告变现(IAA)", "sharePct": 55 },
{ "id": "iap", "name": "内购(IAP)", "sharePct": 30 },
{ "id": "hybrid", "name": "混合/其他", "sharePct": 15 }
],
"platformShare": [
{ "id": "wechat", "name": "微信", "sharePct": 45 },
{ "id": "douyin", "name": "抖音", "sharePct": 25 },
{ "id": "others", "name": "其他平台", "sharePct": 30 }
]
}
下面这段是 S 层仿真脚本,读上面的 config/,用整数百分比复合递推算出三情景逐年规模、终年结构拆分,产出 JSON + CSV + SVG。幂运算走 Python 原生大整数,除法一律地板除。
#!/usr/bin/env python3
# file: sim/ch01_market_size.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch01_market_size.py --config config/ --out out/
"""市场规模与增长测算:读 config/ch01_market.json,用整数百分比复合递推算出
三情景逐年规模、终年结构拆分,产出 JSON + CSV + SVG。
模型(全整数,无浮点):
- size(y) = baseSizeYi * (100+growthPct)^(y-baseYear) // 100^(y-baseYear)。
- 结构拆分:part = terminalSize * sharePct // 100,余数补给占比最大项。
纯标准库,图表用 xml.etree 手写 SVG,零依赖。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
COLORS = {"bear": "#9ca3af", "base": "#2563eb", "bull": "#16a34a"}
def series(cfg, growth_pct):
base, by = cfg["baseSizeYi"], cfg["baseYear"]
return [{"year": y, "sizeYi": (base * (100 + growth_pct) ** (y - by)) // (100 ** (y - by))}
for y in cfg["years"]]
def split_by_share(total, items):
rows, acc = [], 0
for it in items:
part = total * it["sharePct"] // 100
acc += part
rows.append({"id": it["id"], "name": it["name"], "sharePct": it["sharePct"], "sizeYi": part})
if acc != total: # 地板除余数补给占比最大项,保证合计等于 total
big = max(rows, key=lambda r: r["sharePct"])
big["sizeYi"] += total - acc
big["remainderAdjusted"] = 1
return rows
def write_svg(path, cfg, series_map):
W, H, PAD = 720, 420, 56
years = cfg["years"]
x0, x1 = min(years), max(years)
y_max = max(p["sizeYi"] for s in cfg["scenarios"] for p in series_map[s["id"]])
sx = lambda x: PAD + (W - 2 * PAD) * ((x - x0) / (x1 - x0))
sy = lambda y: H - PAD - (H - 2 * PAD) * (y / y_max)
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(svg, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(svg, "text", {"x": str(PAD), "y": "24", "font-size": "16",
"font-family": "sans-serif"}).text = (
f"小游戏市场规模三情景({cfg['unit']},同机相对量级,起点 {cfg['baseSizeYi']})")
for i in range(6):
y = PAD + (H - 2 * PAD) * i / 5
ET.SubElement(svg, "line", {"x1": str(PAD), "y1": f"{y:.1f}", "x2": str(W - PAD),
"y2": f"{y:.1f}", "stroke": "#e5e7eb"})
ET.SubElement(svg, "text", {"x": str(PAD - 8), "y": f"{y + 4:.1f}", "font-size": "11",
"text-anchor": "end", "fill": "#6b7280"}).text = str(int(y_max * (1 - i / 5)))
ET.SubElement(svg, "line", {"x1": str(PAD), "y1": str(H - PAD), "x2": str(W - PAD),
"y2": str(H - PAD), "stroke": "#9ca3af"})
ET.SubElement(svg, "line", {"x1": str(PAD), "y1": str(PAD), "x2": str(PAD),
"y2": str(H - PAD), "stroke": "#9ca3af"})
for s in cfg["scenarios"]:
pts = " ".join(f"{sx(p['year']):.2f},{sy(p['sizeYi']):.2f}" for p in series_map[s["id"]])
ET.SubElement(svg, "polyline", {"points": pts, "fill": "none",
"stroke": COLORS[s["id"]], "stroke-width": "2"})
last = series_map[s["id"]][-1]
ET.SubElement(svg, "text", {"x": f"{sx(last['year']) + 6:.1f}", "y": f"{sy(last['sizeYi']) + 4:.1f}",
"font-size": "12", "fill": COLORS[s["id"]]}).text = f"{s['name']} {last['sizeYi']}"
for y in years:
ET.SubElement(svg, "text", {"x": f"{sx(y):.1f}", "y": str(H - PAD + 16), "font-size": "11",
"text-anchor": "middle"}).text = str(y)
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
return os.path.getsize(path)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = json.load(open(os.path.join(args.config, "ch01_market.json"), encoding="utf-8"))
os.makedirs(args.out, exist_ok=True)
smap = {s["id"]: series(cfg, s["growthPct"]) for s in cfg["scenarios"]}
with open(os.path.join(args.out, "ch01_market_size.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["year"] + [s["id"] for s in cfg["scenarios"]])
for i, y in enumerate(cfg["years"]):
w.writerow([y] + [smap[s["id"]][i]["sizeYi"] for s in cfg["scenarios"]])
svg_bytes = write_svg(os.path.join(args.out, "ch01_market_size.svg"), cfg, smap)
terminal = cfg["years"][-1]
base_terminal = smap["base"][-1]["sizeYi"]
summary = {
"version": 1, "source": "sim/ch01_market_size.py",
"baseYear": cfg["baseYear"], "baseSizeYi": cfg["baseSizeYi"], "terminalYear": terminal,
"baseScenarioTerminalYi": base_terminal,
"baseScenarioMultipleX100": base_terminal * 100 // cfg["baseSizeYi"],
"bearTerminalYi": smap["bear"][-1]["sizeYi"], "bullTerminalYi": smap["bull"][-1]["sizeYi"],
"segmentsTerminal": split_by_share(base_terminal, cfg["segments"]),
"platformTerminal": split_by_share(base_terminal, cfg["platformShare"]),
"series": smap, "svgBytes": svg_bytes,
}
with open(os.path.join(args.out, "ch01_market_size.json"), "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(json.dumps({
"base": [p["sizeYi"] for p in smap["base"]],
"bear": [p["sizeYi"] for p in smap["bear"]],
"bull": [p["sizeYi"] for p in smap["bull"]],
"terminalSegments": {r["id"]: r["sizeYi"] for r in summary["segmentsTerminal"]},
"baseScenarioMultipleX100": summary["baseScenarioMultipleX100"],
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
同机实跑(节选,同机相对量级,绝对数值不可移植):
base: [398, 457, 526, 605, 696, 800, 920]
bear: [398, 429, 464, 501, 541, 584, 631]
bull: [398, 485, 592, 722, 881, 1075, 1312]
terminalSegments: {iaa:506, iap:276, hybrid:138}
baseScenarioMultipleX100: 231 # 即基准情景下 2030 约为 2024 的 2.31 倍
读法:倍数、结构与平台集中度
三个数字值得展开:
- 基准情景 2024→2030 是 2.31 倍(398 → 920 亿元),对应年化约 15%。保守情景只有 1.59 倍(631),乐观情景是 3.30 倍(1312)——三情景的终值差了 2 倍以上,说明「增长率差几个百分点」在这个尺度上会被复利放大成完全不同的结论。
- 终年变现结构:广告变现(IAA)506 亿、内购(IAP)276 亿、混合/其他 138 亿。IAA 占 55%,意味着这个市场的大头仍然靠广告,而不是靠玩家付费——这直接决定了小游戏的变现设计要以广告为主线(卷三第 1 章展开)。
- 平台集中度:微信 414 亿(45%)、抖音 230 亿(25%)、其他平台合计 276 亿(30%)。头部平台占了近一半,这既是风险(平台规则变化直接影响大盘)也是机会(长尾平台 30% 的份额尚未被充分开发)。
口径敏感性基准(B 层)
上面每个数字看起来都很确定,但它们全都建立在一个估算的基数和一个假设的增长率上。下面这段是 B 层基准脚本:把基数扰动 ±20%、增长率扰动 ±5 个百分点,跑 3×3 组合重算终年规模,报告极差与敏感度。它是软告警——只打印不失败,因为它的目的不是给出「正确」答案,而是量化「同一套模型在口径漂移下能差多少」。
#!/usr/bin/env python3
# file: sim/ch01_market_sensitivity.py
# tier: B
# verify: /opt/homebrew/bin/python3 sim/ch01_market_sensitivity.py --config config/ --out out/
"""口径敏感性基准(B 层,软告警不硬失败):读 config/ch01_market.json,对基数
±20% 与增长率 ±5 个百分点的 3×3 组合重算终年规模,报告极差与敏感度。
目的不是给出「正确」的市场规模,而是量化:同一套模型在口径漂移下能差多少。
极差占中位数的比例超阈值即软告警。纯标准库,产出 JSON;告警只打印不改退出码。
"""
from __future__ import annotations
import argparse
import json
import os
def terminal_size(base, growth_pct, years):
return (base * (100 + growth_pct) ** years) // (100 ** years)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
ap.add_argument("--base-pct", type=int, default=20, help="基数扰动幅度(百分点)")
ap.add_argument("--growth-delta", type=int, default=5, help="增长率扰动(百分点)")
ap.add_argument("--warn-spread-pct", type=int, default=60, help="极差/中位数 超此值软告警")
args = ap.parse_args()
cfg = json.load(open(os.path.join(args.config, "ch01_market.json"), encoding="utf-8"))
os.makedirs(args.out, exist_ok=True)
base = cfg["baseSizeYi"]
growth = next(s["growthPct"] for s in cfg["scenarios"] if s["id"] == "base")
years = cfg["years"][-1] - cfg["baseYear"]
bases = [base * (100 - args.base_pct) // 100, base, base * (100 + args.base_pct) // 100]
growths = [growth - args.growth_delta, growth, growth + args.growth_delta]
grid = [{"baseYi": b, "growthPct": g, "terminalYi": terminal_size(b, g, years)}
for b in bases for g in growths]
vals = sorted(r["terminalYi"] for r in grid)
lo, hi, mid = vals[0], vals[-1], vals[len(vals) // 2]
spread_pct = (hi - lo) * 100 // mid
warns = []
if spread_pct > args.warn_spread_pct:
warns.append(f"口径敏感度高:终年规模极差 {hi - lo} 亿元(占中位 {spread_pct}%),"
f"任何单一数字都不应作为决策依据")
report = {
"version": 1, "source": "sim/ch01_market_sensitivity.py", "years": years,
"basePerturbPct": args.base_pct, "growthDeltaPct": args.growth_delta, "grid": grid,
"terminalMinYi": lo, "terminalMedianYi": mid, "terminalMaxYi": hi,
"spreadPct": spread_pct, "warnings": warns,
}
with open(os.path.join(args.out, "ch01_market_sensitivity.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({"terminalMinYi": lo, "terminalMedianYi": mid, "terminalMaxYi": hi,
"spreadPct": spread_pct, "warnings": warns}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
同机实跑(节选,同机相对量级):
terminalMinYi: 563 terminalMedianYi: 920 terminalMaxYi: 1424 spreadPct: 93
warnings: [口径敏感度高:终年规模极差 861 亿元(占中位 93%),任何单一数字都不应作为决策依据]
这个结果是本节最重要的一课。 只是把基数挪 ±20%、增长率挪 ±5 个百分点(都在合理范围内),2030 年的规模就从 563 亿变到 1424 亿——极差 861 亿,占了中位数 920 亿的 93%。也就是说:任何一篇只报「2030 年小游戏市场将达到 X 亿」的文章,那个 X 都不该被当成事实。正确的用法是:拿它当建模起点,自己做情景与敏感性,看结论在多大范围内稳健——这正是本套把「可运行性」放在核心的原因。
结构性机会:从数字里读出的三处真空
结合上面的结构拆分,可以读出三处结构性(而非玩法层面)的机会,它们都属于「市场结构」而非「选哪个品类」——品类选择是第 3 章的事:
- 平台结构的真空:头部平台占 45%,长尾平台合计 30%。长尾平台的竞争强度通常低于头部,对中小团队而言「在长尾平台做头部」比「在头部平台做长尾」更现实。
- 变现结构的真空:IAA 占 55%、IAP 占 30%,说明内购仍未被充分挖掘。混合变现(IAA + IAP)是当前占比最小的「混合/其他」项,也往往是单位用户价值最高的组合。
- 认知结构的真空:上面 B 层已经证明,市场数字的噪声极大。「不轻信单一数字、自己会算」本身就是一种稀缺能力——它让你在评审时能对一份「市场 800 亿」的 PPT 提出正确的追问。
小结
- 市场规模先分清口径(玩家支出 / 平台流水 / 工具服务),本节用的是玩家支出口径。
- 基准情景 2024→2030 为 2.31 倍(398 → 920 亿元),终年 IAA 占 55%、微信占 45%。
- B 层基准证明:口径漂移下终年规模极差占中位 93%——市场数字只能当建模起点,不能当事实。
- 结构性机会在长尾平台、混合变现与「自己会算」这三处真空;具体选哪个品类,从第 2 章的榜单结构开始。
阅读导航:上一节:1.2 平台生态与分发机制 · 下一节:榜单结构与品类分布 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。