下载 App
耦合 V1
已分析当前 scripts/ 用户代码,未修改文件。整体看,项目是一个 main.lua 主入口 + 投资页面模块 + 纸上谈兵插件模块 + 网络/数据代理模块 的结构,耦合点比较集中。
1. 主入口如何耦合其他模块
主入口是:
- scripts/main.lua
它分服务端和客户端两条启动路径。
服务端路径
main.lua 先判断服务端模式:
- scripts/main.lua:7:判断 IsServerMode
- scripts/main.lua:8:加载 Network.便签墙服务端
- scripts/main.lua:10:调用 WallServer.Start()
服务端链路基本是:
textmain.lua └── Network.便签墙服务端 └── WallServer.Start()
虽然文件名叫“便签墙服务端”,但它实际上同时承载了:
- 便签墙同步
- 用户数据代理
- 管理员导出
- 纸上谈兵排行榜
- 纸上谈兵安全屋公开页
所以服务端模块职责已经比较集中。
客户端路径
客户端直接 require 了主业务模块:
- scripts/main.lua:19:urhox-libs/UI
- scripts/main.lua:20:Data.Store
- scripts/main.lua:21:Network.数据代理
- scripts/main.lua:22:Pages.登录
- scripts/main.lua:23:Pages.持仓
- scripts/main.lua:24:Pages.记录
- scripts/main.lua:25:Pages.分析
- scripts/main.lua:26:Pages.怪物工坊
- scripts/main.lua:27:Pages.纸上状态
- scripts/main.lua:28:Pages.设置
- scripts/main.lua:29:Data.数据迁移
- scripts/main.lua:30:Audio.界面音效
另外:
- scripts/main.lua:34 通过 pcall(require, "Network.便签墙客户端") 可选加载便签墙客户端。
也就是说,main.lua 是显式知道大部分业务模块的,它不是一个纯启动器,而是同时负责:
text启动生命周期 UI 初始化 登录流程 页面路由 全局音效注入 全局鼠标样式 纸上谈兵入口 Store 同步驱动
2. UI 页面耦合方式
投资主界面:静态创建 + SetVisible 切换
主 UI 在 BuildUI() 中一次性创建四个页面:
- scripts/main.lua:285:BuildUI()
- scripts/main.lua:287:Holdings.Create()
- scripts/main.lua:288:Record.Create()
- scripts/main.lua:289:Analysis.Create()
- scripts/main.lua:290:Settings.Create()
- scripts/main.lua:392:SwitchToPage(pageId)
路由方式是:
textmain.lua ├── BuildUI() │ ├── 持仓页 │ ├── 记录页 │ ├── 分析页 │ └── 设置页 └── SwitchToPage() ├── 当前页 SetVisible(false) ├── 新页 SetVisible(true) └── 调用新页 OnActivate()
这是比较直接的 UI 耦合:主模块持有页面实例,并直接调用页面生命周期方法。
纸上谈兵:替换整个 UI Root
纸上谈兵不是普通 tab 页,而是通过全局函数重建 UI:
- scripts/main.lua:467:JumpToMonsterWorkshop(overlayPanel)
多个模块直接调用这个全局函数:
- scripts/Pages/登录.lua:682
- scripts/Pages/纸上排行.lua:102
- scripts/Pages/纸上排行.lua:136
- scripts/Pages/纸上排行.lua:224
- scripts/Pages/纸上建家.lua:305
- scripts/Pages/纸上建家.lua:346
- scripts/Pages/纸上关卡.lua:306
- scripts/Pages/怪物工坊/游戏循环.lua:273
这是一种强全局耦合:纸上谈兵子模块不通过参数或路由服务跳转,而是直接依赖 main.lua 暴露的全局函数。
3. 数据中心耦合
Data.Store 实际是一个 shim:
- scripts/Data/Store.lua:3:return require("Data.数据中心")
真正的数据中心在:
- scripts/Data/数据中心/init.lua
它创建一个 Store 单例,然后由子模块向这个表安装方法:
- scripts/Data/数据中心/init.lua:6:格式工具
- scripts/Data/数据中心/init.lua:7:持仓数据
- scripts/Data/数据中心/init.lua:8:账户数据
- scripts/Data/数据中心/init.lua:9:云同步
- scripts/Data/数据中心/init.lua:16:创建 Store
- scripts/Data/数据中心/持仓数据.lua:17:安装 Store.AddHolding
- scripts/Data/数据中心/账户数据.lua:16:安装账户相关方法
- scripts/Data/数据中心/云同步.lua:77:安装 Store.SyncToCloud
整体关系是:
textData.Store └── Data.数据中心 ├── 持仓数据.Install(Store) ├── 账户数据.Install(Store) └── 云同步.Install(Store)
页面模块普遍直接 require Store 并读写它,例如:
- scripts/Pages/持仓/init.lua:8
- scripts/Pages/记录/init.lua:9
- scripts/Pages/记录/保存逻辑.lua:8
- scripts/Pages/分析/指数对比.lua:8
- scripts/Pages/设置.lua:8
典型写入点:
- scripts/Pages/记录/保存逻辑.lua:79:Store.AddCashFlow
- scripts/Pages/记录/保存逻辑.lua:325:Store.AddHolding
- scripts/Pages/记录/保存逻辑.lua:367:Store.AddTransaction
所以投资业务的数据耦合主要是:
text页面模块 └── Store 单例 ├── 直接读状态 ├── 调业务方法 └── MarkDirty 后由 main.lua 定时同步
同步驱动在:
- scripts/main.lua:866:Store.UpdateSync(dt)
4. 网络模块耦合
网络事件统一定义在:
- scripts/Network/共享定义.lua:12:Shared.EVENTS
- scripts/Network/共享定义.lua:116:注册服务端事件
- scripts/Network/共享定义.lua:122:注册客户端事件
服务端集中处理
服务端模块:
- scripts/Network/便签墙服务端.lua:63:WallServer.Start()
- scripts/Network/便签墙服务端.lua:67:注册服务端事件
- scripts/Network/便签墙服务端.lua:70-88:订阅便签、用户数据、管理员、排行、安全屋事件
这意味着多个业务插件的后端逻辑都塞在同一个服务端模块里。
客户端数据代理
Network.数据代理 是多人模式下的用户数据代理:
- scripts/Network/数据代理.lua:38:UserDataProxy.Init()
- scripts/Network/数据代理.lua:52:订阅 USER_DATA_RESPONSE
- scripts/Network/数据代理.lua:53:订阅 USER_DATA_SAVE_ACK
- scripts/Network/数据代理.lua:175:BatchSave
- scripts/Network/数据代理.lua:262:发送 USER_DATA_REQUEST
- scripts/Network/数据代理.lua:375:发送 USER_DATA_SAVE
主入口调用:
- scripts/main.lua:220:登录页显示前初始化 UserDataProxy
- scripts/main.lua:857:未登录时 UserDataProxy.CheckTimeout(dt)
比较隐蔽的耦合是:UserDataProxy 自己不直接监听 ServerReady,而是由便签墙客户端通知:
- scripts/Network/便签墙客户端.lua:41:订阅 ServerReady
- scripts/Network/便签墙客户端.lua:53:调用 UserDataProxy.OnServerReady()
所以用户数据加载依赖便签墙客户端初始化流程。
5. 纸上谈兵插件耦合
这是当前耦合最密集的部分。
入口转发:
- scripts/Pages/怪物工坊.lua:4:return require("Pages.怪物工坊.init")
聚合模块:
- scripts/Pages/怪物工坊/init.lua
它 require 了大量插件模块:
- scripts/Pages/怪物工坊/init.lua:7:Pages.纸上状态
- scripts/Pages/怪物工坊/init.lua:8:Pages.纸上战斗
- scripts/Pages/怪物工坊/init.lua:9:Pages.纸上关卡
- scripts/Pages/怪物工坊/init.lua:10:Pages.纸上背包
- scripts/Pages/怪物工坊/init.lua:11:Pages.纸上建家
- scripts/Pages/怪物工坊/init.lua:12:Pages.纸上排行
- scripts/Pages/怪物工坊/init.lua:13:Pages.纸上存档
- scripts/Pages/怪物工坊/init.lua:23:合成面板
- scripts/Pages/怪物工坊/init.lua:24:战斗机制
- scripts/Pages/怪物工坊/init.lua:25:游戏循环
- scripts/Pages/怪物工坊/init.lua:26:渲染模块,副作用定义全局 HandleBattleRender
核心状态单例
Pages.纸上状态 是所有纸上谈兵模块共享的状态表:
- scripts/Pages/纸上状态.lua:6:local State = {}
- scripts/Pages/纸上状态.lua:13:State.battleActive
- scripts/Pages/纸上状态.lua:14:State.modalOpen
- scripts/Pages/纸上状态.lua:16:State.soldier
- scripts/Pages/纸上状态.lua:19-24:子弹、敌人、掉落、粒子、爆炸、障碍
- scripts/Pages/纸上状态.lua:70-85:背包
- scripts/Pages/纸上状态.lua:94-120:安全屋/建家状态
这部分耦合方式是:
text纸上状态 State 单例 ├── 战斗模块直接读写 ├── 关卡模块直接读写 ├── 背包模块直接读写 ├── 建家模块直接读写 ├── 排行模块间接影响 UI 状态 └── 游戏循环按 State 驱动运行
这是典型“共享可变状态”耦合。
游戏循环回调注入
怪物工坊/init.lua 给游戏循环注入回调:
- scripts/Pages/怪物工坊/init.lua:294:游戏循环.Install({ ... })
游戏循环再给战斗机制注入:
- scripts/Pages/怪物工坊/游戏循环.lua:32:M.Install(workshop)
- scripts/Pages/怪物工坊/游戏循环.lua:35:战斗机制.Install(...)
- scripts/Pages/怪物工坊/游戏循环.lua:36:战斗机制.SetGameOverCallback(...)
这是比全局函数稍好的耦合方式:通过 Install 注入依赖,但仍然依赖共享 State。
6. 全局变量和全局函数耦合
这是当前最需要注意的部分。
CursorStyle
定义在:
- scripts/main.lua:104:CursorStyle = "pointer"
纸上谈兵多个模块直接修改它:
- scripts/Pages/怪物工坊/init.lua:186
- scripts/Pages/怪物工坊/init.lua:201
- scripts/Pages/怪物工坊/init.lua:243
- scripts/Pages/怪物工坊/init.lua:250
- scripts/Pages/纸上关卡.lua:305
- scripts/Pages/纸上建家.lua:304
- scripts/Pages/纸上排行.lua:109
- scripts/Pages/怪物工坊/游戏循环.lua:679
这使鼠标样式成为跨模块隐式状态。
JumpToMonsterWorkshop
定义在:
- scripts/main.lua:467
被多个纸上谈兵模块直接调用。这个函数实际承担了“纸上谈兵路由器”的角色,但目前是全局函数。
BackToMainMenu
定义在:
- scripts/main.lua:427
设置页调用:
- scripts/Pages/设置.lua:128
全局事件 handler
由于事件订阅使用字符串函数名,项目里存在不少全局 handler:
- scripts/main.lua:107:Start
- scripts/main.lua:123:HandleGlobalCursor
- scripts/main.lua:838:HandleUpdate
- scripts/Pages/怪物工坊/渲染.lua:390:HandleBattleRender
- scripts/Network/便签墙客户端.lua:48:HandleServerReady
- scripts/Network/数据代理.lua:267:HandleUserDataResponse
- scripts/Network/纸上安全屋客户端.lua:76:HandlePaperWarHomeClaimResult
- scripts/Pages/设置.lua:1137:HandleAdminPlayersList
- scripts/Pages/设置.lua:1262:HandleAdminExportResult
这类耦合风险是:函数名冲突、重复订阅、不易追踪调用来源。
7. 主模块与插件模块的耦合方式总结
可以分成 6 类:
| 耦合方式 | 说明 | 代表位置 |
|---|---|---|
| require 显式依赖 | 主入口直接加载页面、数据、网络模块 | scripts/main.lua:19-30 |
| 单例数据中心 | 所有投资页面共享 Store | scripts/Data/Store.lua:3 |
| 共享可变状态 | 纸上谈兵共享 Pages.纸上状态 | scripts/Pages/纸上状态.lua:6 |
| 全局函数 | 插件直接调用主模块函数 | scripts/main.lua:467 |
| 全局变量 | 多模块读写 CursorStyle | scripts/main.lua:104 |
| 事件总线 | 引擎事件和 RemoteEvent 通过字符串 handler 连接 | scripts/Network/共享定义.lua:12 |
8. 主要风险点
- main.lua 职责过重
同时负责启动、登录、路由、全局音效、光标、Store 同步、纸上谈兵入口。 - 纸上谈兵依赖主模块全局函数
JumpToMonsterWorkshop() 是事实上的插件路由器,但不是模块化接口。 - CursorStyle 是隐式全局状态
多个模块直接写,后续新增弹窗/模式时容易互相覆盖。 - 服务端模块职责过宽
Network.便签墙服务端.lua 同时处理便签、用户数据、排行、安全屋、管理员功能。 - WallClient 和 UserDataProxy 存在隐式启动顺序依赖
用户数据代理依赖便签墙客户端收到 ServerReady 后转发。 - 事件 handler 全局命名有碰撞风险
尤其是 HandleServerReady、HandleUserDataResponse 这类通用名称。 - Store 和 State 都是大单例
好处是简单,坏处是副作用传播广,测试和局部重构成本高。
9. 如果后续要解耦,优先级建议
建议先从低风险处开始:
- 把 JumpToMonsterWorkshop 封装成 Pages.纸上路由 或 WorkshopRouter 模块,纸上谈兵模块 require 它,而不是调用全局函数。
- 把 CursorStyle 封装成 CursorService.SetStyle()/GetStyle()。
- 拆分 Network.便签墙服务端.lua:便签墙、用户数据代理、排行、安全屋、管理员导出分别独立模块。
- 让 UserDataProxy 自己监听或显式接收 ServerReady,避免依赖 WallClient。
- main.lua 只保留启动和顶层路由,登录流程、主菜单、纸上谈兵入口可下沉到独立控制器。




