3.2 品类的商业特征矩阵

给 8 个小游戏品类在开发成本、变现效率、留存天花板等 8 条商业特征维度上打分,做成一张 8×8 矩阵,并用 S 层脚本把矩阵算成可横向比较的分数:综合强度分与偏科极差,并指出均值能排序但不能替代决策,为下一节可调权重的加权评分与敏感度分析打底。

3.1 节画好了格子,这一节要往格子里填数字。先把定位对齐:站点里 小游戏创作平台专题 那 24 篇是做小游戏创作平台(SaaS 创业口径),关心的是编辑器与平台商业模式;本节则是为单款游戏做品类选型决策,关心的是「这个品类值不值得做」。同一批品类,两种完全不同的提问方式。

从分类到打分

分类解决「是什么」,打分解决「值不值」。把两者分开,是这一章的核心方法。3.1 的 genres 是互斥落格,本节要新建一套评价轴,让每个品类都能在每条轴上得到一个分数。评价轴选得对不对,直接决定后面的加权评分有没有意义。

选轴的原则有三条:

  • 与决策相关:每条轴都要能改变「做不做、怎么做」的答案。不相关的轴只会稀释信号。
  • 方向一致:统一成「分越高越有利」,避免有的轴越高越好、有的越低越好,导致加总时符号混乱。
  • 可估可争:每条轴都能给一个 0–100 的量级判断,且不同人给出的分差在可讨论范围内。

八条商业特征维度

本书用 8 条维度刻画品类的商业特征,全部统一为「越高越有利」:

  • 开发成本可控度:越高表示同等团队规模下越快做出来。
  • 变现效率:越高表示人均收入潜力越大。
  • 留存天花板:越高表示长期留存的可用上限越高。
  • 买量可控度:越高表示获客越便宜、越可控。
  • 裂变潜力:越高表示越容易自发传播。
  • 内容复用度:越高表示一套内容能撑更久。
  • 平台适配度:越高表示越契合小游戏的包体与首包约束。
  • 差异化空间:越高表示越容易做出不同。

注意每条维度都带了「可控度 / 天花板 / 潜力」这类限定词。这不是文字游戏——它提醒打分者:我们打的是结构性的上限与难易,不是某一款具体产品的成败。一款做得好的超休闲,留存也可能很高;但超休闲这个品类的留存天花板确实低。矩阵描述的是品类,不是个案。

商业特征矩阵(D 层)

下面这段是 D 层静态配置,即 8 个品类 × 8 条维度的完整打分矩阵。每个单元格是 0–100 的整数有利度分,全部标为示例 / 公开可观察口径的行业归纳,不是任何平台后台实测;绝对数值不可移植。

{
  "version": 1,
  "note": "品类 × 商业特征矩阵:每个单元格是 0–100 的【示例 / 公开可观察】有利度分(越高越有利)。行业归纳的建模输入,不是任何平台后台实测;用于第 3 章加权评分与敏感度分析,绝对数值不可移植。",
  "scale": 100,
  "dimensions": ["devCost", "monetization", "retentionCeiling", "uaDifficulty", "viralPotential", "contentReuse", "platformFit", "differentiation"],
  "scores": {
    "hyper-casual": { "devCost": 95, "monetization": 52, "retentionCeiling": 34, "uaDifficulty": 40, "viralPotential": 45, "contentReuse": 30, "platformFit": 92, "differentiation": 34 },
    "merge": { "devCost": 70, "monetization": 71, "retentionCeiling": 62, "uaDifficulty": 58, "viralPotential": 55, "contentReuse": 74, "platformFit": 85, "differentiation": 60 },
    "idle": { "devCost": 65, "monetization": 73, "retentionCeiling": 72, "uaDifficulty": 60, "viralPotential": 50, "contentReuse": 78, "platformFit": 84, "differentiation": 56 },
    "level": { "devCost": 50, "monetization": 68, "retentionCeiling": 58, "uaDifficulty": 62, "viralPotential": 64, "contentReuse": 50, "platformFit": 88, "differentiation": 62 },
    "io": { "devCost": 64, "monetization": 60, "retentionCeiling": 45, "uaDifficulty": 55, "viralPotential": 82, "contentReuse": 40, "platformFit": 90, "differentiation": 48 },
    "sim": { "devCost": 30, "monetization": 74, "retentionCeiling": 76, "uaDifficulty": 52, "viralPotential": 42, "contentReuse": 76, "platformFit": 62, "differentiation": 68 },
    "tower-defense": { "devCost": 50, "monetization": 68, "retentionCeiling": 65, "uaDifficulty": 58, "viralPotential": 45, "contentReuse": 75, "platformFit": 88, "differentiation": 58 },
    "puzzle": { "devCost": 58, "monetization": 62, "retentionCeiling": 60, "uaDifficulty": 60, "viralPotential": 52, "contentReuse": 56, "platformFit": 90, "differentiation": 54 }
  }
}

矩阵是「宽表」:行是品类,列是维度。宽表的好处是一眼能看出每个品类的形状——超休闲在 devCost(95)和 platformFit(92)上极高,在 retentionCeiling(34)和 contentReuse(30)上极低,形状非常「尖」;模拟经营在 devCost(30)上极低,在 retentionCeiling(76)上高,形状更「平」。形状本身就是信息。

把矩阵算成可比较的分数(S 层)

宽表好看但不好比。下面这段是 S 层仿真脚本,读 config/ch03_matrix.json 与 config/ch03_genres.json,对每个品类求 8 条维度的算术平均(整数地板除)作为「综合强度分」,同时算出极差 spread 作为「偏科度」,产出 JSON + CSV + SVG。

#!/usr/bin/env python3
# file: sim/ch03_matrix.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ch03_matrix.py --config config/ --out out/
"""把品类 × 商业特征矩阵算成可比较的分数:读 config/ch03_matrix.json(分数)
与 config/ch03_genres.json(品类中文名),对每个品类求:

- mean:8 个维度的算术平均(整数地板除)——可横向比较的「综合强度分」。
- min / max / spread:最弱项、最强项与极差——spread 越大越「偏科」。
- argmin / argmax:最弱与最强的维度 id。

产出 JSON + CSV + SVG(各品类综合强度分条形图)。全整数,无浮点。
"""

from __future__ import annotations

import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET


def load_json(path: str) -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def score_rows(genres: list[dict], dims: list[str], scores: dict) -> list[dict]:
    rows = []
    for g in genres:
        gid = g["id"]
        row = scores[gid]
        vals = [row[d] for d in dims]
        total = sum(vals)
        argmin = min(dims, key=lambda d: row[d])
        argmax = max(dims, key=lambda d: row[d])
        rows.append({
            "genre": gid,
            "cn": g["cn"],
            "mean": total // len(dims),
            "sum": total,
            "min": min(vals),
            "max": max(vals),
            "spread": max(vals) - min(vals),
            "argmin": argmin,
            "argmax": argmax,
        })
    rows.sort(key=lambda r: (-r["mean"], r["genre"]))
    for i, r in enumerate(rows, 1):
        r["meanRank"] = i
    return rows


def write_csv(path: str, rows: list[dict], dims: list[str]) -> None:
    with open(path, "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["genre", "cn"] + dims + ["mean", "spread", "meanRank"])
        for r in rows:
            w.writerow([r["genre"], r["cn"]] + [r["_scores"][d] for d in dims]
                       + [r["mean"], r["spread"], r["meanRank"]])


def write_svg(path: str, rows: list[dict], title: str) -> int:
    W, H, PAD, gap = 640, 400, 150, 10
    n = len(rows)
    plot_h = H - 2 * 40
    plot_w = W - PAD - 60
    bar_h = (plot_h - gap * (n + 1)) / n
    vmax = max(r["mean"] for r in rows) or 1
    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": "40", "y": "26", "font-size": "15",
                                "font-family": "sans-serif"}).text = title
    for i, r in enumerate(rows):
        y = 40 + gap + i * (bar_h + gap)
        w = r["mean"] / vmax * plot_w
        ET.SubElement(svg, "rect", {"x": str(PAD), "y": f"{y:.1f}",
                                    "width": f"{w:.1f}", "height": f"{bar_h:.1f}",
                                    "fill": "#0ea5e9"})
        ET.SubElement(svg, "text", {"x": str(PAD - 8), "y": f"{y + bar_h / 2 + 4:.1f}",
                                    "font-size": "12", "text-anchor": "end",
                                    "font-family": "sans-serif"}).text = r["cn"]
        ET.SubElement(svg, "text", {"x": f"{PAD + w + 8:.1f}", "y": f"{y + bar_h / 2 + 4:.1f}",
                                    "font-size": "12", "font-family": "sans-serif"}).text = str(r["mean"])
    ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
    return os.path.getsize(path)


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--config", default="config/")
    ap.add_argument("--out", default="out/")
    args = ap.parse_args()
    os.makedirs(args.out, exist_ok=True)
    gcfg = load_json(os.path.join(args.config, "ch03_genres.json"))
    mcfg = load_json(os.path.join(args.config, "ch03_matrix.json"))
    dims = mcfg["dimensions"]
    rows = score_rows(gcfg["genres"], dims, mcfg["scores"])
    for r in rows:
        r["_scores"] = mcfg["scores"][r["genre"]]

    write_csv(os.path.join(args.out, "ch03_matrix.csv"), rows, dims)
    svg_bytes = write_svg(os.path.join(args.out, "ch03_matrix.svg"), rows,
                          "各品类综合强度分(8 维度算术平均,越高越有利)")

    for r in rows:
        del r["_scores"]
    report = {
        "version": 1,
        "source": "sim/ch03_matrix.py",
        "scale": mcfg["scale"],
        "dimensionCount": len(dims),
        "rows": rows,
        "topByMean": rows[0]["genre"],
        "mostPolarized": max(rows, key=lambda r: r["spread"])["genre"],
        "csv": os.path.join(args.out, "ch03_matrix.csv"),
        "svg": os.path.join(args.out, "ch03_matrix.svg"),
        "svgBytes": svg_bytes,
    }
    with open(os.path.join(args.out, "ch03_matrix.json"), "w", encoding="utf-8") as f:
        json.dump(report, f, ensure_ascii=False, indent=2)
    print(json.dumps({
        "meanRank": [(r["cn"], r["mean"], r["spread"]) for r in rows],
        "topByMean": report["topByMean"],
        "mostPolarized": report["mostPolarized"],
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()

这个脚本刻意不做加权:算术平均是「所有维度同等重要」的假设,它给出的是「中性视角」下的强度排序。加权要等 3.3 节,因为权重体现的是你的决策偏好,而偏好是下一节的主角。先把中性结果做出来,才能看清加权改变了什么。

实跑结果

执行 /opt/homebrew/bin/python3 sim/ch03_matrix.py --config config/ --out out/,得到的综合强度分(降序,同机相对量级,绝对数值不可移植):

[
  ["放置",     67, 34],
  ["合成",     66, 30],
  ["塔防",     63, 43],
  ["闯关",     62, 38],
  ["益智",     61, 38],
  ["IO",       60, 50],
  ["模拟经营", 60, 46],
  ["超休闲",   52, 65]
]

同时写出 out/ch03_matrix.csv(含每个维度原始分与 mean / spread / meanRank)与 out/ch03_matrix.svg(综合强度分条形图)。CSV 头部如下:

genre,cn,devCost,monetization,retentionCeiling,uaDifficulty,viralPotential,contentReuse,platformFit,differentiation,mean,spread,meanRank
idle,放置,65,73,72,60,50,78,84,56,67,34,1
merge,合成,70,71,62,58,55,74,85,60,66,30,2

矩阵读法:均值与偏科

结果里有三个值得展开的观察。

第一,中性视角下「放置」综合最强(67),「超休闲」垫底(52)。 这不是说超休闲不好,而是说:如果 8 条维度同等重要,超休闲在 retentionCeiling / contentReuse / differentiation 三条上的低分,把它在 devCost / platformFit 上的高分拉平了。均值是一种「不看偏好」的视角,它天然惩罚偏科。

第二,偏科度是另一种信息。 spread 最大的两个品类是超休闲(65)和 IO(50),最小的两个是合成(30)和放置(34)。超休闲的 spread 高达 65——它的最强项 95(开发成本)与最弱项 30(内容复用)差了 65 分,是典型的「一招鲜、短板硬」。合成和放置的 spread 在 30 出头,形状更均衡。

第三,均值并列会暴露口径问题。 益智(61)与 IO(60)、模拟经营(60)靠得很近,IO 与模拟经营甚至同为 60。均值相同不代表品类相同——IO 的 60 来自「高裂变 + 低留存」,模拟经营的 60 来自「高留存 + 低开发可控度」,两者形状完全相反。这正是下一节要引入加权的原因:一旦你明确「我更看重留存还是裂变」,这两个并列的品类会立刻分开。

边界与陷阱

用矩阵时有三个常见陷阱:

  • 把示例分当事实:矩阵里的分数是建模输入,标为公开可观察口径的示例值。它们的作用是让讨论有结构,不是让讨论有定论。不同团队完全可以给出不同的分数,只要把打分口径写下来,就是可复现的。
  • 用均值替代决策:均值假设「维度同等重要」,这只在你还不知道自己要什么时成立。一旦有了明确的团队优势与目标,就该上权重,而不是继续看均值。
  • 忽略维度的相关性:8 条维度并不完全独立(例如 contentReuse 与 retentionCeiling 常常同向)。加权时若给两条高度相关的维度都压重注,会不知不觉放大同一个信号。这一点在 3.3 节的敏感度分析里会更清楚。

小结

本节把 3.1 的品类骨架填成了 8×8 商业特征矩阵,并用 S 层脚本算出了中性的综合强度分与偏科极差。核心结论是:均值能排序,但不能替代决策——它把「放置 67、超休闲 52」这样的强弱读出来,却读不出「你更看重什么」。下一节引入可调权重与敏感度分析,把这个缺口补上。

阅读导航:上一节:品类分类体系 · 下一节:品类选择加权评分与敏感度分析 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「minigame」更多文章

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