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

06/231 浏览综合

新功能以插件形式接入,不破坏主系统。总结开发过程中踩过的坑。
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
适用: 纸上谈兵及后续插件功能
3