上一节把广告(IAA)这一半布局清楚了:激励视频打底、插屏补位、横幅原生兜底,示例配置算出 34.3 分/DAU 的广告收益。这一节补上另一半——内购(IAP,In-App Purchase),以及两者拼在一起时的混合变现结构。同样先划边界:内购点定价策略、混合比例协同、礼包通行证的深度建模是卷三第 2 章的主题,本节只做变现设计前置——在写商城之前,先把「有哪些内购点、每个点卖给谁、能贡献多少」想清楚。
如果只记得一句话:内购卖的不是道具,是动机。上一节那篇独立开发者以为靠广告就能赚钱的失败复盘 里,开发者把广告当成了唯一的收入假设;而一款能活下来的轻度小游戏,通常是广告打底、内购提上限。这一节就是把「提上限」的那部分拆开。
四类付费动机,对应四类内购点
轻度小游戏的内购点看起来五花八门,但按玩家为什么掏钱,其实只有四类动机:
- 买省心(去广告):玩家不讨厌游戏,只是讨厌被打断。去广告月卡卖的是「接下来 30 天不弹插屏」。注意——它和上一节的 IAA 收入直接对立:买了月卡的玩家不再贡献广告收入。
- 买时间(加速):体力礼包、加速道具,卖的是「跳过等待」。适合放置、合成这类有等待节奏的玩法。
- 买面子(装扮):限定皮肤、岛屿装扮,卖的是「被别的玩家看到」。它的付费动机来自社交展示,第一节 9.1 讲的社交面越强,这类越好卖。
- 买确定性(成长基金 / 首充):一次性付费换长期回报,卖的是「我已经投入了,不能浪费」。它同时制造沉没成本,提升留存。
四类动机对应四类内购点,价格带和购买率差别很大。把它们写成配置,就能看清结构,而不是凭感觉摆礼包。
这里要多说一句「首充礼包」,因为它同时踩中两类动机。首充卖的是「确定性」——一次性小额付费换一份超值回报;但它更隐蔽的作用是打破付费心理门槛。一个从没付过钱的玩家,第一次掏 6 元和第一次掏 60 元的心理阻力完全不同:前者是「试一下」,后者是「我是不是上瘾了」。所以首充的定价通常压得很低、给的东西却很超值,目的不是赚这 6 元,而是把「非付费用户」转成「付费用户」——一旦跨过这道线,后续月卡、基金的转化率会显著提高。这个「首充拉新付费」的机制,是内购结构里最值得单独设计的一环。
D 层:把内购点写成价格 + 日均购买率
下面这份是 D 层配置。它把每个内购点写成「价格(分)+ 日均购买千分比」,并额外带一个假设的 IAA 日收益——也就是上一节算出来的 343 deciFen,好让本节直接把两半拼起来看结构。
{
"version": 1,
"game": "merge-island",
"note": "内购点与混合变现结构(S 层)。priceFen 与 purchaseRatePermille 均为示例假设参数;purchaseRatePermille 是「日均购买千分比」(把一次性购买按周期摊平)。真实付费分布需真实业务环境,命令行不可复现(X)。",
"iaaArpdauDeciFen": 343,
"iapPoints": [
{ "id": "first-charge", "name": "首充礼包", "priceFen": 600, "purchaseRatePermille": 8, "oneTime": true },
{ "id": "monthly-pass", "name": "去广告月卡", "priceFen": 1800, "purchaseRatePermille": 5, "oneTime": false },
{ "id": "stamina-pack", "name": "体力礼包", "priceFen": 600, "purchaseRatePermille": 6, "oneTime": false },
{ "id": "deco-pack", "name": "装扮礼包", "priceFen": 1200, "purchaseRatePermille": 4, "oneTime": false },
{ "id": "growth-fund", "name": "成长基金", "priceFen": 3000, "purchaseRatePermille": 3, "oneTime": true }
]
}
两个字段要解释清楚。priceFen 是定价,单位分(600 分 = 6 元)。purchaseRatePermille 是日均购买千分比——每 1000 个日活里,平均每天有多少次购买。这里有个必须点破的坑:月卡是一个月买一次的东西,不是每天买。所以这个字段是「把一次性购买按周期摊平后的日均值」,这样不同周期的东西才能放进同一张表比较。oneTime 标记区分一次性礼包(首充、基金)和周期性商品(月卡、体力、装扮)。
同样必须标注:价格和购买率全是示例假设参数,真实付费分布取决于产品、渠道和运营,属于「需真实业务环境,命令行不可复现」的范畴(X)。本节能算的是结构占比,不是「你能收多少钱」。
S 层:算内购贡献与 IAA / IAP 收入构成
有了配置,就能把每个内购点的日收益和整体的收入构成算出来。下面这段是 S 层仿真脚本,全程整数,收益用 0.1 分记账,产出 JSON + CSV + SVG,并用一条 1000‰ 宽的堆叠横条画出混合结构。
#!/usr/bin/env python3
# file: sim/ch10_iap.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch10_iap.py --config config/ --out out/
"""内购点与混合变现结构:读 config/ch10_iap.json 的内购点表与假设的 IAA 日收益,
算出每个内购点的日均收益与 IAA / IAP 的收入构成(整数千分比),
产出 JSON + CSV + SVG。全程整数,收益用「0.1 分」的 deciFen 记账。
公式(整数地板除):
iapRevenueDeciFen = priceFen * purchaseRatePermille // 100
sharePermille = part * 1000 // total
注意:priceFen 与 purchaseRatePermille 均为示例假设参数,真实付费分布
需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load(cfg_dir):
with open(os.path.join(cfg_dir, "ch10_iap.json"), encoding="utf-8") as f:
return json.load(f)
def iap_revenue_deci_fen(price_fen, rate_permille):
"""日均收益(0.1 分):price*(rate/1000) 分 ×10。"""
return price_fen * rate_permille // 100
def draw_svg(path, iaa, iap_total, points, iaa_share, iap_share):
"""纯 Python SVG:一条 1000‰ 宽的堆叠横条,展示 IAA / IAP 及内购点构成。"""
W, H = 720, 220
PAD, BAR_Y, BAR_H = 40, 96, 44
bar_w = W - PAD * 2
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 = (
"《合成小岛》混合变现结构(示例参数,千分比)"
)
colors = ["#2563eb", "#059669", "#f59e0b", "#7c3aed", "#dc2626"]
x = PAD
# IAA 段
w_iaa = bar_w * iaa_share // 1000
ET.SubElement(svg, "rect", {"x": str(x), "y": str(BAR_Y), "width": str(w_iaa), "height": str(BAR_H),
"fill": "#0ea5e9"})
ET.SubElement(svg, "text", {"x": str(x + w_iaa // 2), "y": str(BAR_Y + 26), "font-size": "12",
"text-anchor": "middle", "fill": "#ffffff"}).text = f"IAA {iaa_share}‰"
x += w_iaa
# IAP 各点段
for i, p in enumerate(points):
w = bar_w * p["sharePermille"] // 1000
ET.SubElement(svg, "rect", {"x": str(x), "y": str(BAR_Y), "width": str(w), "height": str(BAR_H),
"fill": colors[i % len(colors)]})
x += w
ET.SubElement(svg, "text", {"x": str(PAD), "y": str(BAR_Y + BAR_H + 26), "font-size": "12", "fill": "#374151"}).text = (
f"IAA {iaa / 10:.1f}分/DAU({iaa_share}‰) · IAP {iap_total / 10:.1f}分/DAU({iap_share}‰)"
)
# 图例
lx, ly = PAD, BAR_Y + BAR_H + 48
ET.SubElement(svg, "rect", {"x": str(lx), "y": str(ly), "width": "12", "height": "12", "rx": "2", "fill": "#0ea5e9"})
ET.SubElement(svg, "text", {"x": str(lx + 17), "y": str(ly + 11), "font-size": "11", "fill": "#374151"}).text = "IAA"
lx += 66
for i, p in enumerate(points):
ET.SubElement(svg, "rect", {"x": str(lx), "y": str(ly), "width": "12", "height": "12", "rx": "2",
"fill": colors[i % len(colors)]})
ET.SubElement(svg, "text", {"x": str(lx + 17), "y": str(ly + 11), "font-size": "11", "fill": "#374151"}).text = p["name"]
lx += 24 + 13 * len(p["name"])
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()
cfg = load(args.config)
iaa = cfg["iaaArpdauDeciFen"]
points = []
for p in cfg["iapPoints"]:
rev = iap_revenue_deci_fen(p["priceFen"], p["purchaseRatePermille"])
points.append({
"id": p["id"], "name": p["name"], "priceFen": p["priceFen"],
"purchaseRatePermille": p["purchaseRatePermille"], "oneTime": p["oneTime"],
"revenueDeciFen": rev,
})
iap_total = sum(p["revenueDeciFen"] for p in points)
total = iaa + iap_total
for p in points:
p["sharePermille"] = p["revenueDeciFen"] * 1000 // total if total else 0
iaa_share = iaa * 1000 // total if total else 0
iap_share = iap_total * 1000 // total if total else 0
os.makedirs(args.out, exist_ok=True)
svg_bytes = draw_svg(os.path.join(args.out, "ch10_iap.svg"), iaa, iap_total, points, iaa_share, iap_share)
report = {
"version": cfg["version"],
"game": cfg["game"],
"iaaArpdauDeciFen": iaa,
"iapArpdauDeciFen": iap_total,
"arpdauDeciFen": total,
"iaaSharePermille": iaa_share,
"iapSharePermille": iap_share,
"shareSumPermille": iaa_share + iap_share,
"points": points,
"svg": os.path.join(args.out, "ch10_iap.svg"),
"svgBytes": svg_bytes,
}
with open(os.path.join(args.out, "ch10_iap.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "ch10_iap.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "name", "priceFen", "purchaseRatePermille", "oneTime", "revenueDeciFen", "sharePermille"])
for p in points:
w.writerow([p["id"], p["name"], p["priceFen"], p["purchaseRatePermille"], p["oneTime"],
p["revenueDeciFen"], p["sharePermille"]])
print(json.dumps({"arpdauDeciFen": total, "iaaSharePermille": iaa_share,
"iapSharePermille": iap_share, "shareSumPermille": iaa_share + iap_share},
ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑它,输出是这样的(同机相对量级,绝对数值不可移植):
{"arpdauDeciFen": 655, "iaaSharePermille": 523, "iapSharePermille": 476, "shareSumPermille": 999}
读法:IAA 343 + IAP 312 = 655 deciFen,也就是 65.5 分/DAU。其中广告占 523‰、内购占 476‰——一个接近五五开的混合结构。逐内购点看 out/ch10_iap.csv:
| 内购点 | 价格(分) | 日均购买‰ | 一次性 | 日收益(0.1分) | 占比‰ |
|---|---|---|---|---|---|
| 去广告月卡 | 1800 | 5 | 否 | 90 | 137 |
| 成长基金 | 3000 | 3 | 是 | 90 | 137 |
| 首充礼包 | 600 | 8 | 是 | 48 | 73 |
| 装扮礼包 | 1200 | 4 | 否 | 48 | 73 |
| 体力礼包 | 600 | 6 | 否 | 36 | 54 |
两个「贵」的内购点(月卡 18 元、基金 30 元)虽然日均购买率最低,却贡献了和首充、装扮并列的最高收益——因为单价高。而购买率最高的首充(8‰)因为只卖 6 元,收益反而不突出。这就是内购结构和广告结构的根本差别:广告靠量(次数 × eCPM),内购靠价(单价 × 付费率),两条曲线的斜率不同。
混合变现的两个结构性判断
把 IAA 和 IAP 拼起来,会冒出两个必须正面回答的问题。
第一,去广告月卡是不是在拆自己的台? 表面上,月卡让玩家不看广告,等于主动放弃广告收入。但算总账时,月卡带来两样东西:一是即时的 18 元现金(在本模型里折成 90 deciFen/DAU,占内购的 29%);二是留存提升——不再被插屏打断的玩家,生命周期更长,反过来给 IAA 贡献了更多「能看的广告」。所以月卡的定价不该只覆盖「放弃的广告收入」,还要算上留存红利。这两者的量化对照,正是卷三第 2 章「混合比例协同」要做的。
第二,IAA 和 IAP 是竞争还是协同? 在本模型里,两者共同服务同一批用户的不同时刻:玩家在等待时看广告(IAA),在想要确定性时买礼包(IAP)。真正的冲突只在「去广告」这一个点上。设计上要做的,是把冲突点收窄到最小——只让月卡去掉「强制插屏」,保留激励视频(玩家自愿看,不算被打断),这样冲突面就从「全部广告」缩小到「插屏」,协同面被保住。
落到设计上的三条规则
- 内购点按动机分四类铺,别只铺一类。 只做加速,会筛掉不吃等待的玩家;只做装扮,会依赖社交强度。四类动机覆盖四类人,结构才稳。
- 把「日均购买千分比」写进配置,而不是记住一个付费率。 不同周期商品必须摊平到同一时间尺度才能比较;把摊平逻辑写进配置,账才算得清。
- 去广告是唯一的结构性冲突点,要主动设计它的边界。 默认只去掉强制插屏、保留激励视频,把 IAA 和 IAP 从「互相拆台」变成「各管一段」。
还要强调一点:本节的「五五开」结构是示例参数算出来的一个点,不是普适结论。换一组购买率,IAA / IAP 的比例就会变;有些休闲游戏是 80/20,有些中重度是 30/70。本节真正给出的,是一套把两半拼起来、算清结构的方法——同一份配置、同一个脚本,换数字就能重算,这才是可复用的东西。
到这里,IAA 和 IAP 两半都摆上了桌。但一个真正的难题还悬着:广告频次每加一档,收益涨多少、留存掉多少,加到哪里会得不偿失?上一节我们把广告频次当成给定值,这一节把内购当成给定值,都没有回答「频次本身该定多少」。
下一节用一个完整的权衡模型来回答这个问题——把「频次 → 日收益」和「频次 → 留存」两条曲线乘起来,找净 LTV 的峰值。这也是本章唯一带 B 基准敏感性分析的一节:我们会扰动模型参数,看最优频次到底稳不稳。
阅读导航:上一节:IAA 广告形态与场景布局 · 下一节:变现与体验的平衡 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。