☰
UE4SS RemoteObject 基类解析:Lua 脚本中游戏对象指针的封装、继承体系与 IsValid 有效性检测
2026/10/3 19:00:59 网站建设 项目流程
  • 游戏开发
  • 逆向工程

【免费下载链接】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
点击查看免费下载

RemoteObject是 UE4SS Lua 脚本绑定体系中的两大基类之一,它封装了一个指向游戏进程内存中 C++ 对象的指针,是所有"远程"(由游戏持有生命周期)Lua 对象的类型源头。本文以官方文档 remoteobject.md 为主体,结合 LuaMadeSimple 与 LuaType 的源码实现,完整讲解 RemoteObject 的设计定位、继承关系、IsValid()等方法的底层原理与实战用法,帮助你在编写 Lua Mod 时正确判断对象有效性、避免访问已失效的游戏对象。

RemoteObject 是什么:两大基类体系中的"远程"侧

在 UE4SS 的 Lua 绑定体系中,几乎所有暴露给 Lua 的类型都派生自两个基础对象之一,其余类型都直接或间接继承自它们:

基类内部数据所有权归属对应文档
RemoteObject一个指向 C++ 对象的指针对象通常由游戏持有,Lua 侧只保存地址remoteobject.md
LocalObject内联(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 = ObjectBase<DerivedType, LuaMadeSimple::Type::RemoteObject, ObjectName>; template <typename DerivedType, typename ObjectName> using LocalObjectBase = ObjectBase<DerivedType, 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(即最终继承自RemoteObject):

Lua 类型C++ 模板基类头文件
UObject(UObjectBase)RemoteObjectBase<DerivedType, ObjectName>LuaUObject.hpp
UWorldRemoteObjectBase<Unreal::UWorld, UWorldName>LuaUWorld.hpp
UEnumRemoteObjectBase<Unreal::UEnum, UEnumName>LuaUEnum.hpp
TArrayRemoteObjectBase<Unreal::FScriptArray, TArrayName>LuaTArray.hpp
TMapRemoteObjectBase<Unreal::FScriptMap, TMapName>LuaTMap.hpp
TSetRemoteObjectBase<Unreal::FScriptSet, TSetName>LuaTSet.hpp
FOutputDeviceRemoteObjectBase<Unreal::FOutputDevice, FOutputDeviceName>LuaFOutputDevice.hpp
LuaModRefRemoteObjectBase<RC::LuaMod, ModName>LuaModRef.hpp
XProperty及各类X*PropertyRemoteObjectBase<Unreal::FProperty, ...>如 LuaXProperty.hpp
UE4SSBaseObjectObjectBase<uint8_t, RemoteObject, UE4SSBaseObjectName>LuaUObject.hpp

从源码结构看,凡是"指向游戏内存中已存在对象"的类型,几乎都被归入RemoteObject这条继承链;UObject又是其中最关键的一环——几乎所有 Unreal 反射类型(Class、Function、Property、ScriptStruct、AActor 等)最终都经由UObjectBase间接继承自RemoteObject。这也是为什么文档示例中会说"StaticFindObject返回的 UObject 继承自 RemoteObject"。

IsValid():判断 Lua 对象是否仍指向有效的游戏对象

签名与语义

IsValid()是RemoteObject上唯一在官方文档中正式列出的方法:

  • Return type:bool
  • Returns:该对象是否有效

对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::StaticFindObject<Unreal::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_userdata<RemoteObject>(); lua.set_bool(lua_object.m_cpp_object && lua_object.m_cpp_object != special_invalid_ptr()); return 1; });

这里有两个值得注意的细节:

  1. 指针非空是必要条件:m_cpp_object为nullptr时直接判定无效。

  2. special_invalid_ptr()哨兵值:RemoteObject保留了一个特殊指针值(uintptr_t最大值减0x1000)用于标记"字段存在但值无效"的状态。即使内部指针非空,只要它等于这个哨兵值,IsValid()同样返回false。这一设计是为了支持反射系统:让IsValidField可以返回true(字段确实存在)而IsValid返回false(值不可用),两者语义互不混淆。相关常量定义见 LuaObject.hpp。

  3. 多态类型的防护:如果某个多态类型没有自行覆盖IsValid,调用时会直接抛出 Lua 错误而非返回错误结果,避免静默地给出误导性返回值。

为什么 RemoteObject 的 IsValid 需要被覆盖

RemoteObject的IsValid只是基类层面的通用实现。上层类型在继承时会通过ObjectBase::setup_member_functions对其覆盖重写(见 LuaUObject.hpp):

table.add_pair("IsValid", [](const LuaMadeSimple::Lua& lua) -> int { const auto& lua_object = lua.get_userdata<SelfType>(); if constexpr (!LuaObjectBase<ObjectType>::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_cast<uintptr_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("对象不可用") end

2. 区分 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-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
点击查看免费下载
上一篇:FUXA 快速上手指南:开源 Web 版 SCADA/HMI 可视化平台的架构、部署与项目构建
下一篇:Compiler Explorer 开发指南:面向 AI Agent 的仓库协作、构建测试与 SQS 编译工作线程架构解析

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

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

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

立即咨询