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 实战帖,语法、参数与示例一篇讲透。进入文章地图 · 查看全部 →

进阶实战游戏功能

Lua+string.len:字符串长度的字节本质

【语法算法】 闭包工厂的本质就这一行: 外层函数接收配置,返回一个闭包——闭包捕获配置,后续调用使用捕获的配置——闭包工厂的…

2026-09-29 16:47 996 技术组
进阶实战游戏功能

Lua+闭包工厂:函数返回函数的工厂本质

【语法算法】 闭包工厂的本质就这一行: 外层函数接收配置,返回一个闭包——闭包捕获配置,后续调用使用捕获的配置——闭包工厂的…

2026-09-29 16:47 996 技术组
进阶实战游戏功能

Lua+泛型for的四种形态:从简单到完整的进化本质

【语法算法】 泛型 for 的四种形态就这两行: 泛型 for 的四种形态覆盖了 Lua 所有的遍历需求——从无序遍历到有序…

2026-09-29 16:46 996 技术组
进阶实战游戏功能

Lua+string.find的起始偏移:模式搜索的范围本质

【语法算法】 string.find 的起始偏移就这一行: 第三个参数 init 是搜索的起始偏移——从字符串的第 init…

2026-09-29 16:46 996 技术组
进阶实战游戏功能

Lua+CPU时间与墙钟时间:两种计时的分界本质

【语法算法】 CPU 时间和墙钟时间的分界就这两行: os.clock 返回 CPU 时间——程序实际占用处理器的秒数——o…

2026-09-29 16:46 996 技术组
进阶实战游戏功能

Lua+元方法__unm:一元负号的自定义本质

【语法算法】 元方法 __unm 的触发就这一行: 对带 __unm 的表做一元负号操作 -t 时,Lua 调用 __unm…

2026-09-29 16:46 996 技术组