RE-UE4SS 升级指南
本文详细介绍不同 RE-UE4SS 版本之间的升级方式。各节对应特定版本升级,说明不兼容变更、弃用项,以及成功迁移代码所需的步骤。
从 3.x 升级到 4.x
发布日期: 待定(原文为 TBD)。
PR: 原文保留待补链接占位。
不兼容变更
FName 构造函数的默认参数变更
变更内容:
FName 构造函数中 EFindName 参数的默认值由 FNAME_Find 改为 FNAME_Add。
变更前(v3.x): 仅查找已有名称。
// Default was FNAME_Find - only found existing names
explicit FName(const CharType* StrName, EFindName FindType = FNAME_Find, void* FunctionAddressOverride = nullptr);
变更后(v4.x): 名称不存在时创建名称。
// Default is now FNAME_Add - creates name if it doesn't exist
explicit FName(const CharType* StrName, EFindName FindType = FNAME_Add, void* FunctionAddressOverride = nullptr);
迁移步骤:
-
如果依赖旧行为,即名称不存在时构造函数返回
NAME_None,需要显式指定FNAME_Find:// Old (implicit FNAME_Find) FName TestName(STR("NonExistentName")); // Would result in NAME_None if name doesn't exist // New (explicit FNAME_Find to maintain old behavior) FName TestName(STR("NonExistentName"), FNAME_Find); // Explicitly specify FNAME_Find -
如果在构造后检查名称是否有效,需要使用
FNAME_Find检查是否存在:// Old code that might break FName TestName(STR("PotentiallyNewName")); if (!TestName) { // This check no longer works as expected // Handle invalid name } // New approach FName TestName(STR("PotentiallyNewName"), FNAME_Find); // Use FNAME_Find if you need to check existence if (!TestName) { // Handle non-existent name } -
已经显式指定
FindType参数的代码无需修改。
注意事项:
- 该变更影响全部三个接受字符串参数的 FName 构造函数重载。
- 新默认行为
FNAME_Add会在名称不存在时向名称表中添加 FName 项。 - 如果频繁以可能不存在的字符串创建 FName,可能影响性能。
- 操作 Unreal Engine 内部结构时应谨慎;意外创建新名称可能产生非预期行为。
TObjectPtr 实现变更
变更内容:
TObjectPtr<> 从简单包装类增强为具备完整行为的智能指针。
变更前(v3.x):
// Simple class that makes everything compile.
template<typename UnderlyingType>
class TObjectPtr
{
public:
UnderlyingType* UnderlyingObjectPointer;
};
变更后(v4.x):
template<typename T>
class TObjectPtr
{
public:
using ElementType = T;
// Constructors
TObjectPtr() : Ptr(nullptr) {}
TObjectPtr(TYPE_OF_NULLPTR) : Ptr(nullptr) {}
TObjectPtr(const TObjectPtr& Other) : Ptr(Other.Ptr) {}
explicit TObjectPtr(ElementType* InPtr) : Ptr(InPtr) {}
// Assignment operators
TObjectPtr& operator=(const TObjectPtr& Other) { Ptr = Other.Ptr; return *this; }
TObjectPtr& operator=(TYPE_OF_NULLPTR) { Ptr = nullptr; return *this; }
TObjectPtr& operator=(ElementType* InPtr) { Ptr = InPtr; return *this; }
// Conversion operators
FORCEINLINE operator T* () const { return Get(); }
template <typename U>
UE_OBJPTR_DEPRECATED(5.0, "Explicit cast to other raw pointer types is deprecated. Please use the Cast API or get the raw pointer with ToRawPtr and cast that instead.")
explicit FORCEINLINE operator U* () const { return (U*)Get(); }
explicit FORCEINLINE operator UPTRINT() const { return (UPTRINT)Get(); }
FORCEINLINE T* operator->() const { return Get(); }
FORCEINLINE T& operator*() const { return *Get(); }
// Comparison operators
bool operator==(const TObjectPtr& Other) const { return Ptr == Other.Ptr; }
bool operator!=(const TObjectPtr& Other) const { return Ptr != Other.Ptr; }
bool operator==(const ElementType* InPtr) const { return Ptr == InPtr; }
bool operator!=(const ElementType* InPtr) const { return Ptr != InPtr; }
bool operator==(TYPE_OF_NULLPTR) const { return Ptr == nullptr; }
bool operator!=(TYPE_OF_NULLPTR) const { return Ptr != nullptr; }
// Additional API compatibility with UE's TObjectPtr
bool operator!() const { return Ptr == nullptr; }
explicit operator bool() const { return Ptr != nullptr; }
ElementType* Get() const { return Ptr; }
private:
ElementType* Ptr;
};
迁移步骤:
-
原先通过
UnderlyingObjectPointer直接访问指针的代码,改用隐式转换或Get():// Old TObjectPtr<UClass> ClassPtr; UClass* RawPtr = ClassPtr.UnderlyingObjectPointer; // New TObjectPtr<UClass> ClassPtr; UClass* RawPtr = ClassPtr; // implicit conversion // or UClass* RawPtr = ClassPtr.Get(); // explicit access -
对
TObjectPtr内容取地址的操作,原文给出的迁移示例如下:// Old &ObjectPtr.UnderlyingObjectPointer // New &ObjectPtr.Get() // or simply &ObjectPtr
译注: 上面的
&ObjectPtr.Get()为原文示例。按本页展示的Get()按值返回指针的实现,不能把这一写法直接当作通用可编译代码;请结合所用头文件和实际取地址目的检查。
-
指针转整数(例如格式化地址)时,通过
Get()或ToRawPtr()提取原始指针:// Old uintptr_t addr = reinterpret_cast<uintptr_t>(ObjectPtr.UnderlyingObjectPointer); // New uintptr_t addr = reinterpret_cast<uintptr_t>(ObjectPtr.Get()); // or uintptr_t addr = reinterpret_cast<uintptr_t>(ToRawPtr(ObjectPtr)); // using helper to extract a raw pointer from TObjectPtr
FString API 变更
GetCharArray() 行为变更
变更内容:
GetCharArray() 现在返回实际数组,而非字符指针。
变更前(v3.x):
// Returned a character pointer
auto GetCharArray() const -> const CharType*;
变更后(v4.x):
// Returns the array itself
FORCEINLINE DataType& GetCharArray() { return Data; }
FORCEINLINE const DataType& GetCharArray() const { return Data; }
// Use operator* for character pointer access
FORCEINLINE const TCHAR* operator*() const { return Data.Num() ? Data.GetData() : TEXT(""); }
迁移步骤:
-
获取字符指针(原先使用
GetCharArray())时,改用解引用运算符:// Old const TCHAR* ptr = myString.GetCharArray(); // New const TCHAR* ptr = *myString; -
获取数组(原先使用
GetCharTArray())时,改用GetCharArray():// Old TArray<TCHAR>& arr = myString.GetCharTArray(); // New TArray<TCHAR>& arr = myString.GetCharArray();
FSoftObjectPath 成员变量改为访问器
变更内容:
FSoftObjectPath 不再公开 AssetPathName 和 SubPathString 成员变量。结构体现在存储不透明字节,并在运行时解析引擎的实际布局。原因是 UE 5.1 改变了布局:FName AssetPathName 变为 FTopLevelAssetPath AssetPath,SubPathString 从偏移 0x8 移至 0x10。旧的编译期成员布局会在所有 5.1 及更新版本的游戏中读取错误内存。
变更前(v3.x):
FSoftObjectPath Path = ...;
FName AssetName = Path.AssetPathName;
FString& SubPath = Path.SubPathString;
Path.SubPathString = NewSubPath;
变更后(v4.x):
FSoftObjectPath Path = ...;
FName AssetName = Path.GetAssetPathName();
FString SubPath = Path.GetSubPathString();
Path.SetSubPathString(NewSubPath);
// New: package/asset name pair, works on every engine version
FTopLevelAssetPath AssetPath = Path.GetAssetPath();
迁移步骤:
- 将
Path.AssetPathName替换为Path.GetAssetPathName()。 - 读取
Path.SubPathString时改用Path.GetSubPathString(),写入时改用Path.SetSubPathString()。GetSubPathString()返回副本,修改返回的字符串不会修改路径。 - 如果需要分别取得包名和资产名,优先使用
GetAssetPath()。它在所有引擎版本中都返回FTopLevelAssetPath;在 5.1 之前的版本中,通过拆分路径合成该值。
注意事项:
- 在 5.1 及更新版本的引擎中,内存中不存在单个 FName 形式的表示,因此
GetAssetPathName()会按需使用FNAME_Add创建组合的/Package/Path.AssetFName。 - 写入资产路径仍使用
SetPath()或operator=(const FString&),与以前相同。 - UE 5.6 以 UTF-8 存储子路径。所有引擎版本的
GetSubPathString()和SetSubPathString()都与FString相互转换,因此 getter 无法返回引用。 TArray<FSoftObjectPath>现在以运行时大小FSoftObjectPath::StaticSize()为步长,修复 5.1 及更新版本、以及保留大小写构建中的元素访问问题。- Lua API(
GetAssetPathName()、GetSubPathString())保持不变,Lua Mod 不受影响。
软对象指针成员变量改为访问器
变更内容:
FSoftObjectPtr 及其基类 TPersistentObjectPtr<FSoftObjectPath> 不再公开 ObjectID 和 TagAtLastTest 成员变量。它们的布局根据引擎反射得到的软对象/软类属性大小来解析;无法获得反射信息时,回退到按引擎版本确定布局。这也适用于 TSoftObjectPtr<T> 和 TSoftClassPtr<T> 内部的存储。
UE 5.3 移除了 TagAtLastTest,把 ObjectID 从偏移 0x10 移到 0x08。这与上面的 UE 5.1 FSoftObjectPath 变更是独立的。在旧偏移读取路径可能把字符串长度和容量当成指针,导致 Issue #1433 中报告的崩溃:FUObjectArray::AllocateSerialNumber 将对象转换为软引用时发生故障。
变更前(v3.x / 较早的实验构建):
FSoftObjectPtr SoftPtr;
FSoftObjectPath& Path = SoftPtr.ObjectID;
int32 Tag = SoftPtr.TagAtLastTest;
SoftPtr.TagAtLastTest = 0;
变更后(v4.x):
FSoftObjectPtr SoftPtr;
FSoftObjectPath& Path = SoftPtr.GetUniqueID();
int32 Tag = SoftPtr.GetTagAtLastTest();
SoftPtr.SetTagAtLastTest(0);
迁移步骤:
- 将直接访问
ObjectID改为GetUniqueID()。const 和非 const 重载均返回引用。 - 读取标记时用
GetTagAtLastTest(),写入时用SetTagAtLastTest(value)。 - 替换路径时优先使用
SoftPtr = NewPath,这也会重置缓存的弱指针。若通过非 const 的GetUniqueID()引用修改路径,之后应调用ResetWeakPtr()。 - 手动遍历引擎拥有的内存时,使用
FSoftObjectPtr::StaticSize()、TSoftObjectPtr<T>::StaticSize()或TSoftClassPtr<T>::StaticSize(),不要使用sizeof或硬编码步长。sizeof包含最大受支持布局所需的存储,对本地分配的 C++ 对象仍应使用它。 - 使用更新后的 UE4SS 头文件和库重新构建受影响的 C++ Mod。
注意事项:
- 对没有标记字段的引擎,
GetTagAtLastTest()返回零,SetTagAtLastTest()不产生效果。 WeakPtr仍可访问。TSoftObjectPtr<T>和TSoftClassPtr<T>的现有方法,如Get()、Reset()、ToSoftObjectPath(),仍然可用。- 应通过 C++ 复制和赋值操作复制软指针,确保路径中的
FString正确复制;按字节复制不会建立字符串所有权。 - Lua 的
GetWeakPtr()、GetObjectID()和GetTagAtLastTest()保持不变。Lua Mod 无需迁移;引擎没有标记字段时,GetTagAtLastTest()返回零。
FText 成员变量改为访问器
变更内容:
FText 不再公开 Data、SharedRefCollector、Flags 和 Unk 成员变量。UE 5.4 将内部表示从 TSharedRef<ITextData>(FText 大小为 0x18 字节)改为 TRefCountPtr<ITextData>(0x10 字节),因此固定成员布局无法适配所有版本。现在 FText 存储由 FText::StaticSize() 确定大小的不透明字节,复制、赋值和析构都通过引擎自身的 FTextProperty 值操作完成,从而在所有引擎版本中正确维护引用计数。
变更前(v3.x):
FText Text = ...;
ITextData* Data = Text.Data;
uint32 Flags = Text.Flags;
变更后(v4.x):
FText Text = ...;
ITextData* Data = Text.GetTextData();
uint32 Flags = Text.GetFlags();
迁移步骤:
- 将
Text.Data替换为Text.GetTextData()。 - 将
Text.Flags替换为Text.GetFlags()。 - 移除所有
SharedRefCollector和Unk的使用;它们是对布局进行推测时留下的产物。引用控制器现由引擎管理。 - 构造、复制、赋值或转换 FText 的代码(
ToString()、ToFString()、SetString()、FText(FString))无需更改。
注意事项:
- 通过 C++ 创建的副本现在持有引用,在 FText 析构时自动释放。对 FText 做按字节复制(例如
memcpy到比源对象存活更久的缓冲区)应改为赋值;与引擎参数缓冲区交互时,可使用FText::StealFromBuffer/CopyBorrowedTo。 - 按值向引擎函数传递 FText 的方式保持不变,临时值持有自己的引用。
- Lua API 保持不变,Lua Mod 不受影响。
弃用项
GetCharTArray()已弃用,但仍保留以兼容旧代码。它转发至GetCharArray(),将在未来版本移除。
合并属性头文件
变更内容:
单独的属性头文件(Unreal/Property/*.hpp 和 Unreal/FProperty.hpp)已弃用。几乎所有属性类型都已合并到 Unreal/CoreUObject/UObject/UnrealType.hpp。
变更前(v3.x):
#include <Unreal/FProperty.hpp>
#include <Unreal/Property/FArrayProperty.hpp>
#include <Unreal/Property/FObjectProperty.hpp>
#include <Unreal/Property/FStructProperty.hpp>
// etc.
变更后(v4.x):
// Single include for all property types
#include <Unreal/CoreUObject/UObject/UnrealType.hpp>
迁移步骤:
-
将下面任意与属性有关的包含项替换为
Unreal/CoreUObject/UObject/UnrealType.hpp:// Remove these deprecated includes: // #include <Unreal/FProperty.hpp> // #include <Unreal/Property/FArrayProperty.hpp> // #include <Unreal/Property/FBoolProperty.hpp> // #include <Unreal/Property/FClassProperty.hpp> // #include <Unreal/Property/FDelegateProperty.hpp> // #include <Unreal/Property/FInterfaceProperty.hpp> // #include <Unreal/Property/FLazyObjectProperty.hpp> // #include <Unreal/Property/FMapProperty.hpp> // #include <Unreal/Property/FMulticastDelegateProperty.hpp> // #include <Unreal/Property/FMulticastInlineDelegateProperty.hpp> // #include <Unreal/Property/FMulticastSparseDelegateProperty.hpp> // #include <Unreal/Property/FNameProperty.hpp> // #include <Unreal/Property/FNumericProperty.hpp> // #include <Unreal/Property/FObjectProperty.hpp> // #include <Unreal/Property/FSetProperty.hpp> // #include <Unreal/Property/FSoftClassProperty.hpp> // #include <Unreal/Property/FSoftObjectProperty.hpp> // #include <Unreal/Property/FStructProperty.hpp> // #include <Unreal/Property/FWeakObjectProperty.hpp> // #include <Unreal/Property/NumericPropertyTypes.hpp> // Replace with: #include <Unreal/CoreUObject/UObject/UnrealType.hpp> -
无需修改其他代码;所有属性类型仍使用相同名称和 API。
变更原因:
- 简化包含依赖。
- 减少编译时间。
- 与 Unreal Engine 自身的头文件组织一致;其几乎所有属性类型都定义在
UnrealType.h中。 - 消除包含顺序问题。
属性遍历 API 改进
变更内容:
原文称现有的 ForEachProperty() 和 ForEachPropertyInChain() 仍可使用且完全受支持,同时建议直接使用 Epic 的 TFieldIterator<> 和 TFieldRange<> 模式,使代码与 Unreal Engine 保持一致,并更清晰地控制遍历。
现有 API(仍受支持):
// Iterator-based iteration (still works)
for (FProperty* Property : MyStruct->ForEachProperty())
{
// Process property
}
for (FProperty* Property : MyStruct->ForEachPropertyInChain())
{
// Process property including super classes
}
推荐 API(v4.x 新增):
// TFieldIterator - Epic's pattern for child->parent iteration
#include <Unreal/UnrealType.hpp>
// Iterate properties in this class only (excludes super classes)
for (FProperty* Property : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::None))
{
// Process property
}
// Iterate all properties including super classes (child->parent order)
for (FProperty* Property : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::IncludeSuper))
{
// Process property including inheritance chain
}
// Include deprecated properties
for (FProperty* Property : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::IncludeDeprecated))
{
// Process all properties including deprecated ones
}
// Combined flags
for (FProperty* Property : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::IncludeSuper | EFieldIterationFlags::IncludeDeprecated))
{
// Process all properties in chain including deprecated
}
// Iterate in parent->child order (reverse)
for (FProperty* Property : TReverseFieldRange<FProperty>(MyStruct, EFieldIterationFlags::None))
{
// Process properties from parent to child
}
// You can also iterate other field types
for (UFunction* Function : TFieldRange<UFunction>(MyClass, EFieldIterationFlags::IncludeSuper))
{
// Process functions
}
上述示例依次展示:仅遍历本类属性;按子类到父类顺序包含继承属性;包含弃用属性;组合标记;按父类到子类反向遍历;遍历 UFunction 等其他字段类型。
EFieldIterationFlags 选项:
enum EFieldIterationFlags : uint8
{
None = 0, // Only iterate fields in this struct/class
IncludeSuper = 1 << 0, // Include inherited fields from super classes
IncludeDeprecated = 1 << 1, // Include deprecated properties
IncludeInterfaces = 1 << 2, // Include interface properties (classes only)
// Convenience aliases
Default = IncludeSuper, // Default Epic behavior
};
其中 None 只遍历当前结构体/类;IncludeSuper 包含父类;IncludeDeprecated 包含弃用属性;IncludeInterfaces 包含接口属性(仅用于类);Default 等于 IncludeSuper。
迁移示例:
-
简单属性遍历:
// Old (still works) for (FProperty* Prop : MyStruct->ForEachProperty()) { Output::send(STR("{}\n"), Prop->GetName()); } // New (recommended) for (FProperty* Prop : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::None)) { Output::send(STR("{}\n"), Prop->GetName()); } -
包含继承的遍历:
// Old (still works) for (FProperty* Prop : MyStruct->ForEachPropertyInChain()) { Output::send(STR("{}\n"), Prop->GetName()); } // New (recommended) for (FProperty* Prop : TFieldRange<FProperty>(MyStruct, EFieldIterationFlags::IncludeSuper)) { Output::send(STR("{}\n"), Prop->GetName()); } -
按父类到子类顺序遍历:
// Old (still works) for (FProperty* Prop : OrderedForEachPropertyInChain(MyStruct)) { Output::send(STR("{}\n"), Prop->GetName()); } // New (recommended) for (FProperty* Prop : TReverseFieldRange<FProperty>(MyStruct, EFieldIterationFlags::None)) { Output::send(STR("{}\n"), Prop->GetName()); } -
直接使用迭代器,获得更细致的控制:
// More control with explicit iterator for (TFieldIterator<FProperty> It(MyStruct, EFieldIterationFlags::IncludeSuper); It; ++It) { FProperty* Prop = *It; Output::send(STR("{}\n"), Prop->GetName()); }
采用新 API 的理由:
- 一致性: 与 Epic 的 UE4/UE5 代码模式完全一致。
- 灵活性: 细致控制是否包含父类、弃用属性和接口。
- 类型安全: 可遍历任何字段类型,包括
FProperty、UFunction、UEnum等。 - 熟悉度: 了解 Epic API 的开发者已经熟悉这种模式。
- 明确性: 调用处清楚表明遍历行为。
注意事项:
- 旧的
ForEachProperty()方法已弃用,将来可能移除。 - 使用
TFieldIterator/TFieldRange可以获得更好的性能,并与 Epic 的模式一致。
译注: 原文同时出现“仍完全受支持”和“已弃用”的表述,本译文保留这一区别。请优先采用推荐的新 API,并检查所用版本的头文件。
弃用项
ExecuteAsync 和 LoopAsync
变更内容:
ExecuteAsync 和 LoopAsync 已弃用,推荐使用新的延迟动作系统。旧函数虽然仍可运行,但缺少新系统提供的控制和安全特性。
迁移原因:
- 取消: 旧函数一旦启动就无法取消。
- 暂停/恢复: 无法暂停异步循环。
- 查询状态: 无法检查动作是否待执行,或还剩多少时间。
- Mod 隔离: 旧函数没有所有权跟踪。
- 线程安全: 新系统有更好的线程安全性,尤其是在与游戏对象交互时。
迁移对应关系:
| 旧函数 | 新函数 |
|---|---|
ExecuteAsync(delayMs, callback) |
ExecuteInGameThreadWithDelay(delayMs, callback) |
LoopAsync(delayMs, callback) |
LoopInGameThreadWithDelay(delayMs, callback) |
-- OLD (deprecated)
ExecuteAsync(1000, function()
print("After 1 second\n")
end)
LoopAsync(1000, function()
print("Every second\n")
return false -- return true to stop
end)
-- NEW (recommended)
ExecuteInGameThreadWithDelay(1000, function()
print("After 1 second\n")
end)
local loopHandle
loopHandle = LoopInGameThreadWithDelay(1000, function()
print("Every second\n")
if shouldStop then
CancelDelayedAction(loopHandle)
end
end)
迁移后的能力:
- 随时使用
CancelDelayedAction(handle)取消循环。 - 使用
PauseDelayedAction(handle)和UnpauseDelayedAction(handle)暂停/恢复。 - 使用
IsDelayedActionActive(handle)、GetDelayedActionTimeRemaining(handle)等查询状态。 - 使用
ClearAllDelayedActions()一次清理当前 Mod 的所有计时器。
新功能
延迟动作系统
新增内容:
Lua API 新增完整的计时器/延迟动作系统,提供 UE 风格的延迟执行,并可控制计时、暂停、取消和循环。这是 ExecuteAsync 和 LoopAsync 的推荐替代方案。
新增函数:
ExecuteInGameThreadWithDelay(delayMs, callback):延迟后执行回调,返回句柄。ExecuteInGameThreadWithDelay(handle, delayMs, callback):仅在句柄未激活时执行,类似 UE 的 Delay。RetriggerableExecuteInGameThreadWithDelay(handle, delayMs, callback):再次调用时重置计时器。LoopInGameThreadWithDelay(delayMs, callback):重复计时器。ExecuteInGameThreadAfterFrames(frames, callback):按帧延迟。LoopInGameThreadAfterFrames(frames, callback):按帧重复的计时器。MakeActionHandle():生成供延迟函数使用的唯一句柄。
计时器控制函数:
CancelDelayedAction(handle):取消待执行动作。PauseDelayedAction(handle):暂停计时器。UnpauseDelayedAction(handle):恢复已暂停的计时器。ResetDelayedActionTimer(handle):按原始延迟重新启动。SetDelayedActionTimer(handle, newDelayMs):修改延迟并重新启动。ClearAllDelayedActions():取消当前 Mod 的所有动作。
查询函数:
IsValidDelayedActionHandle(handle):检查句柄是否存在。IsDelayedActionActive(handle):检查计时器是否正在运行。IsDelayedActionPaused(handle):检查计时器是否暂停。GetDelayedActionRate(handle):获取配置的延迟。GetDelayedActionTimeRemaining(handle):获取剩余时间。GetDelayedActionTimeElapsed(handle):获取已流逝时间。
新增全局变量:
EngineTickAvailable:布尔值,表示 EngineTick 钩子是否可用。ProcessEventAvailable:布尔值,表示 ProcessEvent 钩子是否可用。
新增枚举:
EGameThreadMethod.ProcessEvent:使用 ProcessEvent 钩子。EGameThreadMethod.EngineTick:使用 EngineTick 钩子(每帧一次)。
ExecuteInGameThread 增强:
ExecuteInGameThread 现在接受可选的第二个参数,用于指定执行方式:
-- Default behavior (uses config setting)
ExecuteInGameThread(function() print("Hello\n") end)
-- Explicit method selection
ExecuteInGameThread(function() print("Hello\n") end, EGameThreadMethod.EngineTick)
ExecuteInGameThread(function() print("Hello\n") end, EGameThreadMethod.ProcessEvent)
默认调用使用配置设置;也可显式选择 EngineTick 或 ProcessEvent。
使用示例:
-- Simple delay
local handle = ExecuteInGameThreadWithDelay(1000, function()
print("Fired after 1 second\n")
end)
-- Retriggerable timer (resets each call)
local debounceHandle = MakeActionHandle()
RegisterKeyBind(Key.F, function()
RetriggerableExecuteInGameThreadWithDelay(debounceHandle, 500, function()
print("Debounced action\n")
end)
end)
-- Self-cancelling loop
local loopHandle
loopHandle = LoopInGameThreadWithDelay(1000, function()
print("Tick!\n")
if someCondition then
CancelDelayedAction(loopHandle)
end
end)
示例依次展示简单延迟、每次调用都重置的可重触发计时器,以及能够自行取消的循环。
注意事项:
-
基于句柄的所有权可以防止 Mod 互相干扰对方的计时器。
-
ClearAllDelayedActions()只清理调用它的 Mod 所拥有的动作。 -
按帧延迟需要 EngineTick 钩子,可检查
EngineTickAvailable。 -
在闭包中捕获循环句柄时,应先声明变量再赋值:
local loopHandle -- Declare first loopHandle = LoopInGameThreadWithDelay(1000, function() CancelDelayedAction(loopHandle) -- Now correctly captured end)
报告迁移问题
升级遇到问题时:
- 检查未关闭的 Issue 是否已有类似报告。
- 如果尚无对应报告,创建新的升级问题 Issue。
原文:docs/upgrade-guide.md · 固定提交 560411f18512 · 来源与 MIT 许可