10.1 自发行 vs 代理发行的决策

自发行还是交给代理发行,本质是两条现金流的比较:自发行自己掏买量预算、独享平台扣成后的收入;代理发行把买量交给发行商、让出分成,换来更大的流水与保底。本节用整数公式把「分成比例 × 流水情景」铺成一张净收入对照表,配可跑的 Python 仿真脚本。

站内有一篇代理发行合同与谈判指南 ,那是一份八百多行的合同与谈判长文:条款怎么读、话术怎么说、坑在哪里。本节不重写它,只回答它没算的那件事——给定一个分成比例和一份流水,自发行与代理发行到底哪个净收入更高。前九章我们把「怎么花钱买量、怎么在渠道上卖」讲完了;《上线变现与长线运营》 则把收入模型与回本讲透了。这一章补上最后一块商业拼图:要不要把发行这件事外包出去。分工很明确——合同文本归那篇长文,本节只做净收入的量化对照。

自发行与代理发行,差在两条现金流

把游戏交给平台卖,第一刀永远是平台扣成:无论谁发行,平台先拿走一部分,剩下的才叫「可分成收入」。自发行与代理发行的差别,出现在第二刀:

谁买量谁拿分成收入上限收入下限
自发行你自己(先付现金)你独享平台扣成后的全部高(不用分人)低(买亏了全自己扛)
代理发行发行商(用它的钱和资源)你与发行商按比例分看发行商能力有保底兜底

关键点在于:代理发行不是「白拿」。发行商承担了买量,所以它有理由要求分成;它放大流水的能力(本节叫 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,九个组合的净收入对照是(同机相对量级,绝对数值不可移植):

情景报价分成放大自发行净(分)代理净(分)Δ(分)决策
悲观p60600115016000001700000+100000代理(保底兜住)
悲观p30300120016000002352000+752000代理
悲观p50500180016000002520000+920000代理
中性p60600115044000002576000−1824000自发行
中性p30300120044000004704000+304000代理
中性p50500180044000005040000+640000代理
乐观p60600115086000004508000−4092000自发行
乐观p30300120086000008232000−368000自发行
乐观p50500180086000008820000+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 分成谈判的筹码与底线 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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