站内有一篇代理发行合同与谈判指南 ,那是一份八百多行的合同与谈判长文:条款怎么读、话术怎么说、坑在哪里。本节不重写它,只回答它没算的那件事——给定一个分成比例和一份流水,自发行与代理发行到底哪个净收入更高。前九章我们把「怎么花钱买量、怎么在渠道上卖」讲完了;《上线变现与长线运营》 则把收入模型与回本讲透了。这一章补上最后一块商业拼图:要不要把发行这件事外包出去。分工很明确——合同文本归那篇长文,本节只做净收入的量化对照。
自发行与代理发行,差在两条现金流
把游戏交给平台卖,第一刀永远是平台扣成:无论谁发行,平台先拿走一部分,剩下的才叫「可分成收入」。自发行与代理发行的差别,出现在第二刀:
| 谁买量 | 谁拿分成 | 收入上限 | 收入下限 | |
|---|---|---|---|---|
| 自发行 | 你自己(先付现金) | 你独享平台扣成后的全部 | 高(不用分人) | 低(买亏了全自己扛) |
| 代理发行 | 发行商(用它的钱和资源) | 你与发行商按比例分 | 看发行商能力 | 有保底兜底 |
关键点在于:代理发行不是「白拿」。发行商承担了买量,所以它有理由要求分成;它放大流水的能力(本节叫 uplift)就是它要价的依据。而自发行虽然独享收入,却必须先掏一笔买量预算,且这笔钱花砸了没人陪你分担。所以这不是「哪个更好」,而是在一个具体的分成比例与流水规模下,两条现金流谁更厚。
决策公式:两条现金流,一个比较
把上面两条路写成整数公式,单位统一用「分」:
selfNetFen = grossFen × platformNetPermille / 1000 − selfUaFen
pubGrossFen = grossFen × upliftPermille / 1000
pubNetFen = max( minGuaranteeFen,
pubGrossFen × platformNetPermille / 1000 × (1000 − publisherSharePermille) / 1000 )
deltaFen = pubNetFen − selfNetFen
decision = "publisher" if deltaFen > 0 else "self"
四个参数各自对应一个现实量:platformNetPermille 是平台扣成后你能分到的比例;selfUaFen 是自发行必须自己掏的买量预算;upliftPermille 是发行商把流水放大到的倍数(发行能力越强越高);publisherSharePermille 是发行商拿走的比例。minGuaranteeFen 是保底——哪怕流水一塌糊涂,发行商也得按这个数付你,所以它用 max 兜住下限。全部整数地板除,没有浮点。
S 层:把「分成 × 流水」铺成一张表
下面这段是 S 层仿真脚本 sim/o10_decision.py。它读 config/o10_decision.json 的三个流水情景与三个发行报价,把 3×3 共九个组合的净收入、差额与决策全部算出来,并产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/o10_decision.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/o10_decision.py --config config/ --out out/
"""自发行 vs 代理发行净收入决策仿真:把「分成比例 × 流水情景」铺成一张净收入对照表。
读 config/o10_decision.json。对每个(情景 scenario, 报价 offer)组合:
selfNetFen = grossFen * platformNetPermille // 1000 - selfUaFen
pubGrossFen = grossFen * upliftPermille // 1000
pubNetFen = max(minGuaranteeFen,
pubGrossFen * platformNetPermille // 1000
* (1000 - publisherSharePermille) // 1000)
deltaFen = pubNetFen - selfNetFen
decision = "publisher" if deltaFen > 0 else "self"
自发行要自己掏买量预算(selfUaFen),代理发行由发行商承担买量(故不扣 UA),
但要把可分成流水的 publisherSharePermille 让给发行商;minGuaranteeFen 是保底,
兜住下限。全程整数、地板除,无浮点。产出 out/o10_decision.json + .csv + .svg。
分成比例、发行商买量放大倍数、保底额都是「行业惯例」口径的建模假设;真实报价与
真实流水需真实业务环境,命令行不可复现(X)。本文不提供任何法律意见。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load(d: str) -> dict:
with open(os.path.join(d, "o10_decision.json"), encoding="utf-8") as f:
return json.load(f)
def evaluate(cfg: dict, scenario: dict, offer: dict) -> dict:
"""一个(情景, 报价)组合的净收入与决策。"""
plat = cfg["platformNetPermille"]
self_net = scenario["grossFen"] * plat // 1000 - cfg["selfUaFen"]
pub_gross = scenario["grossFen"] * offer["upliftPermille"] // 1000
pub_net_raw = pub_gross * plat // 1000 * (1000 - offer["publisherSharePermille"]) // 1000
pub_net = max(offer["minGuaranteeFen"], pub_net_raw)
delta = pub_net - self_net
return {
"scenarioId": scenario["id"], "scenarioName": scenario["name"], "grossFen": scenario["grossFen"],
"offerId": offer["id"], "offerName": offer["name"],
"publisherSharePermille": offer["publisherSharePermille"], "upliftPermille": offer["upliftPermille"],
"selfNetFen": self_net, "pubGrossFen": pub_gross, "pubNetRawFen": pub_net_raw,
"guaranteeBound": offer["minGuaranteeFen"] > pub_net_raw,
"pubNetFen": pub_net, "deltaFen": delta, "decision": "publisher" if delta > 0 else "self",
}
def evaluate_all(cfg: dict) -> list[dict]:
return [evaluate(cfg, s, o) for s in cfg["scenarios"] for o in cfg["offers"]]
def draw_svg(path: str, rows: list[dict], scenarios: list[dict], offers: list[dict]) -> int:
"""纯 Python SVG:每个情景一组,每个报价两根条——自发行净收入 vs 代理发行净收入(分)。"""
W, ROW, PAD = 760, 96, 150
H = 80 + ROW * len(scenarios)
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": "28", "font-size": "15",
"fill": "#111827"}).text = "《合成小岛》自发行 vs 代理发行净收入(分)"
hi = max([max(r["selfNetFen"], r["pubNetFen"]) for r in rows] + [1])
span = W - PAD - 24
for i, s in enumerate(scenarios):
y = 56 + i * ROW
ET.SubElement(root, "text", {"x": "16", "y": str(y + 12), "font-size": "12",
"fill": "#374151"}).text = f'{s["name"]}(流水 {s["grossFen"]} 分)'
for j, o in enumerate(offers):
r = next(x for x in rows if x["scenarioId"] == s["id"] and x["offerId"] == o["id"])
yb = y + 18 + j * 24
for k, (val, color, tag) in enumerate([(r["selfNetFen"], "#94a3b8", "自"),
(r["pubNetFen"], "#2563eb" if r["decision"] == "publisher" else "#f59e0b", "代")]):
w = span * max(val, 0) // hi
ET.SubElement(root, "rect", {"x": str(PAD), "y": str(yb + k * 10), "width": str(w),
"height": "9", "rx": "2", "fill": color})
ET.SubElement(root, "text", {"x": str(PAD + span + 2), "y": str(yb + 14), "font-size": "10",
"fill": "#6b7280"}).text = f'{o["id"]} Δ{r["deltaFen"]} {r["decision"][:3]}'
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/")
a = ap.parse_args()
cfg = load(a.config)
rows = evaluate_all(cfg)
pub_count = sum(1 for r in rows if r["decision"] == "publisher")
# 跨语言对拍摘要:core 侧复算同一串数字,逐位比对(本节 S 层;10.3 给出 P 层对拍示例)。
digest = ",".join(f'{r["scenarioId"]}:{r["offerId"]}:{r["selfNetFen"]}:{r["pubNetFen"]}:{r["decision"]}'
for r in rows)
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "o10_decision.svg"), rows, cfg["scenarios"], cfg["offers"])
with open(os.path.join(a.out, "o10_decision.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
cols = ("scenarioId", "scenarioName", "grossFen", "offerId", "offerName",
"publisherSharePermille", "upliftPermille", "selfNetFen", "pubGrossFen",
"pubNetRawFen", "guaranteeBound", "pubNetFen", "deltaFen", "decision")
w.writerow(cols)
for r in rows:
w.writerow([r[k] for k in cols])
report = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"platformNetPermille": cfg["platformNetPermille"], "selfUaFen": cfg["selfUaFen"],
"scenarioCount": len(cfg["scenarios"]), "offerCount": len(cfg["offers"]),
"publisherWins": pub_count, "selfWins": len(rows) - pub_count,
"rows": rows, "digest": digest,
"svg": os.path.join(a.out, "o10_decision.svg"), "svgBytes": svg}
with open(os.path.join(a.out, "o10_decision.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({
"publisherWins": pub_count, "selfWins": len(rows) - pub_count,
"cells": [{"s": r["scenarioId"], "o": r["offerId"], "selfNet": r["selfNetFen"],
"pubNet": r["pubNetFen"], "delta": r["deltaFen"], "decision": r["decision"],
"guaranteeBound": r["guaranteeBound"]} for r in rows],
"digest": digest}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
参数放在 config/o10_decision.json(D 层配置)。分成比例、放大倍数、保底都是「行业惯例」口径的建模假设,不是任何平台或发行商后台的实测。
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "自发行 vs 代理发行的净收入决策建模假设,口径是「行业惯例」的公开可观察机制,不是任何平台或发行商后台的实测,也不构成法律意见。platformNetPermille=平台扣成后开发者/发行商可分成收入占比(千分比);selfUaFen=自发行必须自己掏的买量预算(分);upliftPermille=发行商买量把流水放大到的千分比;publisherSharePermille=发行商拿走可分成收入的比例(千分比);minGuaranteeFen=保底金额(分)。真实报价与真实流水需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"platformNetPermille": 700,
"selfUaFen": 1200000,
"scenarios": [
{ "id": "bear", "name": "悲观流水", "grossFen": 4000000 },
{ "id": "base", "name": "中性流水", "grossFen": 8000000 },
{ "id": "bull", "name": "乐观流水", "grossFen": 14000000 }
],
"offers": [
{ "id": "p60", "name": "发行C·分成60%·弱放大", "publisherSharePermille": 600, "upliftPermille": 1150, "minGuaranteeFen": 1700000 },
{ "id": "p30", "name": "发行A·分成30%·中放大", "publisherSharePermille": 300, "upliftPermille": 1200, "minGuaranteeFen": 1000000 },
{ "id": "p50", "name": "发行B·分成50%·强放大", "publisherSharePermille": 500, "upliftPermille": 1800, "minGuaranteeFen": 2500000 }
]
}
在本机跑 for f in sim/o10_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done,九个组合的净收入对照是(同机相对量级,绝对数值不可移植):
| 情景 | 报价 | 分成 | 放大 | 自发行净(分) | 代理净(分) | Δ(分) | 决策 |
|---|---|---|---|---|---|---|---|
| 悲观 | p60 | 600 | 1150 | 1600000 | 1700000 | +100000 | 代理(保底兜住) |
| 悲观 | p30 | 300 | 1200 | 1600000 | 2352000 | +752000 | 代理 |
| 悲观 | p50 | 500 | 1800 | 1600000 | 2520000 | +920000 | 代理 |
| 中性 | p60 | 600 | 1150 | 4400000 | 2576000 | −1824000 | 自发行 |
| 中性 | p30 | 300 | 1200 | 4400000 | 4704000 | +304000 | 代理 |
| 中性 | p50 | 500 | 1800 | 4400000 | 5040000 | +640000 | 代理 |
| 乐观 | p60 | 600 | 1150 | 8600000 | 4508000 | −4092000 | 自发行 |
| 乐观 | p30 | 300 | 1200 | 8600000 | 8232000 | −368000 | 自发行 |
| 乐观 | p50 | 500 | 1800 | 8600000 | 8820000 | +220000 | 代理 |
九格里 6 格选代理、3 格选自发行。这张表读出来三件事。
怎么读这张表
第一,报价的「性价比」比报价本身更重要。 p60(分成 60%、放大 1.15×)在中性和乐观情景下都输给自发行——你让出六成收入,换来的流水放大只有 15%,根本不划算。而 p50(分成 50%、放大 1.8×)虽然让出更多,却因为放大倍数高,反而在多数情景下赢。分成高低不是关键,放大倍数才是。
第二,保底只在悲观情景里显形。 悲观情景下 p60 的代理净收入是 max(1700000, 1288000) = 1700000,guaranteeBound 为真——保底把结果从「代理净收入 1288000 < 自发行 1600000」硬生生抬到「代理净收入 1700000 > 自发行 1600000」,决策因此翻盘。这就是保底的价值:它买的不是收益,是下行保护。
第三,流水越大,自发行越占优。 看 p30 这一列:悲观、中性选代理,乐观却选自发行。原因是放大的收益是乘法,但让出的分成也是乘法——当你的基数足够大,让出 30% 的绝对金额会超过发行商带来的那点放大。这也解释了为什么大厂大多自发行、小团队常找代理。
X 层:真实报价与流水不可复现
上面算的是模型侧:给定分成、放大与保底,净收入差多少。下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 真实的平台扣成比例(各平台、各品类、各结算口径不同,且随协议浮动);
- 真实的发行商放大倍数(同一发行商对不同游戏的买量能力天差地别,签约前无法验证);
- 真实的保底金额与它的兑现条件(保底常带「可回收 / 有对赌」等附加条款,本节简化为一个固定下限)。
工程上正确的做法是:把可复现的决策模型与不可复现的商务条件分开——模型回答「如果发行商真能把流水放大到 1.8×,我该不该签」,真实数据要靠尽调与试投放去验证。把「发行商说它能放大 3 倍」直接填进 upliftPermille,只是把对方的承诺算了一遍,不是事实。
自检表
| 检查项 | 合格标准 |
|---|---|
| 两条现金流都建 | 自发行扣买量、代理发行扣分成,口径对等 |
| 保底显式建模 | 用 max(保底, 分成净收入),标出是否被保底兜住 |
| 情景铺开 | 至少悲观 / 中性 / 乐观三档,不只算一个数 |
| 参数可解释 | 每个参数对应一个现实量,不拍脑袋 |
| 决策有据 | 逐格给出 Δ 与决策,不笼统说「代理更好」 |
| 边界清晰 | 真实扣成 / 放大 / 保底标 X,不写成「实测」 |
自发行与代理发行不是信仰之争,是一道现金流比较题:把分成、放大、保底三个参数填进去,答案自己就出来了。真正难的不是算,是填对参数——这就要看下一节:当你坐到谈判桌前,手上到底有哪些筹码,底线又该定在哪里。
阅读导航:上一节:9.3 渠道包与差异化配置 · 下一节:10.2 分成谈判的筹码与底线 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。