- 游戏开发
- 逆向工程
【免费下载链接】RE-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
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 |
UWorld | RemoteObjectBase<Unreal::UWorld, UWorldName> | LuaUWorld.hpp |
UEnum | RemoteObjectBase<Unreal::UEnum, UEnumName> | LuaUEnum.hpp |
TArray | RemoteObjectBase<Unreal::FScriptArray, TArrayName> | LuaTArray.hpp |
TMap | RemoteObjectBase<Unreal::FScriptMap, TMapName> | LuaTMap.hpp |
TSet | RemoteObjectBase<Unreal::FScriptSet, TSetName> | LuaTSet.hpp |
FOutputDevice | RemoteObjectBase<Unreal::FOutputDevice, FOutputDeviceName> | LuaFOutputDevice.hpp |
LuaModRef | RemoteObjectBase<RC::LuaMod, ModName> | LuaModRef.hpp |
XProperty及各类X*Property | RemoteObjectBase<Unreal::FProperty, ...> | 如 LuaXProperty.hpp |
UE4SSBaseObject | ObjectBase<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; });这里有两个值得注意的细节:
指针非空是必要条件:
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.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("对象不可用") 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-UE4SS
Injectable LUA scripting system, SDK generator, live property editor and other dumping utilities for UE4/5 games
相关推荐
JavaScript-Algorithms之面向对象:类与继承的算法封装
JavaScript Algorithms之面向对象:类与继承的算法封装 在JavaScript算法实现中,面向对象编程(Object Oriented Pro
30分钟用上ERPNext:这套免费开源ERP系统如何接管财务、库存与销售
30分钟用上ERPNext:这套免费开源ERP系统如何接管财务、库存与销售 月底那天的下午,你又一次对着三张 Excel 表发愣:仓库说发了 200 件货,销售
后端企业应用business-machine-learning项目全面解析:6大业务部门的AI应用指南
business machine learning项目全面解析:6大业务部门的AI应用指南 Business Machine Learning BML 项目是一
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考