1.1 平台分层与流量结构

海外小游戏不是一个市场,而是三层流量结构:超级 App 内嵌、独立网页门户、应用商店,各自把用户引到游戏的方式完全不同。本节给出一张平台分层矩阵、一段读配置算免费流量占比与可发现用户规模的仿真,并显式对照 Steam 的 PC 口径,说明为什么 H5 小游戏不走 Steam。

站内有一篇 H5 游戏产业全景 ,那是 H5 产业的全景综述——技术栈、生态、变现、政策一网打尽,覆盖面广但每条都浅。本节不重做那张全景图,只做一件事:把海外小游戏平台按「用户从哪来」分成三层,并算清每一层的流量结构。站内还有一组 Steam 发布专题 (166 篇),那是 PC 商店口径;本节末会用一节显式对照「为什么 H5 小游戏不走 Steam」。至于「小游戏被包体、加载、宿主三重约束重新定义」,卷一 1.1 小游戏的定义与边界 已经用一张能力矩阵讲透,本节不重做那张包体表——它回答「装不装得下」,本节回答「装下之后,用户从哪来」。

海外小游戏的三层结构

国内小游戏绕不开微信、抖音、快手这几家超级 App。出海面对的平台要碎得多,但碎归碎,按用户如何发现游戏这个唯一标准,它们仍然只分三层:

层代表平台用户怎么发现游戏你的角色
超级 App 内嵌Facebook Instant Games、YouTube Playables、Snapchat、Discord Activities信息流 / 推荐位 / 社交分享,在 App 里点开即玩内容供给方,吃宿主算法与社交链
独立网页门户Poki、CrazyGames、Y8、GameDistribution门户首页、搜索引擎、站点联盟嵌入货架上的一个 SKU,吃门户分发
应用商店App Store、Google Play(+ Steam 作 PC 对照)商店搜索、榜单、买量需下载安装,吃商店排名与投放

三层的差别不在「游戏好不好玩」,而在流量从哪来。同一款《合成小岛》放进这三层,获客成本、留存曲线、可用的变现通道是三套完全不同的账。所以「选平台」的第一步不是比谁分成高,而是先认清这个平台的流量结构——它决定了你能靠什么拿到用户。

平台分层矩阵(D 层)

下面这段是 D 层配置,把十一个平台各记一行:所属层、可触达月活、新游戏被发现的概率、四类流量来源占比、分发入口。数值是按各平台公开机制整理的建模输入,标为公开可观察(观察时点 2026-10),不是任何平台后台实测。

{
  "version": 1,
  "game": "merge-island",
  "observedAt": "2026-10",
  "note": "海外小游戏平台分层与流量结构。reachMillions=平台可触达月活(百万,建模假设);discoverabilityPermille=新游戏在平台内被发现的概率(千分比,建模假设);mix 四类流量来源占比(千分比,四项之和恒 1000):natural=自然/搜索、editorial=平台编辑与算法推荐、social=社交传播、paid=付费/联盟导入。全部为「公开可观察机制」口径的建模假设,观察时点 2026-10;不是任何平台后台实测,真实流量与推荐机制需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
  "playRatePermille": 150,
  "tiers": [
    { "id": 1, "name": "超级 App 内嵌" },
    { "id": 2, "name": "独立网页门户" },
    { "id": 3, "name": "应用商店" }
  ],
  "platforms": [
    { "id": "fb_instant", "name": "Facebook Instant Games", "tier": 1, "isContrast": 0, "reachMillions": 300, "discoverabilityPermille": 120, "mix": { "naturalPermille": 200, "editorialPermille": 300, "socialPermille": 380, "paidPermille": 120 }, "discoveryEntry": "信息流 / 游戏标签页" },
    { "id": "yt_playables", "name": "YouTube Playables", "tier": 1, "isContrast": 0, "reachMillions": 500, "discoverabilityPermille": 90, "mix": { "naturalPermille": 150, "editorialPermille": 450, "socialPermille": 100, "paidPermille": 300 }, "discoveryEntry": "首页 / 推荐位" },
    { "id": "snapchat", "name": "Snapchat 小游戏", "tier": 1, "isContrast": 0, "reachMillions": 400, "discoverabilityPermille": 70, "mix": { "naturalPermille": 120, "editorialPermille": 200, "socialPermille": 480, "paidPermille": 200 }, "discoveryEntry": "社交分享 / 发现页" },
    { "id": "discord", "name": "Discord Activities", "tier": 1, "isContrast": 0, "reachMillions": 150, "discoverabilityPermille": 60, "mix": { "naturalPermille": 100, "editorialPermille": 150, "socialPermille": 600, "paidPermille": 150 }, "discoveryEntry": "服务器内活动" },
    { "id": "poki", "name": "Poki", "tier": 2, "isContrast": 0, "reachMillions": 100, "discoverabilityPermille": 200, "mix": { "naturalPermille": 350, "editorialPermille": 300, "socialPermille": 100, "paidPermille": 250 }, "discoveryEntry": "门户首页 / SEO" },
    { "id": "crazygames", "name": "CrazyGames", "tier": 2, "isContrast": 0, "reachMillions": 60, "discoverabilityPermille": 220, "mix": { "naturalPermille": 400, "editorialPermille": 250, "socialPermille": 120, "paidPermille": 230 }, "discoveryEntry": "门户首页 / SEO" },
    { "id": "y8", "name": "Y8", "tier": 2, "isContrast": 0, "reachMillions": 40, "discoverabilityPermille": 180, "mix": { "naturalPermille": 500, "editorialPermille": 150, "socialPermille": 100, "paidPermille": 250 }, "discoveryEntry": "门户首页 / SEO" },
    { "id": "gamedist", "name": "GameDistribution", "tier": 2, "isContrast": 0, "reachMillions": 80, "discoverabilityPermille": 150, "mix": { "naturalPermille": 200, "editorialPermille": 100, "socialPermille": 50, "paidPermille": 650 }, "discoveryEntry": "联盟分发 / 站点嵌入" },
    { "id": "appstore", "name": "App Store", "tier": 3, "isContrast": 0, "reachMillions": 900, "discoverabilityPermille": 40, "mix": { "naturalPermille": 200, "editorialPermille": 250, "socialPermille": 50, "paidPermille": 500 }, "discoveryEntry": "商店搜索 / 榜单" },
    { "id": "gplay", "name": "Google Play", "tier": 3, "isContrast": 0, "reachMillions": 1200, "discoverabilityPermille": 35, "mix": { "naturalPermille": 250, "editorialPermille": 200, "socialPermille": 40, "paidPermille": 510 }, "discoveryEntry": "商店搜索 / 榜单" },
    { "id": "steam", "name": "Steam(PC 对照)", "tier": 3, "isContrast": 1, "reachMillions": 130, "discoverabilityPermille": 30, "mix": { "naturalPermille": 350, "editorialPermille": 400, "socialPermille": 100, "paidPermille": 150 }, "discoveryEntry": "商店 / 榜单 / 愿望单" }
  ]
}

读这张表要抓住三个反直觉点。第一,超级 App 层不一定「大」——它的可触达月活很高,但「新游戏被发现的概率」discoverabilityPermille 并不必然高:Discord 只有 60‰,意味着一个新游戏在它里面被看到的概率比 Poki(200‰)还低,因为它的流量几乎全靠服务器内的社交传播。第二,网页门户层的免费流量占比最高:Poki / CrazyGames / Y8 的自然 + 推荐 + 社交加起来都在 750‰ 上下,靠 SEO 与门户首页吃饭。第三,应用商店层的付费流量最重:App Store 与 Google Play 的一半流量是买来的,这是「上架不等于有量」的算术解释。

流量结构:四类来源决定你该投什么

把每个平台的流量拆成四类来源,比只看「月活多大」有用得多:

来源含义你该做什么
natural 自然搜索引擎、直链、老用户回访做好标题 / 描述 / 站内 SEO
editorial 推荐平台编辑位、算法推荐优化首帧、完播、留存的平台信号
social 社交好友分享、群、服务器传播做分享钩子与可炫耀的进度
paid 付费平台买量、联盟导入备好预算与归因口径(卷四)

关键判断是主导来源(dominant channel):它决定你的运营重心。social 主导的平台(Facebook、Snapchat、Discord),分享机制是命脉;natural 主导的平台(Poki / CrazyGames / Y8),SEO 与门户合作是命脉;paid 主导的平台(App Store / Google Play / GameDistribution),买量与归因是命脉。把资源投错来源,就是给平台交学费。

S 层:分层与流量结构仿真

下面这段是 S 层仿真脚本 sim/g01_traffic_structure.py。它读 config/g01_platform_tiers.json,用整数公式算每个平台的免费流量占比、可发现用户规模(千人)与预期开玩量,并按三层聚合,产出 JSON + CSV + SVG。

#!/usr/bin/env python3
# file: sim/g01_traffic_structure.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g01_traffic_structure.py --config config/ --out out/
"""海外小游戏平台分层与流量结构仿真:读 config/g01_platform_tiers.json,算每个平台的免费
流量占比、可发现用户规模与预期开玩量,并按平台分层聚合。全整数、地板除,无浮点。

    freeSharePermille  = natural + editorial + social
    reachableKiloUsers = reachMillions * discoverabilityPermille        (千人)
    freeReachableKilo  = reachableKiloUsers * freeSharePermille // 1000
    paidReachableKilo  = reachableKiloUsers * paidPermille // 1000
    expectedPlaysKilo  = reachableKiloUsers * playRatePermille // 1000

口径:平台月活与流量结构是「公开可观察机制」口径的建模假设,观察时点 2026-10;不是任何
平台后台实测,真实流量与推荐机制需真实业务环境,命令行不可复现(X)。
产出 out/g01_traffic_structure.json + .csv + .svg。
"""
from __future__ import annotations

import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET

CHANNELS = ("natural", "editorial", "social", "paid")
FREE_CHANNELS = CHANNELS[:3]


def dominant(mix: dict) -> str:
    """流量占比最大的来源;max 稳定取首个最大值(按 CHANNELS 顺序)。"""
    return max(CHANNELS, key=lambda c: mix[f"{c}Permille"])


def evaluate(p: dict, play_rate: int) -> dict:
    mix = p["mix"]
    reach = p["reachMillions"] * p["discoverabilityPermille"]  # 千人
    free = sum(mix[f"{c}Permille"] for c in FREE_CHANNELS)
    return dict(p, reachableKiloUsers=reach, freeSharePermille=free,
                paidSharePermille=mix["paidPermille"], dominantChannel=dominant(mix),
                freeReachableKiloUsers=reach * free // 1000,
                paidReachableKiloUsers=reach * mix["paidPermille"] // 1000,
                expectedPlaysKilo=reach * play_rate // 1000)


def draw_svg(path: str, rows: list[dict]) -> int:
    """每个平台一条堆叠横条:免费(蓝)vs 付费(橙)可发现用户(千人)。"""
    W, H, PAD, ROW = 780, 70 + 40 * len(rows), 210, 40
    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": "16", "y": "28", "font-size": "15",
                                "fill": "#111827"}).text = "各平台可发现用户(千人):免费(蓝)vs 付费(橙)"
    span, hi = W - PAD - 120, max([r["reachableKiloUsers"] for r in rows] + [1])
    for i, r in enumerate(rows):
        y = 52 + i * ROW
        ET.SubElement(svg, "text", {"x": "16", "y": str(y + 18), "font-size": "12",
                                    "fill": "#374151"}).text = r["name"]
        fw = span * r["freeReachableKiloUsers"] // hi
        pw = span * r["paidReachableKiloUsers"] // hi
        ET.SubElement(svg, "rect", {"x": str(PAD), "y": str(y + 4), "width": str(fw),
                                    "height": "18", "rx": "3", "fill": "#2563eb"})
        ET.SubElement(svg, "rect", {"x": str(PAD + fw), "y": str(y + 4), "width": str(pw),
                                    "height": "18", "rx": "3", "fill": "#f59e0b"})
        ET.SubElement(svg, "text", {"x": str(PAD + fw + pw + 6), "y": str(y + 18), "font-size": "11",
                                    "fill": "#6b7280"}).text = f'{r["reachableKiloUsers"]}k · 免费 {r["freeSharePermille"]}‰'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
    return len(ET.tostring(svg))


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, "g01_platform_tiers.json"), encoding="utf-8") as f:
        cfg = json.load(f)
    rows = [evaluate(p, cfg["playRatePermille"]) for p in cfg["platforms"]]

    names = {t["id"]: t["name"] for t in cfg["tiers"]}
    by_tier = {}
    for r in rows:
        if r.get("isContrast"):
            continue
        t = by_tier.setdefault(r["tier"], {"tier": r["tier"], "name": names[r["tier"]],
                                           "platforms": 0, "reachableKiloUsers": 0,
                                           "freeReachableKiloUsers": 0})
        t["platforms"] += 1
        t["reachableKiloUsers"] += r["reachableKiloUsers"]
        t["freeReachableKiloUsers"] += r["freeReachableKiloUsers"]
    tiers = [by_tier[k] for k in sorted(by_tier)]
    digest = ",".join(f'{r["id"]}:{r["freeSharePermille"]}:{r["reachableKiloUsers"]}'
                      f':{r["dominantChannel"]}' for r in rows)

    os.makedirs(args.out, exist_ok=True)
    svg_bytes = draw_svg(os.path.join(args.out, "g01_traffic_structure.svg"), rows)
    report = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
              "playRatePermille": cfg["playRatePermille"], "platformCount": len(rows),
              "platforms": rows, "tiers": tiers, "digest": digest,
              "svg": os.path.join(args.out, "g01_traffic_structure.svg"), "svgBytes": svg_bytes}
    with open(os.path.join(args.out, "g01_traffic_structure.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    cols = ("id", "name", "tier", "isContrast", "reachMillions", "discoverabilityPermille",
            "freeSharePermille", "paidSharePermille", "dominantChannel", "reachableKiloUsers",
            "freeReachableKiloUsers", "paidReachableKiloUsers", "expectedPlaysKilo")
    with open(os.path.join(args.out, "g01_traffic_structure.csv"), "w", encoding="utf-8",
              newline="") as f:
        w = csv.writer(f)
        w.writerow(cols)
        for r in rows:
            w.writerow([r[k] for k in cols])

    print(json.dumps({
        "playRatePermille": cfg["playRatePermille"],
        "platforms": [{"id": r["id"], "tier": r["tier"], "freeSharePermille": r["freeSharePermille"],
                       "dominantChannel": r["dominantChannel"],
                       "reachableKiloUsers": r["reachableKiloUsers"],
                       "freeReachableKiloUsers": r["freeReachableKiloUsers"]} for r in rows],
        "tiers": tiers, "digest": digest}, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

在本机跑 /opt/homebrew/bin/python3 sim/g01_traffic_structure.py --config config/ --out out/,结果是(同机相对量级,绝对数值不可移植):

平台层免费流量‰主导来源可发现用户(千人)免费可发现(千人)
Facebook Instant Games1880社交3600031680
YouTube Playables1700推荐4500031500
Snapchat 小游戏1800社交2800022400
Discord Activities1850社交90007650
Poki2750自然2000015000
CrazyGames2770自然1320010164
Y82750自然72005400
GameDistribution2350付费120004200
App Store3500付费3600018000
Google Play3490付费4200020580
Steam(PC 对照)3850推荐39003315

按三层聚合(不含对照行):超级 App 内嵌层可发现用户 118000 千人、其中免费 93230;独立网页门户层 52400 千人、免费 34764;应用商店层 78000 千人、免费 38580。三层里免费占比最高的是超级 App 内嵌层(79%),但它的绝对规模要靠宿主的社交链撬动;应用商店层规模最大却最贵(免费占比只有 49%)——这正是出海最容易踩的坑:冲着「商店用户多」去,结果把预算全烧在了买量上。

为什么 H5 小游戏不走 Steam

Steam 也在上面的表里,但它是对照行(isContrast: 1)。把它和网页门户层比一比,就明白为什么 H5 小游戏基本不走 Steam:Steam 的可发现用户只有 3900 千人(网页门户里最小的 Y8 都有 7200 千人),discoverabilityPermille 只有 30‰——一个新游戏在 Steam 上被发现的概率比任何一层都低。原因是 Steam 是PC 商店口径:它要求下载安装(不是「点开即玩」)、面向的是愿意为买断制付费的 PC 玩家、分发靠榜单与愿望单。这些特征和 H5 小游戏「免下载、轻量、IAA 为主」的形态天然错位。

本站的 Steam 发布专题 把那套 PC 口径讲得很细——商店页、愿望单、审核、区域定价。本套的结论是:Steam 值得对照,但不值得作为 H5 小游戏的第一落点。要理解 PC 商店那套规则,去读那 166 篇;要理解 H5 怎么出海,留在这一卷。

X 层:真实流量不可复现

边界必须划清。上面算的是流量结构模型:给定月活、可发现概率与来源占比,各层能拿到多少免费与付费用户。下面这些属于 X 层,需真实业务环境,命令行不可复现:

  • 真实的平台月活与真实的新游戏可发现概率(平台侧不公开,且随算法与季节剧烈波动);
  • 真实的流量来源占比(同一款游戏在不同平台、不同时段差异极大,无法用常数刻画);
  • 真实的推荐位分配与社交传播系数(由平台算法决定,不透明且随时改)。

工程上正确的做法是:把可复现的结构模型与不可复现的真实数据分开——模型回答「如果流量结构是这样,各层各能拿到多少」,真实数据用来在发行后反标定模型参数(把实测占比填回 config,模型立刻反映现实)。这也是第 8 章「分语言指标口径」要跟模型字段一一对应的原因。

自检表

检查项合格标准
分层有标准按「用户如何发现游戏」分层,不按品类或体量
流量四类齐全自然 / 推荐 / 社交 / 付费每类都有占比,四项和恒 1000
主导来源明确每个平台给出唯一主导来源,运营重心不模糊
免费占比可算每层给出免费流量占比与可发现用户规模
对照显式Steam 作为 PC 口径对照,写清「为什么不走」
边界清晰真实月活与推荐机制标 X,不写成「实测」

平台分层最危险的错觉,是把「月活大」当成「好上架」。月活大不等于你能分到量——真正决定你拿不拿得到用户的是这个平台的流量结构,以及它把流量分给谁。下一节我们把这层结构落到钱上:同样一条收入,经过不同平台的分成与结算条款,最后到你账上的净额与到账时间差多少。

阅读导航:上一节:目录 · 下一节:1.2 各平台分成与结算条款 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「minigame」更多文章

  1. 附录 F:全书公式、参数与数据集索引
  2. 9.3 同意管理与数据出境
  3. 9.2 COPPA 与儿童向游戏的限制