CHUAN2 DEV ENGINE
996 正版授权研发中心 · 360 授权合作教学中心 · 抖音传奇直播合作授权 · 快手推广运营商授权
OFFICIAL LICENSED ACADEMY 查验官方授权证书 →
// 威海旷世互娱教学基地 · 技术文章
高级技巧语法算法996引擎

Lua热更新:package.loaded里换掉一个模块

2026-09-28 00:54 作者:996 技术组 996引擎Lua教程传奇脚本高级技巧语法算法996引擎

【语法算法】
一、一行代码的魔法

package.loaded["game/mediator/bag"] = nil——把这行打进调试控制台,再敲一次 require,磁盘上的新文件就生效了,游戏不用重启。很多人把这当成玄学,其实 require 的全部行为只有两步:查缓存,miss 才读文件。package.loaded 就是一张以模块名字符串为键的哈希表,值缓存着模块的返回值;把键置 nil 等于驱逐缓存,下一次 require 必然重新 loadfile、重新执行模块体、返回一张崭新的模块表。热更新的"热",全部来自这一行。

二、底层原理:require 的查找链与真正的难点

require 的查找顺序是 package.loaded 命中即返回、package.preload 次之、最后才按 package.path 找文件读入。也就是说模块只被"执行"一次,此后所有 require 拿到的是同一个表引用——Lua 模块系统天然的单例。驱逐之后难点不在加载新代码,在旧代码的交接:已经拿到旧模块表的地方,引用不会自动换成新表。

mermaid
flowchart TD
    A["require('bag')"] --> B{"package.loaded<br/>命中?"}
    B -- "命中 --> 返回旧表引用<br/>(零成本单例)" --> C["调用方继续用旧逻辑"]
    B -- "未命中(被驱逐) --> loadfile<br/>执行新模块体" --> D["新模块表<br/>写入 package.loaded"]
    D --> E["Onloaded 重挂:<br/>重新注册通知/命令"]
    E --> F["注意: 调用方若还攥着<br/>旧表引用 --> 新旧并存"]
    F --> G["OnUnloaded 反注册:<br/>旧闭包全部撤下分发器"]
    G --> A

三、正确写法:对称的卸载与重挂

lua
local function reloadMediator(files)
    for _, v in pairs(files) do
        if package.loaded[v] then
            package.loaded[v].OnUnloaded()   -- 退场钩子: 反注册通知
            package.loaded[v] = nil          -- 驱逐缓存
        end
        local mediator = require(v)
        mediator.Onloaded()                  -- 进场钩子: 重新注册
        print("Reload File:" .. v .. ".lua")
    end
end

local function reloadCommand(files)
    for k, v in pairs(files) do
        if package.loaded[v] then
            global.Facade:removeCommand(k)   -- 命令必须先反注册再重挂
            package.loaded[v] = nil
        end
        global.Facade:registerCommand(k, require(v))
    end
end

四、幽灵回调:热重载的头号事故

不写 OnUnloaded 直接驱逐会怎样?旧模块创建的回调闭包还挂在通知分发器上——闭包由分发器持有,GC 看不见"模块已驱逐"这件事。于是新模块的回调也在,旧的也在,一条通知触发两份逻辑:技能放一次扣两 CD、面板开一次注册两遍监听。这类 bug 的诡异之处在于它只在线上热更后出现,重启就消失,人称幽灵回调。解法就是上文的对钩子:驱逐前把旧模块注册进全局的一切(通知、命令、定时器)显式撤干净。

五、边界与收口

三件事驱逐不掉:upvalue 里捕获的旧模块引用、全局变量里存着的旧表、已创建对象身上挂着的旧 class 元表。所以热重载的适用边界是"行为逻辑层"——Mediator、Command、工具函数;持有大量长生命周期实例的管理层不要热更,重启成本反而更低。调试期把重载清单外置成 localReload.lua,个人清单不进版本库,reload 一次全组共享同一个心智模型:驱逐、交接、验证,三步缺一不可。

作者履历与出处

本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理。团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与商业版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。

← 返回文章地图返回研学路径

幂尔框架 · 实战干货 · 接口调用

LATEST ARTICLES

全站技术干货持续更新:996 引擎 / Lua 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

烟幕弹:放一颗烟雾金蝉脱壳

【游戏功能】 一、一次裸眼抓哑巴的潜行 烟幕弹玩法首版测试,玩家丢完烟幕就站在烟里发呆,怪在烟边上转圈也不进来——表现对了,…

2026-09-28 08:07 996 技术组
进阶实战游戏功能

跺地震波:一圈圈踉跄往外推

【游戏功能】 一、一个震了个寂寞的大招 跺地震波类技能首版,特效华丽、震屏到位,测试却打回:"这大招怎么只炸了一下就没了?"…

2026-09-28 08:07 996 技术组
进阶实战游戏功能

推箱谜题:一步走错全盘僵

【游戏功能】 一、一个卡进墙里的箱子 推箱谜题移植首版,测试发现一个无解局面:箱子被推到墙角,四个方向都推不动,谜题卡死只能…

2026-09-28 08:07 996 技术组
进阶实战游戏功能

星轨接电:转一转接亮满天星

【游戏功能】 一、一个永远差一口气的接线 星轨接电类旋转解谜首版,测试卡在第三关:四段线路怎么转都差一口气,明明视觉上头尾相…

2026-09-28 08:07 996 技术组
进阶实战游戏功能

龙卷风:把怪全卷进风眼里

【游戏功能】 一、一个被风卷走的判空 龙卷风聚怪技能首测,最灵异的 bug:怪被吸到风眼附近后集体"抽搐"——坐标每帧在风眼…

2026-09-28 08:07 996 技术组
进阶实战游戏功能

斩钢一闪:穿身而过的刀痕

【游戏功能】 一、一个只闪不中的斩击 斩钢闪类突进斩首版被吐槽"人过去了刀没过去"——突进的位移做了,斩击的刀痕却只在终点画…

2026-09-28 08:07 996 技术组