《移动游戏产品的架构·客户端与移动运行时》13.1 模块矩阵与依赖报告

卷一的收口从一张矩阵开始:每个模块必须同时回答属于哪一层、提供什么能力、谁负责、是共享还是隔离。本节给出可运行的矩阵构建与审计脚本,用三类问题清单把「架构图」变成每次版本都能重跑的依赖报告,并给出双案例 2/6/2/1 的层分布与五项共享能力。

三张不一样的矩阵

版本评审会上,三位负责人各自投出一张模块矩阵。客户端负责人按代码目录分,列出 14 个模块;发行同学按渠道能力分,列出 9 个;数据同学按埋点域分,列出 6 个。三张表都「对」,但没有一张能回答评审真正关心的问题:哪些模块两款产品共用,哪些必须隔离,出了问题找谁。

这不是沟通问题,而是矩阵缺了列。一张能用的模块矩阵至少要有四列:

列回答的问题缺失的后果
层这个模块属于 platform / system / gameplay / presentation 哪一层依赖方向无法校验
能力它提供的是账号、支付、玩法内核还是界面能力同名模块被当成同一个东西
负责人出问题时谁响应、谁批准改动共享模块无人维护
共享性两款产品共用、各自隔离,还是产品专属该隔离的被复用,该复用的被复制

本节把这张表做成一个可运行的审计产物:输入模块清单,输出矩阵、分层计数与问题列表。它不替代架构图,但能让架构图每年至少被机器检查一次。

共享性不是态度,是规则

「这个模块要不要共用」经常被当成政治问题。实际上它可以被压成三条规则,先看规则再看人情:

  1. 共享能力只能落在 platform 或 system 层。 账号、支付、活动配置、埋点、渠道构建这五项是典型共享能力。如果它们出现在 gameplay 或 presentation,说明共享逻辑已经渗进了玩法或界面。
  2. 隔离能力不能落在 platform 层。 玩法内核、资源包、性能预算、发布窗口属于产品隔离项。把它们下沉到 platform,等于让两款产品在最底层纠缠。
  3. 既不是共享也不是隔离的,标为产品能力。 例如 screen-stack、engine-host。它们可以由一款产品独有,也可以在另一款产品里重写;矩阵只记录事实,不强迫统一。

这三条规则来自第 2 章的分层契约与第 3 章的壳层差异。它们不是新原则,只是把已有原则变成了可检查的断言。

一个可运行的矩阵审计

下面的模块把规则写成代码。它不读文件、不访问网络,输入一组模块记录,输出矩阵行、分层计数和问题清单。

// file: src/lab/module-matrix.ts
// tier: L1
// verify: node --test test/lab/module-matrix.test.ts

import { ISOLATED_CAPABILITIES, isSharedCapability } from "../core/layers.ts";
import type { Layer } from "../core/layers.ts";

export type Sharing = "shared" | "isolated" | "product";

export interface ModuleRecord {
  readonly id: string;
  readonly layer: Layer;
  readonly capability: string;
  readonly owner: string;
}

export interface MatrixRow extends ModuleRecord {
  readonly sharing: Sharing;
}

export interface MatrixAudit {
  readonly rows: readonly MatrixRow[];
  readonly byLayer: Readonly<Record<Layer, number>>;
  readonly problems: readonly string[];
}

const LAYERS: readonly Layer[] = ["platform", "system", "gameplay", "presentation"];
const SHARED_LAYERS: readonly Layer[] = ["platform", "system"];

export function classifySharing(capability: string): Sharing {
  if (isSharedCapability(capability)) {
    return "shared";
  }
  if ((ISOLATED_CAPABILITIES as readonly string[]).includes(capability)) {
    return "isolated";
  }
  return "product";
}

export function buildMatrix(modules: readonly ModuleRecord[]): MatrixAudit {
  const seen = new Set<string>();
  const problems: string[] = [];
  const byLayer: Record<Layer, number> = {
    platform: 0,
    system: 0,
    gameplay: 0,
    presentation: 0,
  };
  const rows: MatrixRow[] = [];

  for (const module of modules) {
    if (seen.has(module.id)) {
      problems.push(`duplicate module: ${module.id}`);
      continue;
    }
    seen.add(module.id);

    if (!LAYERS.includes(module.layer)) {
      problems.push(`unknown layer: ${module.id} -> ${module.layer}`);
      continue;
    }
    if (module.owner.trim() === "" || module.capability.trim() === "") {
      problems.push(`empty owner or capability: ${module.id}`);
    }

    const sharing = classifySharing(module.capability);
    if (sharing === "shared" && !SHARED_LAYERS.includes(module.layer)) {
      problems.push(`shared capability in ${module.layer}: ${module.id}`);
    }
    if (sharing === "isolated" && module.layer === "platform") {
      problems.push(`isolated capability in platform: ${module.id}`);
    }

    byLayer[module.layer] += 1;
    rows.push({ ...module, sharing });
  }

  return { rows, byLayer, problems };
}

export function formatMatrix(audit: MatrixAudit): string {
  const header = "id\tlayer\tcapability\tsharing\towner";
  const body = audit.rows
    .map((row) => `${row.id}\t${row.layer}\t${row.capability}\t${row.sharing}\t${row.owner}`)
    .join("\n");
  return body === "" ? header : `${header}\n${body}`;
}

export function coverageGaps(audit: MatrixAudit): readonly Layer[] {
  return LAYERS.filter((layer) => audit.byLayer[layer] === 0);
}

export function sharedCount(audit: MatrixAudit): number {
  return audit.rows.filter((row) => row.sharing === "shared").length;
}

export const DEFAULT_MODULES: readonly ModuleRecord[] = [
  { id: "host", layer: "platform", capability: "engine-host", owner: "客户端" },
  { id: "channel", layer: "platform", capability: "channel-build", owner: "发行中台" },
  { id: "account", layer: "system", capability: "account", owner: "账号组" },
  { id: "payment", layer: "system", capability: "payment", owner: "支付组" },
  { id: "activity", layer: "system", capability: "activity-config", owner: "运营平台" },
  { id: "analytics", layer: "system", capability: "analytics", owner: "数据组" },
  { id: "match3-core", layer: "gameplay", capability: "gameplay-core", owner: "消消客户端" },
  { id: "ark-core", layer: "gameplay", capability: "gameplay-core", owner: "方舟客户端" },
  { id: "resource-packs", layer: "system", capability: "resource-packs", owner: "客户端" },
  { id: "perf-budget", layer: "system", capability: "perf-budget", owner: "性能组" },
  { id: "ui", layer: "presentation", capability: "screen-stack", owner: "客户端" },
];

buildMatrix 做四件事:去重、校验层名、标记共享性、按层计数。任何一条不满足规则,都进入 problems,而不是被静默忽略。coverageGaps 负责回答另一个问题:哪一层在矩阵里是空的。

怎么读这份报告

用默认模块清单跑一遍,formatMatrix 输出的是这样一张表:

id            layer         capability      sharing   owner
host          platform      engine-host     product   客户端
channel       platform      channel-build   shared    发行中台
account       system        account         shared    账号组
payment       system        payment         shared    支付组
activity      system        activity-config shared    运营平台
analytics     system        analytics       shared    数据组
match3-core   gameplay      gameplay-core   isolated  消消客户端
ark-core      gameplay      gameplay-core   isolated  方舟客户端
resource-packs system       resource-packs  isolated  客户端
perf-budget   system        perf-budget     isolated  性能组
ui            presentation  screen-stack    product   客户端

得到的是一个非常具体的分布:platform 2、system 6、gameplay 2、presentation 1,共享能力 5 项,问题 0 条。这个分布本身就是判断:

  • system 层最厚,因为账号、支付、活动、埋点、资源包、性能预算都在这里。它是两款产品之间真正的接缝层。
  • gameplay 层有两个模块,分别是消消的 match3-core 与方舟的 ark-core。同属 gameplay-core 能力,但 id 不同——这是刻意的:它们共享「纯逻辑内核」这条规则,不共享实现。
  • presentation 只有一项,因为界面能力大多跟随产品独立演进,不适合做共享矩阵的最小单位。

问题清单则分三类,每一类对应一种真实事故:

问题触发条件对应事故
duplicate module同一 id 出现两次两拨人改了同名模块,合并时互相覆盖
shared capability in gameplay支付或账号出现在玩法层玩法代码直接调支付 SDK,换渠道时重写玩法
isolated capability in platform玩法内核下沉到平台层两款产品的玩法被锁进同一个底层,无法独立发版

双案例:五项共享,两层隔离

用这张矩阵对照双案例,边界会变得很清楚。

共享的是五项能力:账号、支付、活动配置、埋点、渠道构建。它们在矩阵里全部落在 platform 或 system 层,负责人分别属于账号组、支付组、运营平台、数据组与发行中台。这也是「潮汐」IP 能共用一套中台的直接原因。

隔离的是玩法与资源:消消的 match3-core 与方舟的 ark-core 分属两个模块,资源包、性能预算、发布窗口也各自独立。案例设定中,消消首包 120MB、总资源 480MB,方舟首包 350MB、总资源 1.8GB——资源体量差近四倍,任何「统一资源包」的尝试都会把两款产品拖进同一套优化约束。

矩阵不阻止共享,它只是让每一次共享都留下负责人和层级记录。没有负责人的共享,等于没有共享。

矩阵不是一次性文档

一份矩阵画完就归档,三个月后一定过期。维护它不需要开大会,只需要四个触发条件:

触发条件需要更新的列常见遗漏
新增一项共享能力能力、负责人、共享性只加代码,不登记负责人
两款产品开始合并资源或性能策略共享性、层实际已共享,矩阵仍标隔离
新渠道或新区域接入能力(渠道构建、区域配置)把渠道差异写进玩法层
团队合并或负责人转岗负责人事故时找不到响应人

每次更新只做两件事:改对应行,然后重跑 buildMatrix。如果 problems 从 0 变成非 0,先解决新增问题,再合并代码。问题清单的变化比矩阵本身更有信息量:它通常提前一到两个版本暴露「某个人绕过规则直接调用」的迹象。

评审时也有一个简单用法:不看矩阵全表,只看三处——coverageGaps 是否为空、sharedCount 是否与上次一致、problems 是否出现新条目。三处都正常,矩阵这一项就算过。

与相邻章节的关系

本节依赖第 2 章的分层契约(LAYER_CONTRACTS)与第 3 章的壳层边界;没有那两章,矩阵只能列出名字,无法判断层级归属。

本节产出的是「结构对不对」,下一节 13.2 负责「数值够不够」:把内存水位、帧预算与机型分档合并成两张可复核的基线表。13.3 再把结构、数值与评审清单合并成进入卷二的门禁。

小结

  • 模块矩阵必须有四列:层、能力、负责人、共享性;少一列就无法审计。
  • 共享能力只能落在 platform 或 system;隔离能力不能落在 platform;其余标为产品能力。
  • 矩阵的价值不在画出来,而在每次版本都能重跑并输出问题清单。
  • 双案例默认矩阵是 2/6/2/1 的层分布与 5 项共享能力;玩法内核按产品分 id,不共享实现。
  • 一份没有 problems 字段的矩阵,只是架构图,不是依赖报告。
  • 维护矩阵只需四个触发条件:新增共享能力、开始合并资源或性能策略、接入新渠道或区域、负责人变更。
  • 评审时只看三处:coverageGaps 为空、sharedCount 与上次一致、problems 没有新条目。

阅读导航:上一节:12.3 线上问题定位闭环 · 下一节:13.2 性能预算与机型分档基线 。

常见错误与边界

  1. 按组织架构画矩阵。 矩阵应按「层 + 能力」切,不按汇报关系切;否则一合并团队,矩阵就失效。正确做法是负责人作为一列,而不是分组维度。
  2. 把共享性当成一次性决定。 共享性会随产品阶段变化。消消上线初期可以把活动配置做成产品内模块,进入双产品阶段后再上移;矩阵要能记录这种迁移。
  3. 只检查重复 id,不检查空负责人。 空负责人的模块在事故时无人响应,比重复 id 更危险。empty owner 必须进问题清单。
  4. 把产品能力强行标成共享。 界面栈、玩法内核这类能力一旦共享,改动就要同时过两款产品的评审,发布节奏被锁死。矩阵应如实标 product。
  5. 矩阵跑一次就归档。 每季度或每次大版本前重跑一次,对比 problems 的变化;问题数量上升通常早于线上事故。
  6. 用矩阵替代依赖检查。 本节检查的是模块归属,不是调用关系。跨层调用仍需第 2 章的边界测试来拦截,两者不能互相替代。
  7. 能力名称随手改。 同一个能力在矩阵里叫 payment、在代码里叫 iap、在埋点里叫 purchase,共享性判断就会失效。能力名应来自统一清单,新增名称先改清单再改代码。
  8. 只看总数不看分布。 「共 11 个模块」不说明任何问题;2/6/2/1 的层分布、5 项共享能力、0 条问题,才是可复核的状态描述。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查