上一章给资源与货币分了层,用账本给「资源不会凭空产生」立了守恒约束。这一节走到等式的另一侧:资源从哪来。站内那篇通用的经济平衡框架 用放置类举例,讲的是「产出—消耗—成长」的一般结构,落点是怎么让三条曲线互相咬合;本节换成 SLG 的大地图口径,先只做产出侧——把产出端拆成一组可测量的速率,而不是一张凭感觉列的清单。产出与消耗合起来的闭环留到第 12 章。
产出端要回答的三个问题
设计产出端时,最容易犯的错是把它做成一张「资源清单」:木材来自伐木场,石头来自采石场,粮草来自农田……清单列完就以为设计完了。但清单只回答了「谁产」,没回答「产多快、谁说了算、波动多大」。真正决定经济能不能跑稳的,是后面三件事:
- 速率(rate):单位时间内产多少。这是唯一能被脚本精确计算的量,也是后面所有仿真的输入。
- 可控性(control):产出量由谁决定。玩家点升级就能改的(建筑),和玩家完全控制不了的(对手掠夺),在数值上的处理方式完全不同。
- 方差(variance):同样一段时间,不同玩家拿到的产出差多少。零方差的稳定流和方差 80% 的脉冲流,对经济水位的影响是两回事。
这三个维度合起来,才是一份能拿去做平衡的「产出源清单」。
七种产出源与它们的可控性
《王朝纪元》把产出源收敛成七种。前两种是连续型(只要在线或满足条件就持续产出),后五种是脉冲型(在某个时点一次性发放):
| 源 | 类型 | 谁决定产出量 | 设计可控性 | 方差 | 载体 |
|---|---|---|---|---|---|
| 地块产出 | 连续 | 玩家(占地的数量与等级) | 高 | 0 | 四类基础资源 |
| 建筑产出 | 连续 | 玩家(升级节奏) | 高 | 0 | 四类基础资源 |
| 任务与成就 | 脉冲 | 设计师(固定奖励) | 高 | 0 | 混合 |
| 活动奖励 | 脉冲 | 运营(活动排期) | 高 | 20% | 混合 |
| 掠夺 | 脉冲 | 对手(战力与在线) | 低 | 80% | 混合 |
| 系统赠送 | 脉冲 | 运营(补偿/福利) | 高 | 10% | 混合 |
| 付费获取 | 脉冲 | 商业化(礼包设计) | 高 | 0 | 混合 |
看这张表,会发现一个反直觉的结论:七种源里,六种都高度可控,只有掠夺一种不可控,但它偏偏是方差最大的那一个。这就是为什么第 14、15 章要专门处理市场与打金——产出端唯一的「野变量」全在玩家对抗上。
连续型与脉冲型的分野,直接决定了它们在数值表里怎么存:连续型存成每小时速率(一个小数点都不该有,见下节),脉冲型存成一次性数量 + 触发条件。
连续型产出:建筑与地块
建筑产出是唯一可以用一条公式精确描述的源。config/buildings.json 给每栋建筑定义了基准产出与增长率,都用整数比表达:
output(L) = floor(outputBase × outputGrowthNum^(L-1) / outputGrowthDen^(L-1))
伐木场的 outputBase 是 60、outputGrowthNum/Den 是 122/100,所以 1 级每小时 60 木材,2 级是 floor(60 × 122 / 100) = 73,一路到 10 级 359。采石场、冶铁厂、农田同理,只是基准与增长率不同(农田 80 起、增长率 120/100;冶铁厂 18 起、增长率 124/100)。
地块产出在结构上与建筑产出同源——占领一块地,相当于获得一份固定速率——所以本节的脚本把两者合并统计,只在配置里分开标注 driver(谁是产出量的决定方)。
用 S 层脚本把速率表算出来
下面这份是 D 层配置,把七种产出源的特征写成可读的数据。它只描述「是什么」,不含任何计算:
{
"version": 1,
"note": "产出源盘点:连续型(地块/建筑)与脉冲型(任务/活动/掠夺/赠送/付费)。速率一律整数,百分比一律整数百分比。",
"baseLevels": { "lumberyard": 5, "quarry": 5, "ironworks": 3, "farm": 6 },
"sources": [
{ "id": "tile", "name": "地块产出", "kind": "continuous", "driver": "player", "control": "high", "variancePct": 0, "carrier": "wood/stone/iron/grain" },
{ "id": "building", "name": "建筑产出", "kind": "continuous", "driver": "player", "control": "high", "variancePct": 0, "carrier": "wood/stone/iron/grain" },
{ "id": "quest", "name": "任务与成就", "kind": "burst", "driver": "designer", "control": "high", "variancePct": 0, "carrier": "mixed" },
{ "id": "event", "name": "活动奖励", "kind": "burst", "driver": "ops", "control": "high", "variancePct": 20, "carrier": "mixed" },
{ "id": "plunder", "name": "掠夺", "kind": "burst", "driver": "opponent", "control": "low", "variancePct": 80, "carrier": "mixed" },
{ "id": "gift", "name": "系统赠送", "kind": "burst", "driver": "ops", "control": "high", "variancePct": 10, "carrier": "mixed" },
{ "id": "pay", "name": "付费获取", "kind": "burst", "driver": "monetization", "control": "high", "variancePct": 0, "carrier": "mixed" }
]
}
下面这段是 S 层仿真脚本:它只读 config/buildings.json 与这份配置,把四栋建筑十级的小时产出算成一张表,并汇总产出源清单。它不修改任何既有配置。
# file: sim/ch10_source_inventory.py
# tier: S
# verify: python3 sim/ch10_source_inventory.py --config config/ --out out/
"""产出源盘点:从 config/buildings.json 算出各建筑各等级的小时产出表,汇总产出源清单。
只读 buildings.json / resources.json(主控拥有,不修改);ch10_sources.json 描述地块、
任务、活动、掠夺、赠送、付费这些非建筑源的特征。
产出:out/ch10_building_output.csv + out/ch10_source_inventory.csv + out/ch10_source_inventory.json
"""
from __future__ import annotations
import argparse, csv, json, os
def load(cfg_dir, name):
with open(os.path.join(cfg_dir, f"{name}.json"), encoding="utf-8") as f:
return json.load(f)
def output_at_level(b, level):
"""每小时产出 floor(base × Num^(L-1) / Den^(L-1)):整数幂保证跨语言逐位一致。"""
n = b["outputGrowthNum"] ** (level - 1)
d = b["outputGrowthDen"] ** (level - 1)
return (b["outputBase"] * n) // d
def building_table(buildings):
return [{"building": b["id"], "resource": b["resource"], "level": lv,
"outputPerHour": output_at_level(b, lv)}
for b in buildings["buildings"] for lv in range(1, b["maxLevel"] + 1)]
def continuous_rate(buildings, levels):
out = {}
for b in buildings["buildings"]:
lv = levels.get(b["id"], 0)
if lv > 0:
out[b["resource"]] = out.get(b["resource"], 0) + output_at_level(b, lv)
return out
def main():
here = os.path.dirname(os.path.abspath(__file__))
ap = argparse.ArgumentParser()
ap.add_argument("--config", default=os.path.join(here, "..", "config"))
ap.add_argument("--out", default=os.path.join(here, "..", "out"))
args = ap.parse_args()
os.makedirs(args.out, exist_ok=True)
buildings = load(args.config, "buildings")
resources = load(args.config, "resources")
sources = load(args.config, "ch10_sources")
rows = building_table(buildings)
with open(os.path.join(args.out, "ch10_building_output.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f); w.writerow(["building", "resource", "level", "outputPerHour"])
for r in rows:
w.writerow([r["building"], r["resource"], r["level"], r["outputPerHour"]])
keys = ("id", "name", "kind", "driver", "control", "variancePct")
src = [{k: s[k] for k in keys} for s in sources["sources"]]
with open(os.path.join(args.out, "ch10_source_inventory.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f); w.writerow(list(keys))
for s in src:
w.writerow([s[k] for k in keys])
control_hist = {}
for s in src:
control_hist[s["control"]] = control_hist.get(s["control"], 0) + 1
report = {
"version": 1,
"resourceCount": len(resources["resources"]),
"buildingCount": len(buildings["buildings"]),
"levelRows": len(rows),
"sourceCount": len(src),
"continuousCount": sum(1 for s in src if s["kind"] == "continuous"),
"burstCount": sum(1 for s in src if s["kind"] == "burst"),
"controlHist": control_hist,
"baseLevels": sources["baseLevels"],
"continuousRatePerHour": continuous_rate(buildings, sources["baseLevels"]),
"maxOutputPerHour": max(r["outputPerHour"] for r in rows),
"minOutputPerHour": min(r["outputPerHour"] for r in rows),
}
with open(os.path.join(args.out, "ch10_source_inventory.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(json.dumps(report, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机实跑(Python 3.14.6),得到:
== 40 行等级表;7 个产出源(连续 2 / 脉冲 5)
continuousRatePerHour: wood=132 stone=110 iron=27 grain=199
maxOutputPerHour: 412(农田 10 级) minOutputPerHour: 18(冶铁厂 1 级)
controlHist: {"high": 6, "low": 1}
这份输出就是本节的核心产物。几点读法:
- 连续速率是一张四元组:基础等级(伐木 5、采石 5、冶铁 3、农田 6)下,每小时产木材 132、石料 110、铁矿 27、粮草 199。它是后面所有资源曲线的起点。
- 等级表的上下界:最小的产出是冶铁厂 1 级的 18/小时,最大是农田 10 级的 412/小时——铁矿作为高级资源,基准速率被刻意压低,这是第 9 章「资源分层」在产出端的体现。
- 可控性直方图
{"high": 6, "low": 1}:一眼看出产出端的风险敞口——七种源里只有掠夺不可控。
脉冲型产出:任务、活动、掠夺、赠送、付费
脉冲型产出没有「每小时速率」,它的量纲是一次给多少。设计上要额外标注两件事:触发条件(什么时候给)和上限(一天最多给几次)。这三者的区别很大:
- 任务与成就:量固定、一次性、跟着成长走,是新手期的主要补给。
- 活动奖励:量可调、跟着版本排期走,是运营手里的调节阀。
- 掠夺:量随对手波动、方差 80%,是唯一会「被别人拿走」的源——它既是产出也是对手的消耗,双向计入经济。
- 系统赠送与付费获取:量由运营与商业化决定,前者是补偿手段,后者是变现入口(付费点的建模在卷二)。
把脉冲源直接换算成「平均每日贡献」是常见的偷懒做法,但它会抹掉方差。本节只登记它们的量纲与可控性,把方差留给第 12 章的闭环仿真去消化。
速率之外:上限、结算与口径
速率表算出来,还不能直接当资源曲线用,中间隔着两道口径:
- 上限截断。
resources.json给每种资源设了cap(木材/石料/粮草 20 万、铁矿 12 万)。产出结算时是stock = min(cap, stock + rate × hours)——一旦触顶,超出部分直接蒸发。所以「速率 × 时间」只是上限内的乐观估计。 - 结算粒度。速率是每小时量,结算是按 tick 累加。连续型源按小时结算,脉冲型源在触发时点一次性入账——两者不能混在同一个循环里,否则会出现「脉冲被当成速率重复发」的经典 bug。
这两条也是第 9 章账本模型要求「每笔产出都要有来源凭证」的原因:速率表负责「应该产多少」,账本负责「实际入账多少」,两者对不上就是配置或代码有 bug。
原型层:同一张表的两种消费
同一份 config/,仿真层用来算平衡,原型层用来跑可玩逻辑。下面这段是 P 层 TypeScript 原型里产出表的关键片段(完整文件是 proto/src/ch10_production.ts),它复用 core.ts 的 outputAtLevel,保证与 Python 侧走同一个整数幂公式:
// snippet: proto/src/ch10_production.ts(产出表节选,完整文件见工作区)
// 与 sim/ch10_source_inventory.py 同构:读同一份 config/buildings.json 产出等级表。
import { outputAtLevel, loadConfig, type BuildingDef } from "./core.ts";
export function productionTable() {
const rows: { building: string; resource: string; level: number; outputPerHour: number }[] = [];
for (const b of loadConfig<{ buildings: BuildingDef[] }>("buildings").buildings) {
for (let lv = 1; lv <= b.maxLevel; lv++) {
rows.push({ building: b.id, resource: b.resource, level: lv, outputPerHour: outputAtLevel(b, lv) });
}
}
return rows;
}
断言同一张表在两种语言里逐位一致(完整断言见 proto/test/ch10.test.ts):
✔ 10.1 建筑小时产出表:4 建筑 × 10 级 = 40 行,关键点对齐
✔ 10.1 产出源汇总:7 源(2 连续 / 5 脉冲),基础等级连续速率
小结
产出源清单不是一张名词表,而是一组速率 + 可控性 + 方差的三元组。把七种源按这两类分完,产出端的设计空间就收敛了:连续型决定经济的水位,脉冲型决定经济的节拍,而唯一的低可控源(掠夺)留给风控章节专门处理。
下一节把「时间」这个变量引进来——同一份速率表,在线玩家和离线玩家拿到的资源并不一样,回归玩家又要不要补产出。
阅读导航:上一节:账本模型与守恒约束 · 下一节:产出与时间的关系 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。