7.1 KiB
模块联动规范
本文档定义模块间联动逻辑的处理方案:谁调用谁、变更怎么传播、如何保证一致性。联动场景包括「建筑需要弟子才有产出」「炼丹需输入药材→产出丹药」「消耗药材→库存减少」等跨系统交互。
1. 方案组合结论
| 层 | 选择 | 理由 |
|---|---|---|
| 主动操作联动(炼丹/交货/种植) | 直接调用 + 依赖注入 | 逻辑显式、可断点调试、单测零成本 |
| 回合结算顺序 | 固定管线(MainGame 依次调用) | 消除时间耦合,顺序可预期 |
| 状态变更通知(UI/统计/音效) | 信号广播(观察者,单向) | 不引入依赖,加监听者零改动 |
| 全局事件总线 / 规则引擎 | 不做,数据形状预留 | P1~P3 联动类型仅个位数,通用引擎成本大于收益 |
明确不用的方案:
- 不用事件总线处理权威状态变更(如扣库存):请求-响应型联动需要双向信号,代码量反超直接调用,且结算顺序难追踪。事件广播只用于通知,不用于变更。
- 不用规则引擎全量执行联动:为 4 类联动(种植收获/炼丹/交货/修炼)写通用执行器是净亏损。P4 功法/术法批量进场(联动类型持续增长)时再评估抽成引擎。
2. 联动场景的本质
| 场景 | 本质 | 参与方 |
|---|---|---|
| 建筑需要弟子才有产出 | 前置条件检查(人手占用) | 生产系统 ↔ 弟子系统 |
| 炼丹需输入药材→产出丹药 | 资源转换(事务) | 炼丹系统 ↔ 库存系统 |
| 消耗药材→库存减少 | 库存变更 + 广播 | 调用方 → 库存 → UI/统计 |
统一模式:A 向 B 提需求(检查)→ B 执行变更 → 变更通知所有关心者。
3. 依赖管理铁律
3.1 依赖图无环(DAG)
库存、经济是叶子节点,谁也不依赖;生产类依赖库存/弟子;订单依赖库存+经济。任何 Manager 不得依赖其上层,出现环即设计错误。
OrderManager ──→ InventoryManager(叶子)
└──→ EconomyManager(叶子)
AlchemyManager ─→ InventoryManager
FieldManager ───→ InventoryManager
DiscipleManager ─→ EconomyManager(修炼扣灵石)
FoodManager ─────→ InventoryManager
└─→ DiscipleManager
已知隐患(P2 启动前必须复查):
- 忠诚/抽成:弟子抽成 → 经济结算方向仍是 弟子→经济,不构成环;但若经济系统反过来查询弟子(按忠诚发分红)会成环,届时改走信号通知。
- 收徒拜师礼:一次性收入走 EconomyManager,同向,无环。
3.2 注入点唯一
所有 setup() 集中在 MainGame 一处,Manager 自身零 autoload 引用(TimeSystem/SaveSystem 除外按需)。这是依赖可管理的根源:
# MainGame._ready() 中:
economy_manager.setup()
inventory_manager.setup()
field_manager.setup(inventory_manager)
alchemy_manager.setup(inventory_manager)
food_manager.setup(disciple_manager, inventory_manager)
order_manager.setup(inventory_manager, economy_manager)
disciple_manager.setup(economy_manager)
好处:测试场景只注入 Mock 依赖即可单独运行;依赖关系读一遍 MainGame 全貌可见。
3.3 结算顺序管线化
month_passed 固定按序调用各 Manager 的 settle_month(),消除时间耦合:
灵田(收获入库存) → 炼丹(自动生产) → 弟子(修炼结算)
→ 口粮(凡人弟子消耗) → 订单(过期检查) → 经济(固定收入)
顺序依据:口粮必须在收获后扣(吃的是新粮);经济最后(汇总本月收支)。新增系统只插入管线,不改变既有顺序。
4. 原子事务约定(check-take-give)
联动变更必须原子:先检查所有前置条件与材料,全部通过后再执行扣除与产出,不允许扣一半失败。
4.1 收敛到库存层
「先检查后扣除」的手工纪律收敛为库存层的两个方法,调用方不可能写错:
# InventoryManager
func try_take(items: Dictionary) -> bool # 全量检查,够则返回 true
func take(items: Dictionary) -> void # 实际扣除(try_take 通过后调用)
func add_item(item_id: String, count: int) -> void
func has_items(items: Dictionary) -> bool # 只查不扣(供 UI 显示可用性)
4.2 调用方模板
# AlchemyManager.refine() —— 主动操作联动标准写法
func refine(recipe_id: String, disciple: Disciple) -> bool:
var r: Dictionary = ContentDB.get_recipe(recipe_id)
if disciple.alchemy_skill < r["skill_req"]:
return false
if not inventory.try_take(r["inputs"]):
return false
inventory.take(r["inputs"])
inventory.add_item(r["output_id"], r["output_count"])
pill_refined.emit(r["output_id"])
return true
5. 单一写入口
每种数据只允许其归属 Manager 变更:
| 数据 | 唯一写入口 | 外部只能 |
|---|---|---|
| 库存物品 | InventoryManager.take/add_item |
调用这两个方法 |
| 灵石余额 | EconomyManager.spend/earn |
调用这两个方法 |
| 弟子属性 | DiscipleManager.set_attr |
调用该方法 |
| 弟子增删 | DiscipleManager.add/remove_disciple |
调用该方法 |
违反此规则的直接后果:数据被谁改的不可追踪,信号漏发,UI 不同步。
6. 信号分工
| 信号类型 | 用途 | 方向 |
|---|---|---|
Manager 发出的领域信号(inventory_changed/pill_refined…) |
UI 刷新、统计、音效 | 后端 → 前端/其他监听者,单向 |
TimeSystem 的 month_passed 等 |
回合结算管线入口 | 已定,不改 |
| UI 调用 Manager 公开方法 | 玩家操作 | 前端 → 后端,直接调用不走信号 |
原则:信号只通知,不携带状态变更职责。UI 收到信号后从后端读最新值,不依赖信号参数里的数据(参数仅作提示)。
7. 规则引擎的预留(P4 退路)
P1 定义配方数据时,形状直接写成 requires / consumes / produces 三段(逻辑在代码手写):
{
"id": "refine_peiyuan",
"requires": [{"type": "disciple_skill", "skill": "alchemy", "min": 3}],
"consumes": [{"item": "herb_lingzhi", "count": 2}],
"produces": [{"item": "pill_peiyuan", "count": 1}],
}
将来联动类型膨胀(P4 功法/术法、炼丹失败率、双产出等)时,把各 Manager 手写逻辑抽成通用执行器(检查 requires → 扣 consumes → 给 produces,天然原子),数据文件一行不用改。
评估时机:新增联动类型需要改多个 Manager 的代码时,即考虑抽取。当前(P1)不抽取。
8. 违背规范的常见症状
| 症状 | 根因 | 对应规范 |
|---|---|---|
| 库存数量莫名变化 | 绕过 InventoryManager 直接改 | §5 单一写入口 |
| 扣了一半材料失败 | 未先 try_take 全量检查 | §4 原子事务 |
| 结算结果依赖按钮点击顺序 | 未走固定管线 | §3.3 结算顺序 |
| Manager 互相调用成环 | 依赖方向失控 | §3.1 DAG |
| 换 UI 后数据不刷新 | 变更未发信号 | §6 信号分工 |