9.3 付费节奏与体验保护

付费点铺好了,还要决定它们什么时候出现,以及怎样不把免费玩家逼走。本节用 config/n09_protection.json 定义免费 / 付费玩家的日产出与内容需求里程碑,用 S 层脚本逐日算免费玩家达成度曲线、定位付费墙出现的天数,并对体验保护门槛做 PASS/WARN 判定与敏感性扫描。

9.1 定了付费点摆在哪,9.2 拆了三类常规付费点的价值。还剩两个问题:这些付费点在什么时候出现,以及怎样在赚钱的同时不把免费玩家逼走。这两个问题在本节合成一件事——付费节奏与体验保护。节奏是时间轴上的布局,保护是免费玩家的底线。前者管「什么时候卖」,后者管「免费玩家还能不能好好玩」。

先明确一个立场:体验保护不是慈善,是长期收入的前提。免费玩家是付费玩家的对手、队友和内容消费者——一个没有免费玩家的 SLG,付费玩家也留不住。所以保护免费体验,本质是保护付费玩家的游戏环境。本节把它写成可量化的门槛,而不是一句口号。

付费节奏:什么时候出现

付费节奏指的是每个付费点在生命周期第几天首次出现。全都在第 1 天铺上去,玩家一进游戏就被付费页糊脸,直接劝退;全都不出现,又浪费了转化窗口。本套的节奏表在 config/n09_protection.json 的 rhythm 里:

天数付费点说明
第 1 天首充新手首充,第 1 天即出现
第 1 天赛季通行证赛季开服即上架,绑定整个赛季
第 3 天月卡度过最早期后再上架,避免劝退
第 7 天资源礼包成长期第一次资源缺口时补给

节奏设计有两条原则:一是新手期只放「低门槛、高感知」的点(首充 6 元、通行证一眼看到全部奖励),让玩家先感受到「这游戏值得付一次」;二是把「资源缺口」和「付费点」对齐——资源礼包放在第 7 天,正是因为成长期第一次出现资源紧张,这时候玩家才有购买动机。在玩家不需要的时候卖东西,是无效曝光;在玩家最需要的时候卖,才是节奏。

体验保护:免费玩家的达成度

体验保护要回答一个具体问题:免费玩家能不能在合理时间内跟上内容。这里引入一个可量化的指标——达成度:

达成度(第 d 天) = 免费玩家累计资源 / 第 d 天的内容需求资源 × 100%
  • 累计资源 = 免费玩家日均产出 × 天数。
  • 内容需求资源 = 游戏到第 d 天累积释放的内容(城建、科技、兵种解锁)所需的资源总量,按里程碑插值。

达成度越高,说明免费玩家越跟得上;达成度首次跌破门槛的那一天,就是「付费墙」——从这里开始,免费玩家的进度明显落后于内容释放速度。下面这段是 D 层配置,定义免费 / 付费玩家的日产出、内容需求里程碑与保护门槛:

{
  "version": 1,
  "note": "第 9 章付费节奏与体验保护参数(n09 前缀)。免费/付费玩家日资源产出、内容需求里程碑、体验保护门槛与付费节奏表。资源一律整数单位,比率整数百分比,禁止浮点。",
  "players": {
    "free": { "dailyResource": 60000 },
    "payer": { "dailyResource": 180000 }
  },
  "contentMilestones": [
    { "day": 0, "requiredResource": 0 },
    { "day": 7, "requiredResource": 450000 },
    { "day": 14, "requiredResource": 1150000 },
    { "day": 30, "requiredResource": 3200000 },
    { "day": 60, "requiredResource": 7600000 }
  ],
  "protection": {
    "minDay7AchievementPct": 75,
    "minPaywallDay": 10,
    "achievementFloorPct": 80
  },
  "sensitivityFreeDaily": [54000, 60000, 66000],
  "rhythm": [
    { "day": 1, "point": "first_charge", "note": "新手首充,第 1 天即出现" },
    { "day": 1, "point": "battle_pass", "note": "赛季开服即上架,绑定整个赛季" },
    { "day": 3, "point": "monthly_card", "note": "度过最早期后再上架,避免劝退" },
    { "day": 7, "point": "resource_bundle", "note": "成长期第一次资源缺口时补给" }
  ]
}

保护门槛有三个:minDay7AchievementPct: 75(第 7 天达成度不低于 75%)、minPaywallDay: 10(付费墙不早于第 10 天)、achievementFloorPct: 80(达成度门槛,跌破即算付费墙)。下面这段是 S 层仿真脚本,逐日算达成度曲线、定位付费墙、做门槛判定与敏感性扫描:

# file: sim/n09_rhythm_protection.py
# tier: S
# verify: python3 sim/n09_rhythm_protection.py --config config/ --out out/
"""付费节奏与体验保护仿真:读 config/n09_protection.json 的免费 / 付费玩家日产出、
内容需求里程碑与保护门槛,逐日算免费玩家的达成度曲线(累计资源 / 内容需求),
找出「付费墙」出现的天数(达成度首次跌破门槛),并对保护门槛给出 PASS/WARN 判定,
再做免费日产出 ±20% 的敏感性扫描。产出 out/n09_protection.json + .csv + .svg。"""

from __future__ import annotations

import argparse
import csv
import json
import os

from svg_line import render


def load(cfg_dir: str, name: str) -> dict:
    with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as fh:
        return json.load(fh)


def required_at(day: int, milestones: list[dict]) -> int:
    """内容需求资源:在里程碑之间线性插值,超出最后里程碑按最后一段斜率外推。"""
    if day <= milestones[0]["day"]:
        return milestones[0]["requiredResource"]
    for i in range(1, len(milestones)):
        a, b = milestones[i - 1], milestones[i]
        if day <= b["day"]:
            span = b["day"] - a["day"]
            return a["requiredResource"] + (b["requiredResource"] - a["requiredResource"]) * (day - a["day"]) // span
    a, b = milestones[-2], milestones[-1]
    slope = (b["requiredResource"] - a["requiredResource"]) // (b["day"] - a["day"])
    return b["requiredResource"] + slope * (day - b["day"])


def achievement_pct(cum: int, required: int) -> int:
    if required <= 0:
        return 100
    return min(100, cum * 100 // required)


def paywall_day(daily: int, milestones: list[dict], floor_pct: int, horizon: int = 60) -> int:
    """达成度首次跌破门槛的天数;若整个窗口都达标返回 0。"""
    for d in range(1, horizon + 1):
        if achievement_pct(daily * d, required_at(d, milestones)) < floor_pct:
            return d
    return 0


def main() -> int:
    here = os.path.dirname(os.path.abspath(__file__))
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default=os.path.join(here, "..", "config"))
    ap.add_argument("--out", default=os.path.join(here, "..", "out"))
    args = ap.parse_args()
    os.makedirs(args.out, exist_ok=True)

    cfg = load(args.config, "n09_protection")
    ms = cfg["contentMilestones"]
    prot = cfg["protection"]
    free_daily = cfg["players"]["free"]["dailyResource"]
    payer_daily = cfg["players"]["payer"]["dailyResource"]
    horizon = ms[-1]["day"]

    rows = []
    curve = []
    for d in range(1, horizon + 1):
        req = required_at(d, ms)
        fc, pc = free_daily * d, payer_daily * d
        fa = achievement_pct(fc, req)
        pa = achievement_pct(pc, req)
        curve.append(fa)
        rows.append({"day": d, "required": req, "freeCum": fc, "freePct": fa, "payerPct": pa})

    wall = paywall_day(free_daily, ms, prot["achievementFloorPct"], horizon)
    day7 = achievement_pct(free_daily * 7, required_at(7, ms))

    checks = [
        {"id": "day7_achievement", "want": f">={prot['minDay7AchievementPct']}", "got": day7,
         "pass": day7 >= prot["minDay7AchievementPct"]},
        {"id": "paywall_day", "want": f">={prot['minPaywallDay']}", "got": wall,
         "pass": wall >= prot["minPaywallDay"]},
    ]

    sens = []
    for daily in cfg["sensitivityFreeDaily"]:
        w = paywall_day(daily, ms, prot["achievementFloorPct"], horizon)
        sens.append({"freeDaily": daily, "paywallDay": w, "day7Pct": achievement_pct(daily * 7, required_at(7, ms))})

    with open(os.path.join(args.out, "n09_protection.csv"), "w", encoding="utf-8", newline="") as fh:
        w = csv.writer(fh)
        w.writerow(["day", "required", "freeCum", "freePct", "payerPct"])
        for r in rows:
            w.writerow([r["day"], r["required"], r["freeCum"], r["freePct"], r["payerPct"]])

    render(curve, os.path.join(args.out, "n09_protection.svg"),
           "free player achievement pct by day (floor = 80)")

    report = {
        "version": 1, "freeDaily": free_daily, "payerDaily": payer_daily,
        "floorPct": prot["achievementFloorPct"], "paywallDay": wall, "day7AchievementPct": day7,
        "checks": checks, "allPass": all(c["pass"] for c in checks), "sensitivity": sens,
        "rhythm": cfg["rhythm"],
        "sample": [{"day": r["day"], "required": r["required"], "freePct": r["freePct"], "payerPct": r["payerPct"]}
                   for r in rows if r["day"] in (7, 14, 30, 60)],
    }
    with open(os.path.join(args.out, "n09_protection.json"), "w", encoding="utf-8") as fh:
        json.dump(report, fh, ensure_ascii=False, indent=2)
    print(json.dumps(report, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

本机实跑(python3 sim/n09_rhythm_protection.py --config config/ --out out/)产出 out/n09_protection.json 的关键字段:

freeDaily=60000  payerDaily=180000  floorPct=80
paywallDay=11  day7AchievementPct=93
checks: day7_achievement(>=75)=93 PASS ; paywall_day(>=10)=11 PASS
sensitivity: 54000->paywall 8, day7 84 | 60000->paywall 11, day7 93 | 66000->paywall 15, day7 100

out/n09_protection.csv 里几个里程碑天的达成度(免费 / 付费):

day,required,freeCum,freePct,payerPct
7,450000,420000,93,100
14,1150000,840000,73,100
30,3200000,1800000,56,100
60,7600000,3600000,47,100

读法:免费玩家第 7 天达成度 93%(内容需求 45 万,累计 42 万),跟得上;到第 14 天掉到 73%,第 30 天 56%,第 60 天 47%——内容释放速度超过了免费玩家的产出速度,达成度持续下滑。付费玩家(3 倍产出)全程 100%,从未落后。达成度首次跌破 80% 的门槛是在第 11 天(第 11 天累计 66 万,需求 85 万,达成度 77%),这就是付费墙的位置。

门槛判定:PASS 与敏感性

两个门槛判定都是 PASS:第 7 天 93% ≥ 75%、付费墙第 11 天 ≥ 第 10 天。但 PASS 不代表安全——脚本顺手做了免费日产出 ±10% 的敏感性扫描:

免费日产出第 7 天达成度付费墙天数判定
54000(-10%)84%8❌ 早于第 10 天
60000(基准)93%11✅
66000(+10%)100%15✅

免费日产出只要降 10%,付费墙就从第 11 天提前到第 8 天,直接跌破保护线。这揭示了一个关键事实:体验保护的裕度很窄。免费玩家的日产出是一个被很多系统牵动的数(建筑产出、活动补贴、税收)——任何一个系统调低它,都可能把付费墙往前推。所以保护门槛不能只看「当前 PASS」,必须看裕度:基准产出离「付费墙跌破第 10 天」的临界点有多远。本套的基准留了约 10% 的裕度,属于偏紧。

付费墙不是墙,是「加速比」

要正确理解付费墙,得先纠正一个直觉:付费墙不是「不付费就过不去」。看曲线——免费玩家第 60 天达成度 47%,但达成度是累计资源 / 需求,免费玩家并没有停下,只是速度慢。付费玩家 3 倍产出,是加速比 3:1,不是「能玩 / 不能玩」。

加速比 = 付费日产出 / 免费日产出 = 180000 / 60000 = 3

所以体验保护的正确表述不是「免费玩家能到 100%」,而是「免费玩家能在一个可接受的时间内达成核心目标」。核心目标(比如主城升到关键等级、解锁核心兵种)应该在免费玩家愿意投入的时间内可达。如果核心目标需要 90 天,而玩家平均只玩 30 天——那才叫体验崩了。保护的本质是「时间可达性」,不是「零付费可达性」。

这也解释了为什么付费墙定在「达成度跌破 80%」而不是「跌破 0%」:玩家不需要 100% 跟上内容,只需要感觉「我没被落下太多」。80% 是一个心理阈值——低于它,玩家开始明显感到「不付费就跟不上」,负面情绪开始累积。门槛设在哪里,是体验设计的选择;能不能守住,是数值设计的责任。

三个体验保护门槛

把本节的判据抽成三条可复用的门槛:

  1. 新手保护:第 7 天达成度 ≥ 75%。新手期是留存生死线,免费玩家在第一个星期必须「跟得上」。
  2. 付费墙位置:不早于第 10 天。太早出现付费墙,玩家还没建立情感投入就被劝退。
  3. 裕度:基准免费产出离付费墙临界点至少留 10%。只看当前 PASS 不够,要留缓冲——因为免费产出会被多个系统牵动。

三条门槛合起来,就是付费节奏与体验保护的验收标准:节奏表管「什么时候卖」,三条门槛管「免费玩家还留得住吗」。前者是主动作,后者是安全线。

边界必须说清:本节的内容需求里程碑(7 天 45 万、60 天 760 万)是设计假设,不是实测值。真实的内容释放速度取决于具体玩法设计,玩家真实的日产出还受在线时长、活动参与度影响。这些属于 X 层——需真实业务环境,命令行不可复现。本节能做的是:给定产出与需求曲线,算出达成度、付费墙与裕度;它做不了的是:告诉你需求曲线该定成多少。后者要靠真实留存与付费数据回归。

小结

付费节奏与体验保护 = 时间轴上的付费点布局 + 免费玩家达成度门槛。本节的 S 层脚本用 config/n09_protection.json 跑出免费玩家达成度曲线(第 7 天 93% → 第 60 天 47%)、定位付费墙在第 11 天、并做敏感性扫描证明「免费产出降 10% 就跌破保护线」。付费墙在哪、裕度够不够,从今天起是两个能被脚本复算的数。

一句话记住本节:节奏管什么时候卖,保护管免费玩家还留不留得住。前者是主动作,后者是安全线。

回到这一章的三节:9.1 铺布局、9.2 拆常规付费点、9.3 定节奏与保护线——付费点从「摆在哪、值多少、什么时候卖、会不会逼走人」四个问题,全部落成了可跑的数。下一章进入定价本身:给定这些付费点,价格该定几档、锚点怎么设。

阅读导航:上一节:9.2 首充、月卡、通行证 · 下一节:10.1 定价锚点与档位设计 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「slg」更多文章

  1. 12.3 留存与付费的联合分析
  2. 12.2 流失预警与召回
  3. 附录 C:全书公式、参数与数据集索引