☰
UE4SS 的 UDataTable Lua API 完全指南:在运行时读写与遍历游戏数据表
2026/10/3 2:27:40 网站建设 项目流程
  • 游戏开发
  • 逆向工程

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

导读

UDataTable是 Unreal Engine 中用于存储哈希键值对(行名 → 行数据)的核心数据结构,常见于掉落表、升级经验表、角色基础属性等游戏配置。UE4SS 在 Lua 运行时中为UDataTable提供了完整封装,支持运行时读取、写入、删除、清空与遍历整张数据表。阅读本文后,你将掌握UDataTable的 Lua 对象模型、全部 10 个方法/元方法的参数与返回值约定、引用式访问的底层原理,以及基于行结构体的克隆、批量修改等实战用法。本文以 docs/lua-api/classes/udatatable.md 为主体,并结合 LuaUDataTable.cpp 源码进行纵深剖析。

对象模型概览

在 UE4SS 的 Lua 类型系统中,UDataTable的类继承关系为:

UDataTable → UObject → RemoteObject

即 UObject 中定义的所有成员函数(如GetFullName()、GetFName()、GetAddress()、GetClass()、IsValid()、type()等)在UDataTable对象上同样可用。在源码层面,这一关系体现于 LuaUDataTable.hpp 中的class UDataTable : public UObjectBase<Unreal::UDataTable, UDataTableName>:它基于 UObject 构建,并额外注册了数据表专属的元方法与成员函数。

对应的 C++ 侧声明中,UDataTable还关联了一个内部工具结构体FDataTableInfo,用于缓存数据表的行结构体指针(row_struct)、其 FName(row_struct_fname)以及单行内存大小(row_size),所有行级操作都依赖这些信息完成。

元方法

__len

  • 用法:#UDataTable
  • 返回类型:integer
  • 返回数据表的总行数。

该元方法在源码中对应 LuaUDataTable.cpp 的MetaMethod::Length注册逻辑,底层直接读取data_table->GetRowMap().Num(),因此#dt等价于行数统计。

local total = #dataTable print("Total rows:", total)

方法详解

IsValid()

  • 返回类型:bool
  • 返回数据表是否有效(非空指针)。

该方法继承自 UObject 体系,用于在调用行操作前防御性地检查对象是否已被销毁。

if dataTable:IsValid() then -- 安全操作 end

GetRowStruct()

  • 返回类型:UScriptStruct|nil
  • 返回定义该数据表行结构的 UScriptStruct;若未设置则返回 nil。

在源码中,GetRowStruct()调用data_table->GetRowStruct(),若指针为空会显式set_nil()。通过返回的 UScriptStruct 对象,你可以检查行包含哪些字段以及字段类型,例如配合FindFirstOf或反射 API 查看行结构定义。

local rowStruct = dataTable:GetRowStruct() if rowStruct then print("Row struct:", rowStruct:GetFullName()) end

GetRowMap()

  • 返回类型:table
  • 返回包含全部行的 Lua table,以行名为键、行数据为值。

需要注意的是返回值的形式取决于行类型是否存在自定义的"值推送器"(custom pusher)。在 LuaUDataTable.cpp 的GetRowMap()实现中,代码遍历data_table->GetRowMap()(类型为TMap<FName, unsigned char*>),以Pair.Key.ToString()作为 Lua 表键,并将Pair.Value(指向行内存的指针)包装成ScriptStructWrapper后通过UScriptStruct::construct压入 Lua——也就是说默认情况下每个值都是一个UScriptStruct 包装对象,提供引用式访问;如果该行类型注册了专用的 property value pusher,则可能直接以普通 Lua table 形式返回。

local rowMap = dataTable:GetRowMap() for rowName, rowData in pairs(rowMap) do print(rowName, rowData) end

FindRow(string RowName)

  • 返回类型:UScriptStruct|nil
  • 按行名查找行;找到则返回行数据(UScriptStruct),未找到返回 nil。
  • 关键特性:返回的结构体是引用式访问——对其字段的修改会直接作用于数据表本身。
  • 示例:local row = dt:FindRow("Player"); row.Health = 200会真实修改数据表。

底层实现(DataTableOperation::FindRow分支)使用data_table->FindRowUnchecked(row_name)直接取得行内存指针,若为 null 则set_nil();否则构造ScriptStructWrapper{.script_struct = row_struct, .start_of_struct = row_data, .property = nullptr}并压栈。这与 C++ 中FindRow返回指向真实行内存指针的行为一致,因此修改立即生效,无需再写回。

local row = dataTable:FindRow("Player") if row then row.Health = 200 -- 直接修改数据表内的 Player 行 end

AddRow(string RowName, table|UScriptStruct RowData)

  • 向数据表新增一行,指定行名与数据;若同名行已存在则替换。
  • 第二参数接受两种形式:
    • Lua table:字段需与行结构定义匹配(如{Health = 100, MaxHealth = 100});
    • UScriptStruct:数据从该结构体复制(适合用于克隆已有行)。
  • 示例:dt:AddRow("NewPlayer", {Health = 100, MaxHealth = 100})
  • 示例:dt:AddRow("Clone", dt:FindRow("Original"))— 复制一行

源码中的DataTableOperation::AddRow分支展示了完整的处理链路:

  1. 校验第一个参数必须是字符串(行名),否则抛出"AddRow expects a string as the first parameter";
  2. 通过lua.is_userdata(1)/lua.is_table(1)区分传入的是 UScriptStruct 还是 Lua table,两者都不是则抛出"AddRow expects a table or UScriptStruct as the second parameter";
  3. 用FMemory::Malloc(row_size)分配临时内存并InitializeStruct;
    • UScriptStruct 路径:调用CopyScriptStruct(new_row_data, wrapper_data.start_of_struct)从源结构复制字节数据;
    • Lua table 路径:若该行结构在StaticState::m_property_value_pushers中注册了专用 pusher 则调用 pusher,否则回退到convert_lua_table_to_struct(lua, info.row_struct, new_row_data, 1, nullptr)通用转换函数;
  4. 调用data_table->AddRow(row_name, new_row_data, info.row_struct)将数据复制进数据表内部存储;
  5. 由于AddRow已复制数据,随后DestroyStruct+FMemory::Free释放临时分配,避免内存泄漏。
-- 用 table 新增一行 dataTable:AddRow("NewPlayer", {Health = 100, MaxHealth = 100}) -- 克隆已有行 local original = dataTable:FindRow("Original") dataTable:AddRow("Clone", original)

RemoveRow(string RowName)

  • 删除指定名称的行;若行不存在则不做任何事。

对应DataTableOperation::RemoveRow分支,内部直接调用data_table->RemoveRow(row_name),无需额外参数校验或返回值。

dataTable:RemoveRow("ObsoleteRow")

EmptyTable()

  • 清空数据表的所有行。
  • 注意:不会清除 RowStruct 定义,清空后表仍然"知道"自己的行结构,可以继续AddRow。
dataTable:EmptyTable() -- 表已清空,但 GetRowStruct() 依然有效

GetRowNames()

  • 返回类型:table
  • 返回一个 1 起始索引的数组,包含数据表中所有行名。

实现中遍历data_table->GetRowNames(),以i + 1作为键、行名字符串作为值构建 Lua 表。

local names = dataTable:GetRowNames() for i = 1, #names do print(i, names[i]) end

GetAllRows()

  • 返回类型:table
  • 返回一个 1 起始索引的数组,每个元素是包含两个字段的 table:
    • Name:行名(string)
    • Data:行数据(table 或 UScriptStruct)

Data字段同样以ScriptStructWrapper方式包装行内存,因此同样具备引用式访问能力——修改entry.Data.xxx会直接改变数据表内容。

for _, entry in ipairs(dataTable:GetAllRows()) do print("Row:", entry.Name) print("Health:", entry.Data.Health) end

ForEachRow(function Callback)

  • 遍历数据表所有行,并为每一行调用回调函数。
  • 回调参数:string rowName、UScriptStruct rowData
  • rowData提供引用式访问——修改会直接影响数据表。
  • 回调可以返回true提前终止遍历。

该方法的 C++ 实现是setup_member_functions中唯一一个没有走prepare_to_handle分发、而是内联实现的方法。其关键逻辑包括:

  • 构造FDataTableInfo并调用validate_row_struct(lua),若数据表没有 RowStruct 则抛出"DataTable has no RowStruct specified";
  • 每轮迭代通过lua_pushvalue(lua.get_lua_state(), 1)复制回调函数到栈顶,依次压入行名字符串与行数据 UScriptStruct 包装,再以lua.call_function(2, 1)调用;
  • 检查返回值:若第 2 个栈位置为布尔true则break提前退出;否则丢弃栈上的 nil 返回值,防止 Lua 栈在下一轮迭代被污染。
dataTable:ForEachRow(function(rowName, rowData) print(string.format("Row: %s, Health: %d", rowName, rowData.Health)) -- 直接修改行 rowData.Health = rowData.Health + 10 -- 返回 true 停止遍历 if rowName == "TargetRow" then return true end end)

实战组合示例

结合上述 API,可以在运行时完成一次完整的"查找 → 修改 → 克隆 → 删除"数据表维护流程:

local dt = FindFirstOf("DataTable") -- 或通过其他方式获取 UDataTable if not dt or not dt:IsValid() then print("DataTable not found") return end -- 1. 读取行并修改(引用式访问,直接生效) local player = dt:FindRow("Player") if player then player.Health = player.Health + 50 end -- 2. 新增一行 dt:AddRow("NewPlayer", {Health = 100, MaxHealth = 100}) -- 3. 克隆一行 dt:AddRow("Clone", dt:FindRow("Player")) -- 4. 遍历并统计 local count = 0 dt:ForEachRow(function(name, data) count = count + 1 print(name, data.Health) end) -- 5. 清理 dt:RemoveRow("Clone") print("Total rows:", #dt)

底层原理与注意事项

引用式访问从何而来

FindRow、GetRowMap、GetAllRows、ForEachRow返回的UScriptStruct本质都是 ScriptStructWrapper(包含script_struct、start_of_struct、property三个字段)。start_of_struct直接指向数据表内部行内存(即TMap<FName, unsigned char*>的 Value 指针),因此通过 UScriptStruct 的__index/__newindex读写字段时,操作的就是数据表真实内存,无需额外写回步骤。这也解释了为什么FindRow的注释明确强调"modifications directly affect the DataTable"。

行结构校验

FDataTableInfo::validate_row_struct会在FindRow、AddRow、GetAllRows、ForEachRow前强制校验row_struct是否存在,否则抛出"DataTable has no RowStruct specified"。__len、GetRowStruct、GetRowNames、RemoveRow、EmptyTable不依赖行结构定义,因此不受此限制。

空指针保护

GetRowStruct、GetRowMap在data_table为空时都会set_nil()返回;ForEachRow与prepare_to_handle则在数据表指针为空时直接lua.throw_error("DataTable is null")。在实际 mod 代码中建议先以IsValid()或FindFirstOf的结果判空,再执行行操作。

设计背景

UE4SS 对 DataTable 的完整读写支持并非一蹴而就。官方开发日志 datatables-in-ue4ss.md 记录了早期方案的权衡:直接遍历 UE4SS 自研TMap的GetElementsPtr()只适合只读场景,因为AddRow/RemoveRow之后指针会失效;而利用 KismetUDataTableFunctionLibrary的反射函数又受限于GetDataTableRowFromName这类CustomThunk函数在纯 C++ 调用时Stack.MostRecentProperty无法填充的问题。最终当前版本的 Lua API 采用直接包装行内存指针(ScriptStructWrapper)的方案,实现了文档中描述的完整读/写/增/删/遍历能力。

适用前提

  • 本文所述 API 适用于 UE4/UE5 游戏内注入 UE4SS 后的 Lua mod 场景,UDataTable对象可通过FindFirstOf("DataTable")、FindAllOf或遍历GetRowMap等方式获取;
  • 行数据字段名必须与行结构定义一致(参考 UScriptStruct 的__index/__newindex约定),大小写需与游戏内反射出的属性名匹配;
  • 部分游戏的行结构可能包含内存对齐/填充差异(参见 datatables-in-ue4ss.md 中对 FName 对齐的讨论),若遇到异常字段读写,可从行结构的内存布局角度排查。

方法速查表

方法签名返回类型说明
__len#dtinteger行数
IsValiddt:IsValid()bool对象是否有效(继承自 UObject)
GetRowStructdt:GetRowStruct()UScriptStruct/nil行结构定义
GetRowMapdt:GetRowMap()table行名 → 行数据的映射表
FindRowdt:FindRow(name)UScriptStruct/nil按名查行(引用式)
AddRowdt:AddRow(name, data)无新增/替换行
RemoveRowdt:RemoveRow(name)无删除行
EmptyTabledt:EmptyTable()无清空所有行(保留 RowStruct)
GetRowNamesdt:GetRowNames()table行名数组(1 起始)
GetAllRowsdt:GetAllRows()table{Name=..., Data=...}数组
ForEachRowdt:ForEachRow(cb)无遍历;回调返回true可提前退出

相关资源

  • API 文档原文:docs/lua-api/classes/udatatable.md
  • 基类文档:UObject、UScriptStruct
  • 源码实现:LuaUDataTable.hpp、LuaUDataTable.cpp、LuaUScriptStruct.hpp
  • 设计背景与早期方案讨论:datatables-in-ue4ss.md
  • 游戏开发
  • 逆向工程

【免费下载链接】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
点击查看免费下载
上一篇:深入解析 rustc-std-workspace-core:Rust 标准库依赖 crates.io 生态的桥梁与编译期 shim
下一篇:Tabby 数据备份完整指南:SQLite 数据库、事件日志与后台任务日志的备份与恢复

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

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

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

立即咨询