活动配置要下发一份到玩家的会话、怪物的模板要实例化成个体的数据——复制表是高频动作,但 Lua 的赋值只复制引用:浅拷贝的两张表共用内脏,一边改另一边跟着变,这类共享的 Bug 最难查。
---浅拷贝:子表仍然共享
local function shallowCopy(t)
local out = {}
for k, v in pairs(t) do
out[k] = v
end
return out
end
local monTpl = { name = "沃玛教主", drops = { "裁决之杖", "力量戒指" } }
local inst = shallowCopy(monTpl)
inst.drops[1] = nil -- 想清空自己的掉落
print(#monTpl.drops) -- 模板的掉落也被清了!
浅拷贝只复制第一层:inst.drops 与 monTpl.drops 是同一张子表,实例上清掉落把模板也清了——下一个沃玛教主出生就没掉落,这类共享变异的 Bug 在现象与根因之间隔着十万八千里。浅拷贝的适用场景反而明确:确知值域全是标量(数字、字符串、布尔)的一层表,浅拷贝安全又便宜。
---深拷贝:递归复制所有层
local function deepCopy(t, seen)
seen = seen or {}
if seen[t] then return seen[t] end -- 循环引用的守护
local out = {}
seen[t] = out
for k, v in pairs(t) do
if type(v) == "table" then
out[k] = deepCopy(v, seen)
else
out[k] = v
end
end
return out
end
---怪物模板到个体的正确实例化
function Mon.instantiate(tpl)
local inst = deepCopy(tpl)
inst.hp = inst.maxHp
inst.uid = Mon.nextUid()
return inst
end
深拷贝递归复制每一层:子表各自独立,实例的变异不再穿透模板。seen 表处理循环引用(表里套自己)避免无限递归,同一张子表在拷贝里保持单份的共享关系。深拷贝的成本与表的规模成正比:千字段的配置表复制一次毫秒级,热路径的反复深拷贝是 GC 的噩梦——拷贝的时机挑在配置装载与会话建立的一次性节点,运行期的高频路径用字段级读取替代整表复制。拷贝的边界再往前一步是序列化:跨进程的分发用序列化反序列化天然深拷贝,进程内的实例化用 deepCopy,两个工具的分工按边界定。
拷贝的正确性测试:构造三层嵌套加循环引用的样例表,断言拷贝后的独立性与引用的保持;实例化变异的隔离测试,改实例的子表后模板的快照哈希不变。
深拷贝曾经没处理循环引用,自引用的配置表把递归打进栈溢出,seen 的守护修复。浅拷贝曾经用在会话数据上,两个玩家的会话共享子表,A 的BUFF串门到 B 身上,深拷贝替换后绝迹。拷贝曾经把函数字段也试图复制,type 的分支漏了 function 的处理,非表直接赋值的分支天然兜住。
拷贝工具进公共库:浅拷贝的适用注释与深拷贝的循环引用守护内置。模板与实例的命名规范区分(Tpl 后缀),共享变异的隐患从命名上就降低。热路径禁深拷贝的红线进评审,拷贝的时机一次到位。
本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。
全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →
实战应用:用在哪里 付费玩家的付费点集中在商城,成长线里缺一个长期投入的钩子。成长基金让玩家在低等级时一次性买入,随等级提升…
实战应用:用在哪里 行会联赛打得不热闹,观战人数不到参赛人数的一半。联赛竞猜给观战者一张入场券:用竞猜币押注胜负,赔率随投注…
实战应用:用在哪里 七夕限时活动原计划七天,上线第二天下午整点活动面板突然关闭,奖励领了一半,玩家以为被砍了福利,两小时内工…
实战应用:用在哪里 HTTPS 证书过期一次,全服登录白屏了半个多小时,值班才发现证书没人管。证书续期机制把到期日写进系统变…
实战应用:用在哪里 从行会面板点进成员列表再点开个人名片,连按返回键直接退回了主城,中间两层界面全部跳过。界面堆栈管理用一张…
实战应用:用在哪里 限时商城的稀缺商品上架即被抢,玩家错过窗口感觉被区别对待。商城橱窗把每件限时商品的下架时间做成可视化倒计…