下载 App

纸上谈兵 — 模块插件化开发经验

06/232 浏览

新功能以插件形式接入,不破坏主系统。总结开发过程中踩过的坑。
horizontal linehorizontal line

一、模块插件化架构

文件结构

scripts/
├── main.lua                    # 路由器(受保护)
├── Pages/
│   ├── 怪物工坊.lua            # 纸上谈兵主页面(战场)
│   ├── 造兵.lua                # 拼装界面
│   └── 纸上存档.lua            # 持久化模块
└── Network/
    └── 共享定义.lua            # ⚠️ 服务端数据白名单(新key必须加这里)

插件接入规则

步骤操作文件
1创建 Pages/新功能.lua实现 Create/OnActivate/OnDeactivate/Update
2登录页加入口按钮Pages/登录.lua
3main.lua 加全局入口函数JumpToXXX()
4main.lua HandleUpdate 加更新调用Workshop.Update(dt)
5服务端白名单加新keyNetwork/共享定义.lua → USER_DATA_KEYS

main.lua 绝对规则

main.lua 只做:
  ✅ require 模块
  ✅ 页面路由(Tab切换/全屏跳转)
  ✅ HandleUpdate 分发
  ✅ 全局光标管理

main.lua 不做:
  ❌ 具体业务逻辑
  ❌ 覆盖已有函数
horizontal linehorizontal line

二、数据持久化(最重要的坑)

存储链路

客户端 UserDataProxy.Set(key, value)
    ↓ RemoteEvent
服务端 HandleUserDataSave → serverCloud:Set(uid, key, value)
    ↓ 保存成功 ACK

客户端重连时:
服务端 HandleUserDataRequest → serverCloud:BatchGet(uid):Key(k1):Key(k2)...
    ↓ 只返回白名单中的key!
客户端 UserDataProxy 缓存 dataCache[key] = value

最大的坑:服务端白名单

lua复制
lua
-- Network/共享定义.lua Shared.USER_DATA_KEYS = { "inv_meta", "inv_holdings", "inv_settings", "inv_margin", "inv_snapshots", "user_password", "user_nickname", "paper_war_bp", -- ← 新功能的key必须加这里! }
症状:UserDataProxy.Set 返回 ACK=success,但刷新后 GetAsync 返回空。
原因:服务端保存了,但重连推送时只推白名单内的key。
修复:在 USER_DATA_KEYS 中添加新key。

持久化代码模板

lua复制
lua
-- 保存 local cjson = require("cjson") local UserDataProxy = require("Network.数据代理") local function SaveData(key, data) local json = cjson.encode(data) if UserDataProxy.IsReady() then UserDataProxy.Set(key, json) else UserDataProxy.OnReady(function() UserDataProxy.Set(key, json) end) end end -- 加载(异步,必须等 Ready) local function LoadData(key, callback) local function doLoad() UserDataProxy.GetAsync(key, { ok = function(values) local json = values and values[key] if json and #json > 0 then local ok, data = pcall(cjson.decode, json) if ok then callback(data) else callback(nil) end else callback(nil) end end, error = function() callback(nil) end, }) end if UserDataProxy.IsReady() then doLoad() else UserDataProxy.OnReady(doLoad) end end
horizontal linehorizontal line

三、HandleUpdate 必须保留的调用

lua复制
lua
function HandleUpdate(eventType, eventData) local dt = eventData:GetFloat("TimeStep") -- 1. 插件 Update(不依赖登录状态) if Workshop.Update then Workshop.Update(dt) end -- 2. 未登录分支 if not isLoggedIn then UserDataProxy.CheckTimeout(dt) Login.HandleTabKey() return end -- 3. ⚠️ 已登录:Store.UpdateSync 绝不能丢! if Store.UpdateSync then Store.UpdateSync(dt) end end
教训:重写 HandleUpdate 时丢了 Store.UpdateSync(dt),导致交易记录不上传。
horizontal linehorizontal line

四、NanoVG 与 UI 共存

层级关系

渲染顺序(从底到顶):
  1. 3D Scene(无)
  2. UI 系统(urhox-libs/UI)
  3. NanoVG(HandleXXXRender)  ← 在 UI 之上!

规则

场景方案原因
背景装饰(横线、装订线)UI Panel(absolute定位)不遮挡按钮
游戏实体(兵轮廓、子弹)NanoVG需要自由绘制
按钮/菜单UI Panel需要点击交互
光标NanoVG(全局)始终最顶层

NanoVG 的坑

  1. 不要画全屏不透明矩形 — 会盖住所有 UI 按钮
  2. 不要用 input.mouseVisible = false — Web 会触发 Pointer Lock,鼠标跳位
  3. 用 battleActive_ 标志控制渲染 — 切换页面时停止绘制,否则画到其他页面上
horizontal linehorizontal line

五、Lua 闭包陷阱

lua复制
lua
-- ❌ 错误:local x = {...} 内部引用 x 时,x 还不存在 local dropdown = UI.Panel { children = { UI.Button { onClick = function() dropdown:SetVisible(false) -- dropdown 是 nil! end } } } -- ✅ 正确:先声明,再赋值 local dropdown dropdown = UI.Panel { children = { UI.Button { onClick = function() dropdown:SetVisible(false) -- 闭包捕获 upvalue,OK end } } }
horizontal linehorizontal line

六、全局光标管理

lua复制
lua
-- main.lua 中全局变量 CursorStyle = "pointer" -- "pointer" | "crosshair" -- 全局 NanoVG handler(始终订阅) function HandleGlobalCursor(eventType, eventData) if CursorStyle ~= "crosshair" then return end -- 画红色准星... end -- 切换场景时只改变量: CursorStyle = "crosshair" -- 进入战场 CursorStyle = "pointer" -- 返回菜单
不要用 input.mouseVisible = false(Web 平台 Pointer Lock 导致鼠标跳位)。
horizontal linehorizontal line

七、新插件开发检查清单

  • [ ] Pages/ 下创建独立模块文件
  • [ ] 实现 Create() / OnActivate() / OnDeactivate() / Update(dt)
  • [ ] main.lua 只添加 require + 路由入口 + Update调用
  • [ ] 不修改已有的 HandleUpdate 逻辑(只追加)
  • [ ] 不删除 Store.UpdateSync(dt)
  • [ ] 持久化 key 加入 Network/共享定义.lua → USER_DATA_KEYS
  • [ ] 加载数据时等 UserDataProxy.IsReady() 或注册 OnReady 回调
  • [ ] NanoVG 渲染用 active 标志控制,切换页面时关闭
  • [ ] 全局光标用 CursorStyle 变量切换,不隐藏系统鼠标
  • [ ] 测试:保存 → 刷新 → 数据恢复
horizontal linehorizontal line
文档版本: v1.0
创建日期: 2026-06-23
适用: 纸上谈兵及后续插件功能
【求积分】
因为没有积分继续完善框架,目前测试连接进入游戏界面,可能需要如下操作:
1、先随意输入用户名,默认密码123点击“进入系统”
2、稍等一会儿,会读取到当前用户ID的用户名,默认密码123就可以进入了
3、进入游戏修改密码,下次默认密码就不是123了
如果大家都能帮忙来体验,可能这个便签墙上会有越来越多的便签,需要玩家来丰富便签墙截图
【求积分】 因为没有积分继续完善框架,目前测试连接进入游戏界面,可能需要如下操作: 1、先随意输入用户名,默认密码123点击“进入系统” 2、稍等一会儿,会读取到当前用户ID的用户名,默认密码123就可以进入了 3、进入游戏修改密码,下次默认密码就不是123了 如果大家都能帮忙来体验,可能这个便签墙上会有越来越多的便签,需要玩家来丰富便签墙
3 赞
7 回复
耦合性 V2
目前耦合性已经从“偏高”降到了 中等偏低、可控。 简单评价: 大问题已经收口,剩下的是结构继续细分的问题。 当前不会再出现明显的全局入口乱写、页面直接抢 UI Root、Network 回调污染全局这类高风险耦合。但 State、App 启动层、投资数据页面仍有进一步拆分空间。 已明显改善的部分 1. 全局 UI Root 已收口 现在 UI.SetRoot 只剩一处: scripts/App/R
官方
4 赞
耦合性 V5
结论:主游戏业务耦合优化已经基本完成,可以收尾。 不是说项目里完全没有高入度/高出度模块,而是: 主要业务热点已经拆完 原 API 都保持兼容 每批拆分都通过了 LSP 每批拆分都通过了官方构建 每批局部依赖检查都没有循环 当前项目仍然构建成功 当前 Lua LSP errors = 0 当前验证状态 刚刚重新确认: Lua LSP errors = 0 官方构建成功 当前 Lua 文件数 = 4
官方
3 赞
耦合性 V3
当前耦合性已经从“中低耦合”进一步改善到: 低到中低耦合;核心玩法聚合直连基本消除,剩余主要是状态 Facade 和数据 Store 的中心化耦合。 量化结果 只读扫描 scripts/: Lua 文件数:175 内部 require 边数:534 循环依赖数:0 关键聚合模块入度: Pages.纸上障碍 incoming = 0 Pages.怪物工坊.战斗机制 incoming = 0 Page
官方
4 赞
耦合性 V4
当前游戏耦合性评估 结论:低到中低耦合。 经过多轮拆分后,原来的高入度 Facade / shim 基本已经清空业务依赖,当前主要热点已经转为“细粒度状态模块”和“流程编排模块”,结构比最初健康很多。 1. 当前核心指标 Lua 文件数: 200 内部 require 边数: 615 外部 require 引用: 79 平均内部出度: 3.08 循环节点: 0 关键结论: 循环依赖: 0 这是最重
官方
4 赞
耦合 V1
已分析当前 scripts/ 用户代码,未修改文件。整体看,项目是一个 main.lua 主入口 + 投资页面模块 + 纸上谈兵插件模块 + 网络/数据代理模块 的结构,耦合点比较集中。 1. 主入口如何耦合其他模块 主入口是: scripts/main.lua 它分服务端和客户端两条启动路径。 服务端路径 main.lua 先判断服务端模式: scripts/main.lua:7:判断 IsSe
官方
4 赞
01:32
【号外:新游】《小韭成长记》截图
【号外:新游】《小韭成长记》
《小韭成长记》是一款“投资记录 + 游戏化成长”的复合型小游戏: 投资端:记录股票、资金流水、账户管理等投资行为,生成多维度分析(组合估值、融资成本、指数对比等)。 游戏特色:你可以给后面来踢馆的玩家制造麻烦! 游戏端:通过“纸上谈兵”将投资数据转化为游戏战力(核心、甲片、行动点等),支持造兵、闯关、建设安全屋,让投资成果可视化、趣味化。 股票操作:支持「买入/卖出/分红/融资借入/还款」,自
【申请积分】刚把架构搭建了一些,希望能申请一些积分把游戏跑通截图
【申请积分】刚把架构搭建了一些,希望能申请一些积分把游戏跑通
4 赞
3 回复