UE_Angelscript 源码篇 1:UE 的类绑定到 AS 的流程#

UE-Angelscript 是 UE 的一个分支,内容主要包括两个方面:

  1. 对 Unreal Engine 源码的修改;
  2. UE-Angelscript 插件本身的实现。

本文侧重于 UE-Angelscript 插件本身的框架逻辑和关键信息。

以问题为导向,本篇回答的问题是:UE 的类是如何绑定到 Angelscript 的?

当然,本文限于篇幅,不能说明所有细节,读者就当看个大体架构吧。

或有理解错误,还望不吝赐教。

阅读条件——需有 C++ 、 UE 、Angelscript 的相关知识。

前言#

对 UE 源码的修改#

为了与 UE 一体化交互,HAZE 对 UE 源码修改了确实不少。

所有修改通常由如下注释包裹,便于快速定位: // HAZE FIX // END HAZE FIX 可通过全局搜索定位修改区域。

此部分内容零零碎碎分散在各处,本文就不对其进行分析了。

Angelscript 插件本身#

UE-Angelscript 插件的核心内容在引擎目录下的 Plugins/Angelscript 里。 这里提一下,另外的插件 AngelscriptEnhancedInput、AngelscriptGAS、AngelscriptStructUtils 都是专门针对特定模块的绑定,不属于核心内容。

插件的模块结构#

从目录结构和构建脚本来看,插件主要分为以下几个部分,其中 AngelscriptCode 是运行时逻辑的核心模块,AngelscriptEditor 提供编辑器扩展,AngelscriptLoader 负责启动流程及模块间协作:

非编辑器模式
AngelscriptLoader ------> AngelscriptCode -------> AngelScript (3rd)

编辑器模式
AngelscriptLoader ------> AngelscriptEditor ------> AngelscriptCode -------> AngelScript (3rd)

简述 Angelscript 的引入#

Angelscript 是一个脚本语言,一般作为 C++ 的脚本辅助开发,性能很好,语法也类似 C++。

这门语言与 C++ 的交互十分方便,提供了简单易用的 API 和详细的可配置参数。在 C++ 端操作时,一般只需要 Include 一个头文件即可,即 angelscript.h 。

引入 angelscript 之后,UE-Angelscript 插件在不同的地方都对 angelscript 原始的一些方法和类进行了封装,这是为了更好地链接 Cpp 与 Angelscript 之间的代码(实际上是为了少写点重复代码)。

总体而言,Angelscript 提供了轻量、高性能的脚本扩展能力,适合嵌入到以 C++ 为核心的引擎中,从使用来看,UE- Angelscript 的集成确实高效且舒适。

UE 的类绑定到 AS#

先上一张完整的简化流程图。

以下为从 UE 启动到类型绑定的主要调用流程(简化版):

+-----------------------------+
| UE 启动/插件被加载          |
+-----------------------------+
              |
              v
+-----------------------------+
| AngelscriptLoader 模块是入口 |
| void FAngelscriptLoaderModule::StartupModule() |
+-----------------------------+
              |
              v
+-----------------------------+
| AngelscriptCode 模块初始化不做其他事,有一批可被调用的函数,其中有独立的初始化函数 |
| FAngelscriptCodeModule::InitializeAngelscript() |
+-----------------------------+
              |
              v
+-----------------------------+
| 真正的初始化开始于此 |
| FAngelscriptManager::Initialize()  |
| 分三个流程为 |
| PreInitialize_GameThread、Initialize_AnyThread、PostInitialize_GameThread |
+-----------------------------+
              |
              v
+-----------------------------+
| 其中此函数包含有绑定所有类型的部分 |
| FAngelscriptManager::Initialize_AnyThread() |
+-----------------------------+
              |
              v
+-----------------------------+
| FAngelscriptManager::BindScriptTypes() |
+-----------------------------+
              |
              v
+-----------------------------+
| 绑定所有 FBinds |
| void FAngelscriptBinds::CallBinds()  |
+-----------------------------+
              |
              v
+-----------------------------+
| 按照 FBinds 的顺序,调用绑定的 Lambda|
|   GetBindArray().Sort();
	for (auto& Function : GetBindArray())
	    Function.Function();   |
+-----------------------------+

void FAngelscriptManager::PreInitialize_GameThread() 这个函数会创建两个UPackage:/Script/Angelscript 和 /Script/AngelscriptAssets,AS 代码的内容,就在 /Script/Angelscript 的 UPackage 里。PreInitialize阶段还会读取 UAngelscriptSettings 的用户设置,设置 Angelscript 虚拟机。

void FAngelscriptManager::Initialize_AnyThread() 这个函数是核心,本文分析的主要就是这个函数里包含的内容了。这里提一下的是,这个函数可以运行在非游戏线程。

void FAngelscriptManager::PostInitialize_GameThread() 只是提供了一个委托的回调,让外部知道初始化完成。

说明:UE 的类,都是通过 FBinds 的 Lambda 来绑定到 AS 的#

Angelscript 插件通过 FBinds 构造器机制来绑定代码,每个绑定以 Lambda 的形式定义。

GetBindArray() 返回的单例会保存这所有的 Lambda,并且这些 Lambda 拥有顺序,会按照顺序来逐一运行 Lambda。

示例的 FBind 如下。

AS_FORCE_LINK const FAngelscriptBinds::FBind Bind_AActor_Base((int32)FAngelscriptBinds::EOrder::Late-1, []
{
    auto AActor_ = FAngelscriptBinds::ExistingClass("AActor");
    // 略
});

这个变量每次初始化的时候,就会将内容注册到 GetBindArray() 里的单例了,所以,我们如果想将 Cpp 里的类型绑定到 AS ,只需要增加新的 FBind 变量即可。

struct ANGELSCRIPTCODE_API FBind
{
    FBind(int32 BindOrder, TFunction<void()> Function)
    {
       FAngelscriptBinds::RegisterBinds(BindOrder, Function);
    }

    FBind(EOrder BindOrder, TFunction<void()> Function)
    {
       FAngelscriptBinds::RegisterBinds((int32)BindOrder, Function);
    }

    FBind(TFunction<void()> Function)
    {
       FAngelscriptBinds::RegisterBinds(0, Function);
    }
};

struct FBindFunction
{
    int32 BindOrder;
    TFunction<void()> Function;

    bool operator<(const FBindFunction& Other) const
    {
       return BindOrder < Other.BindOrder;
    }

};
// ……

出处:AngelscriptBinds.h: FAngelscriptBinds::FBind(节选,其余行省略)。

问题:UE 那么多类,如何批量处理?#

UE 里需要给脚本使用的类,除了基础类型外,主要是进入反射系统的 UObject、UScriptStruct、UEnum、Delegate 这几种类型了。

其中,量最大的是 UObject、UScriptStruct。

这两个类型,AS 插件进行了批量化的处理。

刚才提到过,所有的绑定,都是通过 FBinds 的 Lambda 实现的。

绑定 UObject 的 FBinds 是这两个变量。

Early 阶段绑定 UObject#

AS_FORCE_LINK const FAngelscriptBinds::FBind Bind_BlueprintType_Declarations(FAngelscriptBinds::EOrder::Early, []
{
    // Bind any clasess that are BlueprintType
    for (UClass* Class : TObjectRange<UClass>())
    {
       if (!ShouldBindEngineType(Class))
          continue;

       // Bind into angelscript engine
       FString ClassName = FAngelscriptType::GetBoundClassName(Class);
       BindUClass(Class, ClassName);
    }

    BindUClassLookup();
});

其中调用的 BindUClass 函数,是主要的绑定函数。绑定好的类型,都会将信息保存在 FAngelscriptTypeDatabase 的单例中。

对于 UClass 的类型,这里特意将 angelscript 的类型和 UClass 关联起来,这样的好处是,AS 里不必区分 StaticClass 和类本身了。

拓展:此处利用了 plainUserData  —— AngelScript 引擎中为  每个类型(asCTypeInfo)结构体上挂载的一个用户自定义指针字段,你可以把它理解成: “给这个脚本类型附加一个自定义的 C++ 指针,以便在 C++ 层可以反查或传参时用到”。

static void BindUClass(UClass* Class, const FString& TypeName)
{
    // Bind into angelscript engine
    auto Class_ = FAngelscriptBinds::ReferenceClass(TypeName, Class);

    // Register angelscript type
    auto Type = MakeShared<FUObjectType>(Class, TypeName, Class_.GetTypeInfo());
    FAngelscriptType::Register(Type);

    // Tell the angelscript type what UClass is associated with it
    auto* TypeInfo = (asCTypeInfo*)Class_.GetTypeInfo();
    if (TypeInfo != nullptr)
    {
       TypeInfo->plainUserData = (SIZE_T)Class;

#if WITH_EDITOR
       const FString& Tooltip = Class->GetMetaData(NAME_Func_Tooltip);
       if (Tooltip.Len() != 0)
          FAngelscriptDocs::AddUnrealDocumentationForType(TypeInfo->GetTypeId(), Tooltip);

       if (IsEditorOnlyClass(Class))
          TypeInfo->flags |= asOBJ_EDITOR_ONLY;

       if (ShouldDisallowInstantiation(Class))
          TypeInfo->flags |= asOBJ_DISALLOW_INSTANTIATION;
#endif
    }
}

这里再深入一层,提一提 FAngelscriptBinds::ReferenceClass 这个绑定 UObject 的专用函数。

FAngelscriptBinds FAngelscriptBinds::ReferenceClass(FBindString Name, UClass* UnrealClass)
{
    auto Binds = FAngelscriptBinds(Name, asOBJ_REF | asOBJ_NOCOUNT | asOBJ_IMPLICIT_HANDLE, 0);
    ((asCObjectType*)Binds.ScriptType)->size = UnrealClass->GetStructureSize();
    Binds.ScriptType->alignment = UnrealClass->GetMinAlignment();
    return Binds;
}

asOBJ_REF: 表示这是引用类型(UObject\*) asOBJ_NOCOUNT: 不做引用计数(UE 自己管理生命周期) ✅ asOBJ_IMPLICIT_HANDLE: 允许脚本使用时省略 @ (@在 AS 里是句柄的意思)

AS 的 UObject 实际上是 UObject@ ,对应于 C++ 里的 UObject\* ,所以 AS 的 UObject 可以为 nullptr。

BindUClassLookup 也是得提一提的,它是 AS 识别一些关于 UObjct 模板的关键。

Unreal Engine 常用的 UObject 指针相关模板类型,如 TSubclassOf 、TObjectPtr 、TWeakObjectPtr 、裸指针等,都可以在一个回调函数里处理,处理好之后将相关的信息存入 FAngelscriptTypeUsage 。

具体如下: 注册一个自定义类型查找器(RegisterTypeFinder),使 Angelscript 能够根据 Unreal 属性的具体类型动态推断绑定类型——是哪个模板类型,或者是裸指针;

FAngelscriptTypeDatabase 里 TArray<FAngelscriptType::FTypeFinder> TypeFinders 会保存回调函数的信息,这是给后续解释的时候调用的。

static void BindUClassLookup()
{
    // Register the type used by TSubclassOf
    auto SubclassOfType = MakeShared<FSubclassOfType>();
    FAngelscriptType::Register(SubclassOfType);

    // Register a type that handles script object types generically
    auto ScriptObjectType = MakeShared<FUObjectType>(nullptr, TEXT("UObject"));
    FAngelscriptType::SetScriptObject(ScriptObjectType);

    // Register the type used by TObjectPtr
    auto ObjectPtrType = MakeShared<FObjectPtrType>();
    FAngelscriptType::Register(ObjectPtrType);

    // Register the type used by TWeakObjectPtr
    auto WeakObjectPtrType = MakeShared<FWeakObjectPtrType>();
    FAngelscriptType::Register(WeakObjectPtrType);

    // Register a type finder into the type system that
    // can look up an ObjectProperty's inner angelscript type.
    FAngelscriptType::RegisterTypeFinder([=](FProperty* Property, FAngelscriptTypeUsage& Usage) -> bool
    {
    // 略
    });
}

BindUClassLookup 比较难理解,它的作用再小结一下,就是注册一批特殊的模板包装类型(如 TSubclassOf、TWeakObjectPtr),并设置 AngelScript 在解析时能够正确识别这些包装器内部的 UObject 类型。

Late 阶段绑定 UObject 成员和成员函数#

之前完成了初始的 UObject 绑定,但是 UObject 里还有很多成员和成员函数,也是需要批量来处理的,这个被放到了更迟一些的绑定顺序了,为什么呢?因为这些成员和成员函数,又不是 UObject 的类型,这些还需要先行绑定了才能让 AS 找到。

排序的作用就是在于此了,一般都是在 Early 阶段绑定类型,之后在 Late 阶段绑定类型的成员和成员函数。

写在 Bind_BlueprintType.cpp

AS_FORCE_LINK const FAngelscriptBinds::FBind Bind_Defaults((int32)FAngelscriptBinds::EOrder::Late+100,
// 略

这个绑定十分的长,因为要处理各种各样的情况,这是大致的绑定流程。

遍历所有UClass
   │
   ├─► 递归收集父类,保证继承顺序
   │
   ├─► 绑定所有 BlueprintType/BlueprintCallable/ScriptCallable 的函数
   │
   ├─► 绑定 BlueprintGetter/Setter
   │
   ├─► 绑定所有蓝图可见/可编辑属性
   │
   ├─► 处理继承,复制父类属性/方法
   │
   ├─► 绑定 StaticClass 和全局变量
   │
   └─► 写入绑定数据库

绑定 UScriptStruct 及其成员#

UScriptStruct 的绑定相对简单一些,因为没有成员函数能被反射系统识别的。

绑定的代码主要写在 Bind_UStruct.cpp ,绑定的写法类似于 UObject 。不再详细说那么多啦。

AS_FORCE_LINK const FAngelscriptBinds::FBind Bind_StructDeclarations((int32)FAngelscriptBinds::EOrder::Early+1, []
{
    HardCodeCallingMetaForUnrealStructs();

    for (UScriptStruct* Struct : TObjectRange<UScriptStruct>())
    {
       if (!ShouldBindEngineType(Struct))
          continue;

       FString TypeName = Struct->GetStructCPPName();

       // Bind into angelscript engine
       FBindFlags BindFlags;
       if (Struct->StructFlags & STRUCT_IsPlainOldData)
          BindFlags.ExtraFlags |= asOBJ_POD;
       BindStructType(TypeName, Struct, BindFlags);
    }

    BindStructTypeLookups();
});

不过还是得提一下 UObject 的不同之处! UScriptStruct 被绑定为了值类型

static void BindStructType(const FString& TypeName, UScriptStruct* Struct, FBindFlags& BindFlags)
{
    auto Binds = FAngelscriptBinds::ValueClass(TypeName, Struct, BindFlags);

    // Register angelscript type
    auto Type = MakeShared<FUStructType>(Struct, TypeName);
    Type->ScriptTypeInfo = Binds.GetTypeInfo();
    Type->ScriptTypeInfo->SetUserData(Struct);
    FAngelscriptType::Register(Type);
}

关于值类型的绑定函数,原代码如下,关键的是—— asOBJ_VALUE | asOBJ_APP_CLASS 表示“这是一个 C++ 定义的 struct/class,它是值类型”

static FAngelscriptBinds ValueClass(FBindString Name, UScriptStruct* StructType, FBindFlags Flags)
{
    int Size;
    auto* Ops = StructType->GetCppStructOps();
    if (Ops != nullptr)
       Size = Ops->GetSize();
    else
       Size = StructType->GetPropertiesSize();
    if (Flags.Alignment == -1)
       Flags.Alignment = StructType->GetMinAlignment();
    return ValueClass(Name, Flags, Size);
}

static FAngelscriptBinds ValueClass(FBindString Name, UScriptStruct* StructType, FBindFlags Flags)
{
    int Size;
    auto* Ops = StructType->GetCppStructOps();
    if (Ops != nullptr)
       Size = Ops->GetSize();
    else
       Size = StructType->GetPropertiesSize();
    if (Flags.Alignment == -1)
       Flags.Alignment = StructType->GetMinAlignment();
    return ValueClass(Name, Flags, Size);
}

绑定为值类型(举例FVector),可以直接声明一个变量,然后修改其中的成员,简单方便,无需工厂函数就直接生成实例。

FVector v(1, 2, 3);
v *= 2;

绑定为引用类型(举例UObject),如果直接声明一个变量,其会是 Nullptr,要生成实例,必须使用 NewObject 等工厂函数或者从别处获取一个实例。

UObject obj = GetSomeObject();
Print(obj.GetName());

BindStructTypeLookups 的函数简单不少,因为没有给 USciptStruct 设定模板类。

绑定其他内容#

UEnum 在 Bind_UEnum.cpp

Delegate 在 Bind_Delegates.cpp

还有一些特殊的类型,TMap、TArray、TSet、TOptional 等模板类型,是有专门处理的。

另外,常用的 FVector \ FTransform 等,好多都是得手工单独处理,这些会有单独的绑定文件。

还有一些全局函数,也是单独写的,比如 NewObject, SpawnActor 等。


未完待续

×

联系

问题反馈

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

合作

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

×

关于

服务条款

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

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

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

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

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

更新于 2026-10-24

隐私政策

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

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

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

更新于 2026-10-24

目录

页面列表

本页目录