Skip to content

02 数值与经济系统 ​

本章目标:从服务端开发者的视角理解游戏的数值与经济系统——不是为了替策划做设计,而是为了在需求评审阶段识别技术风险,在系统设计阶段预留正确的数据结构和计算管线,在上线之后能用数据定位问题。

参考书籍:本章核心概念参考《游戏编程模式》(Robert Nystrom)中的游戏循环与状态模式、《游戏服务器架构与优化》中的数值系统设计章节,以及《百万在线》中的经济系统平衡实践。

阅读提示:本章的每一条设计都对应一个服务端实现决策——属性三层架构对应计算管线,保底状态机对应持久化字段,货币流水对应账表设计。读完可以对照第 8 章数据存储、第 10 章交易系统和第 17 章配置管线一起看。


1. 为什么服务端需要懂策划 ​

很多服务端开发者认为"策划的事跟我无关",这是一种危险的误解。策划的每一个决策——经验值曲线、抽卡概率、活动时间——最终都会变成你需要处理的数据结构和计算逻辑。如果你不理解策划为什么这样设计,你就无法判断一个需求是否合理、是否存在技术风险。

举一个真实案例:某卡牌手游的数值策划设计了一个"装备强化概率递减"系统——每次强化成功率从 80% 逐级降低到 5%,但强化失败装备不消失。结果玩家发现通过反复失败+重试,可以用少量材料把装备升到满级。数值策划认为"概率已经很低了",但服务端如果理解了概率系统的数学本质,就能提前预警:当重试的期望成本低于直接购买时,这个系统会变成一个变相的折扣商店。概率设计的问题不能靠"概率看起来很低"来判断,只能靠期望值计算来回答。

服务端开发者需要理解策划的思维框架,才能做到:在需求评审阶段就发现技术风险,而不是在上线后被玩家利用漏洞。

1.1 策划的职责与协作 ​

策划类型核心工作与服务端的关系服务端需要关注什么
系统策划设计游戏系统(背包、公会、邮件)接口设计、数据结构数据模型是否可扩展、并发安全
数值策划设计数值公式(伤害、属性、经济)计算逻辑、配置表公式是否有溢出风险、配置是否可热更
关卡策划设计关卡内容(副本、地图)关卡配置、怪物刷新关卡数据量、加载策略
战斗策划设计战斗系统(技能、AI、平衡)战斗逻辑、同步方案计算量是否可控、是否需要帧同步
运营策划设计活动和运营策略活动系统、奖励发放活动数据隔离、并发发放安全

关键认知:服务端不是策划的"工具人",而是策划的"技术顾问"。当你发现一个策划方案在技术上不可行或者存在风险时,可以考虑主动提出来,而不是默默实现然后在上线后擦屁股。

1.2 接数值需求时的四个技术提问 ​

数值需求评审时,服务端可以用四个问题快速定位技术风险:

  1. 这个公式在哪里算?——客户端算就是作弊后门,服务端算就要考虑性能和热更(第 2.4 节)
  2. 这个随机要不要可复现?——涉及回放/裁决/跨端一致,就要确定性随机(第 3.3 节)
  3. 这个计数器存在哪?——保底、签到、限购这类"次数状态"是资产,持久化在服务端(第 4.3 节)
  4. 这个数值变化要不要留痕?——涉及货币和资产的变更,流水先行(第 6.3 节)

2. 属性系统:数值计算的基础 ​

属性系统是所有 RPG 类游戏的基础。理解属性计算的层次关系,对于设计合理的服务端数据结构至关重要。

2.1 属性计算的三层架构 ​

游戏属性的计算通常分为三层,从内到外依次是:基础属性 → 百分比加成 → 固定加成。这个顺序非常重要,因为不同的计算顺序会导致完全不同的结果。

基础属性来自角色等级和成长值,是"出生就确定"的底子。百分比加成来自装备、buff、被动技能等,它放大基础属性。固定加成是最终叠加的"附加值",比如"攻击力+50"这种。

为什么这个顺序很重要?因为它直接影响数值膨胀的速度。如果百分比加成和固定加成的顺序搞反(先加固定值再放大),固定加成也会被百分比放大,高等级玩家和低等级玩家的差距会被加速放大,导致平衡失控。《游戏服务器架构与优化》中特别强调:属性计算公式需要在服务端执行,客户端只做展示,否则就是给外挂留后门。

属性说明典型公式
HP生存能力(base + growth × level) × (1 + 百分比加成) + 固定加成
ATK伤害输出(base + growth × level + 装备基础) × (1 + 百分比加成)
DEF减伤能力同上,影响伤害公式的减伤部分
SPD行动顺序base + buff加成,通常不参与百分比计算
CRIT暴击概率base + 装备加成,通常有上限(如 75%)

2.2 加法区与乘法区:多来源合并的规则 ​

当一个属性有多个加成来源时(装备 A +20%、buff B +30%、公会科技 +10%),合并规则是一个明确的策划决策,服务端照做即可,但需要理解两种规则的手感差异:

  • 加法区:(1 + 20% + 30% + 10%) = 1.6。来源越多收益越线性,策划容易预期总量
  • 乘法区:(1 + 20%) × (1 + 30%) × (1 + 10%) = 1.716。收益边际递增,天然鼓励堆叠同类型来源

减伤类属性几乎总是用乘法区合并:多个减伤来源 (1-20%) × (1-30%) 永远不会把减伤叠过 100%,加法合并则可能叠出"完全免伤"的漏洞。服务端实现时不要自作主张统一成一种规则——同一属性在不同来源上用不同合并区("暴击伤害走加法区、全伤害加深走乘法区")是常见做法,配置表里每个来源需要带"作用区"字段。

2.3 服务端实现:属性重算管线 ​

属性的完整值不需要每次访问都重算——重算只在"来源变化"时发生(升级、穿装备、buff 生效/消失)。标准做法是脏标记 + 惰性重算:

go
type Character struct {
    base       BaseStats    // 等级成长 + 装备基础
    modifiers  []Modifier   // 所有加成来源:装备、buff、被动、科技
    final      FinalStats   // 重算结果缓存
    dirty      bool         // 任一来源变化时置位
}

// 来源变化时只做一件事:打脏标记
func (c *Character) OnBuffAdded(m Modifier) {
    c.modifiers = append(c.modifiers, m)
    c.dirty = true
}

// 访问最终属性时,脏了才重算
func (c *Character) Stats() FinalStats {
    if c.dirty {
        c.recalc()
        c.dirty = false
    }
    return c.final
}

// 重算管线:整数运算,万分比
func (c *Character) recalc() {
    var pctAtk, flatAtk int64
    for _, m := range c.modifiers {
        if m.Attr != AttrATK {
            continue
        }
        switch m.Zone {
        case ZonePercent:
            pctAtk += m.Value // 万分比,如 2000 = +20%
        case ZoneFlat:
            flatAtk += m.Value
        }
    }
    base := c.base.ATK
    c.final.ATK = (base*(10000+pctAtk))/10000 + flatAtk // 先百分比后固定
}

三个实现纪律:全程整数运算(万分比,避免浮点累积误差)、重算在逻辑线程内完成(与第 5 章单写者原则一致,modifiers 列表不需要锁)、分层来源的生效与过期由统一的 buff/timer 系统驱动(而不是每处代码自己改 modifiers)。

2.4 为什么数值要服务端计算 ​

一个常见的错误是让客户端计算属性值,服务端只是"记录"结果。这看起来省事,但后果很严重:

  1. 作弊风险:客户端可以伪造任何属性值,你无法验证
  2. 数据不一致:不同客户端版本可能计算结果不同
  3. 热更困难:修改公式需要客户端更新

合理的做法是:客户端只负责展示,服务端负责计算和验证。 客户端可以缓存计算结果用于本地显示,但所有涉及游戏逻辑的属性值需要来自服务端。


3. 伤害公式:策划设计的核心 ​

伤害公式决定了战斗系统的手感和平衡性。不同游戏类型使用完全不同的伤害模型,理解这些模型有助于你设计合理的战斗服务。

3.1 四种主流伤害模型 ​

模型一:简单减伤(适合休闲游戏)。伤害 = 攻击力 - 防御/2,最低为 1。优点是简单直观,缺点是防御力的收益是线性的,高等级时防御力要么完全没用(攻击力远大于防御),要么过于强大(防御接近攻击力的两倍)。

模型二:百分比减伤(适合卡牌游戏)。伤害 = 攻击力 × (1 - 防御/(防御+K))。这里的 K 是一个常数,决定了防御力的"饱和点"。当防御力等于 K 时,减伤 50%。这个模型让防御力的收益递减,更合理。

模型三:乘算减伤(适合 MMO)。在百分比减伤的基础上叠加多个减伤来源(装备减伤、buff 减伤、被动减伤),用乘法而非加法合并,避免减伤率超过 100%(见 2.2 节)。

模型四:多乘区流水线。这是大多数商业游戏使用的模型:

text
伤害计算流水线:

  攻击力 × 技能倍率 → 基础伤害
       ↓
  基础伤害 × (1 - 防御减伤率) → 减伤后伤害
       ↓
  减伤后伤害 × 暴击倍率 → 暴击后伤害
       ↓
  暴击后伤害 × 元素/阵营克制倍率 → 克制后伤害
       ↓
  克制后伤害 × (1 ± 随机浮动) → 最终伤害

多乘区设计的价值在于策划可以独立调每个乘区——削弱某个职业时调克制倍率而不动技能倍率,出限时 buff 时只动特定乘区。服务端的实现要保留这个结构:伤害计算函数接收"乘区参数包",而不是把所有系数揉死在一个公式里。

3.2 服务端实现要点 ​

伤害计算看似简单,但在服务端实现时有几个需要注意的坑:

  1. 浮点精度问题:不要用浮点数做最终的伤害值。浮点运算有精度误差,可能导致"1点伤害"和"0点伤害"的判定出错。实践中推荐全程整数运算(万分比),例如:
go
const RATE = 10000 // 万分比

func calcDamage(p DamageParams) int64 {
    dmg := p.ATK * p.SkillRate / RATE          // 基础伤害
    dmg = dmg * (RATE - p.DefReduce) / RATE    // 防御减伤(万分比)
    if p.IsCrit {
        dmg = dmg * p.CritDamage / RATE        // 暴击倍率(万分比)
    }
    dmg = dmg * (RATE*2 - randRange(p.Min, p.Max)) / (RATE * 2) // 浮动
    if dmg > p.Target.HP*15/100 {              // 伤害封顶:单次 ≤15% 最大生命
        dmg = p.Target.HP * 15 / 100
    }
    if dmg < 1 {
        dmg = 1                                 // 保底 1 点
    }
    return dmg
}
  1. 随机数与确定性:普通玩法用全局随机源即可;帧同步或需要回放/裁决的玩法,需要每场战斗独立的确定性随机源(见 3.3 节)

  2. 伤害溢出:攻击力和技能倍率都很高时,ATK × SkillRate 可能超过 int32。Go 里用 int64 计算再裁剪,C++ 里注意整型提升规则

  3. 伤害封顶:策划通常会要求"单次伤害不超过最大生命值的 X%",这个限制需要在服务端实现——它同时是数值安全和展示体验(防止一击跳屏)的双重需求

3.3 确定性随机:种子管理与回放 ​

只要战斗结果需要跨端一致(帧同步)或事后重演(回放、裁决、离线演算),随机数就需要确定性:相同的种子序列产生相同的数列。

服务端的标准做法:

go
// 每场战斗独立的随机源(创建战斗时调用)
func newBattle(seedA, seedB uint64, frameInputs []FrameInput, logicVersion string) *Battle {
    battle := &Battle{
        rng:    rand.New(rand.NewPCG(seedA, seedB)), // 独立源,不用全局
        seedA:  seedA,   // seed 由服务器生成:matchID ^ 时间戳等
        seedB:  seedB,
    }
    // 战斗日志里记录种子——回放/裁决时重建完全相同的随机序列
    battleLog := BattleLog{
        Seed:    [2]uint64{seedA, seedB},
        Inputs:  frameInputs, // 帧输入序列(完整、有序、可重放)
        Version: logicVersion, // 数值配置版本,回放必须用当时的配置
    }
    _ = battleLog // 演示代码里仅用于说明;真实代码写入持久层
    return battle
}

三个纪律:战斗内所有随机都走这个源(不许混入 math/rand 全局函数或时间戳);数值配置有版本号(回放时用当时的配置表,否则调一次数值所有旧回放全部失真);种子进战斗日志(这是"服务端裁决"能力的根基)。这与帧同步的确定性要求(知识库"同步、战斗与实时交互"一章)是同一件事的两面。


4. 概率系统:玩家最敏感的领域 ​

概率系统是游戏中最容易引发玩家投诉的领域。中国法律规定抽卡概率需要公示,服务端的实现需要经得起审计。

4.1 概率的类型与心理效应 ​

概率类型说明心理效应典型应用
固定概率每次概率相同期望值明确抽卡 1% 出 SSR
保底概率达到阈值必出降低挫败感90 抽保底
动态概率概率随次数变化越抽越兴奋软保底机制
条件概率满足条件才触发增加策略性暴击、连击

4.2 随机源的选择:每玩家独立,可审计 ​

抽卡这类涉及资产的概率,对随机源有三个要求:

  1. 不用全局共享源:math/rand 的全局函数在 goroutine 间有锁竞争,且任何一处代码消耗随机数都会影响别的玩家的序列——每个玩家/每次抽卡独立源(如 PCG)更干净
  2. 种子不可预测:time.Now() 秒级时间戳做种子可以被暴力猜——种子用 crypto 级熵源生成后记录,而不是让玩家可推算
  3. 结果可追溯:每次抽取留下日志(谁、什么池、第几抽、随机值、结果),这是应对投诉和监管审查的证据链

4.3 保底机制的状态机实现 ​

保底有两种:硬保底(达到次数必出)和软保底(达到次数后概率逐步提升)。以常见的手游卡池为例——基础 0.6%,第 74 抽起每抽递增 6%,90 抽硬保底,出金后重新计数,UP 未中则下次必中(大保底):

go
type PityState struct {
    Count      int  // 距上次出金的抽数(持久化字段)
    Guaranteed bool // 大保底:上次 UP 未中(持久化字段)
}

type GachaPool struct {
    BaseRate  float64 // 0.006
    SoftStart int     // 74
    SoftStep  float64 // 0.06
    HardPity  int     // 90
}

func (p *GachaPool) Roll(state *PityState, rng rand.Source) RollResult {
    state.Count++
    rate := p.BaseRate
    if state.Count >= p.SoftStart {
        rate += float64(state.Count-p.SoftStart+1) * p.SoftStep
    }
    if state.Count >= p.HardPity {
        rate = 1.0
    }
    fiveStar := rand.New(rng).Float64() < rate
    if fiveStar {
        state.Count = 0
    }
    // Guaranteed 的翻转在 UP 判定处进行,此处从略
    return RollResult{FiveStar: fiveStar, Rate: rate}
}

实现上的关键点:Count 和 Guaranteed 是资产级状态——必须随抽卡在同一事务里持久化,绝不能信任客户端上报的抽数(外挂伪造抽数可以直接"跳过"保底);换池规则要明确(卡池轮换时计数继承还是清零,是策划决策,但服务端要显式处理而不是隐式沿用);概率曲线参数进配置表而不是硬编码,配合概率公示页一起热更。

4.4 概率审计:每次抽取都留痕 ​

json
{
  "uid": 10086,
  "pool_id": "pool_2026spring",
  "pull_seq": 37,
  "pity_count_before": 36,
  "rate_at_pull": 0.066,
  "random_value": 0.041923,
  "result_item": 90321,
  "is_guarantee": false,
  "server_time": "2026-09-22T12:00:00+08:00",
  "logic_version": "3.2.1"
}

这份日志同时服务三类需求:玩家投诉时重演单次抽取(random_value 是服务端生成后写入的,不是玩家可议的);运营核对实际出率 vs 公示概率(大样本偏差需要能被发现——真实 bug 往往是配置错了而不是随机数"不公平");监管审查时提供概率实现说明。

4.5 概率公示的法律要求 ​

根据中国《网络游戏管理暂行办法》和《关于规范网络游戏运营加强事中事后监管工作的通知》,抽卡概率需要公示:

  1. 每个品级的基础概率
  2. 保底机制的详细说明
  3. 概率提升活动的额外概率
  4. 概率计算的随机种子说明(可选)

服务端需要做到:概率计算在服务端完成,概率结果有日志记录可追溯,保底计数准确且不可被刷。


5. 掉落系统:随机性的艺术 ​

掉落系统是概率系统的具体应用。一个好的掉落系统应该让玩家觉得"努力就有回报",而不是"纯看运气"。

5.1 掉落类型的选择 ​

掉落类型说明适用场景玩家心理
固定掉落必定掉落金币、经验稳定感
随机掉落概率掉落装备、材料期待感
首通奖励首次通关额外稀有道具成就感
保底掉落N次必出稀有装备安全感

5.2 权重轮盘的实现 ​

权重掉落是最常见的随机掉落实现。每种物品一个权重,掉落率 = 该物品权重 / 总权重。实现用前缀和 + 二分,物品数千个时依然高效:

go
type DropTable struct {
    items    []DropItem
    prefix   []int64 // 前缀和
    total    int64
}

func (t *DropTable) Roll(rng rand.Source) int {
    r := rand.New(rng).Int63n(t.total)
    // 二分找第一个 prefix > r 的位置
    return sort.Search(len(t.prefix), func(i int) bool {
        return t.prefix[i] > r
    })
}

对策划友好的一面是:把某物品权重从 100 改成 50,掉率就直观减半,不需要换算百分比。

5.3 稀释陷阱与分池设计 ​

权重制有一个隐蔽陷阱:新增普通物品会稀释稀有物品的实际概率。策划想让 SSR 保持 1% 权重,后来陆续加入 50 个 R 级物品,SSR 的权重占比就从 1% 滑到了 0.7%——没有任何人"改过"它的概率。

工程上的解法是分池:SSR 单独一个池(固定 1% 先判定是否进池),进池后按权重决定是哪一张 SSR;R 级另一个池。这样池之间互不稀释,策划调 R 池不会影响 SSR 出率。分池结构同时是概率公示的天然口径——每个池公示一个概率,与实现一一对应。

配套工具:概率校验脚本接入 CI,每次掉落表变更自动重算各物品的最终概率(含分池),与策划配置的预期概率比对,偏差超阈值就阻断合入。这类防呆的详细管线见附录 A3。


6. 经济系统:游戏的命脉 ​

经济系统决定了游戏的长期生命力。一个设计良好的经济系统可以让游戏运营数年,而一个糟糕的经济系统可能在几个月内就让游戏崩溃。

6.1 货币体系设计原则 ​

游戏货币体系通常呈金字塔结构:顶层是付费货币(钻石),中层是游戏货币(金币),底层是活动货币(代币)。设计原则是:

  • 货币种类不超过 3-4 种:太多会让玩家困惑,太少会限制设计空间
  • 每种货币有明确的产出/消耗场景:不能让一种货币"万能"
  • 付费货币和游戏货币不能逆向转换:否则会影响付费意愿

6.2 产出-消耗平衡的数学模型 ​

经济平衡的核心公式是:每日净流量 = 总产出 - 总消耗。这个值应该略大于零(让玩家有"赚到了"的感觉),但不能太大(否则通胀),也不能小于零(否则通缩,玩家觉得"越玩越穷")。

一个简单的监控模型是检查每种货币的"消耗/产出比":这个值在 0.8 到 1.2 之间是健康的。低于 0.8 意味着产出过剩(通胀风险),高于 1.2 意味着消耗不足(玩家囤积,后续释放会造成冲击)。

6.3 服务端实现:货币流水先行 ​

货币是游戏里最重要的资产,它的实现纪律比公式更值得投入。核心原则:每一笔货币变更,先写流水,再改余额:

go
type CurrencyLog struct {
    ID        uint64    // 全局唯一(雪花ID),幂等键
    PlayerID  uint64
    Currency  uint8     // 货币类型:1=钻石 2=金币 3=活动代币...
    Delta     int64     // 变化量:正为产出,负为消耗
    Balance   int64     // 变更后余额快照(对账锚点)
    Reason    uint16    // 原因枚举:1=副本奖励 2=商店购买 3=GM补发...
    RefID     string    // 关联单号(订单号/活动ID/副本ID),幂等去重键
    CreatedAt time.Time
}

这条表结构承载了四个关键能力:

  • 幂等:RefID 加唯一索引,同一订单号重复请求直接拒绝——网络重试、消息重复投递都不会重复发放
  • 对账:任一时刻的余额 = 初始余额 + 流水累加,Balance 快照字段让增量对账可以分段进行,发现不一致立刻报警
  • 审计:GM 补发、退款扣回都有流水可查,客诉仲裁有据可依
  • 统计:6.2 节的产出/消耗比不需要另做埋点——按 Reason + Delta 正负聚合流水就是权威经济数据,比客户端埋点准(客户端埋点会被弱网丢包、被脚本污染)

并发纪律:同一玩家的货币变更需要串行(单写者上下文或行锁),跨玩家交易走两阶段(先冻结后转移),这些分布式细节见第 10 章交易系统。

6.4 通胀的早期信号 ​

通胀等它显形(物价飞涨)再处理就晚了。流水中能更早看到的信号:

  • 产消比持续走低:某货币连续两周消耗/产出比 < 0.8
  • 头部集中度上升:头部 1% 玩家持有的货币占比快速上升——可能是刷子入场
  • 囤积拐点:人均持有量上升但消耗场景参与率持平——玩家在"存钱等什么",消耗内容供给不足

6.5 通胀控制的四种武器 ​

控制手段原理适用场景副作用
产出控制每日产出上限所有游戏影响玩家体验
消耗设计装备强化递增消耗RPG/MMO新手门槛高
回收机制装备分解、道具回收有装备系统的游戏需要设计回收价值
价值锚定关键道具固定价格有交易所的游戏限制自由度

真实案例:某 MMO 上线半年后,金币通胀达到 300%,一把普通武器的价格从 1000 金币涨到了 30000 金币。原因是策划设计了太多金币产出途径(日常、副本、活动),但消耗途径只有装备强化(而且强化到高等级后消耗增长不够快)。最终的解决方案是引入"金币税"——交易时收取 10% 的手续费,同时开放"金币兑换材料"的限时活动。这个案例说明:经济系统需要在上线前就做好数学模型验证,不能等到出了问题再修。


7. 经济、平衡与风控:同一系统的三个侧面 ​

经济分析、数值平衡和风控分析经常分属不同岗位:策划看平衡,运营看收入,安全团队看异常。但在长线项目里,这三者追踪的是同一个系统的不同侧面——资源和行为是否仍在可控区间。

7.1 为什么不能分家 ​

只要玩家行为会影响资源分布、成长速度和社交竞争,这三类分析就天然耦合:

  • 经济异常可能来自数值投放失控,也可能来自作弊或黑产
  • 付费结构波动可能来自活动设计,也可能来自洗号、退款套利或代充
  • 某个玩法参与率下降,可能是平衡问题,也可能是奖励结构出了偏差

如果团队把这些分析完全切开,就会反复出现"每个人都看到局部异常,但没人能解释整体变化"。

7.2 平衡分析不能只靠主观体验 ​

很多项目的数值调整停留在"感觉太肝""感觉太难"的争论上,没有行为数据支撑的调整容易变成摆锤:看到流失就加福利,结果掏空核心挑战;看到付费下降就上促销,结果打坏价格锚点。更有效的平衡分析同时看:

  • 分层通过率和失败率(哪个战力段的玩家卡在哪一关)
  • 关键养成线的完成时间
  • 核心玩法的参与深度和放弃节点
  • 不同付费层级的成长效率差异

7.3 一个可复用的分析框架 ​

把三类分析放进"事实 → 偏离 → 原因 → 动作"的框架:

  1. 事实:哪些关键指标发生了异常变化(产消比、集中度、通过率)
  2. 偏离:与历史、同层用户、目标区间相比偏在哪里
  3. 原因:是版本调整、活动投放、经济结构变化,还是异常行为
  4. 动作:调参数、改规则、封号,还是接受(有些偏离是预期内的)

风控尤其需要和经济共享数据基础(资产流水、订单流水、登录行为、设备指纹)——否则风控只能看到"可疑",看不到"影响了什么"。刷金脚本不只破坏公平,它会直接放大通胀拖垮所有人的数值体验;这就是为什么反作弊(第 18 章)和经济监控共享同一套流水。


8. 版本更新对经济系统的冲击 ​

每次版本更新都是一次经济体系的"地震"。新玩法、新道具、新活动都会打破原有的产出-消耗平衡。服务端需要提前做好预案。

8.1 经济影响评估 ​

版本更新前,应该做一次经济影响评估:新增了多少产出途径?新增了多少消耗途径?预计通胀变化百分比是多少?哪些道具会受到冲击?

一个简单的评估模型是:净流量变化 = 新增日产出 - 新增日消耗。如果净流量变化导致通胀率超过 15%,就需要调整方案。

8.2 经济失衡的修复工具箱 ​

失衡程度修复策略执行速度副作用
轻微(通胀<10%)微调产出/消耗参数快(热更新)低
中等(通胀10-30%)新增消耗途径+降低产出中(版本更新)中
严重(通胀>30%)紧急回收+市场干预慢(需多版本)高
崩溃(恶性通胀)货币改版/合服极慢极高

注意修复手段的执行速度差异:参数级修复能热更(分钟级),结构性修复需要版本(周级),崩溃级修复需要多版本(月级)。这要求服务端的数值参数全部走配置热更管线(见第 17 章),而结构性规则尽量参数化——把"可能会调的东西"都放进配置,是把月级响应压缩到分钟级的唯一办法。


9. 玩家心理学:设计的底层逻辑 ​

理解玩家心理学不是策划的专利,服务端开发者也需要理解——因为你需要实现这些心理机制对应的技术系统。

9.1 自我决定理论(SDT) ​

SDT 理论认为人类有三种基本心理需求:自主性(自由选择)、胜任感(掌控感)、归属感(社交连接)。对应到服务端:自主性需要多种玩法入口和自由度;胜任感需要难度匹配系统和成长系统;归属感需要公会、组队、聊天等社交系统。

9.2 变比率强化:最强的激励机制 ​

变比率强化(Variable Ratio Reinforcement)是心理学中最强的激励机制——奖励的出现是随机的,但长期期望稳定。在游戏中的应用:抽卡、怪物掉落、宝箱。服务端实现时需要注意:概率需要服务端计算、保底机制需要准确、概率需要公示。这不仅是技术要求,也是法律要求(见 4.5 节)。

9.3 损失厌恶:时间敏感系统的实现要求 ​

损失厌恶指失去 100 元的痛苦大于得到 100 元的快乐。游戏利用这一点设计了大量"限时"和"每日"机制:限时活动、每日签到、体力系统。

服务端对应的是三类时间敏感系统:活动倒计时、签到记录与补签、体力恢复与溢出处理。它们的共同实现要求:时间以服务端为准(客户端时间可被篡改,签到回溯、体力凭空回满是常见漏洞)、跨零点边界正确处理(时区、夏令时、服务器停服跨越零点)、补签/找回语义明确(断签第几天、找回消耗什么,都是配置化的规则而不是代码分支)。


10. 链游经济设计的特殊挑战 ​

链游(GameFi)将游戏经济搬到区块链上,带来了全新的设计挑战。传统游戏的经济系统是中心化控制的,服务端可以随时调整参数;但链游的经济规则写在智能合约里,一旦部署就很难修改。

10.1 双代币模型 ​

链游通常使用双代币模型:游戏内代币(中心化管理,用于游戏内消耗)和治理代币(去中心化,用于交易和治理)。这种设计的目的是将"游戏内经济"和"链上经济"解耦,降低游戏内经济波动对链上价格的影响。

10.2 死亡螺旋:链游的最大风险 ​

死亡螺旋是链游最可怕的噩梦:新玩家减少 → Token 价格下跌 → 老玩家抛售 → 价格进一步下跌 → 更多玩家离开 → 经济崩盘。

Axie Infinity 就是死亡螺旋的典型案例:SLP 代币从最高 $0.36 跌到几乎归零。根本原因是产出远大于消耗——玩家不断产出 SLP,但游戏内没有足够的消耗途径。

预防死亡螺旋的关键:设计多元化的消耗途径、设置产出上限、建立回购机制、控制新玩家增长节奏。但最根本的是:游戏需要有"可玩性"作为基础,而不能纯粹靠"赚钱"吸引玩家。


11. 代码示例:一套可对账的经济管线 ​

本章从属性计算讲到货币流水,代码分散在各节里。这一节把经济系统最要紧的一条纪律——流水先行——组装成一个最小闭环:任何资产变更先留下证据行(6.3 节),幂等键挡住重试与重投,产消比直接从流水聚合(6.2、6.4 节)。示例是本章设计决策的最小 Go 演示实现(非摘自真实项目),小节内代码块合并后可通过 go vet 与 go build 编译,并受全库代码块回归测试约束。

账务闭环:先幂等,再余额,后写流水 ​

go
// accountKey:账户主键 = 玩家 × 货币类型
type accountKey struct {
    UID      uint64
    Currency uint8
}

// CurrencyLog:一行流水 = 一次资产变更的完整证据(6.3 节表结构的精简版)
type CurrencyLog struct {
    RefID    string // 幂等键:订单号/活动ID/副本ID,生产落库时加唯一索引
    UID      uint64
    Currency uint8
    Delta    int64  // 正为产出、负为消耗,全程 int64(3.2 节防溢出纪律)
    Balance  int64  // 变更后余额快照:对账锚点
    Reason   uint16 // 原因枚举:1=副本奖励 2=商店购买 3=GM补发...
}

var (
    ErrDuplicateRef = errors.New("duplicate ref") // 幂等命中
    ErrInsufficient = errors.New("insufficient balance")
)

// Ledger:内存演示版。生产中两个 map 换成数据库表,Apply 的校验顺序不变;
// 整个结构只暴露给玩家的单写者上下文(第 5 章),自身无需加锁
type Ledger struct {
    balances map[accountKey]int64
    seen     map[string]struct{} // RefID 幂等集合(生产为唯一索引)
    logs     []CurrencyLog
}

func NewLedger() *Ledger {
    return &Ledger{
        balances: make(map[accountKey]int64),
        seen:     make(map[string]struct{}),
    }
}

// Apply:流水先行的货币变更。任何调用方都必须带 refID——
// 不给幂等键的资产变更,代码评审阶段就该被打回
func (l *Ledger) Apply(uid uint64, cur uint8, delta int64, reason uint16, refID string) (int64, error) {
    if _, ok := l.seen[refID]; ok {
        return 0, ErrDuplicateRef // 网络重试/消息重投在这里被吸收
    }
    key := accountKey{UID: uid, Currency: cur}
    next := l.balances[key] + delta
    if next < 0 {
        return 0, ErrInsufficient // 余额不为负是账务不变式,不依赖调用方自觉
    }
    l.seen[refID] = struct{}{}
    l.balances[key] = next
    l.logs = append(l.logs, CurrencyLog{
        RefID: refID, UID: uid, Currency: cur,
        Delta: delta, Balance: next, Reason: reason,
    })
    return next, nil
}

三个纪律都落在这一个函数里。幂等在前:RefID 查重是第一道闸,同一订单号的重复请求在这里被吸收,网络重试、消息重复投递都不会变成重复发放;余额校验在中:扣减前置检查保证「余额永不为负」这条不变式;流水与余额一起变更:生产实现里这两步是同一数据库事务(第 8 章),内存演示版把顺序写清楚就够了——先记 seen、再改余额、追加流水,中途任何失败都不会出现「钱变了但没有证据行」的中间态。

产消比:不另做埋点,聚合流水就是权威口径 ​

go
// produceConsumeRatio:某货币的消耗/产出比(6.2 节健康区间 0.8-1.2)。
// 不做埋点:按 Delta 正负聚合流水就是权威口径,Reason 留作下钻维度
func produceConsumeRatio(logs []CurrencyLog, cur uint8) float64 {
    var produced, consumed int64
    for _, e := range logs {
        if e.Currency != cur {
            continue
        }
        if e.Delta > 0 {
            produced += e.Delta
        } else {
            consumed -= e.Delta // 负数取绝对值累加
        }
    }
    if produced == 0 {
        return 0 // 无产出的货币无从谈比值,交由上层按异常数据排查
    }
    return float64(consumed) / float64(produced)
}

6.2 节的健康区间与 6.4 节的通胀早期信号,全部建立在「产消数据可信」之上。这段代码的意义在于统计口径的唯一性:按 Currency 过滤、按 Delta 正负聚合,产出与消耗各算各的,Reason 字段留作下一层下钻的维度(哪个玩法在放水)。客户端埋点会被弱网丢包、被脚本污染,服务端流水不会——这正是 6.3 节说「按 Reason + Delta 正负聚合流水就是权威经济数据」的落地形态。

这套管线的扩展方向都不动摇结构:接数据库事务只是把 seen 换成唯一索引、把顺序写换成同事务写;接 GM 补发只是多一个 Reason 枚举值;接对账系统只是离线扫 logs 核对 Balance 快照链。经济监控(6.4 节)与反作弊(第 18 章)共享的正是这张流水表。


12. 服务端开发 Checklist ​

接到策划需求时,先问这 10 个问题:

text
□ 1. 这个系统的状态需要持久化吗?
□ 2. 这个操作需要事务保证吗?
□ 3. 这个计算应该在客户端还是服务端?
□ 4. 这个功能的并发安全性如何?
□ 5. 这个系统的性能瓶颈在哪里?
□ 6. 这个功能需要跨服支持吗?
□ 7. 这个功能的反作弊策略是什么?
□ 8. 这个功能的数据量预估是多少?
□ 9. 这个功能的上线/下线方案是什么?
□ 10. 这个功能需要灰度发布吗?

数值专项再补 6 个:

text
□ 11. 随机数需要确定性吗?(回放/裁决/跨端一致)
□ 12. 概率结果可追溯吗?(审计日志 + 实际出率监控)
□ 13. 货币/资产变更有流水和幂等键吗?
□ 14. 数值参数全部配置化了吗?(热更能力 = 修复速度)
□ 15. 掉落概率会被新增物品稀释吗?(分池 + CI 概率校验)
□ 16. 上线后看哪些指标?(产消比、集中度、通过率)

13. 小结 ​

关键问题答案
服务端为什么要懂数值?每个数值决策都会变成数据结构和计算逻辑,评审期识别风险的成本远低于上线后补救
属性计算的层次?基础 → 百分比 → 固定;多来源合并区分加法区/乘法区,减伤必须乘法区
伤害计算的技术纪律?全程整数运算(万分比)、int64 防溢出、服务端封顶、确定性随机进战斗日志
保底怎么实现才可信?计数器是服务端资产级状态,随抽卡同事务持久化,绝不信任客户端上报
掉落系统的稀释陷阱?权重制下新增物品会稀释稀有掉率,用分池设计隔离 + CI 概率校验
货币变更的实现纪律?流水先行:幂等键 + 余额快照 + 原因枚举,统计和对账都从流水聚合
通胀的早期信号?产消比走低、头部集中度上升、囤积拐点——都在流水里,不等物价飞涨
数值参数怎么管理?全部配置化热更——参数级修复是分钟级,结构性修复是版本级
经济/平衡/风控的关系?同一系统的三个侧面,共享流水数据基础,用"事实→偏离→原因→动作"框架协作

14. 实战案例:一次「攒了 45 抽白攒」的保底清零排查 ​

案例为教学示例(非摘自真实项目),数字经过取整简化,聚焦排查思路与回填逻辑。

背景与现象

卡池轮换次日,客诉 2417 份,集中同一句话:「换池后保底没了」。公示页写明「抽数继承」,玩家换池时平均已攒 45 抽——按 90 抽硬保底算,正是攒到半程;新池却人人从 0 开始。运营第一反应是显示 bug,客户端与服务端对完账:新池 PityState.Count 真的是 0。

第一步:现象与口径 ​

三个口径对同一段抽数给出三个答案:

  • 公示口径:换池公示写「抽数继承」——4.5 节公示的 4 条里第 2 条「保底机制的详细说明」承诺了保底规则,本池文本写明继承;
  • 实现口径:换池逻辑是新建 PityState{} 零值——Count 与 Guaranteed 双双归零,「继承」在代码里没有对应实现;
  • 审计口径:4.4 节留痕抽样——10 个字段的日志里,pity_count_before 逐人可回溯:抽样换池前有计数的 400 名玩家,新池首抽为 0 的有 400 人(100%)。

口径差不在抽数上,在承诺上:公示「继承」,审计实测 100% 清零——先对齐这条差,再谈归因。

第二步:分层归因 ​

按抽数状态从表象到契约逐层走:

text
客户端层   新池抽数显示 0,与服务端一致——排除显示 bug 与本地缓存
状态机层   换池 = 新建 PityState 零值:Count 与 Guaranteed 双双归零,继承规则从未被实现
配置层     需求单写「继承」,配置表与代码都没有「换池规则」这个字段——无从显式执行
公示层     公示页按需求单写「继承」,实现按默认零值走——承诺与实现差着一整条规则
审计层     4.4 留痕 10 字段完整,pity_count_before 逐人可回溯——清零前的计数全在日志里

账本收敛:换池时有计数的玩家 8000 人,平均清掉 45 抽——正是硬保底 90 抽的一半(90 ÷ 2 = 45);合计 36 万抽(8000 × 45),按单抽 6 元折算 = 216 万元;客诉 2417 份(影响玩家的 30%)只是这批损失里发声的那一角。

第三步:根因 ​

  1. 直接原因:换池实现是「新建 PityState{} 零值」——继承规则只存在于需求单和公示页,代码里从没有被实现过。这不是持久化字段丢失(Count、Guaranteed 每次抽取都写得好好的),是资产在状态机里被重建。
  2. 机制原因:4.3 节的换池关键点「显式处理而不是隐式沿用」走了默认零值这条最隐式的路;对账只对了概率没对规则——4.4 节的「实际出率 vs 公示」不含保底规则。12 节 Checklist 的 16 项里第 9 条「上线/下线方案」没把卡池轮换当上下线方案评审,需求单里「继承」两个字没有任何执行点。
  3. 深层原因:1.2 节的 4 问里,「这个计数器存在哪」是第 3 问——答案「持久化在服务端」全对,但四问覆盖了存不存、在哪存,没覆盖生命周期事件(轮换/合并/回档)谁定规则;保底计数是资产,换池是这个资产最大的一次交易,而它没有契约。

第四步:处置与回填 ​

  1. 止血回补:以 4.4 节留痕为依据,按每人 pity_count_before 恢复计数——8000 人逐人回补、0 失败;审计日志从「应对投诉的证据」变成了修复工具。
  2. 规则显式化:换池规则(继承/清零)进配置表,策划显式配置、代码按配置执行,空配置拒绝上线——对齐 4.3 节「显式处理而不是隐式沿用」。
  3. 公示对账进 CI:公示文本的保底规则与配置表逐字段比对,不一致阻断——把 4.4 节「实际出率 vs 公示」的对账从概率扩展到规则层。
  4. 评审补问:接需求的四问补上第 5 问——「换池、合并、回档这类生命周期事件,规则是什么?」进需求评审模板。

复核:客诉 2417 份 → 0 份,抽样 400 人计数恢复 400 人(100%);下次换池演练 0 起清零——公示、配置、实现三者第一次逐字对上。

回填清单:

回填项落点回链
换池规则进配置表,空配置拒绝上线数值配置第 02 讲 4.3 节 / 12 节 Checklist
公示文本与保底配置 CI 对账概率对账第 02 讲 4.4、4.5 节
抽数生命周期事件补进接需求四问需求评审第 02 讲 1.2 节
留痕回补流程进客诉与风控 SOP客诉处理第 02 讲 4.4 节 / 第 18 讲

案例的三个教训 ​

  1. 资产级状态的敌人不是丢失,而是重建——Count、Guaranteed 每次抽取都在正常持久化,换池时一次零值重建就全没了;状态机要为轮换、合并、回档这类生命周期事件写显式规则(4.3 节「显式处理而不是隐式沿用」)。
  2. 公示是承诺,不是文案——概率能做「实际 vs 公示」的对账(4.4 节),保底规则同样能:公示文本与配置表之间缺一道机器对账,差的正好就是这条规则。
  3. 留痕是最便宜的保险——pity_count_before 让 8000 人逐人回补零失败;没有留痕,同样的客诉只能拍脑袋补偿(1.2 节第 4 问「要不要留痕」)。

游戏知识体系