记录踩过的坑
精华03/24109 浏览开发心得
踩过的坑
1. 单文件膨胀到不可维护项目早期所有 UI 都写在一个文件里,迅速突破 2000 行。Lua 没有 class 关键字、没有 IDE 级别的重构工具,一旦单文件过大,查找函数、理解调用链都变得极其痛苦。后来拆分出 14 个 UI 文件(UIBattle、UIShop、UITech、UIMailbox 等),每个文件只负责一个功能面板,可维护性立刻改善。教训:Lua 项目 800 行就该考虑拆分,不要等到 1500 行才动手。拆分成本随代码量指数增长——早拆比晚拆容易十倍。
2. require 路径与文件路径不一致导致的隐蔽 bugUrhoX 的 require 路径以 scripts/ 为根目录,用 . 分隔而非 /。写成 require("UI/UIMain") 在某些环境下能跑但在另一些环境下报错,正确写法是 require("UI.UIMain")。更隐蔽的是:如果两个文件用不同路径 require 同一个模块(一个用 UI.UIMain,另一个用 UI/UIMain),Lua 会加载两份独立实例,模块内的状态不共享,导致非常难排查的逻辑错误。教训:全项目统一用 . 分隔的 require 路径,绝不混用 /。
3. table 引用共享 vs 深拷贝的陷阱Lua 中 table 赋值是引用传递。在数据初始化、模板复制、存档恢复等场景中极易中招。解决方案:GameState.CreateNew() 中每个 table 都是现场构造的新 table,不引用任何外部模板。对于需要复制的场景,手写深拷贝或用 cjson.decode(cjson.encode(t)) 做序列化拷贝。
4. tonumber/tostring 的隐式转换问题JSON 反序列化后,原本是数字的 table key 会变成字符串。例如存档中的 tech[1]["military"][3] = 5,经过 cjson.encode → cjson.decode 后变成 tech["1"]["military"]["3"] = 5。用数字索引 tech[1] 去访问就是 nil。这个问题在 GameState.FromSaveData() 中反复出现,最终的做法是在反序列化后做一次规范化(normalize),把已知应该是数字的 key 统一转回 tonumber():lua
复制-- 科技数据 key 规范化
5. 全局变量污染——忘记 local 的代价Lua 中漏写 local 会把变量声明为全局变量。在多文件项目中,一个文件的全局变量会悄悄覆盖另一个文件的同名变量。我们遇到过一个 bug:两个 UI 文件都有个未加 local 的 timer 变量,导致倒计时显示互相干扰,排查了很久才发现。教训:所有变量必须加 local。利用 LSP 的 undefined-global 检查来捕获遗漏。文件顶部可以加 ---@diagnostic enable: global-in-nil-env 强制检查。
6. 事件订阅忘记清理SubscribeToEvent 订阅的事件在场景销毁后不会自动取消。如果在 UI 打开时订阅了 Update 事件做动画,关闭 UI 时不取消订阅,回调函数仍然每帧执行,访问已销毁的 UI 引用会导致崩溃。



