乐于分享
好东西不私藏

AngelScript 插件深度解析:实现原理与泛型函数编译时检查

AngelScript 插件深度解析:实现原理与泛型函数编译时检查

本文通过一个完整 .as 示例贯穿全文,在「追踪示例」的过程中自然引出 AngelScript 的完整流水线(初始化 → 绑定 → 扫描 → 预处理 → 四阶段编译 → 类生成 → 运行时),讲清其实现原理;在此基础上,讲解如何实现自定义泛型函数参数匹配合法性检查,使 AddUFunction、BindUFunction、BindToVM 等字符串指定函数的调用在编译阶段即可完成参数个数、类型、输入/输出匹配的校验。


一、引言

AngelScript 插件让 Unreal Engine 支持使用 .as 脚本扩展游戏逻辑。脚本中的类继承自 C++ 类型(如 AActor),使用 UPROPERTY、UFUNCTION、delegate、event 等与蓝图无缝对接。理解从脚本文件到运行时调用的完整流水线,是扩展插件功能(如自定义编译时检查)的前提。

本文先用示例追踪 AngelScript 的整个实现原理,再在此基础上说明如何为泛型函数(接收字符串函数名的 C++ 导出函数)实现自定义参数匹配合法性检查。


二、示例脚本与预期行为速查

以下示例将贯穿全文,用于追踪流水线的每个阶段。

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineevent void FMyEventSignature(UObject Object, float Value);// AS 枚举:预处理 DetectEnum 解析,Stage1 注册到引擎,Stage3 生成 UEnumUENUM()enum EMyScriptState{    Idle,    Running,    Finished};UCLASS()class AMyScriptActor : AActor{    // 有 UPROPERTY:生成 FProperty,蓝图/序列化可见    UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)    int32 ExposedCounter = 0;    // BlueprintSetter/Getter 示例(用于错误检查章节)    UPROPERTY(BlueprintSetter=SetMyValue, BlueprintGetter=GetMyValue)    int32 MyValue = 0;    UPROPERTY()    FMyEventSignature OnSomethingHappened;    // FGameplayTag:C++ 在 BindScriptTypes 中通过 Bind_FGameplayTag 导出,AS 中可用 RequestGameplayTag、MatchesTag 等    UPROPERTY(EditDefaultsOnly)    FGameplayTag StateTag;    // AS 枚举属性    UPROPERTY(EditDefaultsOnly)    EMyScriptState ScriptState = EMyScriptState::Idle;    // 无 UPROPERTY:仅脚本内,无 FProperty    float InternalTimer = 0.f;    UFUNCTION(BlueprintCallable)    void PublicDoSomething() { InternalDoWork(); }    UFUNCTION(BlueprintEvent)    void OnCustomEvent(int32 Value) {}    UFUNCTION(BlueprintPure)    int32 GetMyValue() const { return MyValue; }    UFUNCTION()    void SetMyValue(int32 Val) { MyValue = Val; }    UFUNCTION()    void ExampleHandler(UObject Obj, float Value) { /* ... */ }    void InternalDoWork() { ExposedCounter++; }    void Tick() { }}
成员
谁可见
谁可调用
序列化/复制
ExposedCounter
蓝图、编辑器
脚本、蓝图
MyValue
蓝图(通过 Getter/Setter)
同上
OnSomethingHappened
蓝图(可绑定)
AddUFunction、Broadcast
StateTag (FGameplayTag)
编辑器
RequestGameplayTag、MatchesTag 等
ScriptState (EMyScriptState)
编辑器
枚举常量 Idle/Running/Finished
InternalTimer
仅脚本
仅脚本
PublicDoSomething
蓝图
蓝图、脚本
InternalDoWork
仅脚本
仅脚本
GetMyValue/SetMyValue
蓝图
蓝图
ExampleHandler
AddUFunction 绑定目标
委托触发

流水线总览

流水线顺序(从左到右):Loader 启动 → Manager 初始化 → BindScriptTypes → FindAllScriptFilenames → Preprocessor → ParseIntoChunks → ProcessPropertyMacro / ProcessFunctionMacro / ProcessDelegates → CompileModules → Stage1 注册类型 → Stage2 编译函数体 → Stage3 生成 UClass/UFunction/FProperty → Stage4 全局/静态 → 运行时 ProcessEvent / AngelscriptCallFromBPVM


三、示例追踪(一):从加载到 InitialCompile

3.1 流水线入口

引擎启动时,AngelscriptLoader 模块加载,调用 FAngelscriptCodeModule::InitializeAngelscript()AngelscriptLoaderModule.cpp)。随后进入 FAngelscriptManager::Initialize_AnyThread(),完成:

  1. AngelScript 引擎创建asCreateScriptEngine()
  2. BindScriptTypes:将所有 C++ 类型(类、结构体、枚举)注册到 AngelScript
  3. InitialCompile:首次完整编译所有脚本

3.2 BindScriptTypes:C++ 类型导出

BindScriptTypes() 内部调用 FAngelscriptBinds::CallBinds(),按顺序执行各个 FBind 注册函数。示例中涉及的 C++ 类型在此时完成导出:

  • AActor、UObject:通过 Bind_UObjectBind_BlueprintType 等,RegisterObjectType 注册为 asOBJ_REF
  • int32、float:通过 Bind_Primitives
  • C++ 枚举(如 ECollisionChannel):通过 Bind_UEnum / Bind_EnumsFAngelscriptBinds::Enum(Name) → RegisterEnum(type) + RegisterEnumValue(typeName, valueName, value)FAngelscriptType::Register(MakeShared<FEnumType>(Enum)) 注册类型系统;AS 中通过枚举名和值名(如 ECollisionChannel::ECC_WorldStatic)查找使用
  • FGameplayTag:通过 Bind_FGameplayTagBind_FGameplayTag.cpp),FAngelscriptBinds::ExistingClass("FGameplayTag") 复用已有 C++ 类型,绑定 RequestGameplayTagopEqualsGetTagName 等;AS 中可用 FGameplayTag::RequestGameplayTag(TagName)StateTag.MatchesTag(Other) 等
  • FMyEventSignature:通过 ProcessDelegates 生成的委托类型,由 DeclareDelegate 注册

关键:此时 AMyScriptActor.as 尚未被读取,但 AActorUObject、C++ 枚举、FGameplayTagFMyEventSignature 等已就绪,供脚本继承和引用。

3.3 FindAllScriptFilenames:扫描脚本文件

InitialCompile() 中调用 FindAllScriptFilenames(),扫描项目 Script 目录下的 .as 文件,得到 MyScriptActor.as 等路径列表。

3.4 进入 Preprocessor

脚本路径传入预处理器(FAngelscriptPreprocessor),开始解析。示例文件被加入 Files,随后进入 ParseIntoChunksProcessPropertyMacroProcessFunctionMacroProcessDelegates 等步骤。

小结:示例 AMyScriptActor.as 在 Loader 启动 → Manager 初始化 → BindScriptTypes → FindAllScriptFilenames 之后,作为预处理输入进入下一阶段。


四、示例追踪(二):预处理与宏解析

4.1 ParseIntoChunks:代码分块

预处理器将源文件按类、结构体、枚举、函数等拆成 FChunk,每个 Chunk 有 Type(Class、Struct、Enum 等)和 Content(原始文本或处理后文本)。

4.2 宏解析:UPROPERTY、UFUNCTION、UCLASS

对每个 Chunk,预处理器扫描并解析宏:

  • UENUM() / enum EMyScriptStateDetectEnum(File, Chunk) 解析,生成 FAngelscriptEnumDesc(EnumName、ValueNames、EnumValues);AS 枚举在 Stage1 通过 asCBuilder::RegisterEnum 注册到引擎 allScriptDeclaredTypes,Stage3 生成 UEnum;AS 中通过 EMyScriptState::Idle 等查找使用
  • UCLASS():标记类,生成 FAngelscriptClassDescSuperClass = "AActor"
  • UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)ProcessPropertyMacro 解析,生成 FAngelscriptPropertyDescPropertyName = "ExposedCounter"LiteralType = "int32"
  • UPROPERTY(BlueprintSetter=SetMyValue, BlueprintGetter=GetMyValue):解析元数据,记录 Setter/Getter 函数名
  • UPROPERTY():委托属性,PropertyType 指向 FMyEventSignature 对应的委托类型(AS 中不支持 BlueprintAssignable,故不使用该元数据)
  • UPROPERTY(EditDefaultsOnly) FGameplayTag StateTag:属性类型为 C++ 已导出的 FGameplayTag,AS 中按值类型访问
  • UPROPERTY(EditDefaultsOnly) EMyScriptState ScriptState:属性类型为 AS 枚举,Stage3 生成 FEnumProperty,Enum 指向生成的 UEnum
  • UFUNCTION(BlueprintCallable)ProcessFunctionMacro 解析,生成 FAngelscriptFunctionDescbBlueprintCallable = true
  • 无 UPROPERTY 的 InternalTimer:预处理器为其生成 FAngelscriptPropertyDesc不会加入 Properties;仅在 Stage2 编译后作为脚本类型成员存在于 asCObjectType 中,由 FAngelscriptClassGenerator::Analyze 按需处理(见 4.7)
  • 无 UFUNCTION 的 InternalDoWork:预处理器为其生成 FAngelscriptFunctionDesc不会加入 Methods;仅在 Stage2 编译后作为 asCScriptFunction 存在,Analyze 仅遍历预处理器产出的 Methods 绑定 ScriptFunction,故不生成 UASFunction(见 4.7)

4.3 ProcessDelegates:委托与事件

event void FMyEventSignature(UObject Object, float Value); 被解析为 FAngelscriptDelegateDescbIsMulticast = trueSignature 描述参数和返回值。预处理器会生成委托包装类型的 AngelScript 代码(含 AddUFunctionBroadcast 等),替换原始声明。

4.4 描述符产出

预处理结束后,示例对应的 FAngelscriptModuleDesc 包含:

  • ClassesAMyScriptActor 的 FAngelscriptClassDesc
  • EnumsEMyScriptState 的 FAngelscriptEnumDesc
  • DelegatesFMyEventSignature 的 FAngelscriptDelegateDesc
  • FAngelscriptClassDesc 内:Properties(ExposedCounter、MyValue、OnSomethingHappened、StateTag、ScriptState)、Methods(PublicDoSomething、GetMyValue、SetMyValue、ExampleHandler 等);无 UPROPERTY 的 InternalTimer、无 UFUNCTION 的 InternalDoWork 不在上述列表中,由 Stage2 编译后的 Analyze 阶段按脚本类型成员处理(见 4.7)

小结:示例中的每个 UPROPERTY、UFUNCTION、UENUM、delegate、event 都在预处理阶段被解析为描述符,为四阶段编译提供输入。

4.5 预处理阶段类之间的引用关系

预处理阶段涉及的核心类型及其关系如下(讲到关键点时可按此对照):

FAngelscriptPreprocessor

  • 成员:Files(TArray of FFile)、Preprocess、ParseIntoChunks、DetectClasses、ProcessMacros、ProcessDelegates、CondenseFromChunks
  • 执行顺序:Preprocess → ParseIntoChunks → DetectClasses → ProcessMacros → ProcessDelegates → CondenseFromChunks

FFile

  • 成员:RawCode、ChunkedCode(TChunkedArray of FChunk)、ProcessedCode、GeneratedCode(TArray of FString)、Module(TSharedPtr FAngelscriptModuleDesc)、Delegates

FChunk

  • 成员:Type(Class/Struct/Enum/Global)、Content、Macros、ClassDesc(TSharedPtr FAngelscriptClassDesc)

FAngelscriptModuleDesc

  • 成员:ModuleName、Code(FCodeSection)、Classes、Enums、Delegates
  • 与 Classes/Enums/Delegates 关联

FAngelscriptClassDesc

  • 成员:ClassName、SuperClass、Properties(TArray FAngelscriptPropertyDesc)、Methods(TArray FAngelscriptFunctionDesc)

其他描述符

  • FAngelscriptPropertyDesc:PropertyName、LiteralType、PropertyType、Meta

  • FAngelscriptFunctionDesc:FunctionName、ReturnType、Arguments、Meta

  • FAngelscriptDelegateDesc:DelegateName、Signature、Function

  • FAngelscriptEnumDesc:EnumName、ValueNames、EnumValues

  • FAngelscriptPreprocessor:持有 Files,一次 Preprocess() 内依次对每个 FFile 执行 ParseIntoChunks → DetectClasses → AnalyzeClasses → ProcessMacros → ProcessDelegates → ProcessDefaults → CondenseFromChunks

  • FFile:一个 .as 文件对应一个 FFile;RawCode 读入后拆成 ChunkedCode(FChunk 数组);ProcessDelegates 等会往 GeneratedCode 里追加生成代码,并用 ReplaceWithBlank 抹掉原始 delegate 声明;CondenseFromChunks 把各 Chunk 的 Content 与 GeneratedCode 拼成 ProcessedCode,最终写入 Module->Code

  • FChunk:表示一段顶层结构(Class/Struct/Enum/Global);Content 为该段源码(可能已被替换);ClassDesc 在 DetectClasses/AnalyzeClasses 后指向该类/结构体/枚举的描述符。

  • FAngelscriptModuleDesc:一个模块对应一个;包含 ClassesEnumsDelegates,以及最终要编译的 Code(FCodeSection 数组)。

  • FAngelscriptClassDesc / FAngelscriptPropertyDesc / FAngelscriptFunctionDesc / FAngelscriptDelegateDesc / FAngelscriptEnumDesc:分别描述类、属性、函数、委托、枚举,供 Stage1~3 和错误检查使用。

4.6 预处理过程中自动添加与删除 AS 代码的逻辑

预处理阶段会自动生成一部分 AS 代码并替换原始声明,使 delegate/event 具备 AddUFunctionBroadcast 等能力。

自动添加(GeneratedCode)

  • 来源ProcessDelegates 等步骤会拼接一段完整的 AS 代码(例如委托包装结构体:含 _InnerBroadcastAddUFunction、构造/析构等)。
  • 存放FFile::GeneratedCodeTArray<FString>),每个元素为一段生成代码。
  • 合并时机CondenseFromChunks(File) 中,先把所有 Chunk.Content 拼成 File.ProcessedCode,再在末尾依次追加 File.GeneratedCode 的每一段(每段前加 \n\n)。
  • 代码位置AngelscriptPreprocessor.cpp 约 535~688 行(委托/事件生成)、3755~3778 行(CondenseFromChunks)。

自动删除(ReplaceWithBlank)

  • 目的:原始脚本里的 event void FMyEventSignature(...); 等声明会被生成的包装类型替代,因此需要把原声明从 Chunk 里“抹掉”,避免重复定义。
  • 做法ReplaceWithBlank(File.ChunkedCode[Delegate.ChunkIndex], Delegate.StartPosInChunk, Delegate.EndPosInChunk+1) 将指定 Chunk 的 Content 在 [StartPos, EndPos] 区间替换为空白,并记录到 Chunk.Replacements;后续 ProcessReplacements 在 Condense 前应用这些替换。
  • 结果:最终 ProcessedCode 中不再包含原始 delegate 声明,只包含生成的包装类型代码(来自 GeneratedCode)。

流程小结:ParseIntoChunks → DetectClasses → ProcessDelegates(生成代码入 GeneratedCode,原声明区间 ReplaceWithBlank)→ ProcessDefaults → CondenseFromChunks(Chunk 拼成 ProcessedCode + 末尾追加 GeneratedCode)。因此“添加”与“删除”共同保证了 delegate/event 在 AS 中只有一份生成实现。

4.7 InternalTimer / InternalDoWork 与 FAngelscriptClassGenerator::Analyze

预处理器只对带 UPROPERTY 的成员产出 FAngelscriptPropertyDesc 并加入 ClassDesc->Properties,只对带 UFUNCTION 的成员产出 FAngelscriptFunctionDesc 并加入 ClassDesc->Methods。因此无 UPROPERTY 的 InternalTimer、无 UFUNCTION 的 InternalDoWork不会出现在预处理产出的 Properties/Methods 中。二者在 Stage2 编译后作为脚本类型成员存在于 AngelScript 引擎(asCObjectType 的属性列表、方法列表),其“是否加入描述符、是否生成 FProperty/UASFunction”由 AngelscriptClassGenerator.cpp 中的 FAngelscriptClassGenerator::Analyze 统一处理。

位置AngelscriptClassGenerator.cpp 行 148~394(属性)、511~999(函数)。

属性处理逻辑

  1. 从脚本类型收集全部属性:根据 ClassData.NewClass->ScriptType(Stage2 编译得到的 asCObjectType)遍历 ScriptType->GetPropertyCount(),构建 PropertyIndexMap(名 → 索引)和 PropertyTypes。此时 InternalTimer 已在内(脚本里声明的所有成员都会出现在引擎类型中)。
  2. 按需补充“隐藏”属性:若某脚本属性出现在 ClassData.NewClass->Properties(即预处理器未为其生成描述符),但满足以下之一:类型为 RequiresProperty()(需 GC)、或当前类为 struct 且类型非 NeverRequiresGC(),则为其新增FAngelscriptPropertyDesc 并加入 ClassData.NewClass->Properties,避免 GC/序列化遗漏。InternalTimer 类型为 float,不满足 RequiresProperty(),且类为 UCLASS 非 struct,因此不会被补充进 Properties,即始终没有 FAngelscriptPropertyDesc。
  3. 校验并填充已有描述符:对 ClassData.NewClass->Properties 中每一项(仅预处理器产出的 + 上一步补充的),在 PropertyIndexMap 中查找;找到则填充 PropertyTypeScriptPropertyIndexScriptPropertyOffset 等;找不到则报错「Could not find property %s in class %s to be a UPROPERTY().」。

相关代码(属性:从脚本收集 + 按需补充)

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(line// 从 ScriptType 收集所有属性(含无 UPROPERTY 的 InternalTimer)TMap<FString, int32> PropertyIndexMap;if (ScriptType != nullptr) {    for (int32 i = 0; i < ScriptType->GetPropertyCount(); ++i) {        if (ScriptType->IsPropertyInherited(i)) continue;        const char* Name;        ScriptType->GetProperty(i, &Name);        PropertyIndexMap.Add(ANSI_TO_TCHAR(Name), i);        // ...    }}// 仅对“需 GC 或 struct 中非 NeverRequiresGC”的、且尚未在 Properties 中的脚本成员,补充 FAngelscriptPropertyDescfor (auto& Elem : PropertyIndexMap) {    bool bShouldMakeProperty = false;    FAngelscriptTypeUsage PropertyType = PropertyTypes[Elem.Value];    if (ClassData.NewClass->bIsStruct)        bShouldMakeProperty = !PropertyType.NeverRequiresGC();    if (PropertyType.RequiresProperty())        bShouldMakeProperty = true;    if (!bShouldMakeProperty) continue;    if (ClassData.NewClass->GetProperty(Elem.Key).IsValid()) continue;    // ... 创建 PropDesc,ClassData.NewClass->Properties.Add(PropDesc);}

函数处理逻辑

  1. 从脚本类型收集全部方法:根据 ScriptType->GetMethodCount() 遍历,构建 FunctionMap(名 → asCScriptFunction* 数组)。此时 InternalDoWork 已在内。
  2. 仅处理预处理器产出的 Methodsfor (auto FunctionDesc : ClassData.NewClass->Methods) 只遍历 ClassDesc->Methods(即带 UFUNCTION 的成员);在 FunctionMap 中查找同名脚本函数,绑定 FunctionDesc->ScriptFunction 等。InternalDoWork 不在 ClassData.NewClass->Methods 中,因此不会被绑定到任何 FAngelscriptFunctionDesc,不会生成 UASFunction,仅作为 asCScriptFunction 存在于脚本类型中。
  3. 若某 UFUNCTION 在脚本中找不到(FunctionMap.Find(FunctionDesc->ScriptFunctionName) == nullptr),则报错「Could not find function %s in class %s to be a UFUNCTION().」。

小结:InternalTimer 无 FAngelscriptPropertyDesc、不进入 Properties;InternalDoWork 无 FAngelscriptFunctionDesc、不进入 Methods。二者仅在 Stage2 编译后的 asCObjectType/asCScriptFunction 中存在,由 Analyze 的“仅处理描述符列表 + 按需补充 GC 相关属性”逻辑决定不为其生成 UPROPERTY/UFunction 绑定。


五、示例追踪(三):四阶段编译与类生成

5.1 CompileModules 与四阶段

CompileModules() 对每个模块依次执行四个阶段:

  • Stage1 (CompileModule_Types_Stage1):注册类型(AS 类、结构体、枚举)到 AngelScript 引擎,建立 asCObjectType
  • Stage2 (CompileModule_Functions_Stage2):编译函数体,生成 asCScriptFunction
  • Stage3 (CompileModule_Code_Stage3):生成 UClass、UFunction、FProperty,与 Unreal 反射系统对接
  • Stage4 (CompileModule_Globals_Stage4):编译全局变量、静态初始化等

5.2 示例在各阶段的转化

示例成员
Stage1
Stage2
Stage3
AMyScriptActor
注册 asCObjectType,derivedFrom AActor
编译 Tick、PublicDoSomething 等
生成 UASClass、UASFunction
ExposedCounter
作为属性类型注册
生成 FIntProperty,byteOffset 确定
StateTag (FGameplayTag)
C++ 已导出,作为值类型
生成 FStructProperty,Struct 为 FGameplayTag 的 UScriptStruct
ScriptState (EMyScriptState)
Stage1 注册 AS 枚举
生成 FEnumProperty,Enum 指向生成的 UEnum
InternalTimer
仅 AS 属性
无 FProperty,仅有 ScriptType 内 byteOffset
PublicDoSomething
编译脚本函数体
生成 UASFunction,绑定 ProcessEvent
InternalDoWork
编译脚本函数体
无 UFunction,仅 asCScriptFunction
OnSomethingHappened
委托包装类型已就绪
生成 FMulticastDelegateProperty
FMyEventSignature
委托类型注册
对应 UDelegateFunction
EMyScriptState
DetectEnum 产出 FAngelscriptEnumDesc
Stage1 RegisterEnum
Stage3 生成 UEnum

5.3 AngelscriptClassGenerator 与 UASClass

AngelscriptClassGenerator 负责将 FAngelscriptClassDesc 转化为 UASClass(继承 UClass)。UASClass 持有 ScriptTypePtrasITypeInfo*),与 asCObjectType 关联。脚本对象(asIScriptObject)与 UObject 内存布局兼容,可直接映射。

小结:示例 AMyScriptActor 在 Stage3 后成为可实例化的 UASClassPublicDoSomething 等 UFUNCTION 成为 UASFunction,蓝图可通过 ProcessEvent 调用。


六、示例追踪(四):运行时调用路径

6.1 蓝图调用 PublicDoSomething

蓝图调用 PublicDoSomething 时,走 Unreal 的 ProcessEventUASFunction 的 NativeFunc 指向 AngelscriptCallFromBPVM,该函数:

  1. 获取当前 AngelScript 上下文(或创建新上下文)
  2. 从 UObject* 得到对应的 asIScriptObject*(因内存布局兼容,可直接转换)
  3. 找到 PublicDoSomething 对应的 asCScriptFunction
  4. 设置 this 为脚本对象
  5. 调用 Execute 执行脚本

6.2 脚本内部调用 InternalDoWork

PublicDoSomething 的脚本体中调用 InternalDoWork(),这是纯粹的 AngelScript 函数调用,由引擎直接执行,不经过 ProcessEvent。

6.3 属性访问

  • ExposedCounter:有 FProperty,蓝图、序列化通过 FProperty::GetValue/SetValue 访问
  • InternalTimer:无 FProperty,仅通过 ScriptType->GetProperty 的 byteOffset 在脚本对象内存中读写

6.4 委托 AddUFunction 与 Broadcast

OnSomethingHappened.AddUFunction(self, n"ExampleHandler") 调用 C++ 绑定的 AddUFunction,内部通过 FDelegateOps::SignatureFunction 取得委托签名,CheckAngelscriptDelegateCompatibility 校验 ExampleHandler 与 FMyEventSignature 是否兼容,校验通过后 Delegate->AddUnique(InnerDelegate)Broadcast 时,Unreal 委托系统调用绑定好的 ExampleHandler,进而通过 AngelscriptCallFromBPVM 执行脚本。

小结:示例在运行时的调用路径为 ProcessEvent → AngelscriptCallFromBPVM → asIScriptContext::Execute;属性与委托的访问依赖 Stage3 生成的 FProperty 和委托绑定逻辑。


七、示例追踪(五):调用堆栈

7.1 作用

示例执行时(如蓝图调用 PublicDoSomething → 脚本 InternalDoWork),需要记录调用链(File、Function、Class、LineNumber、This)用于异常报告、调试器、日志。

7.2 FAngelscriptDebugStack 与 AngelscriptLineCallback

每行脚本执行时,AngelscriptLineCallback 被调用,从 asIScriptContext 同步更新 FAngelscriptDebugStack。每帧包含 FileFunctionClassLineNumberThisScriptFunction

位置AngelscriptManager.cpp 行 3941~3995

7.3 脚本可访问的堆栈接口

GetAngelscriptCallstack()FormatAngelscriptCallstack() 绑定为全局函数,脚本内可直接调用获取当前栈字符串。

7.4 异常与 TraceError

FAngelscriptManager::ThrowTraceError 在异常时会附加 FormatAngelscriptCallstack() 输出,便于定位问题。


八、C++ 与 AS 类型:导出、存储与访问(概览)

在示例追踪过程中涉及的 C++/AS 类型机制概览:

类型
导出
存储
示例访问
C++ 类 (AActor)
ReferenceClass → RegisterObjectType(asOBJ_REF)
asCObjectType + SetUserData(UClass*)
Actor.GetWorld() → thunk
C++ 结构体 (FVector)
ValueClass → RegisterObjectType(asOBJ_VALUE)
BindProperty(byteOffset)
V.X = 1.0f → 直接内存
C++ 枚举 (ECollisionChannel)
RegisterEnum + RegisterEnumValue
FEnumType
ECC_WorldStatic
C++ 结构体 (FGameplayTag)
Bind_FGameplayTag → ExistingClass + Method
asCObjectType + SetUserData
RequestGameplayTag、MatchesTag、GetTagName
AS 枚举 (EMyScriptState)
DetectEnum → FAngelscriptEnumDesc,Stage1 RegisterEnum
UEnum,allScriptDeclaredTypes
EMyScriptState::Idle 等
AS 类 (AMyScriptActor)
ClassGenerator → UASClass
ScriptTypePtr, asIScriptObject↔UObject
PublicDoSomething → AngelscriptCallFromBPVM
AS 委托 (FMyEventSignature)
ProcessDelegates → DeclareDelegate
FAngelscriptDelegateDesc, UDelegateFunction
AddUFunction → CheckAngelscriptDelegateCompatibility

详细机制见计划文档第四节;示例中 ExposedCounterOnSomethingHappenedInternalTimer 分别对应 C++ 值类型、AS 委托、纯 AS 属性的不同访问路径。


九、错误检查机制

9.1 调用时机与专属弹框

所有编译时错误通过 ScriptCompileError(Module, LineNumber, Message) 写入 FAngelscriptManager::DiagnosticsTMap<FString, FDiagnostics>,按文件名聚合)。弹框逻辑如下:

触发时机InitialCompile() 或热重载编译失败时(bDidInitialCompileSucceed == false 或热重载返回 Error),在游戏线程或通过 AsyncTask(ENamedThreads::GameThread, ...) 调用 ShowErrorDialog()

弹框逻辑AngelscriptManager.cpp 约 962~1063 行):

  1. 非编辑器:直接 FMessageDialog::Open 显示错误文本并 RequestExit(true)
  2. 编辑器
    • 每次 Tick 调用 FormatDiagnostics() 得到当前所有 Diagnostics 的格式化字符串(多文件、多行错误聚合)。
    • 若 PreviouslyFailedReloadFiles.Num() == 0(热重载成功),关闭弹框并设 bSuccess = true
    • 否则将提示文案(含「Please fix the errors and save the script files to proceed…」和 CompileDiagnostics)写入文本框;并调用 CheckForHotReload(ECompileType::FullReload) 检测文件保存以触发重新编译。
    • 启动热重载线程(StartHotReloadThread()),以便用户修改脚本后自动重试。
    • 创建 SWindow,标题 “Angelscript Compile Errors”,尺寸 800×600,可调。
    • 内容:上半部分为只读多行文本框(SMultiLineEditableTextBox),下半部分为「Open Angelscript workspace (VS Code)」按钮。
    • 注册 GetOnModalLoopTickEvent() 的 Tick 回调:
    • AddModalWindow(PromptWindow) 阻塞直到用户关闭窗口;若未成功则 RequestExit(true)

Diagnostics 来源ScriptCompileError 被调用时,会向 Diagnostics[Module->ModuleName 或 FileName].Diagnostics 追加条目,FormatDiagnostics() 遍历该 Map 并格式化为可读多行文本。

位置AngelscriptManager.cpp 约 987 行(弹框创建)、FormatDiagnostics 与 ShowErrorDialog 同文件。

9.2 BlueprintSetter 规范化检测(VerifyBlueprintSetFunc)

示例中 SetMyValue 作为 BlueprintSetter,需满足:存在且为 UFUNCTION、恰好 1 个参数、参数类型与 MyValue 一致。

位置AngelscriptManager.cpp 行 1321~1380

完整代码

ounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(lineounter(linebool FAngelscriptManager::VerifyBlueprintSetFunc(FString* FuncName,const TSharedRef<FAngelscriptPropertyDesc>& Property, const TSharedRef<FAngelscriptClassDesc>& Class,const TSharedRef<FAngelscriptModuleDesc>& Module){if (FuncName != nullptr)	{		auto FuncDesc = Class->GetMethod(*FuncName);// First make sure we can find the methodif (!FuncDesc.IsValid())		{			ScriptCompileError(Module, Property->LineNumber,				FString::Printf(TEXT("The function '%s' which is specified for BlueprintSetter on property %s::%s "								 "could not be found within the script class. (It also has to be UFUNCTION())"),					**FuncName,					*Class->ClassName,					*Property->PropertyName				)			);return false;		}// Having an BlueprintSetter callback requires a func with one argument matching the property typeif (FuncDesc->Arguments.Num() == 1)		{const FAngelscriptTypeUsage& FuncArgType = FuncDesc->Arguments[0].Type;const FAngelscriptTypeUsage& PropType = Property->PropertyType;// The type of the argument in the function has to be the same as the type of the variable we're// replicating!if (!FuncArgType.EqualsUnqualified(PropType))			{				ScriptCompileError(Module, FuncDesc->LineNumber,					FString::Printf(TEXT("The function '%s' which is specified for BlueprintSetter on property %s::%s "									 "takes an argument of type '%s', but the value written is of type '%s'."),						**FuncName,						*Class->ClassName,						*Property->PropertyName,						*FuncArgType.GetFriendlyTypeName(),						*PropType.GetFriendlyTypeName()					)				);return false;			}		}else		{			ScriptCompileError(Module, Property->LineNumber,				FString::Printf(TEXT("The function '%s' which is specified for BlueprintSetter on property %s::%s "								 "should take exactly 1 argument."),					**FuncName,					*Class->ClassName,					*Property->PropertyName				)			);return false;		}	}return true;}

原理

  1. 入口VerifyPropertySpecifiers 在 CompileModules 的 Stage2 之后、Stage3 之前调用;对每个带 BlueprintSetter=XXX 的 FAngelscriptPropertyDesc,取出 Setter 函数名并调用 VerifyBlueprintSetFunc
  2. 查找函数Class->GetMethod(*FuncName) 在当前类的 FAngelscriptClassDesc::Methods 中查找同名函数;若不存在则报错「could not be found … (It also has to be UFUNCTION())」。
  3. 参数个数:Setter 必须恰好 1 个参数;否则报「should take exactly 1 argument」。
  4. 参数类型FuncDesc->Arguments[0].Type 与 Property->PropertyType 必须 EqualsUnqualified 一致(忽略 const/ref 等限定),否则报「takes an argument of type ‘X’, but the value written is of type ‘Y’」。
  5. 错误输出:统一走 ScriptCompileError(Module, LineNumber, Message),进入 Diagnostics,最终在 “Angelscript Compile Errors” 弹框中展示。

9.3 BlueprintGetter 规范化检测(VerifyBlueprintGetFunc)

GetMyValue 作为 BlueprintGetter,需满足:存在且为 UFUNCTION、BlueprintPure、0 参数、返回值类型与 MyValue 一致。

位置AngelscriptManager.cpp 行 1381~1452

9.4 ReplicatedUsing 规范化检测(VerifyRepFunc)

若有 ReplicatedUsing,对应函数需存在、参数最多 1 个、类型与复制属性一致。

位置AngelscriptManager.cpp 行 1259~1319

9.5 AddUFunction/BindUFunction 运行时校验

当前 AddUFunctionBindUFunction 的签名匹配在运行时由 CheckAngelscriptDelegateCompatibility 完成,不匹配时 Throw 抛异常。下一节说明如何将此类校验前移到编译时,并支持自定义泛型函数。


十、在此基础上实现:自定义泛型函数参数匹配合法性检查

在前文流水线与错误检查机制的基础上,本节说明如何为 AddUFunctionBindUFunctionBindToVM 等泛型函数(接收字符串函数名的 C++ 导出函数)实现编译时参数个数、类型、输入/输出匹配的合法性检查。

10.1 问题与目标

  • 问题OnSomethingHappened.AddUFunction(self, n"ExampleHandler") 中,n"ExampleHandler" 与委托签名的匹配关系在编译时无法由 AngelScript 类型系统直接校验,当前仅在运行时校验。
  • 目标:在 Stage2 或 Stage3 之后(所有模块编译完成、类型与 UClass/UFunction 已就绪)检测此类调用,校验签名一致性,错误展示在 “Angelscript Compile Errors” 弹框。该时机可避免 Preprocess 阶段跨文件解析不全(见 10.6)。

10.2 整体流程

验证时机:Stage2 或 Stage3 之后(单次遍历所有已编译模块),与 10.6 推荐一致。

整体流程(自上而下):

  1. Stage2/Stage3 之后
  2. 遍历已编译模块 / PostCompileClassCollection
  3. 在 Module->Code 或脚本字节码/AST 中搜索目标调用
  4. 解析:委托表达式、Object、函数名
  5. 解析委托类型 → UDelegateFunction;解析 Object 类型 → ClassDesc/UClass
  6. 解析绑定函数 → FAngelscriptFunctionDesc/UFunction
  7. 签名比较 → 若兼容则通过,若不兼容则 ScriptCompileError

10.3 实现要点

验证时机:在 Stage2 或 Stage3 之后执行(例如在 CompileModules 各模块 Stage3 完成后统一遍历,或挂载 AngelscriptCodeModule.h 的 GetPostCompileClassCollection() 等回调),以便跨文件、跨模块正确解析类型与 UFunction。

  1. 插入点

    • 在 CompileModules 的所有模块 Stage2/Stage3 完成后增加一步「委托绑定调用校验」;或
    • 在 FAngelscriptCodeModule::GetPostCompileClassCollection() 等编译后回调中,对传入的「已编译模块的类描述集合」做一次遍历与校验
    • 遍历各 FAngelscriptModuleDesc 的 Classes,对每个类的 Module->Code(或已编译脚本的调用信息)进行扫描
  2. 调用来源与正则提取

    • 在 Module->Code(预处理合并后的脚本源码)或脚本编译产物的调用信息中,正则搜索 .AddUFunction(.BindUFunction(BindToVM(
    • (\\w+)\\.AddUFunction\\s*\\(\\s*(\\w+)\\s*,\\s*n\\s*\"([^\"]+)\"\\s*\\) 提取 (DelegateVar, ObjectVar, FuncName)
    • 示例:OnSomethingHappened.AddUFunction(self, n"ExampleHandler") → DelegateVar=OnSomethingHappened, ObjectVar=self, FuncName=ExampleHandler
  3. 委托签名解析(Stage2/Stage3 后类型已就绪)

    • ClassDesc->GetProperty(DelegateVar) 得到 FAngelscriptPropertyDesc
    • 从 PropertyType 或 Module->Delegates 得到 FAngelscriptDelegateDesc,进而得到 UDelegateFunction*(Stage3 后可用)
  4. 绑定函数解析

    • ClassDesc->GetMethod(FuncName) 得到 FAngelscriptFunctionDesc
    • Stage3 之后可直接用 FuncDesc->Function(UFunction*);否则用 AreFunctionSignaturesCompatibleForDelegate(DelegateFunc, FuncDesc) 比较描述符与委托签名
  5. 签名比较

    • UFunction vs UFunction:直接调用 CheckAngelscriptDelegateCompatibilityBind_Delegates.cpp 行 376~426)
    • UDelegateFunction vs FAngelscriptFunctionDesc:实现 AreFunctionSignaturesCompatibleForDelegate,逐参数比较 FAngelscriptTypeUsage 与 FProperty
  6. 错误输出

    • 不兼容时调用 ScriptCompileError(Module, LineNumber, Message)
    • 复用 GetSignatureStringForFunction 格式化委托与绑定函数的签名,便于用户理解

10.4 扩展 BindToVM 等自定义函数

验证时机:与 AddUFunction/BindUFunction 相同,在 Stage2 或 Stage3 之后(单次遍历已编译模块)对 BindToVM 等自定义泛型函数的调用做参数匹配合法性检查。

若项目中有 BindToVM(Delegate, Object, n"FunctionName")

  • 添加对应正则,提取 (DelegateExpr, ObjectExpr, FuncName)
  • 支持 A.B 形式的委托(如 MyButton.OnClicked):解析 A 的类型,在对应 UClass 上查找属性 B,得到 UDelegateFunction*
  • 复用 CheckAngelscriptDelegateCompatibility 或 AreFunctionSignaturesCompatibleForDelegate
  • 通过可配置的待校验函数列表(如 ["AddUFunction","BindUFunction","BindToVM"])灵活扩展

10.5 可复用的现有逻辑

功能
文件
用途
CheckAngelscriptDelegateCompatibility
Bind_Delegates.cpp 376~426
UFunction 参数兼容性,含 CPF_OutParm/CPF_ReferenceParm
ScriptCompileError
AngelscriptManager.cpp
写入 Diagnostics,弹框展示
ClassDesc->GetProperty / GetMethod
AngelscriptManager.h
解析委托属性、绑定函数
GetSignatureStringForFunction
Bind_Delegates.cpp ~451
格式化签名字符串

10.6 跨文件依赖与校验阶段选择

若 B.as 依赖 A.as 中定义的类或枚举(例如 B 中 AddUFunction(self, n"Handler") 的 Handler 在 A 中定义,或委托属性类型来自 A),在 Preprocess 阶段做泛型函数参数校验时,能否正确找到另一个 .as 文件里的 class/枚举?

Preprocess 阶段的可见范围

  • 预处理器按文件为单位处理:每个 FFile 对应一个 .as,ParseIntoChunksProcessPropertyMacroProcessFunctionMacroProcessDelegates 等都是在当前 File 的 Chunk 上操作。
  • FAngelscriptModuleDesc 在 CondenseFromChunks 之后才汇总当前模块内所有 File 的 ClassesEnumsDelegates;而 Preprocess 内部对单个 File 处理时,尚未做跨文件的 Module 级聚合。
  • 因此,在 Preprocess 阶段、且仅基于当前 File 的 Chunk.Content 与当前 File 所属的 Module->Classes/Enums 时:
    • 若 B.as 与 A.as 属于同一模块,且 A 先于 B 被 Preprocess(取决于 Files 顺序),则 B 处理时 A 的 FAngelscriptClassDesc/FAngelscriptEnumDesc 可能已写入 Module->Classes/Module->Enums有机会通过 Module 查到 A 中定义的类型。
    • 若 A、B 属于不同模块,或 Preprocess 时尚未把 A 的描述符并入同一 Module,则无法在 Preprocess 阶段仅凭“当前文件 + 当前模块”可靠地解析到 A 中的 class/枚举。

结论:在 Preprocess 阶段做“自定义泛型函数参数合法性检查”时,不能假定一定能找到“在另一个 .as 文件中定义的 class/结构体/枚举”;这与预处理器的单文件视角和模块汇总时机有关。

可选处理方式

  1. 推迟到 Stage2 或 Stage3 之后在所有模块的 Preprocess 和 Stage1~Stage3 都完成后,再做一次“委托绑定/泛型函数调用”的扫描与校验。此时:

    • 所有 AS 类型已在引擎中注册(Stage1),UClass、UEnum、UFunction 已生成(Stage3),可基于 asITypeInfoUFunction*UEnum* 做跨文件、跨模块的类型解析与签名比较。
    • 代价是错误反馈在“编译流程末尾”才出现,但能正确解析跨文件、跨模块的 class 与枚举。
  2. Preprocess 末尾、Condense 之后做一次“模块内”校验在 Preprocess() 的所有 File 都处理完CondenseFromChunks 已把各 File 的 Classes/Enums/Delegates 合并到 Module 之后,再遍历 Module 内所有类/委托,对 AddUFunction/BindUFunction 等调用做校验。这样同一模块内、不同 .as 文件之间的 class/枚举依赖可以正确解析;跨模块依赖仍建议放到 Stage2/Stage3 之后。

  3. 多轮 Preprocess 或显式依赖顺序若希望尽量在“预处理阶段”报错,可约定模块内文件的处理顺序(按依赖排序),或做多轮:第一轮只收集所有 Class/Enum/Delegate 声明,第二轮再在各 File 的 Chunk 上做泛型调用校验(此时 Module 已包含全部声明)。实现复杂度较高,通常不如方案 1 或 2 直接。

推荐:若项目存在“B.as 依赖 A.as 的类/枚举”的情况,将自定义泛型函数参数匹配合法性检查放在 Stage2 之后或 Stage3 之后(单次遍历所有已编译模块)更稳妥,可 100% 利用已注册的 AS 类型与 UClass/UFunction/UEnum,避免 Preprocess 阶段跨文件解析不全的问题。该时机与 AngelscriptCodeModule.h 中的编译后回调一致:例如 GetPostCompileClassCollection()FAngelscriptPostCompileClassCollection)在编译完成后提供「所有已编译模块的类描述集合」,适合在此处挂载委托绑定(AddUFunction、BindUFunction 等)的合法性检查;GetPreGenerateClasses()GetPostCompile() 等也可作为插入点,在类型与 UClass/UFunction 均已就绪的前提下做跨模块校验。


十一、关键代码索引

功能
文件
行号/说明
专属弹框 “Angelscript Compile Errors”
AngelscriptManager.cpp
~987
BindScriptTypes / CallBinds
AngelscriptManager.cpp / AngelscriptBinds.cpp
767~770 / 39~45
FindAllScriptFilenames
AngelscriptManager.cpp
829~868
InitialCompile
AngelscriptManager.cpp
868~923
ParseIntoChunks, ProcessPropertyMacro, ProcessFunctionMacro
AngelscriptPreprocessor.cpp
多处
ProcessDelegates
AngelscriptPreprocessor.cpp
~550
CompileModule_Types_Stage1 等四阶段
AngelscriptManager.cpp
1768~2578
AngelscriptClassGenerator, UASClass
AngelscriptClassGenerator.cpp, ASClass.cpp
多处
AngelscriptCallFromBPVM
ASClass.cpp
脚本函数入口
AngelscriptLineCallback, FAngelscriptDebugStack
AngelscriptManager.cpp
3941~3995
VerifyBlueprintSetFunc / VerifyBlueprintGetFunc
AngelscriptManager.cpp
1321~1452
CheckAngelscriptDelegateCompatibility
Bind_Delegates.cpp
376~426

十二、总结

  • 流水线:通过示例 AMyScriptActor.as 追踪了 初始化 → 绑定 → 扫描 → 预处理 → 四阶段编译 → 类生成 → 运行时 的完整流程,说明了 UPROPERTY、UFUNCTION、delegate、event 在各阶段的转化方式。
  • 错误检查:AngelScript 通过 ScriptCompileError 和 “Angelscript Compile Errors” 弹框统一展示编译错误;BlueprintSetter/Getter、ReplicatedUsing 等有现成的 Verify 逻辑。
  • 泛型函数编译时检查:在理解上述流水线与错误检查机制的基础上,可在预处理器中增加 VerifyDelegateBindingCallsInFile,通过正则解析、委托签名与绑定函数解析、CheckAngelscriptDelegateCompatibility 或 AreFunctionSignaturesCompatibleForDelegate 完成参数匹配合法性检查,并支持扩展至 BindToVM 等自定义函数。
  • 跨文件依赖:若 B.as 依赖 A.as 中的类/枚举,Preprocess 阶段可能无法可靠解析到另一文件中的类型;建议将此类校验推迟到 Stage2/Stage3 之后,或至少在 Preprocess 所有文件合并到 Module 后再做模块内校验(见 10.6)。
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » AngelScript 插件深度解析:实现原理与泛型函数编译时检查

评论 抢沙发

3 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮