- 游戏开发
- 逆向工程
【免费下载链接】RE-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
RegisterCustomEvent 是 UE4SS 提供给 Lua Mod 的一个全局函数,用于注册一个回调:当游戏中的蓝图函数或事件以指定名称被调用时,该回调会被自动触发。本文基于 官方 API 文档 展开,结合仓库源码(LuaMod.cpp、LuaMod.hpp)与真实 Mod 用例(BPML_GenericFunctions),完整讲解参数约定、回调签名、返回值处理、底层实现原理与最佳实践。读完本文,你将掌握如何在 UE4/UE5 游戏中按函数名拦截蓝图调用、读写参数与返回值,并理解其背后的钩子机制。
RegisterCustomEvent 是什么:按名字钩住任意蓝图事件与函数
在 UE4SS 的 Lua 脚本体系中,绝大多数回调式 API(如RegisterLoadMapPreHook、RegisterBeginPlayPreHook等)钩住的是固定的引擎生命周期事件;而RegisterCustomEvent的特殊之处在于,它钩住的是任意蓝图函数或事件——只要目标函数在运行时以你注册的名字被调用,回调就会触发。
其核心机制可以概括为两步:
- 注册时:把事件名与 Lua 回调函数记录到全局钩子容器中,事件名会被转换成 Unreal 的
FName用于后续匹配; - 触发时:每次蓝图函数/事件被调用时,钩子层会取出当前
UFunction的名字,与所有已注册的事件名逐一比对,命中后调用对应的 Lua 回调。
从源码结构看,这一比对发生在 LuaMod.cpp 的script_hook钩子函数中:它对m_custom_event_callbacks容器调用find_function_hook_data(callback_container, node->GetNamePrivate()),即以UFunction的私有名字(GetNamePrivate)为键进行精确匹配。这也是为什么注册时的事件名必须与蓝图函数/事件的真实名字完全一致(包括大小写)。
典型应用场景包括:
- 监听游戏内特定蓝图事件,实现"事件总线"式的跨 Mod 通信;
- 在蓝图函数调用时截获参数,进行日志记录、数据统计或参数篡改;
- 配合 BP 事件系统,把游戏内事件转发到 Lua 侧处理(参见下文 BPML_GenericFunctions 案例)。
函数签名与参数说明
RegisterCustomEvent在 Lua 侧暴露的签名如下(与 LuaMod.cpp 中定义的错误提示文案一致):
RegisterCustomEvent(string EventName, LuaFunction Callback)| # | 类型 | 说明 |
|---|---|---|
| 1 | string | 要钩住的事件名称(蓝图函数或事件的名称,需与运行时UFunction名字精确一致)。 |
| 2 | function | 事件被调用时执行的回调函数。 |
参数校验非常严格:如果第一个参数不是 string,或第二个参数不是 function,UE4SS 会直接抛出 Lua 错误,错误信息为:
No overload found for function 'RegisterCustomEvent'. Overloads: #1: RegisterCustomEvent(string EventName, LuaFunction Callback)此外,注册全程受std::recursive_mutex(m_thread_actions_mutex)保护(LuaMod.cpp),保证在游戏多线程环境下注册操作的线程安全。
回调函数的调用约定
回调函数触发时,其参数顺序是有固定约定的,这一点在官方文档中没有展开,但在源码与真实 Mod 中有明确体现:
- 第一个参数永远是
ParamContext:即触发本次调用的UObject实例,以RemoteUnrealParam包装(LuaMod.cpp 中LuaType::RemoteUnrealParam::construct(lua, &Context, ...)); - 随后依次是蓝图函数的各个参数:按
UFunction的参数顺序压入,同样以RemoteUnrealParam包装(Operation::GetParam); - 回调可以返回一个值:如果蓝图函数本身有返回值,且 Lua 回调返回了非 nil 的值,UE4SS 会尝试将该值写回蓝图返回值(详见下文"返回值处理")。
因此,回调的标准形态是:
RegisterCustomEvent("MyCustomEvent", function(ParamContext, Param1, Param2, ...) -- ParamContext 是触发事件的 UObject 实例 -- Param1、Param2 等是蓝图函数的参数(RemoteUnrealParam 包装) end)RemoteUnrealParam的详细用法可参考 remoteunrealparam 文档。
快速上手:最小示例
官方文档给出的最小示例(registercustomevent.md):
RegisterCustomEvent("MyCustomEvent", function() print("MyCustomEvent was called\n") end)当游戏中的任何蓝图事件/函数以MyCustomEvent为名被调用时,控制台会输出MyCustomEvent was called。
带参数的完整示例(结合回调调用约定):
RegisterCustomEvent("PlayerDied", function(ParamContext, ParamPlayer, ParamKiller) local Player = ParamPlayer:get() if Player:IsValid() then print(string.format("Player %s was killed\n", Player:GetFullName())) end end)参数访问与类型检查:真实 Mod 中的规范写法
参数虽然以RemoteUnrealParam包装传入,但 UE4SS不会在触发回调前自动校验参数类型。如果蓝图调用方传入了与预期不同的参数类型,脚本可能运行到一半才出错。官方随仓库附带的 BPML_GenericFunctions Mod 明确给出了规范做法——必须在回调内手动做类型检查:
RegisterCustomEvent("PrintToModLoader", function(ParamContext, ParamMessage) -- Retrieve the param value from the param container. local Message = ParamMessage:get() -- We must do type-checking here! -- This is to guard against mods that don't use the correct params for their custom event. -- There's no way to avoid it. if Message:type() ~= "FString" then error(string.format("PrintToModLoader Param #1 must be FString but was %s", Message:type())) end -- Now the 'Message' param is validated and we're safe to use it. local NameParts = Explode(ParamContext:get():GetClass():GetFullName(), "/"); local ModName = NameParts[#NameParts - 1] Log(string.format("[%s] %s\n", ModName, Message:ToString())) end)这段代码演示了三个关键点:
- 取值:用
ParamMessage:get()从包装容器中取出真实值; - 类型检查:用
:type()校验取出的值是否为预期类型(如"FString"),不匹配时直接error(...)抛出明确错误; - 上下文利用:用
ParamContext:get()拿到触发事件的 UObject 实例,进而通过GetClass():GetFullName()反查调用来源(该 Mod 用它解析出发起调用的 Mod 名字)。
返回值与 Out 参数处理
RegisterCustomEvent的回调不仅"只读",还可以写回参数与返回值,这让它具备了拦截并修改蓝图行为的能力。
1. Out 参数回写:Param:set(value)
当蓝图函数带有out参数(即 CPF_OutParm)时,回调中可以通过对应包装参数的:set()方法把值写回蓝图侧。BPML_GenericFunctions 的第二个案例 ConstructPersistentObject 展示了完整流程:
RegisterCustomEvent("ConstructPersistentObject", function(ParamContext, ParamClass, OutParam) -- Param Type Checking local Class = ParamClass:get() if not Class:IsValid() then error("ConstructPersistentObject Param #1 must be a valid UClass") end -- ... 省略类型校验 ... -- Function Logic local GameInstance = FindFirstOf("GameInstance") local GarbageCollectionKeepFlags = 0x0E000000 local PersistentObject = StaticConstructObject(Class, GameInstance, 0, 0, GarbageCollectionKeepFlags, false, false, nil, nil, nil) if not PersistentObject:IsValid() then Log(string.format("Was unable to construct persistent object: %s\n", Class:GetFullName())) end -- Return Value OutParam:set(PersistentObject) end)这里OutParam即蓝图函数的 out/返回值参数,通过OutParam:set(PersistentObject)把 Lua 侧构造的持久化对象回写到蓝图调用方。
2. Lua 返回值写回蓝图返回值
从 script_hook 源码 可以看到,回调调用使用lua.call_function(num_unreal_params + 1, 1)——即允许回调返回 1 个值。后续逻辑会检查:
- 该
UFunction是否声明了返回值(return_value_offset != 0xFFFF); - Lua 栈上返回值是否为非 nil;
- 返回值类型是否注册了对应的属性处理器(
LuaType::StaticState::m_property_value_pushers)。
三者都满足时,UE4SS 会把 Lua 的返回值通过Operation::Set写回蓝图返回值内存。若返回值类型没有注册处理器,则会输出错误日志:Tried altering return value of a custom BP function without a registered handler for return type ...。
需要说明的是,这类"改返回值"能力对参数/返回值类型有依赖:只有 UE4SS 内置了属性处理器的类型(常用 UObject、FString、基本数值等)才能完成回写,自定义未知类型会走错误分支并被安全丢弃。
底层实现剖析
注册流程
RegisterCustomEvent的注册实现位于 LuaMod.cpp,完整流程如下:
- 加锁
m_thread_actions_mutex; - 校验参数类型(string + function),不合法直接抛错;
- 解析事件名,通过
get_mod_ref(lua)/get_hook_lua(mod)找到当前 Mod 对应的 hook Lua 状态; - 用
lua_xmove把回调函数从当前 Lua 状态迁移到 hook 状态; - 用
hook_lua->registry().make_ref()为回调函数建立注册表引用(registry reference),防止其被 GC 回收; - 用
Unreal::FName(event_name, Unreal::FNAME_Add)把事件名转成FName,并通过find_function_hook_data查询是否已存在同名钩子:- 若不存在,则向
m_custom_event_callbacks追加一条FunctionHookData记录; - 若已存在,则不再追加新的钩子记录(即同名事件重复注册不会产生重复触发)。
- 若不存在,则向
数据结构
钩子记录的底层结构定义在 LuaMod.hpp:
LuaCallbackData:保存 hook Lua 状态指针、instance_of_class以及回调的注册表索引(registry_indexes,支持同一事件绑定多个 Lua 状态下的回调);FunctionHookData:由一组Unreal::FName与一个LuaCallbackData组成;m_custom_event_callbacks是std::vector<FunctionHookData>(LuaMod.hpp),与m_script_hook_callbacks并列,共同服务于脚本钩子。
触发机制
每次蓝图函数/事件被调用时,钩子层进入 script_hook:
- 加锁后对
m_custom_event_callbacks执行execute_hook(..., false)——false表示按UFunction名字(GetNamePrivate)匹配而非按对象指针精确匹配; - 命中后遍历该记录的
registry_indexes,逐个取出回调引用(注册表索引为-1表示已注销,跳过); - 每个回调单独用 try/catch 保护(LuaMod.cpp),一个回调抛异常不会中断其余回调,也不会逃逸进钩子层;异常会被记录为
[script_hook] A callback threw an exception: ...并恢复 Lua 栈顶; - 压入参数:先压入
Context(触发实例),再按属性遍历压入蓝图参数,Out 参数通过FindOutParamValueAddress取地址,普通参数通过ContainerPtrToValuePtr(Stack.Locals())取栈局部值; - 调用回调并处理返回值(见上文)。
重复注册与注销的边界行为
值得注意的细节:即使同名事件已注册,再次调用RegisterCustomEvent时新回调仍会被make_ref引用,但不会被加入容器——因为find_function_hook_data命中后直接跳过了emplace_back。从源码行为看,这意味着"同名事件只保留第一个注册的有效钩子"。若需要替换回调,请先注销再注册(见下文)。
配套 API:UnregisterCustomEvent
与RegisterCustomEvent配套的反向操作是UnregisterCustomEvent,其注册于 LuaMod.cpp,签名与错误文案为:
UnregisterCustomEvent(string EventName)| # | 类型 | 说明 |
|---|---|---|
| 1 | string | 要注销的事件名称。 |
实现上,它把事件名转换为字符串后调用remove_function_hook_data(LuaMod::m_custom_event_callbacks, custom_event_name),从容器中移除对应的钩子记录。此后该事件被调用时,已注销的回调不会再触发。
-- 注册 RegisterCustomEvent("MyCustomEvent", function() print("MyCustomEvent was called\n") end) -- 需要停用时注销 UnregisterCustomEvent("MyCustomEvent")注意:钩子数据在 LuaMod.cpp 处也通过erase_from_container(this, m_custom_event_callbacks)在 Lua 状态清理/重载时被统一清除,因此 Mod 重载后旧钩子不会残留。
实战注意事项与最佳实践
- 事件名必须精确匹配:匹配基于
UFunction::GetNamePrivate(),任何大小写或拼写差异都会导致钩子不触发。建议先用游戏内的对象/函数名 dump 工具(如 dumpers)确认目标函数的确切名字。 - 回调内必须做参数类型检查:UE4SS 不会在触发前校验参数类型,错误会在回调内部才暴露;BPML_GenericFunctions 的注释直言"There's no way to avoid it",这是社区验证过的硬性要求。
- 同名事件只注册一次:重复注册同名事件不会产生多个触发,若需替换回调应先
UnregisterCustomEvent再重新注册。 - 善用 ParamContext:第一个参数是触发事件的 UObject 实例,可用于获取调用者信息、判断归属等,是事件处理中最重要的上下文入口。
- 回调内避免重活:钩子会拦截所有同名函数的每次调用,回调执行在游戏线程的脚本钩子路径上,应保持轻量,避免在回调里做阻塞式长任务。
- 类型支持有边界:返回值/参数回写依赖
m_property_value_pushers中注册的属性处理器,超出支持范围的类型只能读取、无法回写,且会输出错误日志,属预期行为。 - 线程安全由框架保证:注册、触发均受递归互斥锁保护,你不需要(也不应该)在 Lua 侧自行加锁。
进一步阅读
- 官方 API 索引:Lua API 总览 与 SUMMARY
- 参数包装类型用法:remoteunrealparam.md
- 底层实现:LuaMod.cpp(注册/注销/触发三段实现)、LuaMod.hpp(数据结构)
- 实战范例:BPML_GenericFunctions/Scripts/main.lua
- IDE 补全定义:shared/Types.lua(
function RegisterCustomEvent(EventName, Callback) end)
通过合理组合RegisterCustomEvent与UnregisterCustomEvent,你可以在不改动游戏代码的前提下,对 UE4/UE5 游戏中的任意蓝图事件与函数实现监听、参数校验、数据回写乃至返回值篡改,这是 UE4SS 脚本体系中连接 Lua 与蓝图世界的关键桥梁之一。
- 游戏开发
- 逆向工程
【免费下载链接】RE-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
相关推荐
终极指南:掌握 deoplete.nvim 自定义事件处理和钩子函数
终极指南:掌握 deoplete.nvim 自定义事件处理和钩子函数 想要让 deoplete.nvim 这个强大的异步补全框架真正为你所用吗?🚀 本文将带你
开发工具Mirai 事件系统(Events)完全指南:事件通道、监听器、自定义事件与协程工具函数
Mirai 事件系统(Events)完全指南:事件通道、监听器、自定义事件与协程工具函数 Mirai 是一个高效率的 QQ 机器人支持库,其大量核心功能(消息收
即时通讯深入掌握Craft.js自定义编辑:事件系统与钩子函数终极指南
🚀 想要构建功能强大的React拖拽页面编辑器吗?Craft.js作为一个可扩展的React框架,提供了完整的事件系统和钩子函数,让你能够完全控制编辑器的行为
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考