14.1 产能度量:人天与吞吐

产能账要能被复算,先得把「人天」拆成日历人天与有效人天。本节用《星屑旅店》一支示例团队,把出勤率、专注率、角色产能与资产需求写成全整数的人天×100 账,算出利用率 1058‰、吞吐 1.20 资产/人天,并说明为什么日历人天会让计划看起来比实际宽松。

14.1 产能度量:人天与吞吐

13.3 工具的采纳率与维护成本 把工具的价值写成「节省人天×100 / 维护人天×100」的整数账。这一节把同一套记账法压到最底层的资源上:人天。既有 game/industry 的生产与外包短文 与 《美术风格指南与 Style Bible》 讲的是方向、协作与管理的道理;本节不重复它们的框架,只做一件它们不做的事——把「这支团队一个迭代到底能产出多少资产」写成可跑、可复算、可对账的整数。

先把口径摆明:本节所有产能数字都是示例参数(出勤率 850‰、专注率 700‰、单件人天成本都是本套构造的),不代表任何真实团队或公司的产能;唯一来自公开可观察信息的是「人天(person-day / man-day)」这个单位本身——它是游戏与外包行业公开使用的计价口径。真实团队的真实产能需真实环境(真实人员、真实项目),命令行不可复现,本节只演示怎么把这样一份口径算成整数账。

14.1.1 两种人天:日历人天与有效人天

排期表上最容易犯的错,是把「一个人干一天」当成一个整人天。实际上一个人一天里只有一部分时间真正落在产出上:请假、会议、等待反馈、被打断都在吃掉它。于是要把一个数拆成两个:

量定义示例值
日历人天人数 × 工作日数6 人 × 10 天 = 60 人天
有效系数出勤率‰ × 专注率‰ // 1000850 × 700 // 1000 = 595‰
有效人天日历人天 × 有效系数‰ // 100060 × 595 // 1000 = 35.70 人天

60 个日历人天只换回 35.70 个有效人天。 这不是悲观,而是把「可计划的工作量」和「发工资的天数」分开——排期要按有效人天排,否则计划永远比现实宽松。两个系数都用千分比整数表示,乘法后整数地板除,跨语言逐位一致。

为什么要把出勤率与专注率分成两个系数?因为它们各自对应不同的管理动作。出勤率(示例 850‰)反映「人在不在」——请假、轮休、跨项目借调;专注率(示例 700‰)反映「在的时候有多少时间真的在产出」——会议、评审、等待反馈、被临时插活打断。合成一个 595‰ 的系数,排期只需乘一次;但真要改善产能,得先知道该动哪一个:出勤率靠排班,专注率靠减少打断,抓手完全不同。

14.1.2 配置:一份示例产能模型

下面是 D 层的一份示例产能模型:四个角色(场景与家具、角色与头像、UI 与图标、特效)各有人数,右侧是这一迭代的资产需求(件数 × 单件人天×100)。需求按角色归属,因为产能是分角色的,不能跨角色平移。

{
  "project": "stardust-inn",
  "chapter": 14,
  "note": "示例参数:本套构造的产能模型,非任何真实团队或公司的数字。",
  "caliber": "出勤率与专注率为示例口径;人天成本为示例参数。人天(person-day)是游戏与外包行业公开使用的计价单位。",
  "workdays": 10,
  "availabilityPermille": 850,
  "focusPermille": 700,
  "roles": [
    { "id": "env", "name": "场景与家具", "people": 2 },
    { "id": "char", "name": "角色与头像", "people": 1 },
    { "id": "ui", "name": "UI 与图标", "people": 2 },
    { "id": "vfx", "name": "特效", "people": 1 }
  ],
  "demand": [
    { "id": "furn_icon", "role": "env", "units": 12, "costPerUnit100": 60 },
    { "id": "furn_prefab", "role": "env", "units": 4, "costPerUnit100": 250 },
    { "id": "portrait", "role": "char", "units": 2, "costPerUnit100": 400 },
    { "id": "ui_icon", "role": "ui", "units": 24, "costPerUnit100": 40 },
    { "id": "vfx_burst", "role": "vfx", "units": 1, "costPerUnit100": 300 }
  ]
}

单件成本 costPerUnit100 是「人天 ×100」:家具图标 60 即 0.60 人天、角色头像 400 即 4.00 人天。用整数而不是小数,是为了让所有除法都走整数地板除,产出可逐位对拍。

14.1.3 S 层:人天账、利用率与吞吐

下面这段是 S 层脚本,读上面的配置,算三笔账:日历人天 → 有效人天、每个角色的产能对需求、以及整体利用率与吞吐:

#!/usr/bin/env python3
# file: sim/ta14_capacity.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta14_capacity.py
"""14.1 产能度量:人天与吞吐(S 层,卷二第 14 章)。

读 ``config/ta14_capacity.json`` 的示例产能模型,算三笔整数账:

1. **日历人天 → 有效人天**:有效系数 = 出勤率‰ × 专注率‰ // 1000(整数地板除);
2. **角色产能 vs 需求**:``calendar100 = people × workdays × 100``,
   ``effective100 = calendar100 × factor‰ // 1000``;需求 ``demand100 = Σ units × costPerUnit100``;
3. **利用率与吞吐**:``util‰ = demand100 × 1000 // effective100``(>1000 即超产能),
   ``throughput100 = units × 10000 // effective100``(每有效人天的资产数 ×100)。

全整数、无浮点。数字是**示例参数**(本套构造),非任何真实团队。落 ``out/ta14_capacity.json``。
"""
import json
import os
import sys

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CFG = os.path.join(ROOT, "config/ta14_capacity.json")
OUT = os.path.join(ROOT, "out")


def build_report(cfg):
    """把示例产能模型算成整数账。"""
    workdays = cfg["workdays"]
    factor = cfg["availabilityPermille"] * cfg["focusPermille"] // 1000
    roles = {r["id"]: r for r in cfg["roles"]}
    calendar, capacity = {}, {}
    for rid, r in roles.items():
        c = r["people"] * workdays * 100
        calendar[rid] = c
        capacity[rid] = c * factor // 1000
    demand = {rid: 0 for rid in roles}
    units = {rid: 0 for rid in roles}
    for d in cfg["demand"]:
        demand[d["role"]] += d["units"] * d["costPerUnit100"]
        units[d["role"]] += d["units"]
    rows = []
    for rid in sorted(roles):
        eff, dem = capacity[rid], demand[rid]
        rows.append({
            "role": rid, "people": roles[rid]["people"],
            "calendar100": calendar[rid], "effective100": eff,
            "demand100": dem, "units": units[rid],
            "utilPermille": dem * 1000 // eff if eff else 0,
        })
    cal_total = sum(calendar.values())
    eff_total = sum(capacity.values())
    dem_total = sum(demand.values())
    unit_total = sum(units.values())
    return {
        "project": cfg["project"], "workdays": workdays,
        "availabilityPermille": cfg["availabilityPermille"],
        "focusPermille": cfg["focusPermille"], "effectiveFactorPermille": factor,
        "calendarTotal100": cal_total, "effectiveTotal100": eff_total,
        "demandTotal100": dem_total, "unitTotal": unit_total,
        "gap100": dem_total - eff_total,
        "utilPermille": dem_total * 1000 // eff_total if eff_total else 0,
        "naiveUtilPermille": dem_total * 1000 // cal_total if cal_total else 0,
        "throughput100": unit_total * 10000 // eff_total if eff_total else 0,
        "avgCost100": dem_total // unit_total if unit_total else 0,
        "roles": rows,
    }


def main() -> int:
    cfg = json.load(open(CFG, encoding="utf-8"))
    rep = build_report(cfg)
    os.makedirs(OUT, exist_ok=True)
    with open(os.path.join(OUT, "ta14_capacity.json"), "w", encoding="utf-8") as f:
        json.dump(rep, f, ensure_ascii=False, indent=2, sort_keys=True)
        f.write("\n")
    print(f"factor={rep['effectiveFactorPermille']}permille "
          f"calendar100={rep['calendarTotal100']} "
          f"effective100={rep['effectiveTotal100']} demand100={rep['demandTotal100']}")
    print(f"util={rep['utilPermille']}permille naive={rep['naiveUtilPermille']}permille "
          f"gap100={rep['gap100']} throughput100={rep['throughput100']} "
          f"avgCost100={rep['avgCost100']}")
    for r in rep["roles"]:
        print(f"  {r['role']:5s} ppl={r['people']} effective100={r['effective100']} "
              f"demand100={r['demand100']} util={r['utilPermille']}permille units={r['units']}")
    print("wrote out/ta14_capacity.json")
    return 0


if __name__ == "__main__":
    sys.exit(main())

实跑 /opt/homebrew/bin/python3 sim/ta14_capacity.py:

factor=595permille calendar100=6000 effective100=3570 demand100=3780
util=1058permille naive=630permille gap100=210 throughput100=120 avgCost100=87
  char  ppl=1 effective100=595 demand100=800 util=1344permille units=2
  env   ppl=2 effective100=1190 demand100=1720 util=1445permille units=16
  ui    ppl=2 effective100=1190 demand100=960 util=806permille units=24
  vfx   ppl=1 effective100=595 demand100=300 util=504permille units=1
wrote out/ta14_capacity.json

14.1.4 读账:利用率 1058‰ 意味着什么

三笔账读下来,结论很直接:

  • 整体利用率 = 1058‰,也就是需求 113.60 人天(demand100=3780)对产能 35.70 有效人天(effective100=3570)——超产能 1058 - 1000 = 58‰。这份计划排不下。
  • 如果按日历人天算,利用率只有 630‰(naive=630),看起来「还有 37% 余量」。这正是日历人天的陷阱:它把不产出的时间也算成了产能。同一个计划,换个分母就从「宽裕」变成「超标」,这就是为什么必须先做 14.1.1 那一步拆解。
  • 角色之间严重不均衡:场景与家具 1445‰、角色与头像 1344‰ 双双爆表,而 UI 与图标只有 806‰、特效只有 504‰ 反而有余量。产能不能跨角色平移——UI 的余量救不了场景,因为技能不通用。
  • 吞吐 = 1.20 资产/有效人天(throughput100=120),平均单件成本 0.87 人天(avgCost100=87)。这两个数把「产能」翻译成了「件数」,是 15.1 计价模型的直接输入。

把四行摊开,每个角色的账目是(单位:人天×100):

角色人数日历人天×100有效人天×100需求人天×100利用率
场景与家具 env22000119017201445‰
角色与头像 char110005958001344‰
UI 与图标 ui220001190960806‰
特效 vfx11000595300504‰
合计66000357037801058‰

一句话:利用率的分母选错,整张排期表都会骗你。 1058‰ 与 630‰ 是同一个计划的两种读法,差的就是那 595‰ 的有效系数。

14.1.5 反推:这份产能最多能出多少件

利用率是「需求占产能的比例」,反过来问同样有用:把一个角色的产能全投到某一类资产上,最多能出多少件? 答案就是 有效人天×100 // 单件成本×100(整数地板除):

角色有效人天×100目标资产单件成本×100最多件数
场景与家具 env1190家具图标6019
场景与家具 env1190家具 prefab2504
角色与头像 char595角色头像4001
UI 与图标 ui1190UI 图标4029
特效 vfx595特效3001

这张表是 15.1 计价模型的直接输入:「一个迭代能做多少件」不是拍脑袋,而是 有效人天×100 // 单件成本×100。 注意 1190 // 250 = 4(不是 4.76)——整数地板除把「做不完的那 0.76 件」直接砍掉,这正是排期要保守的地方:产能账宁可少算一件,也不要在交付日发现差半件。

利用率超过 1000‰ 时,能动的只有三条杠杆,它们在账面上都只是「改一个数」,代价却完全不同:

杠杆改动有效人天×100整体利用率
加人env 从 2 人加到 3 人4165907‰
砍范围总需求砍 20%3570847‰
提系数专注率 700‰ → 750‰3822989‰

加人涨成本、砍范围降交付、提系数通常要改流程。三条杠杆都能把 1058‰ 压到 1000‰ 以下,但只有先算清「是哪一条在起作用」,才能决定动哪一条——这正是本节把产能拆成显式整数的意义。

14.1.6 有效系数是敏感参数

effectiveFactorPermille = 595‰ 不是常量,它是两个示例参数的乘积。把专注率从 700‰ 调到 650‰(多留一点会议与协作时间),有效系数变成 850 × 650 // 1000 = 552‰,有效人天从 35.70 掉到 33.12,整体利用率从 1058‰ 升到 1141‰:

专注率‰有效系数‰有效人天×100整体利用率
70059535701058‰
65055233121141‰

同一份需求,专注率降 50‰,利用率就涨 83‰。这说明产能账对「有效系数」这个输入非常敏感——所以它必须先被显式声明,而不是藏在某个人的直觉里。本节的 595‰ 是示例口径,真实团队要用自己的实测值替换;替换之后整张表会同步变化,但算法与对账口径不变。

14.1.7 四条口径边界

  • 数字全是示例参数。出勤率、专注率、单件成本都是本套构造的,目的是演示算法而非给出可移植的产能基准。任何真实团队的产能需真实环境(真实人员与项目数据),命令行不可复现。
  • 不做「提升 N 倍」的断言。本节只给同机、同口径的相对账,绝对数值不可移植;把 focusPermille 从 700 改成 800 会让所有利用率同步变化,但那只说明「参数敏感」,不说明任何真实收益。
  • 人天是最小颗粒,不是最优颗粒。真实排期还要处理任务可分割、多人协作、评审往返,这些会进 14.2 的依赖 DAG;本节先把「一支团队一个迭代有多少有效产能」这件事算成可复算的整数。
  • 利用率不是越低越好。1058‰ 说明计划排不下,但 600‰ 也未必健康——产能长期闲置意味着人力在烧钱。真正要看的是角色间的均衡:env 1445‰ 与 vfx 504‰ 并存,说明瓶颈在特定角色,而 14.2 的关键路径正是处理这种不均衡的工具。

阅读导航:上一节:13.3 工具的采纳率与维护成本 · 下一节:14.2 排期与美术依赖 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「gameart」更多文章

  1. 附录 A:风格参数表、量化规则与数据集索引
  2. 9.3 等距资产的制作约束
  3. 9.2 等距 tile 与遮挡规则