第27章 · 多职业成长与数值经济

📑 目录

导语:「远港 Online」开服第 41 天,运营拉出一条告警曲线:全服金币总量 30 天翻了三倍,拍卖行的铜矿从 2 银涨到 9 银,新号连药水都买不起。策划连夜改 Excel,程序给了个更坏的消息——经验曲线的指数写死在 C++ 里,调参要走完整发版流程,灰度三天。那一周的复盘会只总结出一句话:数值是活的,写死常数的代码是死的。本章讲怎么把成长、掉落、经济这三台发动机全部搬进表里,以及搬进表之后,旋钮该怎么拧。

本章地图

mindmap
  root((第27章
多职业成长与数值经济)) 核心循环 练级 装备 团本 四环 每个环节的乐趣与疲劳 立场:一切数值先进表 成长曲线 A["L(x)=a·x^b"] a 管总时长 b 管前松后紧 1到60级时间预算表 职业平衡 机动 生存 输出 辅助 不可替代性大于相等 等效DPS统一货币 GearScore四条军规 掉落随机 方差制造愤怒少数派 保底收敛方差 防工作室收益衰减 经济系统 水龙头:任务 卖店 掉落 水槽:修理 拍卖行税 坐骑 通胀等于流入减流出 远港落地 六张数值表结构 结算器代码零常数 赛季节奏滚动

本章的顺序跟着一台 MMO 的呼吸走:先解剖核心循环这台"希望发动机",再逐个拆它的四台分发动机——成长、职业、掉落、经济——最后把「远港 Online」的六张数值表从电子表格一路接到第 22 章的 GE 管道上。读这一章不需要你会玩 MMO,但需要你接受一个立场:程序员的职责是把公式做成配置驱动的结算器——把常数写死在代码里,那是过去式。

27.1 补课·数值策划:核心循环是一台"希望发动机"

补课:本节补数值策划的世界观——把数值理解为一台发动机的转速表,比当成一堆数字有用一百倍。策划出身的读者可以直接跳到 27.2 看公式。

单机游戏的数值是一次性剧本。「回响峡谷」主角的攻击力从 10 涨到 40,通关就结束,涨错了顶多重打一个 Boss。MMO 的数值是一台要运转五年的机器:几十万玩家同时踩踏板,任何一个齿轮转速不对,三个月后都会以运营事故的形式回来找你。

《魔兽世界》二十年没换过的核心循环,拆开来只有四个齿轮:

flowchart LR
    A["练级
任务与杀怪"] --> B["升级
解锁新技能"] B --> C["换装
属性上台阶"] C --> D["团本
协作挑战"] D --> E["掉落
更强装备"] E --> C E --> F["毕业
等待新赛季"] A2["乐趣:成长可见
疲劳:机械重复"] -.-> A B2["乐趣:新玩具
疲劳:满级即终点"] -.-> B D2["乐趣:协作与荣光
疲劳:组队成本高"] -.-> D E2["乐趣:开奖瞬间
疲劳:二十连空"] -.-> E

四个齿轮,每个都同时挂着乐趣来源和疲劳点。练级的乐趣是成长肉眼可见,疲劳是杀第三十只野猪时的机械重复;升级的乐趣是新技能这个"新玩具",疲劳是满级一刻的终点落差;团本的乐趣是协作与首杀荣光,疲劳是固定团考勤式的组队成本;掉落的乐趣是开奖瞬间的多巴胺,疲劳是连刷二十次颗粒无收的挫败。

数值策划的全部工作,是在这四个齿轮上调节乐趣的密度与疲劳的间隔:乐趣密度高于疲劳积累速度,玩家留下;反过来,玩家流失。满级送一把超越五人本的好武器,是把"终点落差"往后推;团本掉落加保底,是把"二十连空"的尾巴砍掉。每一条具体手法后面都会展开。

四环之外还有一圈元循环:赛季。装备总会毕业,而毕业是疲劳的终极形态——没有下一个台阶可追,乐趣密度直接归零。赛季制用一次软重置给循环重新上弦:新团本、新装等、新水槽,旧毕业装一夜变成新本的入场券。玩家嘴上骂"肝白废了",留存曲线却在每次赛季更新时准点回弹——他们要的从来都是"又有得追了",保值只是嘴上说说。

这套循环之所以值得逐环建模,是因为每一环都可以被计算:练级速度是经验曲线的积分,换装强度是等效 DPS 的台阶,团本门槛是一次属性检查,掉落是带保底的几何分布,物价是货币存量的函数。玄学部分——手感、情怀、社区情绪——真实存在,但底盘全是可算的。后面四节,每台发动机配一个模型。

程序在这条循环里的位置要说清楚。决定 60 级该花 117 小时的那个人是策划——你的工作是把"117 小时"做成策划 Excel 里可改的一行。判断标准只有一条:策划想调任何一个数字,如果需要拉上程序发版本,这个系统就已经失败了。公式留在代码里,经过评审、可单测;常数全部进表,策划自治。这个分工和数据库一模一样——程序写存储引擎,策划写数据,没有人会接受"改一行用户数据要重新编译 MySQL"。

27.2 成长曲线:a 管五年总时长,b 管前松后紧

行业里的升级经验几乎都在用幂函数族,形式干净得不像话:

L(x)=axbL(x) = a \cdot x^{b}

L(x)L(x) 是从 xx 级升到 x+1x+1 级所需的经验,aa 是基础量级,bb 是形状指数。整个五年运营的节奏感,就藏在这两个旋钮里。

bb 是手感旋钮,拧大拧小是两种完全不同的游戏:

b 的取值曲线形状玩家体验代价
b = 1(线性)每级匀速变难平稳,没有节奏感缺乏钩子,中规中矩
b 拧大(1.8~2.2)前松后紧新手期飞快,留存曲线好看50 级后每级时长陡增,后期流失
b 拧小(0.7~0.9)前紧后松满级很快,满级内容早消费升级感贬值,第 58 次金光内心毫无波澜

免费游戏偏爱大 b:先把人钩住再说。但 b 拧大的账会在两个月后一起结——50 级之后的流失曲线斜率和 b 正相关。b 拧小的账更隐蔽:玩家比计划早两周满级,团本还没做完,运营被迫提前放内容,整个内容排期被牵着鼻子走。

aa 是总时长旋钮:形状不变,aa 翻倍等于全部升级时间翻倍。赛季末的"追赶机制",就是给回归玩家临时把 aa 打六折。

正确的开工顺序是先定时间预算,再反推 a 和 b。预算是体验目标,公式只是实现手段。「远港 Online」的 1–60 级预算表长这样:

等级段每级时长累计时间该段主要内容
1–10约 20 分钟约 3 小时新手村与教学
11–20约 40 分钟约 10 小时第一张完整地图
21–30约 1.2 小时约 22 小时双地图 + 首个五人本
31–40约 2 小时约 42 小时阵营冲突区
41–50约 3 小时约 72 小时高难任务链
51–60约 4.5 小时约 117 小时冲刺满级 + 团本开门任务链

117 小时满级,对标经典 MMO 的 100–150 小时区间,是"硬核但可达"的口径。有了预算表,aabb 就是解方程的事:取 b1.35b \approx 1.35 让曲线微微上翘,再调 aa 让各段落在预算线上。

预算表的另一半是经验获取速率。总时长其实是这么算出来的:

T1N=x=1N1axbr(x)T_{1\to N} = \sum_{x=1}^{N-1} \frac{a \cdot x^{b}}{r(x)}

r(x)r(x) 是该等级段的平均经验获取速率(杀怪加任务),它同样进表。策划真正的日常战场在 r(x)r(x) 上——aabb 是开服前定好的,而 r(x)r(x) 曲线要光滑,持续维护全靠它。一旦出现"这一段升级突然快三倍"的断裂带,玩家会扎堆刷同一个任务,其余内容变鬼城,地图设计师的心血直接作废。

「远港 Online」二测时就踩过一次:32 级某条任务链的单次经验被错配成同级均值的三倍,一周内全服 32 级玩家挤爆同一张地图,Zone 负载(见第 25 章)跟着报警。修表、回滚溢出经验、补发全服补偿,一套流程走了两天——那串数字好在是在表里,如果写死在代码里,还得再加三天发版。

属性成长是另一道选择题:线性还是指数。指数模型(每 10 级属性翻倍)到 60 级就是 64 倍差距,旧副本瞬间变灰,新人永远追不上老人,最后只能做"属性压缩"大手术——WOW 历史上做过两次,每次都伴随社区地震。「远港 Online」的选择是:基础属性分段线性,成长感的大头交给装备。 装备是可控的台阶,还能兼任经济回收手段(见 27.5)。一句话记住分工:等级管时间,装备管强度,两条曲线别搅在一起。

属性压缩值得单独说,因为它是所有指数膨胀的终点站。当满级血量涨到六位数、伤害数字蹦出七位数,界面排版、int32 上限、玩家的读数快感会同时出问题。压缩的做法是全服数值除以一个常数再拉平曲线——听起来只是除法,实际要重算数年积累的所有怪物血量、技能系数与装备属性,任何一处漏除都会变成秒杀 bug。WOW 的两次压缩都动用以季为单位的工程量。起点选线性,终点就不用做这台手术。

27.3 职业平衡:瞄准的是"缺了谁都不行"——谁比谁数字大,从来是假目标。

新手平衡师最容易立错的目标函数,是"三个职业 DPS 相等"。真做到了,得到的是三碗同质化的面,社区照骂——玩家嘴里喊的是"我要数值公平",心里要的其实是"我选的职业有存在的意义"。

平衡的正确定义:每个职业至少拥有一个不可替代的场景。 拿「远港 Online」的三职业画一张团队需求矩阵:战士是唯一的坦克,减伤链和换坦机制只有他扛得住;法师是唯一的群体控制加爆发 AOE,ADD 小怪波次没有他就清不掉;牧师是唯一的群体治疗与复活,团队血线缺口只有她能补。缺任何一个,副本机制无解。这三个"只有我能",比任何 DPS 排行榜都值钱。

四维定位图能看清三职业各自站在哪:

quadrantChart
    title "远港 Online 三职业定位矩阵"
    x-axis "偏辅助" --> "偏输出"
    y-axis "偏机动" --> "偏生存"
    quadrant-1 "重装前线"
    quadrant-2 "团队之锚"
    quadrant-3 "游走支援"
    quadrant-4 "玻璃大炮"
    "战士": [0.62, 0.85]
    "法师": [0.85, 0.25]
    "牧师": [0.20, 0.55]

战士蹲在高生存的"重装前线",法师站在高输出低生存的"玻璃大炮",牧师守在"团队之锚"。四维打分(机动、生存、输出、辅助)同样进表管理,总和不必相等,但每个职业必须有一格全服第一——那一格就是他的不可替代性来源。哪天战士的辅助分被调到和牧师一样,两个职业会同时失去存在意义:一个没人组,一个没人怕。

不可替代性是可以被数据验证的。每周看三职业的团本出场率:长期低于 25% 的职业,说明他的"只有我能"被机制或数值偷走了——要么别的职业也能干他的活,要么他的活在当前副本里不重要。出场率均衡不代表强度相等,它只证明每个职业都有进本的理由。这个指标比任何木桩 DPS 排行都更接近平衡的本质。

定位定性之后,强度要定量。暴击、急速、命中这些属性各有各的单位,没法直接比较,得先折算成统一货币——等效 DPS:

E[DPS]=DbaseTcast(1+c(k1))(1+h)phitE[DPS] = \frac{D_{base}}{T_{cast}} \cdot \bigl(1 + c\,(k-1)\bigr) \cdot (1 + h) \cdot p_{hit}

逐项说人话:DbaseTcast\frac{D_{base}}{T_{cast}} 是裸伤除以读条时间;暴击率 cc、暴击倍率 kk(通常为 2),期望增益是 c(k1)c(k-1);急速 hh 提升施法频率,近似独立乘区 (1+h)(1+h);命中率 phitp_{hit} 直接乘——未命中就是零伤害,所以满命中之前命中的边际收益通常全场最高,"先堆命中"这条铁律就是这么推出来的。

代一组法师面板进去:火球均伤 500,读条 2.25 秒,暴击 20%,急速 10%,命中 95%,得到 222×1.2×1.1×0.95279222 \times 1.2 \times 1.1 \times 0.95 \approx 279 DPS。在这个面板上求偏导,得到当天的"汇率":1% 暴击 ≈ 2.3 DPS,1% 急速 ≈ 2.5 DPS,1% 命中 ≈ 2.9 DPS。所有属性从此可以摆进同一个天平。

装备评分(Gear Score)就是这个天平的应用:每件装备的分数等于各属性乘上权重再求和。

GS(item) = \sum_{i} w_i \, s_i, \qquad w_i = \left. \frac{\partial \, E[DPS]}{\partial \, s_i} \right|_{\text{基准面板}}

权重 wiw_i 是该属性在基准面板上的边际收益。GS 怎么算才不蠢,四条军规:

  1. 权重从边际收益导出。 拍脑袋的权重会在第二周被玩家用模拟器打脸。
  2. 权重随面板漂移,按赛季重算。 暴击堆到 40% 之后,1% 暴击的边际收益会下降,统一货币的"汇率"是浮动的。每赛季重算一次权重表,写进运营日历。
  3. GS 只在同职业内部可比。 跨职业比 GS 是论坛骂战的第一弹药,官方评分体系绝不背书这种比较。
  4. 套装特效与主动技能单独折算一列,别揉进主分。揉进去的评分会失去解释性,玩家问"这件凭什么高 5 分"时你答不上来。

坦克和治疗不在 DPS 榜上,但同样要建模。威胁值(Threat)= 伤害 × 威胁系数 + 技能附加威胁,系数进表;OT(仇恨失控)灭团是团本经典事故,威胁系数是坦克的隐形强度——它不出现在任何排行榜上,却决定团队能不能开打。治疗则折算成"等效生存秒数":团队承伤曲线与治疗输出曲线之间的缺口,就是牧师的工作量和存在理由。

27.4 纯随机掉落为什么会被骂上热搜

先算一道小学概率题:掉率 10% 的坐骑,连刷 20 次不出的概率是 0.92012.2%0.9^{20} \approx 12.2\%。翻译成人话:每 8 个玩家里就有 1 个,刷了 20 次颗粒无收。10 万日活的游戏,每天产出一万多个"20 连空"玩家,其中哪怕只有 1% 发帖,也是每天一百多条"这游戏暗改掉率"——热搜素材就是这么批量生产的。

玩家的概率直觉是"十次出一次",第 11 次不出就觉得被针对。说到底是人对独立随机事件的直觉天生有偏差(赌徒谬误),别往玩家数学水平上归因。你没法教育全体玩家,只能改系统。

真正的敌人是方差。纯随机的期望次数是 10 次,但几何分布的尾巴极长——44 次不出的概率仍有约 1%。保底不改期望,只砍尾巴:分布从几何分布变成"封顶 20 次"的截断分布。这个思路在业界早有先例,Dota 2 的暴击用的就是递增概率(PRD),目的相同:砍掉"连续三刀不爆"的方差尾巴。

保底还有一个白捡的心理学加成:它把赌博变成进度条。玩家的心智从"下一次会不会出"变成"再打 7 次必出",挫败感变成目标感。期望支出不变,玩家体验质变——游戏设计里少有的零成本双赢。

严格说也不是零成本,代价藏在分布形状里。两组分布摆在一起看最直观(两边都标定到约 10% 的实际掉率,模拟 20 万次击杀):

指标纯随机 10%递增概率 + 20 次硬保底
期望击杀次数10.0≈ 9.9
中位数79
90% 玩家拿到22 次内18 次内
99% 玩家拿到44 次内20 次内
最坏情况理论上无穷20 次

期望几乎不变,尾巴从 44 次砍到 20 次;代价是中位数从 7 推到 9——"三次就爆"的欧皇时刻变少了。保底削掉的是分布的两个极端:极端的幸运和极端的不幸,而社区音量永远来自后者,这笔交换稳赚。

参数标定这步别省。base 和 step 拧完必须跑蒙特卡洛验证实际掉率:拍脑袋拍一组 5% + 5%,期望掉率会被悄悄抬到约 19%,等于全服装备通胀——保底没做成,先把装备做贬值了。上面那组能用的参数是 2.5% + 每次递增 1.25%,20 次硬保底,实际掉率 10.1%。

「远港 Online」的保底结算器长这样,全部参数来自掉落表:

# 伪代码:个人掉落保底结算(服务器侧,每次击杀 Boss 调用)
# 策划只配三个数:base_chance / step / hard_pity,全部来自掉落表
class PityState:
    misses: int = 0          # 连续未中次数,随角色档案持久化(见第 26 章)

def roll_boss_loot(cfg, state) -> bool:
    # 概率随未中次数线性爬坡:第 n 次未中后,命中率 = base + step * n
    chance = min(cfg.base_chance + cfg.step * state.misses, 1.0)

    if state.misses + 1 >= cfg.hard_pity:   # 硬保底:第 N 次必掉
        chance = 1.0

    if rand() < chance:
        state.misses = 0                    # 中奖清零,重新计数
        grant_loot(cfg.item_id)
        return True

    state.misses += 1
    save_dirty(state)                        # 标脏,等待批量回存
    return False

三个工程要点。一是计数器按角色持久化,跨天、跨副本都有效——玩家上周刷了 17 次没出,这周上线第 3 次必出,这个"记忆"是保底体验的根基,落库走第 26 章的脏标记回存。二是结算只在服务器,客户端上报"我出了"一律按作弊处理,权威模型见第 21 章。三是保底对打金工作室同样有效,所以要配收益衰减:同一目标当日第 50 次击杀之后掉率打 2 折,衰减起点也进表。让脚本的边际收益低于电费,比封号便宜得多。

27.5 经济系统:盯紧水龙头与水槽的差额

MMO 金币的本质是一家无限发行的央行:每只怪都是印钞机,每次交任务都在做量化宽松。通胀不通胀,不取决于某个水龙头开多大,而取决于全体产出与全体回收的差额。差额长期为正,水位就涨,物价就跟。

「远港 Online」经济模型:水龙头与水槽(每日全服口径)任务奖励怪物掉落金币垃圾卖店+3.2 亿/日+3.6 亿/日+1.2 亿/日产出合计 8.0 亿/日水位线 = 全服金币存量 M金币池(货币存量 M)M 持续上涨→ 物价 P 跟涨装备修理 −1.8 亿拍卖行税 5% −2.6 亿坐骑·学费·传送 −2.1 亿净流入 +1.5 亿/日:M2 约 30 天翻倍,通胀在路上P ≈ M·V / Q

通胀的机理用货币数量论一句话讲清:

PMVQP \propto \frac{M \cdot V}{Q}

MM 是全服金币存量,VV 是流通速度,QQ 是可购买的商品与服务总量。新资料片上线后,玩家刷本热情高涨,MM 日均涨 2%;而 QQ(消耗品、材料、服务的有效供给)只涨 0.3%。差值全部变成物价——铜矿从 2 银涨到 9 银就是这么来的。最受伤的是新玩家:他们的收入曲线没跟上物价曲线,等于被动缴纳"新人税",新手村留存应声下跌。开服第 41 天那场事故,剧本就是这么写的。

公式里还有一个容易被忽视的 VV(流通速度)。同样 100 亿存量,躺在大户仓库里不动,和每天在拍卖行倒手三次,对物价的影响差出数倍。囤金是"冻结的水位",扫货是"活水位"——所以物价指数要选高流通商品做篮子——全服均富是个幻觉,别用它。VV 的最大放大器是线下打金交易:工作室产出的金不消费,全流向买家变成即期购买力,等于把冻结水位瞬间解冻。打击打金除了封号,更釜底抽薪的做法是 27.4 的收益衰减加绑定货币——让打金产出进不了流通市场。

回收工具箱按优雅程度排序:

  1. 拍卖行交易税(5%):比例税,交易越活跃回收越多,随经济规模自动缩放,玩家痛感低——首选工具,没有之一。
  2. 修理费与传送费:与活跃行为挂钩,稳定但量级小,适合当底仓。
  3. 高价坐骑、幻化学费:一次性大额回收,资料片必备,效果立竿见影,代价是掏空中产玩家的积蓄,一年最多用一两次。
  4. 绑定货币(拾取绑定的第二种金币):直接开一条不流通的平行货币,激进但有效,WOW 的各类点数就是这个思路。

高价坐骑这类一次性水槽,定价锚在全服人均持有的 3–5 倍:定低了,大户秒买、回收量不足;定高了,中产彻底绝望,连讨论热度都换不来。它的目标很单纯——让前 5% 的囤金大户把冻结水位一次性蒸发——一匹 500 万金的坐骑,回收效率约等于拍卖行抽税半个月。

核心原则一条:回收额必须随产出自动缩放。 固定额回收(每人每天 10 金修理费)在 M2 翻倍之后约等于没有。比例税是唯一跟得上通胀的结构——这也是它排第一的原因。

监控经济不需要复杂模型,每天三个数就够:

-- 每日经济体检(离线库执行,结果推仪表盘;窗口函数语法以所用数据库方言为准)
-- ① 水位:货币总量与人均持有
SELECT SUM(gold) AS m2_total,
       AVG(gold) AS per_capita
FROM   character_wallet;

-- ② 物价指数:30 种基准商品(草药/矿石/附魔材料)当日成交中位价
SELECT item_id,
       PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY unit_price) AS median_price
FROM   auction_deals
WHERE  deal_time >= CURRENT_DATE - INTERVAL '1 day'
  AND  item_id IN (SELECT item_id FROM econ_price_basket)
GROUP  BY item_id;

警戒线画在"M2 周环比 > 3% 且连续两周":触发即启动回收预案——拍卖行税上调 1–2 个点,或提前投放坐骑水槽。经济事故的黄金处理期是头两周,等玩家在论坛"感觉"到通胀,物价已经涨完一轮,回收成本翻倍。

这里和第 26 章的分工要划清:第 26 章保证每一笔交易原子、不丢不重,管的是微观正确;本章管总量政策,是宏观健康。拍卖行"扣款与发货"的事务边界是 26 章的事,"每笔交易抽走的 5% 税去哪"是本章的事——答案是:从系统里直接蒸发,不进任何 NPC 的口袋。 蒸发是回收与再分配的本质区别,税收一旦回流成 NPC 补贴,回收就失效了一半。

27.6 「远港 Online」数值表落地:从 Excel 到 GE

前面五节的所有数字,最后都收进同一条管线。先看全貌:

classDiagram
    class SheetSource {
        +策划维护的电子表格
        +Git 管版本,改动要评审
        LEVEL_XP / ATTR_GROWTH / SKILL_COEF 等
    }
    class DataTableAsset {
        +UDataTable(CSV/JSON 导入)
        +CI 导出,Cook 进包
        +开发期支持热重载
    }
    class Settlement {
        +结算器:服务器唯一调用点
        +查表、组装数值
        +代码内零常数
    }
    class GameplayEffect {
        +SetByCaller 注入数值
        +管道见第 22/23 章
    }
    class AttributeSet {
        +MaxHealth / Attack / SpellPower
    }
    SheetSource --> DataTableAsset : 导出管线
    DataTableAsset --> Settlement : 运行时查询
    Settlement --> GameplayEffect : 数值注入
    GameplayEffect --> AttributeSet : 修改属性

策划维护电子表格(Excel 或 Google Sheet,Git 管版本——是的,表格也要评审,二进制冲突就用 CSV 中转),CI 导出后导入 UDataTable。UE 原生支持 CSV/JSON 行结构导入,更复杂的批量管线可以走 Dataprep;具体导入器配置在 5.8 中对应官方文档"Data-Driven Gameplay Elements"词条,以文档为准。运行时由结算器查表,数值经 SetByCaller 注入 GE——这条管道就是第 22 章搭好的那条,本章的表是它的上游配置。

表格进了 Git,评审纪律决定这条管线的寿命,三条够用。一是改表必须带理由:commit message 回答"为什么","是什么"泛泛而谈的会被打回,三个月后追查一次装备通胀时你会感谢这条。二是跨表一致性交给 CI 校验:SKILL_COEF 引用的职业必须在 ATTR_GROWTH 里存在,DROP_TABLE 的物品必须在物品主表里存在,孤儿行直接拒合。三是每次合入自动生成 diff 报告发到策划群,数值改动全员可见——暗改在透明管线里活不过一天。

「远港 Online」的电子表格一共六张,结构如下:

erDiagram
    LEVEL_XP ||--o{ ATTR_GROWTH : "level 1..60"
    ATTR_GROWTH ||--o{ SKILL_COEF : "class"
    DROP_TABLE }o--|| BOSS_DEF : "boss_id"
    LEVEL_XP {
        int level PK
        int xp_to_next
        int time_budget_min
        string unlock_note
    }
    ATTR_GROWTH {
        string class PK
        int level PK
        float max_health
        float attack
        float spell_power
    }
    SKILL_COEF {
        int skill_id PK
        string class
        float base_damage
        float coef_attack
        float coef_spell
        float cooldown_s
    }
    DROP_TABLE {
        int boss_id PK
        int item_id PK
        float base_chance
        float step
        int hard_pity
        int daily_decay_start
    }
    GEAR_WEIGHT {
        string class PK
        string stat PK
        float weight
        int season
    }
    ECON_POLICY {
        string key PK
        float value
        string note
    }

两张样例行,感受策划的日常工作界面。属性成长表(分段线性,装备承担强度大头):

classlevelmax_healthattackspell_power
战士1120120
战士6038003400
法师180418
法师60220060620
牧师195614
牧师60260090540

技能系数表(与第 22 章火球术解剖同构,伤害 = 基础值 + 系数 × 属性):

skill_id名称classbase_damage系数cooldown
1001冲锋战士901.2 × 攻击12 s
2001火球法师3200.95 × 法强读条 2.25 s
3001圣言术牧师−450(治疗)1.1 × 法强8 s

第六张 ECON_POLICY 容易被漏掉:税率、修理系数、坐骑价格这些经济政策参数同样进表。把央行利率写死在服务器代码里,等于每次降息都要程序员审批——27.5 的回收预案要求的是当天生效。

结算器的代码只有薄薄一层,全部数字来自查表:

// 成长结算器:所有数字来自 DataTable,代码里没有一个常数
USTRUCT(BlueprintType)
struct FAttrGrowthRow : public FTableRowBase
{
    GENERATED_BODY()
    UPROPERTY(EditAnywhere) int32 Level = 1;
    UPROPERTY(EditAnywhere) float MaxHealth = 0.f;
    UPROPERTY(EditAnywhere) float Attack = 0.f;
    UPROPERTY(EditAnywhere) float SpellPower = 0.f;
};

// 服务器侧调用:等级变化时重算基础属性,经 GE 落地
void UGrowthSettlement::ApplyLevel(UAbilitySystemComponent* ASC, int32 NewLevel) const
{
    const FString RowName = FString::Printf(TEXT("%s_%d"), *ClassId, NewLevel);
    const FAttrGrowthRow* Row =
        GrowthTable->FindRow<FAttrGrowthRow>(*RowName, TEXT("Growth"));
    // 表缺行是内容事故:直接报错,绝不给默认值——默认值等于替策划掩盖事故
    checkf(Row, TEXT("成长表缺行 %s"), *RowName);

    FGameplayEffectSpecHandle Spec =
        ASC->MakeOutgoingSpec(LevelAttrGE, NewLevel, ASC->MakeEffectContext());
    // SetByCaller 的 Tag 在数值登记表里统一分配(见第 23 章命名宪法)
    Spec.Data->SetSetByCallerMagnitude(TAG_Data_MaxHealth,  Row->MaxHealth);
    Spec.Data->SetSetByCallerMagnitude(TAG_Data_Attack,     Row->Attack);
    Spec.Data->SetSetByCallerMagnitude(TAG_Data_SpellPower, Row->SpellPower);
    ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
}

checkf 那一行值得展开:表缺行时给默认值,等于帮策划把内容事故藏起来,它会在线上以更贵的方式爆——某个 37 级战士突然裸奔,客服工单查三天才查到一张表。报错要趁早、要刺耳。

还有一个工程细节决定这套管线在线上好不好用:表怎么到达运行中的服务器。开发期靠编辑器热重载,改完即生效;线上不能重启进程,要走配置下发——服务器记录当前表版本号,运维后台推送新版本,结算器在安全点(副本结算间隙)整体切换,切换前后各落一条审计日志。是整体切换版本,不是热改单行:同一时刻全服只能存在一份生效的表,否则两个玩家在同一秒按不同版本的系数结算,投诉工单会比通胀来得更快。

数值表跟着赛季一起滚,不是一次性交差。节奏大致是这样:

gantt
    title 远港 Online 第一赛季数值节奏
    dateFormat YYYY-MM-DD
    section 练级期
    开服冲级窗口(盯 r(x) 断裂带)     :a1, 2026-03-01, 14d
    section 装备期
    五人本毕业潮(盯掉落保底投诉)      :a2, after a1, 21d
    团本一区开荒(盯职业出场率)        :a3, after a2, 28d
    section 回收期
    团本二区 + 新坐骑水槽投放           :a4, after a3, 35d
    赛季结算:GEAR_WEIGHT 重算与压缩评估 :a5, after a4, 14d

每个阶段盯的指标不同:冲级窗口盯 r(x)r(x) 曲线有没有断裂带,毕业潮盯保底投诉率,开荒期盯三职业出场率——出场率长期低于 25% 的职业,就是不可替代性被动了刀,下赛季的数值表要还回来。赛季末固定做两件事:按当前玩家面板重算 GEAR_WEIGHT 权重,评估属性膨胀是否逼近压缩红线。

一家运营五年的 MMO,数值团队九成的工作是在这六张表里拧旋钮;程序的那一成,是保证拧旋钮永远不需要重新编译。


小结

一切数值先进表:公式在代码里(评审、可单测),常数在表里(策划自治);策划调一个数字需要程序发版本,这个系统就已经失败了。成长曲线先定时间预算再反推参数:aa 管总时长,bb 管前松后紧;等级管时间、装备管强度,属性走分段线性,把指数膨胀挡在装备层。平衡瞄准的是不可替代性而非数字相等;等效 DPS 把暴击、急速、命中折成统一货币,Gear Score 权重从边际收益导出并按赛季重算。纯随机掉落的方差每天批量制造"20 连空"的愤怒少数派;保底不改期望、只砍尾巴,把赌博变成进度条,计数器必须按角色持久化。经济盯住产出与回收的差额:回收要随产出自动缩放(比例税是唯一结构),监控 M2、人均持有、物价指数三个数,税从系统里直接蒸发,不回流到任何 NPC 口袋。

上手任务

  1. 打通表格管线:把 1–60 级经验表与三职业属性表做成 CSV 导入 UDataTable,实现 27.6 的成长结算器。验收:修改表里某一级经验后,不改一行代码、不重新编译,重导表即在 PIE 生效;删掉某一行,启动时 checkf 直接报错并打出缺失行名。
  2. 保底模拟器:用 27.4 的伪代码跑 20 万次击杀模拟,对照纯随机 10%。先自己标定一组 base / step 参数,再做对照。验收:保底组"超过 20 次未中"的比例为 0%,纯随机组约 7%;两组实际掉率相差不超过 1 个百分点(提示:5% + 5% 的拍脑袋参数会把实际掉率抬到 19% 左右,标定不合格)。
  3. 算一张权重表:按 27.3 的等效 DPS 公式,为法师基准面板(火球 500 均伤、读条 2.25 秒、暴击 20%、急速 10%、命中 95%)计算各属性边际收益,给两件"装等相同、属性不同"的装备打分。验收:评分排序与训练木桩实测 DPS 排序一致;把暴击面板抬到 40% 后重算,指出哪件装备的排名发生了翻转。
  4. 伪造一周经济数据:在测试库造 7 天产出大于回收的钱包与拍卖行数据,跑 27.5 的体检 SQL。验收:仪表盘在"M2 周环比 > 3%"时标红,并能用一条 UPDATE econ_policy 把拍卖行税从 5% 调到 7%,无需重启服务器。

下一章

数值与经济都进了表,最后剩下的问题是有人想"改表"——作弊者不只盯客户端内存,还盯协议、盯结算器、盯掉落随机数。第 28 章上线与运营:反作弊分层、CI/CD 与热更新,把游戏从"能玩"推到"敢运营"。

延伸阅读

  • 官方文档:Unreal Engine → Data-Driven Gameplay Elements(UDataTable、行结构与 CSV/JSON 导入的权威词条)
  • 官方文档:Unreal Engine → Gameplay Ability System → GameplayEffect 词条(SetByCaller 与 Modifier 管道,配合第 22 章)
  • 源码:Engine/Source/Runtime/Engine/Classes/Engine/DataTable.hFindRow 的实现与上下文字符串的用途)
  • 书目:Richard Bartle《Designing Virtual Worlds》(虚拟世界经济与玩家类型的奠基之作,水龙头-水槽模型的思想源头之一)
  • GDC Vault 主题方向:魔兽世界团队关于属性膨胀与"item squish"(属性压缩)的分享,以及暗黑破坏神 III 拍卖行经济失败的复盘(检索 economy / itemization 关键词,看真实事故报告)