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:仅脚本内,无 FPropertyfloat 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() { }}
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
流水线总览
流水线顺序(从左到右):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(),完成:
-
AngelScript 引擎创建: asCreateScriptEngine() -
BindScriptTypes:将所有 C++ 类型(类、结构体、枚举)注册到 AngelScript -
InitialCompile:首次完整编译所有脚本
3.2 BindScriptTypes:C++ 类型导出
BindScriptTypes() 内部调用 FAngelscriptBinds::CallBinds(),按顺序执行各个 FBind 注册函数。示例中涉及的 C++ 类型在此时完成导出:
-
AActor、UObject:通过 Bind_UObject、Bind_BlueprintType等,RegisterObjectType注册为asOBJ_REF -
int32、float:通过 Bind_Primitives -
C++ 枚举(如 ECollisionChannel):通过 Bind_UEnum/Bind_Enums,FAngelscriptBinds::Enum(Name)→RegisterEnum(type)+RegisterEnumValue(typeName, valueName, value);FAngelscriptType::Register(MakeShared<FEnumType>(Enum))注册类型系统;AS 中通过枚举名和值名(如ECollisionChannel::ECC_WorldStatic)查找使用 -
FGameplayTag:通过 Bind_FGameplayTag(Bind_FGameplayTag.cpp),FAngelscriptBinds::ExistingClass("FGameplayTag")复用已有 C++ 类型,绑定RequestGameplayTag、opEquals、GetTagName等;AS 中可用FGameplayTag::RequestGameplayTag(TagName)、StateTag.MatchesTag(Other)等 -
FMyEventSignature:通过 ProcessDelegates生成的委托类型,由DeclareDelegate注册
关键:此时 AMyScriptActor.as 尚未被读取,但 AActor、UObject、C++ 枚举、FGameplayTag、FMyEventSignature 等已就绪,供脚本继承和引用。
3.3 FindAllScriptFilenames:扫描脚本文件
InitialCompile() 中调用 FindAllScriptFilenames(),扫描项目 Script 目录下的 .as 文件,得到 MyScriptActor.as 等路径列表。
3.4 进入 Preprocessor
脚本路径传入预处理器(FAngelscriptPreprocessor),开始解析。示例文件被加入 Files,随后进入 ParseIntoChunks、ProcessPropertyMacro、ProcessFunctionMacro、ProcessDelegates 等步骤。
小结:示例 AMyScriptActor.as 在 Loader 启动 → Manager 初始化 → BindScriptTypes → FindAllScriptFilenames 之后,作为预处理输入进入下一阶段。
四、示例追踪(二):预处理与宏解析
4.1 ParseIntoChunks:代码分块
预处理器将源文件按类、结构体、枚举、函数等拆成 FChunk,每个 Chunk 有 Type(Class、Struct、Enum 等)和 Content(原始文本或处理后文本)。
4.2 宏解析:UPROPERTY、UFUNCTION、UCLASS
对每个 Chunk,预处理器扫描并解析宏:
-
UENUM() / enum EMyScriptState: DetectEnum(File, Chunk)解析,生成FAngelscriptEnumDesc(EnumName、ValueNames、EnumValues);AS 枚举在 Stage1 通过asCBuilder::RegisterEnum注册到引擎allScriptDeclaredTypes,Stage3 生成UEnum;AS 中通过EMyScriptState::Idle等查找使用 -
UCLASS():标记类,生成 FAngelscriptClassDesc,SuperClass = "AActor" -
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly): ProcessPropertyMacro解析,生成FAngelscriptPropertyDesc,PropertyName = "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解析,生成FAngelscriptFunctionDesc,bBlueprintCallable = 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); 被解析为 FAngelscriptDelegateDesc,bIsMulticast = true,Signature 描述参数和返回值。预处理器会生成委托包装类型的 AngelScript 代码(含 AddUFunction、Broadcast 等),替换原始声明。
4.4 描述符产出
预处理结束后,示例对应的 FAngelscriptModuleDesc 包含:
-
Classes:AMyScriptActor的FAngelscriptClassDesc -
Enums:EMyScriptState的FAngelscriptEnumDesc -
Delegates:FMyEventSignature的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:一个模块对应一个;包含
Classes、Enums、Delegates,以及最终要编译的Code(FCodeSection 数组)。 -
FAngelscriptClassDesc / FAngelscriptPropertyDesc / FAngelscriptFunctionDesc / FAngelscriptDelegateDesc / FAngelscriptEnumDesc:分别描述类、属性、函数、委托、枚举,供 Stage1~3 和错误检查使用。
4.6 预处理过程中自动添加与删除 AS 代码的逻辑
预处理阶段会自动生成一部分 AS 代码并替换原始声明,使 delegate/event 具备 AddUFunction、Broadcast 等能力。
自动添加(GeneratedCode):
-
来源: ProcessDelegates等步骤会拼接一段完整的 AS 代码(例如委托包装结构体:含_Inner、Broadcast、AddUFunction、构造/析构等)。 -
存放: FFile::GeneratedCode(TArray<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(函数)。
属性处理逻辑:
-
从脚本类型收集全部属性:根据 ClassData.NewClass->ScriptType(Stage2 编译得到的asCObjectType)遍历ScriptType->GetPropertyCount(),构建PropertyIndexMap(名 → 索引)和PropertyTypes。此时 InternalTimer 已在内(脚本里声明的所有成员都会出现在引擎类型中)。 -
按需补充“隐藏”属性:若某脚本属性未出现在 ClassData.NewClass->Properties(即预处理器未为其生成描述符),但满足以下之一:类型为RequiresProperty()(需 GC)、或当前类为 struct 且类型非NeverRequiresGC(),则为其新增FAngelscriptPropertyDesc并加入ClassData.NewClass->Properties,避免 GC/序列化遗漏。InternalTimer 类型为float,不满足RequiresProperty(),且类为 UCLASS 非 struct,因此不会被补充进 Properties,即始终没有 FAngelscriptPropertyDesc。 -
校验并填充已有描述符:对 ClassData.NewClass->Properties中每一项(仅预处理器产出的 + 上一步补充的),在PropertyIndexMap中查找;找到则填充PropertyType、ScriptPropertyIndex、ScriptPropertyOffset等;找不到则报错「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);}
函数处理逻辑:
-
从脚本类型收集全部方法:根据 ScriptType->GetMethodCount()遍历,构建FunctionMap(名 →asCScriptFunction*数组)。此时 InternalDoWork 已在内。 -
仅处理预处理器产出的 Methods: for (auto FunctionDesc : ClassData.NewClass->Methods)只遍历ClassDesc->Methods(即带 UFUNCTION 的成员);在FunctionMap中查找同名脚本函数,绑定FunctionDesc->ScriptFunction等。InternalDoWork 不在ClassData.NewClass->Methods中,因此不会被绑定到任何FAngelscriptFunctionDesc,不会生成UASFunction,仅作为asCScriptFunction存在于脚本类型中。 -
若某 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 示例在各阶段的转化
|
|
|
|
|
|---|---|---|---|
|
|
asCObjectType,derivedFrom AActor |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5.3 AngelscriptClassGenerator 与 UASClass
AngelscriptClassGenerator 负责将 FAngelscriptClassDesc 转化为 UASClass(继承 UClass)。UASClass 持有 ScriptTypePtr(asITypeInfo*),与 asCObjectType 关联。脚本对象(asIScriptObject)与 UObject 内存布局兼容,可直接映射。
小结:示例 AMyScriptActor 在 Stage3 后成为可实例化的 UASClass,PublicDoSomething 等 UFUNCTION 成为 UASFunction,蓝图可通过 ProcessEvent 调用。
六、示例追踪(四):运行时调用路径
6.1 蓝图调用 PublicDoSomething
蓝图调用 PublicDoSomething 时,走 Unreal 的 ProcessEvent。UASFunction 的 NativeFunc 指向 AngelscriptCallFromBPVM,该函数:
-
获取当前 AngelScript 上下文(或创建新上下文) -
从 UObject*得到对应的asIScriptObject*(因内存布局兼容,可直接转换) -
找到 PublicDoSomething对应的asCScriptFunction -
设置 this为脚本对象 -
调用 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。每帧包含 File、Function、Class、LineNumber、This、ScriptFunction。
位置:AngelscriptManager.cpp 行 3941~3995
7.3 脚本可访问的堆栈接口
GetAngelscriptCallstack()、FormatAngelscriptCallstack() 绑定为全局函数,脚本内可直接调用获取当前栈字符串。
7.4 异常与 TraceError
FAngelscriptManager::Throw、TraceError 在异常时会附加 FormatAngelscriptCallstack() 输出,便于定位问题。
八、C++ 与 AS 类型:导出、存储与访问(概览)
在示例追踪过程中涉及的 C++/AS 类型机制概览:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
详细机制见计划文档第四节;示例中 ExposedCounter、OnSomethingHappened、InternalTimer 分别对应 C++ 值类型、AS 委托、纯 AS 属性的不同访问路径。
九、错误检查机制
9.1 调用时机与专属弹框
所有编译时错误通过 ScriptCompileError(Module, LineNumber, Message) 写入 FAngelscriptManager::Diagnostics(TMap<FString, FDiagnostics>,按文件名聚合)。弹框逻辑如下:
触发时机:InitialCompile() 或热重载编译失败时(bDidInitialCompileSucceed == false 或热重载返回 Error),在游戏线程或通过 AsyncTask(ENamedThreads::GameThread, ...) 调用 ShowErrorDialog()。
弹框逻辑(AngelscriptManager.cpp 约 962~1063 行):
-
非编辑器:直接 FMessageDialog::Open显示错误文本并RequestExit(true)。 -
编辑器: -
每次 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;}
原理:
-
入口: VerifyPropertySpecifiers在 CompileModules 的 Stage2 之后、Stage3 之前调用;对每个带BlueprintSetter=XXX的FAngelscriptPropertyDesc,取出 Setter 函数名并调用VerifyBlueprintSetFunc。 -
查找函数: Class->GetMethod(*FuncName)在当前类的FAngelscriptClassDesc::Methods中查找同名函数;若不存在则报错「could not be found … (It also has to be UFUNCTION())」。 -
参数个数:Setter 必须恰好 1 个参数;否则报「should take exactly 1 argument」。 -
参数类型: FuncDesc->Arguments[0].Type与Property->PropertyType必须EqualsUnqualified一致(忽略 const/ref 等限定),否则报「takes an argument of type ‘X’, but the value written is of type ‘Y’」。 -
错误输出:统一走 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 运行时校验
当前 AddUFunction、BindUFunction 的签名匹配在运行时由 CheckAngelscriptDelegateCompatibility 完成,不匹配时 Throw 抛异常。下一节说明如何将此类校验前移到编译时,并支持自定义泛型函数。
十、在此基础上实现:自定义泛型函数参数匹配合法性检查
在前文流水线与错误检查机制的基础上,本节说明如何为 AddUFunction、BindUFunction、BindToVM 等泛型函数(接收字符串函数名的 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 推荐一致。
整体流程(自上而下):
-
Stage2/Stage3 之后 -
遍历已编译模块 / PostCompileClassCollection -
在 Module->Code 或脚本字节码/AST 中搜索目标调用 -
解析:委托表达式、Object、函数名 -
解析委托类型 → UDelegateFunction;解析 Object 类型 → ClassDesc/UClass -
解析绑定函数 → FAngelscriptFunctionDesc/UFunction -
签名比较 → 若兼容则通过,若不兼容则 ScriptCompileError
10.3 实现要点
验证时机:在 Stage2 或 Stage3 之后执行(例如在 CompileModules 各模块 Stage3 完成后统一遍历,或挂载 AngelscriptCodeModule.h 的 GetPostCompileClassCollection() 等回调),以便跨文件、跨模块正确解析类型与 UFunction。
-
插入点
-
在 CompileModules 的所有模块 Stage2/Stage3 完成后增加一步「委托绑定调用校验」;或 -
在 FAngelscriptCodeModule::GetPostCompileClassCollection()等编译后回调中,对传入的「已编译模块的类描述集合」做一次遍历与校验 -
遍历各 FAngelscriptModuleDesc的Classes,对每个类的Module->Code(或已编译脚本的调用信息)进行扫描 -
调用来源与正则提取
-
在 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 -
委托签名解析(Stage2/Stage3 后类型已就绪)
-
ClassDesc->GetProperty(DelegateVar)得到FAngelscriptPropertyDesc -
从 PropertyType或Module->Delegates得到FAngelscriptDelegateDesc,进而得到UDelegateFunction*(Stage3 后可用) -
绑定函数解析
-
ClassDesc->GetMethod(FuncName)得到FAngelscriptFunctionDesc -
Stage3 之后可直接用 FuncDesc->Function(UFunction*);否则用AreFunctionSignaturesCompatibleForDelegate(DelegateFunc, FuncDesc)比较描述符与委托签名 -
签名比较
-
UFunction vs UFunction:直接调用 CheckAngelscriptDelegateCompatibility(Bind_Delegates.cpp行 376~426) -
UDelegateFunction vs FAngelscriptFunctionDesc:实现 AreFunctionSignaturesCompatibleForDelegate,逐参数比较FAngelscriptTypeUsage与FProperty -
错误输出
-
不兼容时调用 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 可复用的现有逻辑
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10.6 跨文件依赖与校验阶段选择
若 B.as 依赖 A.as 中定义的类或枚举(例如 B 中 AddUFunction(self, n"Handler") 的 Handler 在 A 中定义,或委托属性类型来自 A),在 Preprocess 阶段做泛型函数参数校验时,能否正确找到另一个 .as 文件里的 class/枚举?
Preprocess 阶段的可见范围:
-
预处理器按文件为单位处理:每个 FFile对应一个 .as,ParseIntoChunks、ProcessPropertyMacro、ProcessFunctionMacro、ProcessDelegates等都是在当前 File 的 Chunk 上操作。 -
FAngelscriptModuleDesc在CondenseFromChunks之后才汇总当前模块内所有 File 的Classes、Enums、Delegates;而 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/结构体/枚举”;这与预处理器的单文件视角和模块汇总时机有关。
可选处理方式:
-
推迟到 Stage2 或 Stage3 之后在所有模块的 Preprocess 和 Stage1~Stage3 都完成后,再做一次“委托绑定/泛型函数调用”的扫描与校验。此时:
-
所有 AS 类型已在引擎中注册(Stage1),UClass、UEnum、UFunction 已生成(Stage3),可基于 asITypeInfo、UFunction*、UEnum*做跨文件、跨模块的类型解析与签名比较。 -
代价是错误反馈在“编译流程末尾”才出现,但能正确解析跨文件、跨模块的 class 与枚举。 -
Preprocess 末尾、Condense 之后做一次“模块内”校验在
Preprocess()的所有 File 都处理完、CondenseFromChunks已把各 File 的 Classes/Enums/Delegates 合并到Module之后,再遍历 Module 内所有类/委托,对AddUFunction/BindUFunction等调用做校验。这样同一模块内、不同 .as 文件之间的 class/枚举依赖可以正确解析;跨模块依赖仍建议放到 Stage2/Stage3 之后。 -
多轮 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 均已就绪的前提下做跨模块校验。
十一、关键代码索引
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
十二、总结
-
流水线:通过示例 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)。
夜雨聆风
