UE_Angelscript 实践篇——关于类成员的垃圾回收与反射问题#

UObject 的垃圾回收与反射本来在 C++ 里已经足够复杂了,Angelscript 对其进行了一些简化处理,然而,真正深究起来,发现这个改变是合理的,但是理解难度会增加一点点了。

先说一个 Angelscript 里值得重视的结论:因为 Angelscript 默认将类成员中的 UObject 类型及TArray | TMap | TSet 类型里的 UObject都放进了 GC,所以 TWeakObjectPtr 应该值得被推广使用,可以进行很多解耦操作,因为它不阻止 GC,防止引用链过度膨胀。

本文更侧重于垃圾回收。

垃圾回收的基础概念#

垃圾回收,即 GC,是 UE 里单独管理 UObject 的生命周期的系统,只有 UObject 被纳入管理中,平常我们 NewObject 之后,其实可以不用去管理它,GC 系统会自动扫描判断它还是不是被使用,不被使用就清理掉。

但是 GC 系统只能看到标记给它看的内容,所以 C++ 里经常会有 UObject 作为类成员,然后标记上 UPROPERTY() ,仅仅是为了让它可以被 GC 看到。

UPROPERTY() 这个标记,对于 UObject 类成员来说,有两种意义,一个是纳入 GC 系统,另外一个是纳入反射系统。

C++ 没有对 UPROPERTY() 关于 UObject 类成员的两种功能进行拆分,但是 Angelscript 进行拆分了,struct | class 里的 UObject 类成员以及 TArray | TMap | TSet 类成员里的 UObject,都纳入了 GC 系统,无论其是否标记 UPROPERTY,然后 Angelscript 的 UPROPERTY() 就仅仅作为暴露给反射系统及蓝图的标记。

有趣的事实#

关于反射系统#

struct 的成员都默认加入反射系统,而 class 不是。

AS 里的 class 与 struct 不一样的一个点,struct 默认都给加入反射系统了,通过 PropertyLink 的反射遍历可以随意访问,而 class 却没有这个默认行为。原因是 class 没必要也不能默认加入,不只是为了减少一点开销,而是确实有些类型无法加入反射系统,所以,需要反射的时候再自己手动加 UPROPERTY();而 struct 是被当作数据持有类型的简单容器,就都纳入反射系统了,不能纳入的类型是不可以作为类成员的。

虽然 struct 默认都给加入反射系统了,但是加入了反射系统不代表就暴露给蓝图访问,如果想要蓝图能访问到,还是得加上 UPROPERTY() 标记。

// Angelscript 代码
struct FStructTest
{
	UObject Obj; // 这实际上是 UPROPERTY,反射可以访问到
}
// Angelscript 代码
class UTestObj
{
	UObject Obj; // 这就是一个普通的指针,反射无法被访问到
}

关于 GC 系统#

struct 和 class 都默认将 UObject 类成员和 TArray | TMap | TSet 三大容器里的 UObject 加入 GC。

这种行为的变化,实际上是利用代码解释器来做到的,其加入的代码在:

void FAngelscriptClassGenerator::DetectAngelscriptReferences(TSharedPtr<FAngelscriptClassDesc> ClassDesc)
 {
	 // skip ...
	if (!bAddedAsUnrealProperty)
	{
	    if(PropertyType.HasReferences())
	    {
	       RefParams.AtOffset = PropertyOffset;
	       RefParams.Names.Push(Name);
	       PropertyType.EmitReferenceInfo(RefParams);
	       RefParams.Names.Pop();
	    }
	}
}

所以,总结之后,对 UObject 成员类型的各种情况的精确表述是

// Angelscript 中的真实情况
class UTestObj : public UObject
{
    // 情况1:受GC保护(通过UPROPERTY)
    UPROPERTY()
    UObject ProtectedObj;  // GC会追踪此引用,对象不会被意外回收

    // 情况2:受GC保护(通过UPROPERTY)
    UPROPERTY()
    TObjectPtr<UObject> ProtectedObj;

    // 情况3:受GC保护(即使没有UPROPERTY,与C++不同!)
    UObject UnprotectedObj;  // 没有UPROPERTY,但GC仍会追踪此引用

    // 情况4:安全的弱引用
    TWeakObjectPtr<UObject> WeakObj;  // 不会阻止GC,但可安全检查
};

啊,没错,受 GC 保护 有三种写法,而不受 GC 保护的写法通常只有一种。(暂时忽略掉其他可能存在的相关封装类型。)

还有一点值得提的是,不管是,上述情况 1 到 3,都是用的时候最好要判空的,因为虽然理论上受到保护,一般不会被清理,但是万一有地方写了强制回收的代码呢,比如 有哪些地方写了 MarkAsGarbage(),那么就会导致悬垂指针的出现。

不阻止 GC —— TWeakObjectPtr 的使用#

封装 TWeakObjectPtr#

要方便的使用 TWeakObjectPtr,可以考虑封装一层来用,这首先源于一个脚本自身的问题。

AS 里存在一个问题,TWeakObjectPtr<UObject> 的使用,那是一直报错啊,它是最基础的类别了,但是居然函数调用出错,而子类可以正常使用。(也许以后会修好吧。)

	{
		TWeakObjectPtr<UObject> WOP;
		// 注:此问题仅出现在UObject基类,具体子类如AActor则正常
		// 报错:No matching signatures to 'TWeakObjectPtr::Get()'
		auto Obj = WOP.Get();
	}

	{
		TWeakObjectPtr<AActor> WOP;
		// no error
		auto Obj = WOP.Get();
	}

那么怎么办,暂时理不清插件绑定源码里出现了什么问题, 就干脆自己写一个包装类好了,让其可以无缝地与 TWeakObjectPtr<T> 类型交互。

    TWeakObjectPtr<UObject> WOP;
    FWeakObjectPtrWrapper Wrapper = WOP;
    Wrapper.Get();

这种包装类在 cpp 里写十分简单

// Make TWeakObjectPtr<UObject> useful
USTRUCT(BlueprintType)
struct FWeakObjectPtrWrapper
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TWeakObjectPtr<UObject> WeakObjPtr = nullptr;

然后给它附加一模一样的接口就可以了。(本专栏里的其他文章已经有详细介绍,这里不再赘述。)

TArray 嵌套与自动推断#

有个不错的建议是,顺带也把它的 TArray 形式也写了。这个在 Angelscript 也可以写,但是在 Cpp 写有好处的,特别是自动推断的便利。

// Make TArray<FWeakObjectPtrWrapper> useful
USTRUCT(BlueprintType)
struct FWeakObjectPtrWrapperArray
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<FWeakObjectPtrWrapper> WeakObjPtrArray;
};

为什么呢?因为 Angelscript 里不允许 TArray 里套 TArray,为以后省事。

TArray 等容器时不时遇到推断的问题,每次 Cast 是很烦的,可以在 Cpp 写点自动推断的函数来辅助,这个技巧很好用,省掉很多类型转换的代码。 例如:

// Get all the valid Typed UObjects to an array
UFUNCTION(ScriptCallable, Meta = (DeterminesOutputType = "TypedClass", ScriptMethod))
static TArray<UObject*> GetTypedArray(const FWeakObjectPtrWrapperArray& PtrWrapperArray, const TSubclassOf<UObject>& TypedClass)
{
    TArray<UObject*> ReturnArray;
    for (auto& PtrWrapper: PtrWrapperArray.WeakObjPtrArray)
    {
       if (PtrWrapper.WeakObjPtr.IsValid() && PtrWrapper.WeakObjPtr->IsA(TypedClass))
       {
          ReturnArray.Add(PtrWrapper.WeakObjPtr.Get());
       }
    }
    return ReturnArray;
}

总结#

Angelscript 默认加反射、加 GC 的行为是好的,减少了很多代码负担,但是这也会造成 C++ 与 Angelscript 之间表达的不一致,不过总体利大于弊。

struct 默认加反射不会影响游戏逻辑本身,反而增加了很多好处,利远远大于弊。 struct 和 class 默认给 UObject 加 GC,可能会导致引用链的增加,需要注意清理引用(如果忘记清理,一般只是增加内存消耗,对游戏逻辑影响比较少),所以不阻止 GC 反而成为了需要特别重视的情况,也就是 TWeakObjectPtr 得用得更多一些,总体利大于弊。


×

联系

问题反馈

问题与反馈请提交至 GitHub Issues。

合作

合作事宜请发送邮件至 contact@cuviss.com。

×

关于

服务条款

各插件的许可证见社区贡献页。

网站内容可在注明来源后引用。

软件和文档按“现状”提供,不作任何保证。

Unreal Engine 等第三方项目及名称相关商标归各自所有者所有。soloAS 与 Epic Games、Hazelight 均无关联。

致谢和案例展示请求均不具约束力,详见首页说明。

更新于 2026-10-24

隐私政策

本网站不使用分析工具、Cookie 或跟踪,也没有数据表单或第三方脚本。

网站托管于 GitHub Pages。GitHub 可能按其隐私声明处理标准访问日志。

通过邮件发送的内容会提供给我们;通过 GitHub Issues 发送的内容会提供给 GitHub。

更新于 2026-10-24

目录

页面列表

本页目录