13.1 把 ARPDAU 拆成了 IAA 与 IAP 两块,得到 523‰ : 476‰ 这样一个「几乎五五开」的结果。但那个比例不是天上掉下来的,它是变现结构设计的直接后果。站内 小游戏平台专题(minigame) 那 25 篇是平台型 SaaS 的商业计划与财务口径——平台怎么定价、怎么抽成、财务模型怎么搭;本节是单款小游戏自己的收入结构——广告和内购各占多少、这个比例由什么决定。与第 10 章的分工也再明确一次:第 10 章讲形态与场景(放哪些广告位、卖哪些内购点),本节把这些形态换算成收入比例并比较不同结构的含义。LTV 与回本仍是卷三第 3 章的主题,本节只做结构拆解。
同一笔收入,不同的结构
13.1 的基准口径里,IAA 34.3 分、IAP 31.2 分。但换个设计,同一款游戏完全可以做出广告主导(IAA 占七成)或内购主导(IAP 占七成)的结构。结构变了,产品性格就变了:
- 广告主导:靠高频激励视频赚钱,用户不花钱也能玩得很好,付费意愿低。
- 内购主导:广告克制,靠礼包与通行证赚钱,付费深度高。
- 均衡混合:两块都做,风险分散。
问题在于:这三种结构,收入总量会差多少、收入构成会差多少?13.1 的模型只给了一个点,本节要把它变成一条线。
混合结构配置(D 层)
下面这段是 D 层配置:base 是基准口径(与 13.1 同一组数),structures 里每个候选结构用两个整数百分比改写基准——adLoadPct 缩放人均广告请求次数,arppuPct 缩放 ARPPU。这里编码了一个关键假设:广告看得多的用户,付费更浅(看广告免费拿奖励,就没必要掏钱),所以广告负载上调时 ARPPU 下调,反之亦然。
{
"version": 1,
"game": "merge-island",
"note": "混合变现结构(D 层)。base 是基准口径,structures 里每个结构用两个整数百分比改写它:adLoadPct 缩放人均广告请求次数,arppuPct 缩放 ARPPU(广告看得多的用户付费更浅)。比例一律整数百分比,金额一律「分」;真实结构需真实业务环境,命令行不可复现(X)。",
"base": {
"adRequestsPerDau": 26,
"fillPct": 88,
"ecpmFen": 1500,
"payRatePermille": 26,
"arppuFen": 1200
},
"structures": [
{ "id": "iaa-heavy", "name": "广告主导", "adLoadPct": 140, "arppuPct": 70 },
{ "id": "balanced", "name": "均衡混合", "adLoadPct": 100, "arppuPct": 100 },
{ "id": "iap-heavy", "name": "内购主导", "adLoadPct": 60, "arppuPct": 160 }
]
}
为什么用百分比缩放而不是直接写绝对数?因为这样能表达「相对基准的口径偏移」,而且不同结构可以共享同一套 fillPct / ecpmFen / payRatePermille——那些是平台或大盘变量,不该随你的结构选择而变。你的设计自由度体现在 adLoadPct 与 arppuPct 上:前者是你摆了多少广告位,后者是你设计了多深的付费点。
结构拆解仿真(S 层)
下面这段是 S 层仿真脚本,读 config/ch13_hybrid.json,对每个结构算出 IAA / IAP 两个 ARPDAU 分量,再把「IAA : IAP」折算成整数千分比,产出 JSON + CSV + 堆叠 SVG。
#!/usr/bin/env python3
# file: sim/ch13_hybrid.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch13_hybrid.py --config config/ --out out/
"""混合变现的收入结构:IAA : IAP 的整数千分比拆解。
读 config/ch13_hybrid.json 的基准口径与若干候选结构。每个结构用两个整数百分比
改写基准:adLoadPct 缩放人均广告请求次数,arppuPct 缩放 ARPPU(广告看得多的用户
付费更浅)。再按与 sim/ch13_revenue.py 相同的整数公式算出 IAA / IAP 两个 ARPDAU
分量,并把「IAA : IAP」的比例折算成整数千分比。
req = base.adRequestsPerDau * adLoadPct // 100
arppu = base.arppuFen * arppuPct // 100
iaa = req * fillPct * ecpmFen // 10000 (0.1 分/DAU)
iap = arppu * payRatePermille // 100 (0.1 分/DAU)
iaaPermille = iaa * 1000 // (iaa + iap);iapPermille = 1000 - iaaPermille
全程整数、地板除,无浮点。产出 out/ch13_hybrid.json + out/ch13_hybrid.csv + out/ch13_hybrid.svg。
注意:所有参数均为示例假设;真实结构需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
IAA_COLOR = "#2563eb"
IAP_COLOR = "#f59e0b"
def load(cfg_dir: str) -> dict:
with open(os.path.join(cfg_dir, "ch13_hybrid.json"), encoding="utf-8") as f:
return json.load(f)
def decompose(base: dict, st: dict) -> dict:
"""把一个结构拆成 IAA / IAP 两个 ARPDAU 分量与千分比。"""
req = base["adRequestsPerDau"] * st["adLoadPct"] // 100
arppu = base["arppuFen"] * st["arppuPct"] // 100
iaa = req * base["fillPct"] * base["ecpmFen"] // 10000
iap = arppu * base["payRatePermille"] // 100
total = iaa + iap
iaa_permille = iaa * 1000 // total if total else 0
return {
"id": st["id"], "name": st["name"],
"adLoadPct": st["adLoadPct"], "arppuPct": st["arppuPct"],
"adRequestsPerDau": req, "arppuFen": arppu,
"iaaArpdauDeciFen": iaa, "iapArpdauDeciFen": iap, "arpdauDeciFen": total,
"iaaPermille": iaa_permille, "iapPermille": 1000 - iaa_permille,
}
def draw_svg(path: str, rows: list[dict]) -> int:
"""纯 Python SVG:每个结构一根堆叠横条,左段 IAA、右段 IAP。"""
W, H, PAD, ROW = 760, 60 + 52 * len(rows), 150, 52
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": "26", "font-size": "15",
"fill": "#111827"}).text = (
"《合成小岛》混合变现结构:IAA / IAP 堆叠(宽度 ∝ 合计 ARPDAU,示例参数)"
)
lx = 16
for color, label in ((IAA_COLOR, "IAA 广告"), (IAP_COLOR, "IAP 内购")):
ET.SubElement(root, "rect", {"x": str(lx), "y": "38", "width": "12", "height": "12",
"rx": "2", "fill": color})
ET.SubElement(root, "text", {"x": str(lx + 17), "y": "49", "font-size": "11",
"fill": "#374151"}).text = label
lx += 96
bar_max = W - PAD - 24
hi = max(r["arpdauDeciFen"] for r in rows) or 1
for i, r in enumerate(rows):
y = 66 + i * ROW
ET.SubElement(root, "text", {"x": "16", "y": str(y + 20), "font-size": "12",
"fill": "#374151"}).text = r["name"]
full = bar_max * r["arpdauDeciFen"] // hi
w_iaa = full * r["iaaPermille"] // 1000
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(y + 6), "width": str(w_iaa),
"height": "22", "rx": "4", "fill": IAA_COLOR})
ET.SubElement(root, "rect", {"x": str(PAD + w_iaa), "y": str(y + 6),
"width": str(full - w_iaa), "height": "22", "rx": "4",
"fill": IAP_COLOR})
ET.SubElement(root, "text", {"x": str(PAD + full + 8), "y": str(y + 22),
"font-size": "11", "fill": "#6b7280"}).text = (
f'{r["iaaPermille"]}‰ : {r["iapPermille"]}‰ · 合计 {r["arpdauDeciFen"] / 10:.1f}分'
)
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()
cfg = load(args.config)
base = cfg["base"]
rows = [decompose(base, st) for st in cfg["structures"]]
os.makedirs(args.out, exist_ok=True)
svg_bytes = draw_svg(os.path.join(args.out, "ch13_hybrid.svg"), rows)
report = {
"version": cfg["version"],
"game": cfg["game"],
"base": base,
"structureCount": len(rows),
"structures": rows,
"permilleSumCheck": [r["iaaPermille"] + r["iapPermille"] for r in rows],
"csv": os.path.join(args.out, "ch13_hybrid.csv"),
"svg": os.path.join(args.out, "ch13_hybrid.svg"),
"svgBytes": svg_bytes,
}
with open(os.path.join(args.out, "ch13_hybrid.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "ch13_hybrid.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "name", "adRequestsPerDau", "arppuFen", "iaaArpdauDeciFen",
"iapArpdauDeciFen", "arpdauDeciFen", "iaaPermille", "iapPermille"])
for r in rows:
w.writerow([r["id"], r["name"], r["adRequestsPerDau"], r["arppuFen"],
r["iaaArpdauDeciFen"], r["iapArpdauDeciFen"], r["arpdauDeciFen"],
r["iaaPermille"], r["iapPermille"]])
print(json.dumps({
"structures": [{"name": r["name"], "iaaPermille": r["iaaPermille"],
"iapPermille": r["iapPermille"], "arpdauDeciFen": r["arpdauDeciFen"]}
for r in rows],
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
脚本用 iapPermille = 1000 - iaaPermille 而不是各自取整,是为了保证两者严格加起来等于 1000——比例表的自洽比各自四舍五入更重要。报告的 permilleSumCheck 会逐行验证这一点。
实跑结果
执行 /opt/homebrew/bin/python3 sim/ch13_hybrid.py --config config/ --out out/,输出:
{
"structures": [
{ "name": "广告主导", "iaaPermille": 685, "iapPermille": 315, "arpdauDeciFen": 693 },
{ "name": "均衡混合", "iaaPermille": 523, "iapPermille": 477, "arpdauDeciFen": 655 },
{ "name": "内购主导", "iaaPermille": 284, "iapPermille": 716, "arpdauDeciFen": 697 }
]
}
permilleSumCheck 三行都是 [1000, 1000, 1000]。CSV 里还留了每档的中间量:
id,name,adRequestsPerDau,arppuFen,iaaArpdauDeciFen,iapArpdauDeciFen,arpdauDeciFen,iaaPermille,iapPermille
iaa-heavy,广告主导,36,840,475,218,693,685,315
balanced,均衡混合,26,1200,343,312,655,523,477
iap-heavy,内购主导,15,1920,198,499,697,284,716
堆叠 SVG 一眼能看出构成差异(左蓝 IAA、右橙 IAP):广告主导那条几乎全是蓝的,内购主导几乎全是橙的,均衡混合蓝橙各半。三个结构的总长度几乎一样——这正是下一节要解释的现象。
三种结构各自的含义
把三档并排看(同机相对量级,绝对数值不可移植):
| 结构 | 广告请求 | ARPPU | IAA ARPDAU | IAP ARPDAU | 合计 | IAA : IAP |
|---|---|---|---|---|---|---|
| 广告主导 | 36 次 | 8.4 元 | 47.5 分 | 21.8 分 | 69.3 分 | 685‰ : 315‰ |
| 均衡混合 | 26 次 | 12 元 | 34.3 分 | 31.2 分 | 65.5 分 | 523‰ : 477‰ |
| 内购主导 | 15 次 | 19.2 元 | 19.8 分 | 49.9 分 | 69.7 分 | 284‰ : 716‰ |
三条可操作的结论:
- 结构决定「钱从哪来」,也决定「谁在付钱」。广告主导的收入来自绝大多数不付费用户,你依赖的是用户规模;内购主导的收入来自少数高付费用户,你依赖的是付费深度。前者的风险是 eCPM 与填充率波动(X,平台变量),后者的风险是付费用户流失。
- 广告主导的广告请求是均衡的 1.4 倍(36 vs 26)。这不是免费的——更多广告意味着更强的打断,第 10.3 节讲的「变现与体验平衡」在这里有直接代价:结构越偏广告,留存压力越大(X,需真实业务环境验证)。
- 内购主导要求 ARPPU 高达 19.2 元,是广告主导的 2.3 倍。这需要足够深的付费点设计(礼包、通行证、装扮),不是随便挂个充值入口就能达到的。
选择哪个结构,不是「哪个赚钱多」的问题(三档总量接近),而是「你的产品性格与团队能力匹配哪个」的问题。
结构的边界:为什么总收入几乎不变
细心的读者会发现:三档的合计 ARPDAU 是 69.3 / 65.5 / 69.7,差别很小。这不是巧合,而是本模型设计使然——广告负载上调(+40%)时 ARPPU 下调(−30%),两者对总收入的贡献此消彼长,于是总量大致守恒。
这个设计传达的是一个真实经验:在同一个大盘里,混合结构主要改变的是收入构成,而不是收入总量。但它也有明确的边界:
- 总量守恒是假设,不是定律。真实世界里,广告负载与 ARPPU 的替代关系不是线性的(X,需真实业务环境),过度堆广告可能同时压低 ARPPU 和留存,总量反而下降。
- 模型没有 DAU 的反馈。广告弹太多 → 留存掉 → DAU 掉,这个回路本节不建模(与 13.1 的边界一致)。
- ARPPU 与付费率被当成独立参数,但内购主导结构往往伴随更高的付费率(更多付费点 = 更多人付费),本模型没有耦合这两者。
换句话说:本节回答「结构改变了什么」,不回答「哪个结构最优」——后者需要引入留存与体验成本,属于第 10.3 节与卷三的范畴。
小结
本节把 ARPDAU 拆成了 IAA : IAP 的整数千分比,用「广告负载」与「ARPPU」两个整数百分比改写基准口径,跑出三种结构:广告主导 685‰ : 315‰、均衡混合 523‰ : 477‰、内购主导 284‰ : 716‰。关键结论是结构主要改变收入构成,而非收入总量,而选哪种结构取决于产品性格与团队能力。下一节把「给定结构」推进到「不确定时怎么办」——用悲观 / 基准 / 乐观三档情景,以及「哪个参数最影响结论」的敏感性排序,给收入预测一个区间。
阅读导航:上一节:收入拆解:DAU × ARPDAU · 下一节:收入预测与情景分析 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。