纸上谈兵 — 模块插件化开发经验
06/231 浏览综合
新功能以插件形式接入,不破坏主系统。总结开发过程中踩过的坑。


一、模块插件化架构
文件结构
scripts/
├── main.lua # 路由器(受保护)
├── Pages/
│ ├── 怪物工坊.lua # 纸上谈兵主页面(战场)
│ ├── 造兵.lua # 拼装界面
│ └── 纸上存档.lua # 持久化模块
└── Network/
└── 共享定义.lua # ⚠️ 服务端数据白名单(新key必须加这里)
插件接入规则
| 步骤 | 操作 | 文件 |
| 1 | 创建 Pages/新功能.lua | 实现 Create/OnActivate/OnDeactivate/Update |
| 2 | 登录页加入口按钮 | Pages/登录.lua |
| 3 | main.lua 加全局入口函数 | JumpToXXX() |
| 4 | main.lua HandleUpdate 加更新调用 | Workshop.Update(dt) |
| 5 | 服务端白名单加新key | Network/共享定义.lua → USER_DATA_KEYS |
main.lua 绝对规则
main.lua 只做:
✅ require 模块
✅ 页面路由(Tab切换/全屏跳转)
✅ HandleUpdate 分发
✅ 全局光标管理
main.lua 不做:
❌ 具体业务逻辑
❌ 覆盖已有函数


二、数据持久化(最重要的坑)
存储链路
客户端 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。
原因:服务端保存了,但重连推送时只推白名单内的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


三、HandleUpdate 必须保留的调用
lua复制
luafunction 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),导致交易记录不上传。


四、NanoVG 与 UI 共存
层级关系
渲染顺序(从底到顶):
1. 3D Scene(无)
2. UI 系统(urhox-libs/UI)
3. NanoVG(HandleXXXRender) ← 在 UI 之上!
规则
| 场景 | 方案 | 原因 |
| 背景装饰(横线、装订线) | UI Panel(absolute定位) | 不遮挡按钮 |
| 游戏实体(兵轮廓、子弹) | NanoVG | 需要自由绘制 |
| 按钮/菜单 | UI Panel | 需要点击交互 |
| 光标 | NanoVG(全局) | 始终最顶层 |
NanoVG 的坑
- 不要画全屏不透明矩形 — 会盖住所有 UI 按钮
- 不要用 input.mouseVisible = false — Web 会触发 Pointer Lock,鼠标跳位
- 用 battleActive_ 标志控制渲染 — 切换页面时停止绘制,否则画到其他页面上


五、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 } } }


六、全局光标管理
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 导致鼠标跳位)。


七、新插件开发检查清单
- [ ] 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 变量切换,不隐藏系统鼠标
- [ ] 测试:保存 → 刷新 → 数据恢复


文档版本: v1.0
创建日期: 2026-06-23
适用: 纸上谈兵及后续插件功能
创建日期: 2026-06-23
适用: 纸上谈兵及后续插件功能


