上一节朝内画了品类图谱,这一节朝外:我们要服务的到底是谁,别人已经在服务谁,我们打算和谁不一样。 这三问听起来像市场部的事,其实是产品架构的前置输入——目标用户决定核心循环的节奏,竞品拆解决定哪些系统可以借鉴、哪些必须绕开。
需要先立一条纪律。本节涉及的《率土之滨》《三国志·战略版》《万国觉醒》《COK》都是真实产品,但它们只作「公开可观察机制」引用:观察时点 2026-10,来源为官方公告、商店页描述与公开试玩。任何竞品后台的留存、流水、付费率都属 X——需真实业务环境,命令行不可复现,本篇一个数字都不编。下面所有可跑的结论,输入都只是「某机制是否存在」这类公开事实。
目标用户不是「喜欢策略的人」
「25–40 岁男性、喜欢策略、有一定付费能力」——这句话对产品设计没有任何约束力,因为它同时命中三个互不兼容的方向。真正能驱动架构的用户定义,必须绑定可观察的行为约束:
| 用户类型 | 可观察行为 | 对架构的硬约束 |
|---|---|---|
| 碎片时间中度玩家 | 单次在线 10–20 分钟 | 一次有效决策必须在一次会话内完成 |
| 长线经营玩家 | 连续 3 个月每日登录 | 需要长线目标与赛季承接 |
| 联盟社交玩家 | 主动加入并参与联盟活动 | 联盟必须是长期目标的唯一路径 |
| 冲突驱动玩家 | 主动进攻其他玩家 | 需要可控的冲突出口(但未必是掠夺) |
关键:用户定义里每一条都必须能翻译成一条系统约束。 翻译不了的,就是营销话术,删掉。这四类不互斥,《王朝纪元》选择的是前三类为主、第四类受控——这个选择会在 1.3 变成可判定的设计约束。
竞品拆解的三条纪律
拆竞品最容易犯的错,是「我听说某产品次留 45%,所以我们要做到 40%」——这个 45% 从哪来?多半是编的。可复现的拆解只做三件事:
- 只记公开可观察机制。「有没有赛季重置」「地图是不是格子」「有没有英雄抽卡」,这些看商店页和试玩就能确认;「次留多少」确认不了。
- 标观察时点。机制会变,2026-10 的结论不能当 2028 的结论用。
- 不可复现处标 X。竞品后台数据一律 X,正文显式写明「需真实业务环境,命令行不可复现」,绝不伪装成实测。
下面这段是 D 层配置:把四个公开可观察的竞品与自研《王朝纪元》放在 8 个机制轴上,值只有 0/1,表示机制缺失/存在。
{
"version": 1,
"observation": "2026-10",
"source": "公开可观察机制(官方公告、商店页描述、公开评测与试玩录像)归纳",
"disclaimer": "本表只记录「某机制是否存在」这类公开可观察事实,不含任何竞品后台留存、流水、付费率数据;后者属 X,需真实业务环境,命令行不可复现。数值 0/1 仅表示机制缺失/存在。",
"axes": [
{ "id": "persistentWorld", "name": "持续世界" },
{ "id": "tileGrid", "name": "格子地块" },
{ "id": "realtimeMarch", "name": "实时行军" },
{ "id": "heroGacha", "name": "英雄卡牌抽取" },
{ "id": "seasonReset", "name": "赛季重置" },
{ "id": "allianceWar", "name": "联盟战" },
{ "id": "territoryOccupy", "name": "领地占领" },
{ "id": "resourceRaid", "name": "资源掠夺" }
],
"titles": [
{ "id": "shuaitu", "name": "率土之滨", "values": [1, 1, 1, 0, 1, 1, 1, 1] },
{ "id": "sanzhan", "name": "三国志·战略版", "values": [1, 1, 1, 1, 1, 1, 1, 1] },
{ "id": "wgjx", "name": "万国觉醒", "values": [1, 0, 1, 1, 1, 1, 0, 1] },
{ "id": "cok", "name": "COK", "values": [1, 0, 1, 0, 0, 1, 0, 1] }
],
"target": {
"id": "dynasty-era",
"name": "王朝纪元",
"values": [1, 1, 1, 0, 1, 1, 1, 0]
}
}
品类采用率与差异向量(S 层)
有了矩阵,两个问题又能用算术回答:哪些机制是这个品类的「公共惯例」?《王朝纪元》在哪些轴上背离了主流? 下面这段是 S 层仿真脚本,算每轴的采用率与自研产品的差异向量,产出 JSON + CSV + SVG。
# file: sim/ch01_competitor_scan.py
# tier: S
# verify: python3 sim/ch01_competitor_scan.py --config config/ --out out/
"""竞品拆解(公开可观察机制口径):读 config/ch01_competitor_axes.json,
算每个机制的「品类采用率」,再算自研《王朝纪元》相对品类主流的差异向量,产出 JSON + CSV + SVG。
纪律:输入只有「某机制是否存在」这类公开可观察事实,观察时点写在配置里。
不含任何竞品后台留存/流水/付费率(那属 X,命令行不可复现),也不做「实测」表述。
"""
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 adoption(titles: list[dict], n_axes: int) -> list[dict]:
rows = []
for k in range(n_axes):
vals = [t["values"][k] for t in titles]
rows.append({"values": vals, "adoption": round(sum(vals) / len(vals), 4),
"count": sum(vals), "total": len(vals)})
return rows
def differentiation(target: dict, rows: list[dict]) -> list[dict]:
out = [{"axis": r["axis"], "adoption": r["adoption"], "target": target["values"][k],
"deviation": round(abs(target["values"][k] - r["adoption"]), 4)} for k, r in enumerate(rows)]
out.sort(key=lambda r: (-r["deviation"], r["axis"]))
return out
def write_csv(path: str, axes: list[dict], titles: list[dict], rows: list[dict], target: dict) -> None:
with open(path, "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["title"] + [a["id"] for a in axes])
for t in titles:
w.writerow([t["id"]] + t["values"])
w.writerow(["__adoption__"] + [r["adoption"] for r in rows])
w.writerow([target["id"]] + target["values"])
def write_svg(path: str, axes: list[dict], rows: list[dict], target: dict) -> None:
bar, left, top, gap = 40, 150, 60, 46
width, height = left + 360, top + gap * len(axes) + 30
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": "18",
"font-family": "sans-serif"}).text = "品类采用率 vs 《王朝纪元》选择(公开可观察机制)"
for i, (ax, row) in enumerate(zip(axes, rows)):
y = top + i * gap
ET.SubElement(svg, "text", {"x": str(left - 10), "y": str(y + 16), "font-size": "12",
"text-anchor": "end", "font-family": "sans-serif"}).text = ax["name"]
ET.SubElement(svg, "rect", {"x": str(left), "y": str(y), "width": str(int(300 * row["adoption"])),
"height": str(bar - 14), "fill": "#4a7ebb"})
mx = left + int(300 * target["values"][i])
ET.SubElement(svg, "line", {"x1": str(mx), "y1": str(y - 4), "x2": str(mx), "y2": str(y + bar - 10),
"stroke": "#c0392b", "stroke-width": "3"})
ET.SubElement(svg, "text", {"x": str(left + 310), "y": str(y + 16), "font-size": "12",
"font-family": "sans-serif"}).text = f"{row['adoption']:.2f} / 目标 {target['values'][i]}"
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, "ch01_competitor_axes.json"))
axes, titles, target = cfg["axes"], cfg["titles"], cfg["target"]
os.makedirs(args.out, exist_ok=True)
rows = adoption(titles, len(axes))
for k, row in enumerate(rows):
row["axis"], row["name"] = axes[k]["id"], axes[k]["name"]
diff = differentiation(target, rows)
summary = {"version": 1, "observation": cfg["observation"], "source": cfg["source"], "nTitles": len(titles),
"adoption": rows, "differentiation": diff,
"conventions": [r["axis"] for r in rows if r["adoption"] >= 0.75],
"splits": [r["axis"] for r in rows if 0.25 < r["adoption"] < 0.75]}
with open(os.path.join(args.out, "ch01_competitor_scan.json"), "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
write_csv(os.path.join(args.out, "ch01_competitor_scan.csv"), axes, titles, rows, target)
write_svg(os.path.join(args.out, "ch01_competitor_scan.svg"), axes, rows, target)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
同机实跑(4 个竞品,节选):
conventions(采用率>=0.75): persistentWorld, realtimeMarch, seasonReset, allianceWar, resourceRaid
splits(0.25<采用率<0.75): tileGrid, heroGacha, territoryOccupy
differentiation(偏差降序): resourceRaid 1.0 > heroGacha/tileGrid/territoryOccupy 0.5
> seasonReset 0.25 > allianceWar/persistentWorld/realtimeMarch 0.0
读法:持续世界、实时行军、联盟战、资源掠夺的采用率都是 1.0——这是「不做就不像 SLG」的公共惯例;格子地块、英雄抽卡、领地占领是 0.5 的分水岭轴,正是上一节图谱里的高方差轴;赛季重置 0.75 接近惯例。《王朝纪元》只在 resourceRaid(放弃掠夺)上偏离了强惯例整整 1.0,其余偏离都在分水岭轴上——这是有意的定位,不是漏做。
B 基准:把「背离惯例」变成软告警
偏离强惯例不一定是错,但必须被看见。下面这段是 B 层基准脚本:把品类采用率当期望、把《王朝纪元》的选择当观测,凡是在强惯例轴上偏离超过阈值就出软告警——脚本永远 exit 0,只提醒,不判决。
# file: sim/ch01_competitor_baseline.py
# tier: B
# verify: python3 sim/ch01_competitor_baseline.py --config config/ --out out/
"""B 基准:把「品类主流采用率」当期望,把《王朝纪元》的定位选择当观测,算偏差并出软告警。
软告警不硬失败:脚本永远 exit 0,只把「背离强惯例」的轴列进 warnings 供策划决策,不作对错判决。
产出 out/ch01_competitor_baseline.json。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ch01_competitor_scan import load_config # noqa: E402
STRONG_CONVENTION, WARN_BAND = 0.75, 0.5
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, "ch01_competitor_axes.json"))
axes, titles, target = cfg["axes"], cfg["titles"], cfg["target"]
os.makedirs(args.out, exist_ok=True)
n = len(titles)
warnings, rows = [], []
for k, ax in enumerate(axes):
expected = sum(t["values"][k] for t in titles) / n
observed = target["values"][k]
dev = abs(observed - expected)
rows.append({"axis": ax["id"], "name": ax["name"], "expectedAdoption": round(expected, 4),
"observedChoice": observed, "deviation": round(dev, 4)})
if dev >= WARN_BAND and expected >= STRONG_CONVENTION:
warnings.append(ax["id"])
report = {"version": 1, "kind": "benchmark", "severity": "soft",
"note": "同机相对量级,绝对数值不可移植;这是设计对照,不是对错判决。",
"observation": cfg["observation"], "rows": rows, "softWarnings": warnings,
"acceptedDeviations": [r["axis"] for r in rows
if r["deviation"] >= WARN_BAND and r["axis"] not in warnings]}
with open(os.path.join(args.out, "ch01_competitor_baseline.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2))
sys.exit(0) # B 层软告警:永远成功退出
if __name__ == "__main__":
main()
同机实跑(节选):
severity: soft
softWarnings: [resourceRaid] # 采用率 1.0 的强惯例,被《王朝纪元》整轴放弃
acceptedDeviations: [tileGrid, heroGacha, territoryOccupy] # 分水岭轴上的有意差异
这条软告警才是本节最有用的产物:放弃资源掠夺,意味着《王朝纪元》少了一个最直接的冲突出口和资源再分配手段。它不违法、也不代表错,但产品必须为「玩家之间如何产生冲突」准备替代方案——这个待办会被带到第 3 章的核心循环与第 14 章的交易与市场去兑现。软告警的价值在于:把「我们和别人不一样」从一个卖点,变成一张必须回应的待办清单。
拆解方法的操作步骤
把上面这套沉淀成可重复的流程:
- 圈定 3–5 个公开可观察的竞品,只选能玩到/能看到商店页描述的,避免道听途说。
- 列 6–10 个机制轴,每个轴必须能用 0/1 或序数回答,拒绝「品质高低」这类主观轴。
- 填矩阵,标观察时点,任何后台数字写 X 并注明不可复现。
- 跑采用率与差异向量,识别惯例轴与分水岭轴。
- 对每个背离强惯例的轴出一张待办,写清「放弃它,玩家失去什么、用什么补」。
这套流程的产物不是一份 PPT,而是 config/ch01_competitor_axes.json 加两个可重跑脚本——换一批竞品、改一个观察时点,结论立刻可复算。
小结
- 目标用户必须翻译成系统约束;翻译不了的表述直接删。
- 竞品拆解只碰公开可观察机制,标观察时点,后台数据一律 X,绝不写「实测」。
- 采用率分出惯例轴与分水岭轴;分水岭轴才是可以做差异化的地方。
- B 基准把「背离强惯例」变成软告警,逼产品为每一次「不一样」准备替代方案。
- 下一节把用户与竞品的结论收成一句话定位,并翻译成可判定的设计约束。
阅读导航:上一节:1.1 SLG 品类图谱与细分(4X / 城战 / 卡牌 SLG) · 下一节:1.3 差异化定位与设计约束 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。