上一节把榜单读成了结构,这一节把镜头拉近到单个竞品:榜单告诉你「市场长什么样」,竞品拆解告诉你「别人具体怎么做、做到什么程度」。这里同样要先划清分工:/h5游戏产业全景/ 那篇是 H5 小游戏的产业综述,讲的是格局与趋势;本章给的是可量化的竞品拆解方法——把「感觉这个游戏做得好」翻译成可打分、可排序、可复算的维度分。/posts/products/
下的 PRD 系列是产品需求文档,写的是「我们要做什么」;本章给的是可跑的数据分析与机会评估,回答的是「别人做了什么、我们差在哪」。一个是交付物,一个是分析工具,不要混为一谈。
拆解不是抄,是建坐标系
新手拆竞品最常见的两个极端:一是「抄界面」——把别人的 UI 像素级复刻,却说不清为什么这么设计;二是「看感觉」——玩两把,写一句「体验不错,值得学习」。两种都产不出可用的决策。
可复现的拆解要做的是建坐标系:先定义有限个可判定的维度,再把每个竞品放进这个坐标系里打分。打分是主观的(因为判断本身主观),但坐标系是公开的、准则是写死的、聚合是确定的——换一个人来打,可能会给出不同的分,但只要按同一套准则打,结论就能互相比较、能被复核。这正是它比「凭感觉」强的地方。
本节只做三件事,与后续章节严格分工:
- 拆维度——把竞品拆成「核心循环 / 变现 / 素材」三块,每块下面挂若干可判定的准则。
- 打分聚合——按准则打 1
5 分,聚成 0100 的维度分与加权总分,产出 CSV。 - 读结论——看自研产品在哪个维度领先、哪个维度落后,落后项进待办。
明确不做:这里不选型、不排序决定做哪个品类——那是第 3 章品类选择模型的事;这里也不讲玩法怎么实现——那是第 6 章核心循环的事。本节只到「把竞品量化成一张可比较的表」为止。
三维度拆解模板(核心循环 / 变现 / 素材)
为什么是这三个维度?因为它们对应一款小游戏能否成立的三个基本面:好不好玩(核心循环)、能不能赚钱(变现)、能不能被看到(素材/买量)。三者缺一,产品都很难跑起来。每个维度下再挂 4 条可判定的准则,准则都写成 1~5 分的锚点,避免「好坏」这种无法打分的描述。
下面这段是 D 层配置:一份竞品拆解模板,含三个维度、各自的准则与评分锚点,以及 6 个示例竞品(含自研《合成小岛》)的示例评分。
{
"version": 1,
"observation": "示例数据(非真实竞品评测)",
"scale": 5,
"disclaimer": "本表的竞品名称、评分均为作者构造的示例,用于演示「三维度拆解 + 加权打分」的可复现流程,不代表对任何真实产品的评测结论。真实竞品的留存、流水、付费率、买量成本均需真实业务环境,命令行不可复现(X)。",
"dimensions": [
{
"id": "coreLoop",
"name": "核心循环",
"weight": 40,
"criteria": [
{ "id": "loopLen", "name": "单局循环时长合理性", "rubric": "5=30~90 秒一局,节奏与碎片时间匹配" },
{ "id": "decisionDensity", "name": "单位时间有效决策数", "rubric": "5=每分钟多次有意义选择" },
{ "id": "feedbackDensity", "name": "正反馈密度", "rubric": "5=每次操作都有明确反馈" },
{ "id": "firstMinuteHook", "name": "首分钟钩子强度", "rubric": "5=开局 60 秒内出现第一个爽点" }
]
},
{
"id": "monetization",
"name": "变现",
"weight": 35,
"criteria": [
{ "id": "adFormFit", "name": "广告形态与玩法契合", "rubric": "5=激励视频嵌进核心循环而非硬插" },
{ "id": "adPlacement", "name": "广告点位自然度", "rubric": "5=点位出现在玩家主动想要收益处" },
{ "id": "iapDepth", "name": "内购点深度", "rubric": "5=有清晰的付费阶梯与高价值锚点" },
{ "id": "hybridBalance", "name": "混合变现平衡", "rubric": "5=IAA 与 IAP 互不打架、互为补充" }
]
},
{
"id": "creative",
"name": "素材",
"weight": 25,
"criteria": [
{ "id": "hookVisual", "name": "前 3 秒视觉钩子", "rubric": "5=静音也能看懂卖点" },
{ "id": "gameplayShowcase", "name": "玩法可展示性", "rubric": "5=核心循环可被 15 秒素材讲清" },
{ "id": "styleConsistency", "name": "风格一致性", "rubric": "5=素材与游戏内画面不割裂" },
{ "id": "iterationCadence", "name": "素材迭代产能", "rubric": "5=能稳定周更一批可测素材" }
]
}
],
"competitors": [
{
"id": "jelly-merge",
"name": "果冻合成",
"note": "示例竞品",
"scores": { "loopLen": 4, "decisionDensity": 4, "feedbackDensity": 5, "firstMinuteHook": 4,
"adFormFit": 4, "adPlacement": 4, "iapDepth": 3, "hybridBalance": 3,
"hookVisual": 4, "gameplayShowcase": 5, "styleConsistency": 4, "iterationCadence": 3 }
},
{
"id": "big-watermelon",
"name": "合成大西瓜",
"note": "示例竞品",
"scores": { "loopLen": 5, "decisionDensity": 3, "feedbackDensity": 5, "firstMinuteHook": 5,
"adFormFit": 3, "adPlacement": 3, "iapDepth": 2, "hybridBalance": 2,
"hookVisual": 5, "gameplayShowcase": 5, "styleConsistency": 4, "iterationCadence": 2 }
},
{
"id": "sweet-match3",
"name": "甜甜消消乐",
"note": "示例竞品",
"scores": { "loopLen": 4, "decisionDensity": 4, "feedbackDensity": 4, "firstMinuteHook": 3,
"adFormFit": 5, "adPlacement": 5, "iapDepth": 5, "hybridBalance": 5,
"hookVisual": 4, "gameplayShowcase": 4, "styleConsistency": 5, "iterationCadence": 5 }
},
{
"id": "idle-heroes",
"name": "放置奇兵",
"note": "示例竞品",
"scores": { "loopLen": 3, "decisionDensity": 2, "feedbackDensity": 3, "firstMinuteHook": 3,
"adFormFit": 4, "adPlacement": 4, "iapDepth": 4, "hybridBalance": 4,
"hookVisual": 3, "gameplayShowcase": 3, "styleConsistency": 3, "iterationCadence": 3 }
},
{
"id": "my-cafe",
"name": "我的咖啡店",
"note": "示例竞品",
"scores": { "loopLen": 3, "decisionDensity": 3, "feedbackDensity": 3, "firstMinuteHook": 2,
"adFormFit": 3, "adPlacement": 3, "iapDepth": 4, "hybridBalance": 3,
"hookVisual": 3, "gameplayShowcase": 2, "styleConsistency": 4, "iterationCadence": 2 }
},
{
"id": "merge-island",
"name": "合成小岛",
"note": "自研目标(示例评分)",
"scores": { "loopLen": 5, "decisionDensity": 4, "feedbackDensity": 5, "firstMinuteHook": 5,
"adFormFit": 5, "adPlacement": 4, "iapDepth": 3, "hybridBalance": 4,
"hookVisual": 5, "gameplayShowcase": 5, "styleConsistency": 5, "iterationCadence": 4 }
}
]
}
模板里有三个刻意的设计:一是准则必须可判定——「单局循环时长合理性」能对着锚点打,「美术好不好」不能;二是维度带权重——核心循环 40、变现 35、素材 25,权重是产品定位的直接表达;三是自研与竞品用同一套准则——只有同一把尺子,分数才可比。
打分与聚合(S 层)
打出来的原始分是 15 的整数,直接看不好比。下面这段是 S 层仿真脚本,读 100 的维度分,再按维度权重算总分,产出 CSV + JSON + 一个对照条形 SVG。聚合全用整数算术:维度分 = sum(scores) * 100 // (scale * nCriteria),总分 = sum(dimScore * weight) // 100。config/ch02_competitor_teardown.json,把每个竞品在各维度下的准则分聚成 0
# file: sim/ch02_competitor_score.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch02_competitor_score.py --config config/ --out out/
"""竞品三维度拆解打分(示例数据口径):读 config/ch02_competitor_teardown.json,
把每个竞品在「核心循环 / 变现 / 素材」三个维度上的 1~5 分聚成 0~100 的维度分,
再按维度权重算总分,产出 CSV + JSON + 一个纯 Python SVG 雷达式对照条形图。
纪律:输入是示例竞品与示例评分,不是对真实产品的评测。真实竞品的留存/流水/付费率/买量成本属 X。
评分聚合用整数算术:维度分 = sum(scores) * 100 // (scale * nCriteria),总分 = sum(dimScore * weight) // 100。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def load_config(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def dimension_score(dim: dict, scores: dict, scale: int) -> int:
"""维度分(0~100 整数):把该维度下各准则的 1~scale 分求和后归一化,地板除。"""
n = len(dim["criteria"])
got = sum(scores[c["id"]] for c in dim["criteria"])
return got * 100 // (scale * n)
def score_all(cfg: dict) -> list[dict]:
scale = cfg["scale"]
rows = []
for comp in cfg["competitors"]:
dims = {d["id"]: dimension_score(d, comp["scores"], scale) for d in cfg["dimensions"]}
total = sum(dims[d["id"]] * d["weight"] for d in cfg["dimensions"]) // 100
rows.append({"id": comp["id"], "name": comp["name"], "note": comp["note"],
"dims": dims, "total": total})
rows.sort(key=lambda r: (-r["total"], r["id"]))
return rows
def write_csv(path: str, cfg: dict, rows: list[dict]) -> None:
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
dim_ids = [d["id"] for d in cfg["dimensions"]]
w.writerow(["id", "name"] + dim_ids + ["total"])
for r in rows:
w.writerow([r["id"], r["name"]] + [r["dims"][d] for d in dim_ids] + [r["total"]])
def write_svg(path: str, cfg: dict, rows: list[dict]) -> None:
left, top, group_h, bar_h, gap = 120, 60, 66, 16, 26
width, height = left + 420, top + gap * len(rows) + 24
palette = {"coreLoop": "#2563eb", "monetization": "#db2777", "creative": "#16a34a"}
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(width),
"height": str(height), "viewBox": f"0 0 {width} {height}"})
ET.SubElement(svg, "text", {"x": str(left), "y": "30", "font-size": "16",
"font-family": "sans-serif"}).text = "示例竞品三维度得分(示例数据,0~100)"
for i, r in enumerate(rows):
y = top + i * gap
ET.SubElement(svg, "text", {"x": str(left - 10), "y": str(y + 34), "font-size": "12",
"text-anchor": "end", "font-family": "sans-serif"}).text = r["name"]
for j, d in enumerate(cfg["dimensions"]):
val = r["dims"][d["id"]]
by = y + j * bar_h
ET.SubElement(svg, "rect", {"x": str(left), "y": str(by), "width": str(int(3.4 * val)),
"height": str(bar_h - 4), "fill": palette[d["id"]]})
ET.SubElement(svg, "text", {"x": str(left + 352), "y": str(y + 34), "font-size": "12",
"font-family": "sans-serif"}).text = f"总分 {r['total']}"
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
args = ap.parse_args()
cfg = load_config(os.path.join(args.config, "ch02_competitor_teardown.json"))
os.makedirs(args.out, exist_ok=True)
rows = score_all(cfg)
write_csv(os.path.join(args.out, "ch02_competitor_score.csv"), cfg, rows)
write_svg(os.path.join(args.out, "ch02_competitor_score.svg"), cfg, rows)
report = {"source": "sim/ch02_competitor_score.py", "observation": cfg["observation"],
"disclaimer": cfg["disclaimer"], "scale": cfg["scale"],
"dimensions": [{"id": d["id"], "name": d["name"], "weight": d["weight"]} for d in cfg["dimensions"]],
"ranking": rows, "artifacts": ["out/ch02_competitor_score.csv", "out/ch02_competitor_score.svg"]}
with open(os.path.join(args.out, "ch02_competitor_score.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps({"ranking": rows}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
同机实跑(6 个示例竞品,节选输出):
ranking(总分降序,dims = 核心循环 / 变现 / 素材):
合成小岛 coreLoop=95 monetization=80 creative=95 总分=89 ← 自研目标
甜甜消消乐 coreLoop=75 monetization=100 creative=90 总分=87
果冻合成 coreLoop=85 monetization=70 creative=80 总分=78
合成大西瓜 coreLoop=90 monetization=50 creative=80 总分=73
放置奇兵 coreLoop=55 monetization=80 creative=60 总分=65
我的咖啡店 coreLoop=55 monetization=65 creative=55 总分=58
产物: out/ch02_competitor_score.csv out/ch02_competitor_score.svg
读法:自研《合成小岛》总分 89,靠的是核心循环 95 与素材 95 双高,但变现只有 80,是明显的短板——尤其 iapDepth(内购点深度)只打了 3 分。而排第二的甜甜消消乐恰好相反:变现满分 100,靠的是成熟的付费阶梯与混合变现。这两条放在一起看,结论就很清楚:《合成小岛》在「好玩」和「能被看到」上有优势,在「怎么收钱」上是短板——这张表的价值不在于排名,而在于把短板定位到了一个具体维度、甚至具体准则上。
拆解的纪律
把方法沉淀成可重复的流程,同时守住三条纪律:
- 只碰公开可观察机制。核心循环、广告点位、素材风格,玩一遍、看商店页就能判断;留存、流水、付费率、买量成本确认不了。
- 标观察时点。竞品会改版,2026-10 的判断不能当 2028 的结论。
- 不可复现处标 X。竞品后台数据一律 X,正文显式写明「需真实业务环境,命令行不可复现」,绝不伪装成实测。
打分流程本身也要诚实:分数是主观的,坐标系必须是客观的。准则要写死在配置里,权重要有理由,聚合要能复算。换一个人来打,分可能不同——但只要按同一套准则打,两张表就能放在一起比较,差异会指向「我们对某准则的理解不同」,而不是「谁在拍脑袋」。
一个常见追问:这 6 个示例竞品凭什么打这些分? 诚实回答:这些分是示例数据,用来演示算法;真实场景里,分数应当由团队按准则逐个讨论后填入配置,并把打分依据(试玩录像时间点、商店页截图)附在文档里。本节演示的是流程与工具,不是这 6 款游戏的评测结论。
小结
- 竞品拆解的目标是建坐标系,不是抄界面、更不是凭感觉。
- 三维度——核心循环 / 变现 / 素材——对应「好不好玩 / 能不能赚钱 / 能不能被看到」。
- 准则必须可判定;维度带权重;自研与竞品用同一把尺子。
- 聚合全用整数算术,换一套示例评分,结论立刻可复算。
- 示例跑出:自研《合成小岛》核心循环与素材双高、变现偏弱,短板被定位到具体准则。
- 真实竞品后台数据属 X;本节全部为示例数据。
阅读导航:上一节:2.1 榜单结构与品类分布 · 下一节:2.3 从竞品到差异化机会 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。