站内有两个容易和本章撞车的地方,先把分工说清。小游戏服务平台专题 那 25 篇是做小游戏服务平台——怎么搭一个让开发者上传、分发、变现的 SaaS 平台;本章恰好相反,是在别人搭好的平台上做一款游戏的运营:不关心平台怎么建,只关心我这一个游戏的活动怎么排、奖励怎么发。H5 游戏产业全景 那篇是产业综述口径,讲这条赛道整体怎么长、钱从哪儿流;本章落到一款具体游戏的运营动作上,颗粒度完全不同。还要划清本章边界:变现结构是第 1–4 章、留存漏斗是第 7 章、版本列车与生命周期退场是第 13 章;本章只回答一个问题——活动怎么模板化、奖励怎么定量。
活动不是「灵感」,是「模板 + 预算」
小团队做活动最容易犯的错,是每次当全新创意从零设计:这次排行榜、下次收集、再下次签到,每套都要重新写需求、重新配表、重新压测。真正可持续的做法是把活动抽象成有限的几套模板,每次只换皮、换数值、换排期。模板固定下来之后,运营的边际成本才会下降,core/ 里也才有稳定的配置结构可以驱动。
模板之外还有一件更硬的事:奖励预算。活动几乎必然「赚钱」——发的是虚拟币,成本接近零,所以活动 ROI 天然很高,风险根本不在成本,而在通胀:发放出去的币如果远超回收,游戏内的经济会迅速贬值,之后所有定价、所有内购都会失锚。所以本章的核心不是「活动能不能赚」,而是「这次活动发多少币、回收多少、净通胀多少、有没有超预算」。
四类活动模板
《合成小岛》的活动收敛成四类,各自解决不同的运营目标。四类的差别用三个参数就能刻出来——覆盖人数、人均奖励、发放次数倍数:
- 排行榜赛(ranked):限时合成赛,按分数排名发奖。目标是拉高峰值活跃,奖励集中在头部,参与奖兜底。
- 收集兑换(collection):限定主题的收集与兑换。目标是拉长单次会话、提升中期留存,奖励按完成度分档。
- 每日签到(habit):每天登录领小奖。目标是养成登录习惯,单次奖励极低但发放次数很多(一个月签到 30 次),是通胀的主要来源。
- 回归返场(winback):给流失用户的回归礼包。目标是召回,人均奖励最高、覆盖人数最少。三个参数一乘,发放总量的量级就出来了——这正是预算要卡住的东西。
D 层:把模板和预算写成配置
下面这份是 D 层(静态层)配置:events 是四套模板,每套声明时长、参与人数、日活跃指数曲线、奖励档位(档位覆盖比例 sharePermille 之和 = 1000)、人均回收、发放次数倍数 grantMultiplierPermille 与奖励预算上限 budgetCapCoins;economy 给基准 DAU、基准 ARPDAU 与「每千枚币的机会成本」。全整数,禁浮点。
{
"version": 1,
"series": "《小游戏开发实战》",
"volume": "卷三《上线变现与长线运营》",
"game": "merge-island",
"observedAt": "2026-10",
"seed": 20261012,
"note": "活动模板与奖励预算配置(D 层)。events 是四套活动模板(排行榜赛 / 收集兑换 / 每日签到 / 回归返场),每套声明时长、参与人数、日活跃指数曲线、奖励档位(档位覆盖比例 sharePermille 之和 = 1000)、人均回收、发放次数倍数 grantMultiplierPermille 与奖励预算上限 budgetCapCoins;economy 给基准 DAU、基准 ARPDAU 与「每千枚币的机会成本」。奖励预算 = 发放总量上限;净通胀 = 发放 - 回收。全整数,禁浮点。participants / dailyIndexPermille / uplift 均为示例假设值,真实数值需真实业务环境,命令行不可复现(X)。",
"units": {
"sharePermille": "该奖励档位覆盖的参与者比例,同一活动各档之和 = 1000",
"rewardCoins": "该档位发放的岛屿币数量",
"grantMultiplierPermille": "每名参与者平均被发放的次数 ×1000(签到类 > 1000)",
"dailyIndexPermille": "以首日为 1000 的日活跃指数(相对量级,非绝对人数)",
"coinOpportunityCostPerMilleCents": "每千枚岛屿币折算的机会成本(分)"
},
"economy": {
"baseDau": 12000,
"baseArpdauCents": 12,
"coinOpportunityCostPerMilleCents": 100
},
"events": [
{
"id": "merge-race",
"name": "限时合成赛",
"template": "ranked",
"days": 7,
"participants": 4200,
"dailyIndexPermille": [1000, 820, 690, 610, 560, 520, 470],
"grantMultiplierPermille": 1000,
"tiers": [
{ "band": "前 1%", "sharePermille": 10, "rewardCoins": 800 },
{ "band": "前 10%", "sharePermille": 90, "rewardCoins": 300 },
{ "band": "前 50%", "sharePermille": 400, "rewardCoins": 80 },
{ "band": "参与奖", "sharePermille": 500, "rewardCoins": 20 }
],
"sinkCoinsPerParticipant": 60,
"budgetCapCoins": 500000,
"uplift": { "dauUpliftPermille": 80, "arpdauUpliftPermille": 60 }
},
{
"id": "collection",
"name": "海岛收集兑换",
"template": "collection",
"days": 14,
"participants": 6500,
"dailyIndexPermille": [700, 760, 800, 820, 810, 780, 760, 740, 720, 700, 680, 650, 620, 580],
"grantMultiplierPermille": 1000,
"tiers": [
{ "band": "集齐全套", "sharePermille": 10, "rewardCoins": 500 },
{ "band": "集齐过半", "sharePermille": 190, "rewardCoins": 120 },
{ "band": "零星收集", "sharePermille": 800, "rewardCoins": 30 }
],
"sinkCoinsPerParticipant": 40,
"budgetCapCoins": 600000,
"uplift": { "dauUpliftPermille": 50, "arpdauUpliftPermille": 40 }
},
{
"id": "checkin",
"name": "每日签到",
"template": "habit",
"days": 30,
"participants": 9000,
"dailyIndexPermille": [1000, 900, 840, 800, 780, 760, 740, 720, 700, 690, 680, 670, 660, 650, 640, 630, 620, 610, 600, 590, 580, 570, 560, 550, 540, 530, 520, 510, 500, 490],
"grantMultiplierPermille": 12000,
"tiers": [
{ "band": "每次签到", "sharePermille": 1000, "rewardCoins": 5 }
],
"sinkCoinsPerParticipant": 0,
"budgetCapCoins": 400000,
"uplift": { "dauUpliftPermille": 120, "arpdauUpliftPermille": 20 }
},
{
"id": "winback",
"name": "回归返场礼",
"template": "winback",
"days": 10,
"participants": 1500,
"dailyIndexPermille": [1000, 950, 900, 860, 830, 800, 780, 760, 740, 720],
"grantMultiplierPermille": 1000,
"tiers": [
{ "band": "回归礼包", "sharePermille": 1000, "rewardCoins": 200 }
],
"sinkCoinsPerParticipant": 150,
"budgetCapCoins": 400000,
"uplift": { "dauUpliftPermille": 200, "arpdauUpliftPermille": 150 }
}
]
}
S 层:奖励发放与预算仿真
下面这段是 S 层脚本:读上面这份配置,对每套模板算人均奖励(档位加权)、发放总量、净通胀、预算是否超支,以及活动 ROI(增量收入 ÷ 奖励机会成本)。产出 JSON + CSV + SVG。
#!/usr/bin/env python3
# file: sim/l12_event.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/l12_event.py --config config/ --out out/
"""活动模板与奖励预算仿真:读 config/l12_event.json。
对每套活动模板算四件事:
1) 人均奖励 = Σ(sharePermille * rewardCoins) // 1000(档位加权);
2) 发放总量 grossCoins = participants * 人均奖励 * grantMultiplierPermille // 1000;
3) 净通胀 netInflation = grossCoins - 回收(participants * sinkCoinsPerParticipant);
4) 预算校验 grossCoins <= budgetCapCoins,并算活动 ROI:
rewardCostCents = grossCoins * coinOpportunityCostPerMilleCents // 1000;
upliftRevenueCents = baseDau * days * (uplift 后的 ARPDAU - 基准 ARPDAU)。
产出:out/l12_event.json + .csv + .svg。全整数地板除,禁浮点。participants / uplift 均为示例
假设值,真实数值需真实业务环境,命令行不可复现(X)。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load(cfg_dir: str) -> dict:
with open(os.path.join(cfg_dir, "l12_event.json"), encoding="utf-8") as f:
return json.load(f)
def per_participant_reward(tiers: list) -> int:
return sum(t["sharePermille"] * t["rewardCoins"] for t in tiers) // 1000
def uplift_arpdau(base_cents: int, uplift: dict) -> int:
return base_cents * (1000 + uplift["dauUpliftPermille"]) * (1000 + uplift["arpdauUpliftPermille"]) // 1000000
def run_event(ev: dict, eco: dict) -> dict:
per = per_participant_reward(ev["tiers"])
gross = ev["participants"] * per * ev["grantMultiplierPermille"] // 1000
sink = ev["participants"] * ev["sinkCoinsPerParticipant"]
net = gross - sink
cap = ev["budgetCapCoins"]
cost_cents = gross * eco["coinOpportunityCostPerMilleCents"] // 1000
base_arpdau = eco["baseArpdauCents"]
new_arpdau = uplift_arpdau(base_arpdau, ev["uplift"])
uplift_cents = eco["baseDau"] * ev["days"] * (new_arpdau - base_arpdau)
roi = uplift_cents * 1000 // cost_cents if cost_cents else 0
return {
"id": ev["id"], "name": ev["name"], "template": ev["template"], "days": ev["days"],
"participants": ev["participants"], "perParticipantCoins": per,
"grantMultiplierPermille": ev["grantMultiplierPermille"],
"grossCoins": gross, "sinkCoins": sink, "netInflationCoins": net,
"budgetCapCoins": cap, "overBudget": gross > cap,
"rewardCostCents": cost_cents, "upliftRevenueCents": uplift_cents,
"upliftedArpdauCents": new_arpdau, "roiPermille": roi,
"peakDailyIndexPermille": max(ev["dailyIndexPermille"]),
"dailyIndexPermille": ev["dailyIndexPermille"],
}
def draw_svg(path: str, rows: list, cfg: dict) -> int:
W, ROW, TOP = 760, 42, 74
H = TOP + ROW * len(rows) + 150
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": "28", "font-size": "15", "fill": "#111827"}).text = \
"《合成小岛》活动奖励发放 vs 预算上限(示例参数,相对量级)"
ET.SubElement(svg, "text", {"x": "16", "y": "48", "font-size": "12", "fill": "#6b7280"}).text = \
"蓝=发放总量 红=预算上限 橙点=净通胀(发放-回收)"
maxv = max(max(r["grossCoins"], r["budgetCapCoins"]) for r in rows)
L, BW = 210, 440
for i, r in enumerate(rows):
y = TOP + i * ROW
ET.SubElement(svg, "text", {"x": "16", "y": str(y + 14), "font-size": "12", "fill": "#374151"}).text = r["name"]
g = BW * r["grossCoins"] // maxv
c = BW * r["budgetCapCoins"] // maxv
ET.SubElement(svg, "rect", {"x": str(L), "y": str(y), "width": str(max(g, 2)), "height": "14",
"rx": "3", "fill": "#2563eb" if not r["overBudget"] else "#dc2626"})
ET.SubElement(svg, "rect", {"x": str(L), "y": str(y + 16), "width": str(max(c, 2)), "height": "8",
"rx": "2", "fill": "#fca5a5"})
ET.SubElement(svg, "text", {"x": str(L + max(g, c) + 8), "y": str(y + 13), "font-size": "11",
"fill": "#6b7280"}).text = f"{r['grossCoins']} / 上限 {r['budgetCapCoins']}"
# 日活跃指数曲线(各活动归一叠画)
py0 = TOP + ROW * len(rows) + 34
ET.SubElement(svg, "text", {"x": "16", "y": str(py0 - 12), "font-size": "12", "fill": "#374151"}).text = \
"各活动日活跃指数曲线(首日 = 1000,相对量级)"
colors = ["#2563eb", "#10b981", "#f59e0b", "#8b5cf6"]
maxn = max(len(r["dailyIndexPermille"]) for r in rows)
px = lambda i, n: 60 + (W - 90) * i // max(1, n - 1)
py = lambda v: py0 + 100 - 90 * v // 1000
for k, r in enumerate(rows):
d = r["dailyIndexPermille"]
pts = " ".join(f"{px(i, len(d))},{py(v)}" for i, v in enumerate(d))
ET.SubElement(svg, "polyline", {"points": pts, "fill": "none", "stroke": colors[k % 4], "stroke-width": "2"})
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
return os.path.getsize(path)
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)
eco = cfg["economy"]
rows = [run_event(ev, eco) for ev in cfg["events"]]
total_gross = sum(r["grossCoins"] for r in rows)
total_sink = sum(r["sinkCoins"] for r in rows)
over = [r["id"] for r in rows if r["overBudget"]]
os.makedirs(a.out, exist_ok=True)
sb = draw_svg(os.path.join(a.out, "l12_event.svg"), rows, cfg)
with open(os.path.join(a.out, "l12_event.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["id", "name", "template", "days", "participants", "perParticipantCoins",
"grossCoins", "sinkCoins", "netInflationCoins", "budgetCapCoins", "overBudget",
"rewardCostCents", "upliftRevenueCents", "roiPermille"])
for r in rows:
w.writerow([r["id"], r["name"], r["template"], r["days"], r["participants"],
r["perParticipantCoins"], r["grossCoins"], r["sinkCoins"], r["netInflationCoins"],
r["budgetCapCoins"], r["overBudget"], r["rewardCostCents"],
r["upliftRevenueCents"], r["roiPermille"]])
report = {"version": cfg["version"], "game": cfg["game"], "seed": cfg["seed"],
"eventCount": len(rows), "totalGrossCoins": total_gross, "totalSinkCoins": total_sink,
"totalNetInflationCoins": total_gross - total_sink, "overBudget": over,
"events": rows, "svgBytes": sb}
with open(os.path.join(a.out, "l12_event.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({"eventCount": len(rows), "overBudget": over,
"totalGrossCoins": total_gross, "totalNetInflationCoins": total_gross - total_sink,
"perEvent": {r["id"]: {"perParticipantCoins": r["perParticipantCoins"],
"grossCoins": r["grossCoins"], "netInflationCoins": r["netInflationCoins"],
"budgetCapCoins": r["budgetCapCoins"], "overBudget": r["overBudget"],
"roiPermille": r["roiPermille"]} for r in rows},
"svgBytes": sb}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑它(示例参数),输出如下(同机相对量级,绝对数值不可移植):
{
"eventCount": 4,
"overBudget": [
"checkin"
],
"totalGrossCoins": 1494900,
"totalNetInflationCoins": 757900,
"perEvent": {
"merge-race": {
"perParticipantCoins": 77,
"grossCoins": 323400,
"netInflationCoins": 71400,
"budgetCapCoins": 500000,
"overBudget": false,
"roiPermille": 2597
},
"collection": {
"perParticipantCoins": 51,
"grossCoins": 331500,
"netInflationCoins": 71500,
"budgetCapCoins": 600000,
"overBudget": false,
"roiPermille": 5067
},
"checkin": {
"perParticipantCoins": 5,
"grossCoins": 540000,
"netInflationCoins": 540000,
"budgetCapCoins": 400000,
"overBudget": true,
"roiPermille": 6666
},
"winback": {
"perParticipantCoins": 200,
"grossCoins": 300000,
"netInflationCoins": 75000,
"budgetCapCoins": 400000,
"overBudget": false,
"roiPermille": 16000
}
},
"svgBytes": 2491
}
读这份输出
超支的那一个恰好是最不起眼的活动。checkin 人均只有 5 币,看起来最省,但它 grantMultiplierPermille 是 12000——一个月平均发 12 次,9000 人一乘,发放总量 540000 币,直接顶穿 400000 的预算上限,净通胀 540000(因为它没有回收机制)。这说明一个反直觉的结论:单次奖励低不等于总发放低,次数才是通胀的隐藏乘数。排行榜赛人均 77 币听着多,但只发一次、4200 人,总量 323400,反而在预算内。
四类活动的净通胀量级完全不同。merge-race 净通胀 71400、collection 71500——它们都有回收(人均 60 / 40 币),发放的一部分被消耗掉。winback 净通胀 75000,靠人均 150 币的高回收把 200 币的发放压下来一半。唯独 checkin 回收为 0,发多少全是净通胀。所以做活动预算时,回收机制和发放上限同等重要:没有回收的活动,预算上限就是唯一的安全阀。
ROI 全线很高,这恰恰说明成本不是重点。winback 的 ROI 最高(16000‰,即约 16 倍),因为它召回的是已经流失的高价值用户,增量最大;merge-race 最低(2597‰),因为它提升的多是已有活跃。但这些倍数都远大于 1——活动几乎必然「划算」,真正要管的从来不是 ROI,而是通胀。把 ROI 当活动成败的标准,会让人不断加码奖励,最后把经济搞崩。
小结与下一步
这一节把活动做成了「模板 + 预算」:四类模板用覆盖人数、人均奖励、发放次数三个参数刻画;奖励预算卡的是发放总量与净通胀,不是 ROI。三件事记住就够——**单次奖励低不代表总发放低,次数是隐藏乘数;没有回收的活动,预算上限是唯一安全阀;活动 ROI 天然很高,通胀才是真风险。**下一节把单个活动放大到一整个赛季:赛季怎么分阶段、节奏怎么维持、阶梯奖励怎么定。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。