RE-UE4SS 升级指南

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);

迁移步骤:

  1. 如果依赖旧行为,即名称不存在时构造函数返回 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
    
  2. 如果在构造后检查名称是否有效,需要使用 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
    }
    
  3. 已经显式指定 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;
};

迁移步骤:

  1. 原先通过 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
    
  2. 对 TObjectPtr 内容取地址的操作,原文给出的迁移示例如下:

    // Old
    &ObjectPtr.UnderlyingObjectPointer
    
    // New
    &ObjectPtr.Get()
    // or simply
    &ObjectPtr
    

译注: 上面的 &ObjectPtr.Get() 为原文示例。按本页展示的 Get() 按值返回指针的实现,不能把这一写法直接当作通用可编译代码;请结合所用头文件和实际取地址目的检查。

  1. 指针转整数(例如格式化地址)时,通过 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(""); }

迁移步骤:

  1. 获取字符指针(原先使用 GetCharArray())时,改用解引用运算符:

    // Old
    const TCHAR* ptr = myString.GetCharArray();
    
    // New
    const TCHAR* ptr = *myString;
    
  2. 获取数组(原先使用 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();

迁移步骤:

  1. 将 Path.AssetPathName 替换为 Path.GetAssetPathName()。
  2. 读取 Path.SubPathString 时改用 Path.GetSubPathString(),写入时改用 Path.SetSubPathString()。GetSubPathString() 返回副本,修改返回的字符串不会修改路径。
  3. 如果需要分别取得包名和资产名,优先使用 GetAssetPath()。它在所有引擎版本中都返回 FTopLevelAssetPath;在 5.1 之前的版本中,通过拆分路径合成该值。

注意事项:

  • 在 5.1 及更新版本的引擎中,内存中不存在单个 FName 形式的表示,因此 GetAssetPathName() 会按需使用 FNAME_Add 创建组合的 /Package/Path.Asset FName。
  • 写入资产路径仍使用 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);

迁移步骤:

  1. 将直接访问 ObjectID 改为 GetUniqueID()。const 和非 const 重载均返回引用。
  2. 读取标记时用 GetTagAtLastTest(),写入时用 SetTagAtLastTest(value)。
  3. 替换路径时优先使用 SoftPtr = NewPath,这也会重置缓存的弱指针。若通过非 const 的 GetUniqueID() 引用修改路径,之后应调用 ResetWeakPtr()。
  4. 手动遍历引擎拥有的内存时,使用 FSoftObjectPtr::StaticSize()、TSoftObjectPtr<T>::StaticSize() 或 TSoftClassPtr<T>::StaticSize(),不要使用 sizeof 或硬编码步长。sizeof 包含最大受支持布局所需的存储,对本地分配的 C++ 对象仍应使用它。
  5. 使用更新后的 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();

迁移步骤:

  1. 将 Text.Data 替换为 Text.GetTextData()。
  2. 将 Text.Flags 替换为 Text.GetFlags()。
  3. 移除所有 SharedRefCollector 和 Unk 的使用;它们是对布局进行推测时留下的产物。引用控制器现由引擎管理。
  4. 构造、复制、赋值或转换 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>

迁移步骤:

  1. 将下面任意与属性有关的包含项替换为 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>
    
  2. 无需修改其他代码;所有属性类型仍使用相同名称和 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。

迁移示例:

  1. 简单属性遍历:

    // 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());
    }
    
  2. 包含继承的遍历:

    // 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());
    }
    
  3. 按父类到子类顺序遍历:

    // 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());
    }
    
  4. 直接使用迭代器,获得更细致的控制:

    // 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)
    

报告迁移问题

升级遇到问题时:

  1. 检查未关闭的 Issue 是否已有类似报告。
  2. 如果尚无对应报告,创建新的升级问题 Issue。

原文:docs/upgrade-guide.md · 固定提交 560411f18512 · 来源与 MIT 许可