☰
使用 RegisterCustomEvent 钩住蓝图事件与函数:UE4SS Lua 自定义事件拦截全指南
2026/10/3 17:33:34 网站建设 项目流程
  • 游戏开发
  • 逆向工程

【免费下载链接】RE-UE4SS

Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games

项目地址:https://gitcode.com/gh_mirrors/re/RE-UE4SS
点击查看免费下载

RegisterCustomEvent 是 UE4SS 提供给 Lua Mod 的一个全局函数,用于注册一个回调:当游戏中的蓝图函数或事件以指定名称被调用时,该回调会被自动触发。本文基于 官方 API 文档 展开,结合仓库源码(LuaMod.cpp、LuaMod.hpp)与真实 Mod 用例(BPML_GenericFunctions),完整讲解参数约定、回调签名、返回值处理、底层实现原理与最佳实践。读完本文,你将掌握如何在 UE4/UE5 游戏中按函数名拦截蓝图调用、读写参数与返回值,并理解其背后的钩子机制。

RegisterCustomEvent 是什么:按名字钩住任意蓝图事件与函数

在 UE4SS 的 Lua 脚本体系中,绝大多数回调式 API(如RegisterLoadMapPreHook、RegisterBeginPlayPreHook等)钩住的是固定的引擎生命周期事件;而RegisterCustomEvent的特殊之处在于,它钩住的是任意蓝图函数或事件——只要目标函数在运行时以你注册的名字被调用,回调就会触发。

其核心机制可以概括为两步:

  1. 注册时:把事件名与 Lua 回调函数记录到全局钩子容器中,事件名会被转换成 Unreal 的FName用于后续匹配;
  2. 触发时:每次蓝图函数/事件被调用时,钩子层会取出当前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)
#类型说明
1string要钩住的事件名称(蓝图函数或事件的名称,需与运行时UFunction名字精确一致)。
2function事件被调用时执行的回调函数。

参数校验非常严格:如果第一个参数不是 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)

这段代码演示了三个关键点:

  1. 取值:用ParamMessage:get()从包装容器中取出真实值;
  2. 类型检查:用:type()校验取出的值是否为预期类型(如"FString"),不匹配时直接error(...)抛出明确错误;
  3. 上下文利用:用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,完整流程如下:

  1. 加锁m_thread_actions_mutex;
  2. 校验参数类型(string + function),不合法直接抛错;
  3. 解析事件名,通过get_mod_ref(lua)/get_hook_lua(mod)找到当前 Mod 对应的 hook Lua 状态;
  4. 用lua_xmove把回调函数从当前 Lua 状态迁移到 hook 状态;
  5. 用hook_lua->registry().make_ref()为回调函数建立注册表引用(registry reference),防止其被 GC 回收;
  6. 用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:

  1. 加锁后对m_custom_event_callbacks执行execute_hook(..., false)——false表示按UFunction名字(GetNamePrivate)匹配而非按对象指针精确匹配;
  2. 命中后遍历该记录的registry_indexes,逐个取出回调引用(注册表索引为-1表示已注销,跳过);
  3. 每个回调单独用 try/catch 保护(LuaMod.cpp),一个回调抛异常不会中断其余回调,也不会逃逸进钩子层;异常会被记录为[script_hook] A callback threw an exception: ...并恢复 Lua 栈顶;
  4. 压入参数:先压入Context(触发实例),再按属性遍历压入蓝图参数,Out 参数通过FindOutParamValueAddress取地址,普通参数通过ContainerPtrToValuePtr(Stack.Locals())取栈局部值;
  5. 调用回调并处理返回值(见上文)。

重复注册与注销的边界行为

值得注意的细节:即使同名事件已注册,再次调用RegisterCustomEvent时新回调仍会被make_ref引用,但不会被加入容器——因为find_function_hook_data命中后直接跳过了emplace_back。从源码行为看,这意味着"同名事件只保留第一个注册的有效钩子"。若需要替换回调,请先注销再注册(见下文)。

配套 API:UnregisterCustomEvent

与RegisterCustomEvent配套的反向操作是UnregisterCustomEvent,其注册于 LuaMod.cpp,签名与错误文案为:

UnregisterCustomEvent(string EventName)
#类型说明
1string要注销的事件名称。

实现上,它把事件名转换为字符串后调用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 重载后旧钩子不会残留。

实战注意事项与最佳实践

  1. 事件名必须精确匹配:匹配基于UFunction::GetNamePrivate(),任何大小写或拼写差异都会导致钩子不触发。建议先用游戏内的对象/函数名 dump 工具(如 dumpers)确认目标函数的确切名字。
  2. 回调内必须做参数类型检查:UE4SS 不会在触发前校验参数类型,错误会在回调内部才暴露;BPML_GenericFunctions 的注释直言"There's no way to avoid it",这是社区验证过的硬性要求。
  3. 同名事件只注册一次:重复注册同名事件不会产生多个触发,若需替换回调应先UnregisterCustomEvent再重新注册。
  4. 善用 ParamContext:第一个参数是触发事件的 UObject 实例,可用于获取调用者信息、判断归属等,是事件处理中最重要的上下文入口。
  5. 回调内避免重活:钩子会拦截所有同名函数的每次调用,回调执行在游戏线程的脚本钩子路径上,应保持轻量,避免在回调里做阻塞式长任务。
  6. 类型支持有边界:返回值/参数回写依赖m_property_value_pushers中注册的属性处理器,超出支持范围的类型只能读取、无法回写,且会输出错误日志,属预期行为。
  7. 线程安全由框架保证:注册、触发均受递归互斥锁保护,你不需要(也不应该)在 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

项目地址:https://gitcode.com/gh_mirrors/re/RE-UE4SS
点击查看免费下载

相关推荐

上一篇:TegraRcmGUI:Windows平台最直观的Switch注入工具完全指南
下一篇:3步解锁Nintendo Switch隐藏功能:TegraRcmGUI图形化注入工具终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询