站内那套 46 篇的小程序专题 覆盖的是小程序工程口径——架构、组件、API、审核那一套;本章不复述那些,讲的是小游戏社交传播面的可量化裂变模型:把一个玩法里所有能带来传播的入口拆成一张可读、可算、可排名的表,再往下接传播系数 K 与体验保护。前两章我们建了成长曲线与离线收益,回答了「玩家为什么明天还回来」;从这一节起回答另一个问题:玩家为什么愿意把游戏告诉别人。
小游戏为什么「天生带社交」
把一款小游戏和一盒单机卡带放在一起,最大的差别不是画面,而是它长在别人的关系链里。一个玩家打开小游戏,他的微信 / QQ 好友列表就在同一块屏幕背后;邀请一个朋友,成本不是「发一封邮件」,而是「点一下转发」。这种「关系链内生于平台」的形态,是原生 App 很难复刻的——App 要装、要授权、要跳转,每多一步就掉一批人;小游戏的邀请,落在好友对话里,一步到。
但「天生带社交」不等于「一定会裂变」。真正决定裂变效果的,是一个玩法设计了多少个可被触发的传播面,以及每个面的触达 → 发起 → 接受三段效率。《合成小岛》在设计里埋了七个传播面,下面先把它们写成一张能被脚本读取的表。
传播面的三段结构:触达 → 发起 → 接受
在数任何一个传播面之前,先把它拆成三段。这三段是串联的,任何一段塌了,整个面就废了:
- 触达(
reachPermille):有多少比例的玩家会看到这个入口。分享按钮藏在设置里,触达就低;放在结算页正中央,触达就高。 - 发起(
actPermille):看到入口的玩家中,有多少人真的发起了一次传播。这里考的是动机——给不给奖励、奖励够不够诱人。 - 接受(
acceptPermille):被邀请的人中,有多少转化成了新用户。这里考的是口碑——邀请来自真朋友还是营销号,接受率差很多。
把三段乘起来,就是每 1000 名玩家最终能带来多少新用户。下面这份配置是 D 层(静态层)产物:它把七个传播面正式固化成结构,本身不产生数字。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "《合成小岛》社交传播面清单。全部为整数千分比假设参数,是【示例 / 公开可观察】口径的行业归纳,非任何平台后台实测;真实触达与转化需真实业务环境,命令行不可复现(X)。",
"units": {
"reachPermille": "触达率(‰,看到该传播面的玩家比例)",
"actPermille": "发起率(‰,触达者中真正发起一次传播的比例)",
"acceptPermille": "接受率(‰,被邀请者转化为新用户的比例)"
},
"surfaces": [
{ "id": "share-reward", "cn": "分享得奖励", "type": "主动分享", "reachPermille": 900, "actPermille": 380, "acceptPermille": 180, "trigger": "结算页分享按钮", "note": "给道具 / 体力,最通用的拉新面;接受率偏低,靠量大。" },
{ "id": "friend-boost", "cn": "好友助力", "type": "请求助力", "reachPermille": 820, "actPermille": 460, "acceptPermille": 240, "trigger": "卡关 / 限时活动助力", "note": "带明确的「帮我一下」,接受率高于纯分享。" },
{ "id": "leaderboard", "cn": "排行榜炫耀", "type": "炫耀竞争", "reachPermille": 700, "actPermille": 220, "acceptPermille": 150, "trigger": "周榜结算 / 超越好友", "note": "靠胜负欲驱动,触发频次低但话题性强。" },
{ "id": "team-up", "cn": "组队开黑", "type": "组队", "reachPermille": 640, "actPermille": 340, "acceptPermille": 300, "trigger": "限时多人活动", "note": "需要双方同时在场,接受率最高但覆盖窄。" },
{ "id": "invite-code", "cn": "邀请码", "type": "拉新", "reachPermille": 560, "actPermille": 300, "acceptPermille": 420, "trigger": "新手期填码得双份奖励", "note": "强拉新,接受率最高;但易被薅,需要风控。" },
{ "id": "gift-energy", "cn": "赠礼互赠", "type": "互赠", "reachPermille": 780, "actPermille": 400, "acceptPermille": 200, "trigger": "体力 / 道具互赠", "note": "高频轻互动,留存向大于拉新向。" },
{ "id": "visit-island", "cn": "串门围观", "type": "社交互动", "reachPermille": 520, "actPermille": 260, "acceptPermille": 130, "trigger": "参观好友小岛", "note": "弱拉新、强留存,是社交氛围的地基。" }
]
}
七个面覆盖了「主动分享 / 请求助力 / 炫耀竞争 / 组队 / 拉新 / 互赠 / 社交互动」七种形态。注意每个面都带 type 字段——它比 id 更能说明这个面的动机来源:请求助力 靠「我需要你帮个忙」,炫耀竞争 靠「我比你强」,互赠 靠「我顺手给你一份」。动机不同,三段的形状就不同。
用 S 层脚本把七个面算成一条排名
有了这张表,第一件能算的事就是综合转化:每 1000 名玩家,每个传播面分别能带来多少新用户。下面这段是 S 层仿真脚本,读 config/ch09_social.json,把三段乘成「发起面」与「综合转化」,按综合转化降序排名,并画出条形图。
#!/usr/bin/env python3
# file: sim/ch09_surface.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch09_surface.py --config config/ --out out/
"""社交传播面清单的可量化排序:读 config/ch09_social.json。
每个传播面有三个千分比参数:触达率 reach、发起率 act、接受率 accept。
把三者乘成两个中间量:
发起面 = reach × act // 1000 (每 1000 名玩家中真正发起传播的人数)
转化面 = 发起面 × accept // 1000 (每 1000 名玩家中最终带来的新用户数)
按转化面降序排名,产出 JSON + CSV + SVG 条形图。全程整数地板除,无浮点。
传播参数为假设值,真实触达与转化需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load(cfg_dir: str) -> dict:
with open(os.path.join(cfg_dir, "ch09_social.json"), encoding="utf-8") as f:
return json.load(f)
def per_mille(part: int, whole: int) -> int:
"""整数千分比(地板除),与 TS 侧 Math.floor(part*1000/whole) 等价。"""
return (part * 1000) // whole if whole else 0
def enrich(surfaces: list[dict]) -> list[dict]:
out = []
for s in surfaces:
act = (s["reachPermille"] * s["actPermille"]) // 1000
conv = (act * s["acceptPermille"]) // 1000
out.append(dict(s, actPermille1000=act, convPermille=conv))
return sorted(out, key=lambda x: (-x["convPermille"], x["id"]))
def draw_svg(path: str, rows: list[dict]) -> int:
"""纯 Python SVG:每个传播面一根横条,长度正比于综合转化千分比。"""
W, PAD, ROW = 760, 170, 40
H = 66 + ROW * len(rows)
top = max(r["convPermille"] for r in rows) or 1
bar_max = W - PAD - 90
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": "26", "font-size": "15",
"fill": "#111827"}).text = (
"《合成小岛》各社交传播面综合转化(每 1000 名玩家带来的新用户,‰)"
)
for i, r in enumerate(rows):
y = 50 + i * ROW
ET.SubElement(svg, "text", {"x": "16", "y": str(y + 20), "font-size": "12",
"fill": "#374151"}).text = f'{r["cn"]}({r["type"]})'
w = bar_max * r["convPermille"] // top
ET.SubElement(svg, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w), "height": "20",
"rx": "4", "fill": "#0d9488"})
ET.SubElement(svg, "text", {"x": str(PAD + w + 8), "y": str(y + 21), "font-size": "12",
"fill": "#6b7280"}).text = (
f'触达{r["reachPermille"]}×发起{r["actPermille"]}×接受{r["acceptPermille"]} = {r["convPermille"]}‰'
)
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
return os.path.getsize(path)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load(args.config)
rows = enrich(cfg["surfaces"])
total_conv = sum(r["convPermille"] for r in rows)
top = rows[0]
os.makedirs(args.out, exist_ok=True)
svg_bytes = draw_svg(os.path.join(args.out, "ch09_surface.svg"), rows)
with open(os.path.join(args.out, "ch09_surface.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["rank", "id", "cn", "type", "reachPermille", "actPermille", "acceptPermille",
"actPermille1000", "convPermille"])
for i, r in enumerate(rows):
w.writerow([i + 1, r["id"], r["cn"], r["type"], r["reachPermille"], r["actPermille"],
r["acceptPermille"], r["actPermille1000"], r["convPermille"]])
report = {
"version": cfg["version"],
"game": cfg["game"],
"observedAt": cfg["observedAt"],
"surfaceCount": len(rows),
"sumConvPermille": total_conv,
"topSurface": top["id"],
"ranking": [{"rank": i + 1, "id": r["id"], "cn": r["cn"], "type": r["type"],
"convPermille": r["convPermille"]} for i, r in enumerate(rows)],
"csv": os.path.join(args.out, "ch09_surface.csv"),
"svg": os.path.join(args.out, "ch09_surface.svg"),
"svgBytes": svg_bytes,
}
with open(os.path.join(args.out, "ch09_surface.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({
"surfaceCount": report["surfaceCount"],
"ranking": [(r["cn"], r["convPermille"]) for r in rows],
"topSurface": top["cn"],
}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑它,输出是这样的(同机相对量级,绝对数值不可移植):
{"surfaceCount": 7, "ranking": [["好友助力", 90], ["邀请码", 70], ["组队开黑", 65], ["赠礼互赠", 62], ["分享得奖励", 61], ["排行榜炫耀", 23], ["串门围观", 17]], "topSurface": "好友助力"}
同时落盘 out/ch09_surface.csv(每面一行)与 out/ch09_surface.svg(转化条形图)。CSV 全表是这样:
rank,id,cn,type,reachPermille,actPermille,acceptPermille,actPermille1000,convPermille
1,friend-boost,好友助力,请求助力,820,460,240,377,90
2,invite-code,邀请码,拉新,560,300,420,168,70
3,team-up,组队开黑,组队,640,340,300,217,65
4,gift-energy,赠礼互赠,互赠,780,400,200,312,62
5,share-reward,分享得奖励,主动分享,900,380,180,342,61
6,leaderboard,排行榜炫耀,炫耀竞争,700,220,150,154,23
7,visit-island,串门围观,社交互动,520,260,130,135,17
排名读出来的三个结论
数字出来了,但它自己不会说话。topSurface 是「好友助力」(90‰),而触达最高的「分享得奖励」(900‰)只排第五。这里有三条结论。
第一,触达高不等于转化高。 「分享得奖励」的触达是全场最高的 900‰,几乎人人看得到;它的「发起面」(触达 × 发起)也是 342,全场第二。但它的接受率只有 180‰,是三段里最弱的一环,最后综合转化掉到 61‰,排在第五。反过来,「好友助力」触达只有 820‰,但发起率 460‰(全场最高)加接受率 240‰,把综合转化顶到了 90‰。决定排名的不是入口有多显眼,而是三段里最弱的那一段——这就是串联结构的代价。
第二,请求型传播面强于炫耀型。 排名前二的「好友助力」(90)与「邀请码」(70)都带明确的请求或利益——「帮我一把」「填我码你得双份」。而「排行榜炫耀」(23)是纯炫耀型,发起率只有 220‰、接受率 150‰,两项都垫底。这符合直觉:人们更愿意回应一个具体的请求,而不是围观一个「他好厉害」的展示。炫耀型传播面的价值不在拉新,在留存——它是给留下来的人看的。
第三,接受率的分化远比触达大。 七个面的触达率在 520–900 之间,差距不到两倍;接受率却在 130–420 之间,差了三倍多。这意味着优化接受率的杠杆,比优化触达大得多。想把触达从 820 抬到 900,很难;想把接受率从 180 抬到 240(比如把「分享得奖励」的奖励改成「对方也能领」),相对容易,而效果直接乘以 1.3 倍。
传播面之外:留存面也要一起看
排名表只算了拉新,但七个面里有一半其实主要服务留存。看 type 字段:
| 传播面 | 类型 | 主作用 | 依据 |
|---|---|---|---|
| 好友助力 | 请求助力 | 拉新 + 留存 | 接受率与发起率双高 |
| 邀请码 | 拉新 | 拉新 | 接受率最高(420‰) |
| 组队开黑 | 组队 | 拉新 + 留存 | 接受率高但覆盖窄 |
| 赠礼互赠 | 互赠 | 留存 | 发起高、接受低 |
| 分享得奖励 | 主动分享 | 拉新 | 触达最高、接受最低 |
| 排行榜炫耀 | 炫耀竞争 | 留存 | 触发频次低 |
| 串门围观 | 社交互动 | 留存 | 接受最低(130‰) |
「赠礼互赠」与「串门围观」的综合转化排在后段(62、17),但它们不是为拉新设计的。一个玩家每天上线给好友送体力、去别人小岛转一圈,这个动作本身不带来新用户,但它把「每天回来看一眼」变成了习惯——它服务的是第 5 章那条留存曲线,不是本节这张拉新表。如果只按拉新排名去砍功能,很容易把这两个「低转化但高留存」的面误杀。
这也是为什么传播面必须按作用分开评估:拉新面看综合转化,留存面看它对 D1 / D7 的贡献。混在一张表里排序,会得出「砍掉串门围观」这种错误结论。
一张表能回答的三个设计问题
把传播面写成结构化配置,真正的价值在于它能回答立项阶段最常被问到的三个问题:
- 「我们的传播面够不够?」——七个面覆盖了七种动机形态,任何一款轻度小游戏,少于四五个面通常意味着裂变设计是残缺的。
- 「哪个面最值得投入?」——按综合转化排序,「好友助力」是投入产出最高的,因为它的短板(接受率 240‰)已经比「分享得奖励」(180‰)高一截。
- 「哪个面被高估了?」——「分享得奖励」触达最高、最常被当作主传播面,但综合转化只排第五。它适合「广撒网」,不适合当主力。
这份配置还有第二个用途:它是跨章节共享的真源。下一节算传播系数 K 时,acceptPermille 会直接进入 K 的乘法项;9.3 算社交压力保护时,又会用它去估「自然分享」的接受率。一份 config 被三处消费,任何一处改了数字,另外两处的结论会自动跟着变。
下一节把这七个面收成一个数:传播系数 K。K 是把「每用户发出多少邀请」和「邀请的接受率」乘在一起的一个系数,它决定一条增长曲线是往上翘还是往下沉——而 K 恰好卡在 1 的位置,是一条分水岭。
阅读导航:上一节:离线收益与回归设计 · 下一节:裂变模型与传播系数 K 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。