插件化隔离模型

05/122 浏览攻略
当前架构的隔离现状
已经隔离的部分 ✅               尚未隔离的部分 ⚠️
─────────────────────────      ────────────────────────────
cloud key 按 organId 命名      GameState — 全局单例,所有器官共享
food_queue_oral                BeatSystem — 重置但只有一份实例
food_queue_stomach             Enemy 池 — 清空但数据结构共用
food_queue_small_intestine     Map — 替换但同一个对象
                               Config.OrganRules — 集中定义
问题的本质在于:现在器官之间是"清空后共用",不是"各自独立"。
口腔关卡结束 → 重置 BeatSystem → 胃关卡启动,用的是同一个 BeatSystem 实例。
真正的插件化隔离模型
每个器官是一个自包含的插件包,通道只认接口,不认实现:
┌──────────────────── 通道总线(Pipeline Bus)────────────────────┐
│  FoodPipeline.PushToOrgan / ConsumeFromOrgan / TransferToNext  │
│  唯一跨器官通信方式,其他模块不允许直接访问其他器官的数据        │
└─────────────────────────────────────────────────────────────────┘
         │                    │                    │
    ┌────▼─────┐         ┌────▼─────┐         ┌────▼──────┐
    │  口腔插件 │         │  胃插件  │         │ 小肠插件  │
    │──────────│         │──────────│         │───────────│
    │ 自己的   │         │ 自己的   │         │ 自己的    │
    │ BeatCfg  │         │ BeatCfg  │         │ BeatCfg   │
    │ OrganRule│         │ OrganRule│         │ OrganRule │
    │ 云端命名 │         │ 云端命名 │         │ 云端命名  │
    │ 空间隔离 │         │ 空间隔离 │         │ 空间隔离  │
    └──────────┘         └──────────┘         └───────────┘
         │                    │                    │
    organ.oral.*         organ.stomach.*      organ.small.*
    (私有数据库)         (私有数据库)        (私有数据库)
数据隔离的三个层次
第一层:云端数据命名空间隔离
当前:food_queue_oral          ← 只隔离了队列
理想:organ.oral.food_queue    ← 所有器官数据统一前缀
      organ.oral.beat_stats    ← 节拍统计
      organ.oral.damage_log    ← 伤害记录
      organ.stomach.food_queue
      organ.stomach.beat_stats
每个器官只能读写 organ.{organId}.* 下的 key,跨器官读写必须通过 Pipeline Bus。
第二层:运行时状态隔离
当前:BeatSystem 是全局单例,reset 后给下一个器官用
理想:每个器官插件持有自己的 BeatContext:
      oral.beatCtx   = { beatCount, foodPool, phase, ... }
      stomach.beatCtx = { beatCount, foodPool, phase, ... }
器官卸载时 beatCtx 销毁,不影响其他器官。
第三层:配置声明隔离
当前:Config.OrganRules 集中定义,所有器官写在一个表里
理想:每个器官插件声明自己的 manifest:
      plugins/oral/manifest.lua    → { rules, enemies, tileSet, beatConfig }
      plugins/stomach/manifest.lua → { rules, enemies, tileSet, beatConfig }
主框架动态加载 manifest,而不是硬编码在 Config 里。
插件接口契约(每个器官必须实现的)
器官插件对外只暴露 4 个接口:
OnLoad(organId)          ← 关卡加载时初始化自己的数据
OnFoodArrived(foodData)  ← 接收来自上一器官的食材
OnFoodExiting(foodData)  ← 食材离开前,决定传递给下一器官什么数据
OnUnload()               ← 关卡结束时清理自己的状态
通道总线只调用这 4 个接口,从不直接访问插件内部数据。
和当前架构的差距
维度 当前 插件化目标 改造成本
云端数据 已按 organId 命名 统一前缀规范 低
运行时状态 共用实例 reset 各自持有 context 中
配置声明 集中在 Config.lua 分散到各自 manifest 中
器官接口 无明确契约 4 个标准接口 高(需重构)
跨器官通信 部分直接访问 只通过 Bus 中
结论:三个阶段对多人模式完全无影响
原因在于 CloudProxy 的设计:
FoodPipeline.lua
    cloud:Set("food_queue_oral", data)  ← 只传 key 字符串
           │
           ▼
    CloudProxy:Set(key, value)
           │
           ├─ 多人模式 → client.CloudSave(key, value)   ← 路由到 serverCloud
           └─ 单机模式 → clientCloud:Set(key, value)
CloudProxy 是一个透明路由层,FoodPipeline 只管传 key,完全不知道底层走的是 serverCloud 还是 clientCloud。
逐阶段核实
阶段一:改 cloudKey 函数
lua
复制
-- 改前
local function cloudKey(organId)
    return "food_queue_" .. organId          -- "food_queue_oral"
end
-- 改后
local function cloudKey(organId)
    return "organ." .. organId .. ".food_queue"  -- "organ.oral.food_queue"
end
改的只是 key 的字符串格式。CloudProxy、Client.CloudSave、serverCloud 完全不感知 key 的命名规则,透传即可。多人模式零影响。
唯一需要注意的是:改名后旧 key 的存量数据会丢失(老队列 food_queue_oral 不会被自动迁移)。如果线上有正在积压的队列数据,需要一次性迁移脚本,或者选择在没有积压时切换。
阶段二:拆分 Config.OrganRules
这是纯本地代码结构调整,Config 数据最终读进内存后和现在一模一样,serverCloud 完全不涉及。多人模式零影响。
阶段三:BeatSystem 改 context 机制
BeatSystem 是纯本地运行时状态(内存中的 foodPool、phase、timer),不持久化到云端。多人模式零影响。
唯一需要协调的点
你们是 8 人多人模式,多个玩家的 food_queue_{organId} 数据存在同一个 serverCloud 里。
当前架构下,所有玩家共享同一个 food_queue_oral key,这意味着:
玩家A投喂 → 写入 food_queue_oral
玩家B投喂 → 写入 food_queue_oral(同一个 key,追加到同一列表)
玩家C进入口腔关卡 → 读取 food_queue_oral → 拿到所有人投喂的食材
这是设计上有意为之的(多人互喂、共享消化压力),还是你们希望每个玩家有自己的私有队列?这个问题和阶段一的命名空间改造直接相关,改之前值得确认一下。
多人数据拓扑图
投喂玩家 B ──┐
投喂玩家 C ──┼──→  body.{A的uid}.organ.oral.food_queue
投喂玩家 D ──┘          │
                        ▼
扮演口腔的玩家 ──→  【口腔关卡 TD】── 结算 ──→ body.{A的uid}.organ.oral.stats
                        │
                        ▼ (食材通过出口传递)
扮演胃的玩家   ──→  【胃关卡 TD】  ── 结算 ──→ body.{A的uid}.organ.stomach.stats
                        │
                        ▼
                  body.{A的uid}.blood      ← 血液指标汇总
                  body.{A的uid}.sensors    ← 感官数据
                        │
                        ▼
玩家 A(身体主)──→  【大脑监控室】← 只读聚合所有 organ.*.stats
数据隔离设计:三个命名空间层级
body.{ownerUid}.                         ← 第一层:身体归属(谁的身体)
    organ.{organId}.                     ← 第二层:器官隔离(哪个器官)
        food_queue                       ← 食材待处理队列
        stats                            ← 本器官当前状态(HP、伤害等)
        beat_log                         ← 节拍结算记录
        actor_uid                        ← 当前扮演者的 uid
    blood.                               ← 跨器官汇总(血液指标)
        glucose / lipid / sodium
    sensors.                             ← 感官层汇总
        taste / smell
每一层的读写权限不同:
数据路径 谁可以写 谁可以读
body.A.organ.oral.food_queue 所有投喂玩家 口腔扮演者
body.A.organ.oral.stats 口腔扮演者 玩家A(大脑)、所有人
body.A.organ.stomach.food_queue 口腔扮演者(传递) 胃扮演者
body.A.blood.* 各器官结算后写入 玩家A(大脑)只读
body.A.organ.*.actor_uid 玩家自己报名扮演时 排行榜展示
插件化在这里的完整含义
每个器官插件包含三个边界:
┌──────────────────── 口腔插件 ────────────────────┐
│                                                   │
│  私有输入: body.A.organ.oral.food_queue(只我读)  │
│  私有输出: body.A.organ.oral.stats(只我写)       │
│  下游通道: body.A.organ.stomach.food_queue(我写) │
│                                                   │
│  扮演者绑定: actor_uid(谁在控制这个器官)         │
│                                                   │
└───────────────────────────────────────────────────┘
胃/小肠/大肠结构完全一样,只是 organId 不同。这才是真正的插件化:每个器官有且只有一个输入队列、一个输出队列、一个状态表,边界清晰,互不干扰。
当前架构的最大风险点
基于你描述的玩法,现在最需要解决的不是三个阶段,而是一个更根本的问题:
当前 key 没有 ownerUid 层
现在:food_queue_oral
         ↑ 只有一份,所有玩家共用同一个 key
需要:body.{A的uid}.organ.oral.food_queue
         ↑ 每个身体主有自己独立的一份
如果 8 个人同时在线,每个人的身体是独立的(各自有自己的大脑监控室),那么现在所有人实际上在向同一个 food_queue_oral 写数据,会产生混淆。
因此阶段一的改造比之前说的更重要
lua
复制
-- 改前(所有玩家共享一个队列)
local function cloudKey(organId)
    return "food_queue_" .. organId
end
-- 改后(每个身体主有独立命名空间)
local function cloudKey(ownerUid, organId, dataType)
    return "body." .. ownerUid .. ".organ." .. organId .. "." .. dataType
end
-- 使用示例
cloudKey(12345, "oral", "food_queue")  → "body.12345.organ.oral.food_queue"
cloudKey(12345, "oral", "stats")       → "body.12345.organ.oral.stats"
这一个改动,直接决定了多人模式下各个玩家身体之间的数据隔离是否成立。