UE4SS RemoteObject 基类解析:Lua 脚本中游戏对象指针的封装、继承体系与 IsValid 有效性检测

📅 发布时间:2026/10/3 19:01:03
UE4SS RemoteObject 基类解析:Lua 脚本中游戏对象指针的封装、继承体系与 IsValid 有效性检测
游戏开发逆向工程【免费下载链接】RE-UE4SSInjectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games项目地址https://gitcode.com/gh_mirrors/re/RE-UE4SS点击查看免费下载RemoteObject是 UE4SS Lua 脚本绑定体系中的两大基类之一它封装了一个指向游戏进程内存中 C 对象的指针是所有远程由游戏持有生命周期Lua 对象的类型源头。本文以官方文档 remoteobject.md 为主体结合 LuaMadeSimple 与 LuaType 的源码实现完整讲解 RemoteObject 的设计定位、继承关系、IsValid()等方法的底层原理与实战用法帮助你在编写 Lua Mod 时正确判断对象有效性、避免访问已失效的游戏对象。RemoteObject 是什么两大基类体系中的远程侧在 UE4SS 的 Lua 绑定体系中几乎所有暴露给 Lua 的类型都派生自两个基础对象之一其余类型都直接或间接继承自它们基类内部数据所有权归属对应文档RemoteObject一个指向 C 对象的指针对象通常由游戏持有Lua 侧只保存地址remoteobject.mdLocalObject内联inline存储的 C 对象副本对象由Lua侧持有localobject.md从 Lua 脚本的角度看这两者的差异直接决定了对象的生命周期语义继承自RemoteObject的类型如UObject、UWorld、TArray、UEnum等指向的是游戏内存中的真实对象。当游戏销毁该对象例如 Actor 被移除、UObject 被 GC 回收时Lua 侧持有的指针就会变成悬空指针因此必须用IsValid()反复校验后才能安全访问。继承自LocalObject的类型如FText、FString、FName等在 Lua 侧拥有一份独立拷贝生命周期由 Lua 管理不受游戏 GC 影响IsValid()恒为true这是为历史兼容性保留的行为。在 C 源码层面这种远程 / 本地的区分是通过模板参数实现的。UE4SS 在 LuaUObject.hpp 中定义了两个别名template typename DerivedType, typename ObjectName using RemoteObjectBase ObjectBaseDerivedType, LuaMadeSimple::Type::RemoteObject, ObjectName; template typename DerivedType, typename ObjectName using LocalObjectBase ObjectBaseDerivedType, LuaMadeSimple::Type::LocalObject, ObjectName;可以看到RemoteObjectBase与LocalObjectBase共用同一个ObjectBase模板唯一的区别就是传入的第二个模板参数是LuaMadeSimple::Type::RemoteObject还是LuaMadeSimple::Type::LocalObject。这一设计让上层类型可以复用同一套构造、元方法metamethod与成员函数注册逻辑仅在对象存储方式上分流。继承关系一个孤儿基类但拥有庞大的后裔根据文档RemoteObject自身不继承任何类Inheritance: None它与LocalObject一起构成整个 Lua 类型体系的最顶端。虽然没有父类它却是 UE4SS 类型树中最主要的根。从 UE4SS/include/LuaType 目录下的头文件可以确认以下 Lua 类型均直接继承自RemoteObjectBase即最终继承自RemoteObjectLua 类型C 模板基类头文件UObjectUObjectBaseRemoteObjectBaseDerivedType, ObjectNameLuaUObject.hppUWorldRemoteObjectBaseUnreal::UWorld, UWorldNameLuaUWorld.hppUEnumRemoteObjectBaseUnreal::UEnum, UEnumNameLuaUEnum.hppTArrayRemoteObjectBaseUnreal::FScriptArray, TArrayNameLuaTArray.hppTMapRemoteObjectBaseUnreal::FScriptMap, TMapNameLuaTMap.hppTSetRemoteObjectBaseUnreal::FScriptSet, TSetNameLuaTSet.hppFOutputDeviceRemoteObjectBaseUnreal::FOutputDevice, FOutputDeviceNameLuaFOutputDevice.hppLuaModRefRemoteObjectBaseRC::LuaMod, ModNameLuaModRef.hppXProperty及各类X*PropertyRemoteObjectBaseUnreal::FProperty, ...如 LuaXProperty.hppUE4SSBaseObjectObjectBaseuint8_t, RemoteObject, UE4SSBaseObjectNameLuaUObject.hpp从源码结构看凡是指向游戏内存中已存在对象的类型几乎都被归入RemoteObject这条继承链UObject又是其中最关键的一环——几乎所有 Unreal 反射类型Class、Function、Property、ScriptStruct、AActor 等最终都经由UObjectBase间接继承自RemoteObject。这也是为什么文档示例中会说StaticFindObject返回的 UObject 继承自 RemoteObject。IsValid()判断 Lua 对象是否仍指向有效的游戏对象签名与语义IsValid()是RemoteObject上唯一在官方文档中正式列出的方法Return type:boolReturns:该对象是否有效对RemoteObject而言有效意味着两件事同时成立内部指针不为空非nullptr且该指针不是被保留用于特殊语义的无效哨兵值详见下文源码解析。一旦游戏侧对象被销毁或回收后续调用IsValid()就会返回false。官方示例文档给出的标准用法是配合StaticFindObject查找对象后先做校验再使用-- StaticFindObject 返回一个继承自 RemoteObject 的 UObject。 local Object StaticFindObject(/Script/CoreUObject.Object) if Object:IsValid() then print(Object is valid\n) else print(Object is NOT valid\n) end这段代码的实战价值在于StaticFindObject按路径字符串在游戏中查找对象返回结果可能因为路径错误、对象被 GC、对象尚未加载等原因而无效。在拿到对象后先调用IsValid()再访问其属性、调用其方法是 UE4SS Lua Mod 中防止崩溃和报错的最基本防御手段。在 UE4SS 源码中StaticFindObject最终映射到 Unreal 的全局查找函数例如 LuaUObject.cpp 中的调用形态auto* object_class Unreal::UObjectGlobals::StaticFindObjectUnreal::UClass*(nullptr, nullptr, ensure_str(lua.get_string()));这印证了文档示例的可操作性Lua 层的StaticFindObject确实会返回一个可被IsValid()校验的对象引用。源码级原理指针 哨兵值的双重检查RemoteObject的核心实现位于 LuaMadeSimple 库的 LuaObject.hpp 中。其模板类只保存一个原生指针作为唯一成员template typename ObjectType class RemoteObject : public BaseObject { public: static constexpr bool IsLocal false; // 标记这是“远程”对象 private: ObjectType* m_cpp_object; // 指向游戏内存中 C 对象的指针 public: auto get_remote_cpp_object() const - ObjectType* { return m_cpp_object; } };IsValid的注册逻辑setup_member_functions如下table.add_pair(IsValid, [](const LuaMadeSimple::Lua lua) - int { lua_getiuservalue(lua.get_lua_state(), 1, 5); if (lua_toboolean(lua.get_lua_state(), -1)) { lua.throw_error(Call to RemoteObject:IsValid on polymorphic type is not allowed, please override IsValid.); } const RemoteObject lua_object lua.get_userdataRemoteObject(); lua.set_bool(lua_object.m_cpp_object lua_object.m_cpp_object ! special_invalid_ptr()); return 1; });这里有两个值得注意的细节指针非空是必要条件m_cpp_object为nullptr时直接判定无效。special_invalid_ptr()哨兵值RemoteObject保留了一个特殊指针值uintptr_t最大值减0x1000用于标记字段存在但值无效的状态。即使内部指针非空只要它等于这个哨兵值IsValid()同样返回false。这一设计是为了支持反射系统让IsValidField可以返回true字段确实存在而IsValid返回false值不可用两者语义互不混淆。相关常量定义见 LuaObject.hpp。多态类型的防护如果某个多态类型没有自行覆盖IsValid调用时会直接抛出 Lua 错误而非返回错误结果避免静默地给出误导性返回值。为什么 RemoteObject 的 IsValid 需要被覆盖RemoteObject的IsValid只是基类层面的通用实现。上层类型在继承时会通过ObjectBase::setup_member_functions对其覆盖重写见 LuaUObject.hpptable.add_pair(IsValid, [](const LuaMadeSimple::Lua lua) - int { const auto lua_object lua.get_userdataSelfType(); if constexpr (!LuaObjectBaseObjectType::IsLocal) { if (lua_object.get_remote_cpp_object()) { lua.set_bool(true); } else { lua.set_bool(false); } } else { lua.set_bool(true); // 本地对象恒为有效 } return 1; });也就是说UObject、UWorld、TArray等最终类型的IsValid()实际是各自类层级中的覆盖版本其判定依据是各自持有的远程 C 指针是否为nullptr。对于LocalObject分支则无条件返回true与文档中LocalObject 的 IsValid 恒为 true的描述完全一致。官方文档未单独列出、但源码中存在的相关方法除了文档正式记载的IsValid()从 LuaObject.hpp 源码看RemoteObject基类还注册了另外两个成员函数。它们在 remoteunrealparam.md 中也被一并提及IsValid(), GetAddress()可作为补充了解方法返回值作用备注IsValid()bool对象是否有效官方文档正式列出的方法IsValidField()bool内部指针是否为非哨兵值即字段是否存在/可用供反射系统使用与IsValid语义不同GetAddress()integer返回内部 C 对象指针的整型地址在多态类型上被禁用需由子类覆盖其中GetAddress的基类实现会检查多态标记若未被子类覆盖则抛出错误Call to RemoteObject:GetAddress on polymorphic type is not allowed, please override GetAddress。而UObjectBase等上层类型确实覆盖了它远程类型返回reinterpret_castuintptr_t(lua_object.get_remote_cpp_object())本地类型返回本地存储对象的地址见 LuaUObject.hpp。实战要点与使用建议1. 拿到对象后先 IsValid 再使用无论是通过StaticFindObject、FindAllOf/FindFirstOf之类的查找函数还是通过属性读取如GetPropertyValue间接获得的对象引用都可能是悬空或空引用。统一先执行local Obj StaticFindObject(/Script/Engine.Default__Actor) if Obj ~ nil and Obj:IsValid() then -- 安全地访问属性 / 调用方法 else print(对象不可用) end2. 区分 IsValid 与 nil 检查IsValid()判定的是指针是否指向有效对象它无法替代nil检查如果StaticFindObject查找失败Lua 侧可能直接拿到nil此时调用:IsValid()本身就会报错。稳妥的写法是先判断非nil再调用IsValid()或利用 UE4SS 提供的其他查找辅助函数简化流程。3. 理解生命周期差异选对基类语义对RemoteObject系对象UObject 等不要长期缓存而不校验——游戏 GC 随时可能回收它们对LocalObject系对象FString、FText 等IsValid()恒为true无需将其作为有效性的判断依据可放心直接使用若要在 Mod 内部长期保存某个游戏对象应定期调用IsValid()或在对象销毁时清理引用。UE4SS 内部为此维护了全局 UObject 映射add_to_global_unreal_objects_map见 LuaUObject.hpp并在UObjectBase::construct中跳过哨兵指针值的注册从实现层面保证进入 Lua 的对象映射是有效的。延伸阅读与 RemoteObject 对应的另一半基类LocalObject最重要的 RemoteObject 子类UObject同样直接继承 RemoteObject 的类型UWorld、UEnum、TArray、TMap、TSet、FOutputDevice、Mod、Property源码实现参考LuaMadeSimple 的 RemoteObject 模板、ObjectBase 与 RemoteObjectBase 别名定义赞分享游戏开发逆向工程【免费下载链接】RE-UE4SSInjectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games项目地址https://gitcode.com/gh_mirrors/re/RE-UE4SS点击查看免费下载相关推荐JavaScript-Algorithms之面向对象类与继承的算法封装JavaScript Algorithms之面向对象类与继承的算法封装 在JavaScript算法实现中面向对象编程Object Oriented Pro30分钟用上ERPNext这套免费开源ERP系统如何接管财务、库存与销售30分钟用上ERPNext这套免费开源ERP系统如何接管财务、库存与销售 月底那天的下午你又一次对着三张 Excel 表发愣仓库说发了 200 件货销售后端企业应用business-machine-learning项目全面解析6大业务部门的AI应用指南business machine learning项目全面解析6大业务部门的AI应用指南 Business Machine Learning BML 项目是一上一篇FUXA 快速上手指南开源 Web 版 SCADA/HMI 可视化平台的架构、部署与项目构建下一篇Compiler Explorer 开发指南面向 AI Agent 的仓库协作、构建测试与 SQS 编译工作线程架构解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考