ET 框架源码生成与静态分析体系:cn.etetet.sourcegenerator 包中的 Roslyn Generator、Analyzer 与 CodeFixer 全解析
【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET
本文以
cn.etetet.sourcegenerator包为切入点,系统讲解 ET 框架(Unity3D 客户端 + C# 服务端双端框架)如何借助 Roslyn 编译器平台,将 ECS 架构下的 EntitySystem、消息 Handler、事件、GetComponent 扩展与对象池清理等样板代码自动生成,并通过 40+ 条诊断规则在编译期约束代码风格与框架契约。读完本文,你将理解该包的目录结构与四类组成(Source Generator、Analyzer、CodeFixer、生成器标记类型),掌握新增一条诊断规则时必须同步维护的配置文件和验证流程,并能依据源码证据判断每条规则的具体行为。
一、包的定位与总体组成
cn.etetet.sourcegenerator是 ET 框架中负责编译期代码生成与静态检查的基础包,其定位在包描述中写得很清楚:"et框架的分析器跟代码生成器"。它本身不承载游戏运行时逻辑,而是以 Roslyn 编译器插件的形式存在:在dotnet build阶段扫描项目源码,一边生成 ECS 生命周期 System、消息 Handler 等样板代码,一边对违反框架契约的写法直接报编译错误。
根据 包的 AGENTS.md,该包承载四类组件:
| 组件类型 | 职责 | 仓库中的落地位置 |
|---|---|---|
| Source Generator(源生成器) | 依据标记自动生成 System / Handler / 扩展方法等代码 | DotNet~/ET.SourceGenerator/Generator/ |
| Analyzer(分析器) | 编译期静态检查,产出ET00xx/ET10xx诊断 | DotNet~/ET.SourceGenerator/Analyzer/ |
| CodeFixer(代码修复器) | 针对诊断提供一键修复(如自动生成 EntitySystem) | DotNet~/ET.SourceGenerator/CodeFixer/ |
| 生成器标记类型(Attribute) | 供业务代码使用的特性,是生成与分析的前提 | Scripts/Core/Share/ |
从整体目录结构看,包内分为两大块:
DotNet~/ET.SourceGenerator/:真正的 Roslyn 工程,包含Analyzer/、CodeFixer/、Config/、Generator/四个子目录,以及AnalyzerGlobalSetting.cs、AnalyzerHelper.cs、StringHashHelper.cs等公共设施;Scripts/Core/Share/:暴露给 Unity 业务工程的 Attribute 标记类型(EntitySystemOfAttribute、ComponentOfAttribute、DisableNewAttribute等);Scripts/Model/Share/PackageType.cs:记录包的唯一 ID(与packagegit.json中的Id字段对应,用于包依赖与访问控制分析)。
二、生成器标记类型:业务代码与编译器之间的"契约"
所有生成与分析行为都由 Attribute 驱动。业务代码先打标记,SourceGenerator/Analyzer 再据此工作。标记类型集中在 Scripts/Core/Share 下,其中Core/对应所有程序集通用的标记,Model/对应 Model 程序集专用的标记:
ECS 结构与生命周期标记
EntitySystemOfAttribute(见 EntitySystemOf.cs):标记某个静态类为指定 Entity 的 System 类,构造函数接收 Entity 类型与ignoreAwake(是否忽略生成 AwakeSystem),由它触发 EntitySystem 生成;LSEntitySystemOfAttribute:锁定帧(LockStep)实体 LSEntity 对应的 System 标记;ComponentOfAttribute(见 ComponentOfAttribute.cs):声明组件允许挂载的父级实体类型——"父级实体类型唯一的标记[ComponentOf(typeof(parentType))],不唯一则标记[ComponentOf]";ChildOfAttribute:声明子实体允许的父级类型,是AddChild类型约束分析的依据;EnableAccessEntiyChildAttribute:允许在实体类中直接访问 Child/Component。
能力开关标记
EnableClassAttribute(见 EnableClassAttribute.cs):Model 程序集默认禁止声明非实体类,声明普通类必须加此标记;EnableMethodAttribute、EnableGetComponentAttribute、AllowEntityMemberAttribute:分别放开方法、被禁组件的获取、实体成员声明;DisableGetComponentAttribute、DisableNewAttribute:禁止直接GetComponent/ 禁止new构造,配合ET0037、ET0031两条规则使用;StaticFieldAttribute:静态字段必须打标记(对应ET0015);UniqueIdAttribute:唯一 ID 字段约束(对应ET0011/ET0012);IgnoreCircularDependencyAttribute、ModuleAttribute、BTNodeGenAttribute、SkipAwaitEntityCheck等:分别用于循环依赖豁免、模块声明、行为树节点生成与异步实体检查豁免。
这些标记的字符串全名被集中定义在 Definition.cs(如ET.ChildOfAttribute、ET.ComponentOfAttribute、ET.DisableNewAttribute),Generator 与 Analyzer 均以该文件为统一事实来源,避免硬编码散落各处。
三、诊断规则体系:DiagnosticIds 与 DiagnosticRules
3.1 规则的"身份证":DiagnosticIds.cs
ET 框架的全部诊断 ID 集中维护在 Config/DiagnosticIds.cs,这是"新增诊断时同步维护"约定的落点之一。当前已登记约 40 条规则,按 ID 前缀分为两组:
ET0001~ET0040:框架契约类规则;ET1001~ET1004:代码风格与调用链类规则。
主要规则清单如下(描述取自 DiagnosticRules.cs 中各 Rule 的 Title/MessageFormat):
| ID | 规则 | 核心约束 |
|---|---|---|
| ET0001 | AddChild 类型约束 | 子实体类型必须用ChildOfAttribute标记父级 |
| ET0003 | 实体类限制多层继承 | 直接继承Entity的子类,禁止再被继承 |
| ET0007 | 组件类型约束 | 组件必须用ComponentOfAttribute声明父级实体 |
| ET0008/ET0009 | ETTask 调用规范 | 同步方法内需加.Coroutine();异步方法内需await或.Coroutine() |
| ET0010 | 实体类禁止委托字段/属性 | 防止引用生命周期问题 |
| ET0011/ET0012 | UniqueId 区间/重复约束 | 唯一 ID 必须在指定区间且全局不重复 |
| ET0014 | 禁止直接访问 Child/Component | Entity 基类场景下需EnableAccessEntiyChild |
| ET0015 | 静态字段必须打标记 | 配合StaticFieldAttribute |
| ET0016–ET0019 | ETCancellationToken 规范 | await 后必须判断IsCancel;必须透传同一 token;禁止默认值;禁止传 null |
| ET0020 | 实体类禁止实体字段 | 必须改用EntityRef |
| ET0021 | 禁止void异步方法 | 统一返回ETTask |
| ET0022 | 禁止 Server 引用ET.Client命名空间 | 双端隔离 |
| ET0023 | LSEntity 禁止浮点字段 | 锁定帧确定性要求 |
| ET0024/ET0025 | EntitySystem 生成完整性 | 存在未生成的生命周期函数;标签位置错误 |
| ET0026 | 实体类内必须用 Fiber 输出日志 | 多 Fiber 隔离日志 |
| ET0027 | 实体类 HashCode 禁止重复 | 名称哈希冲突检查 |
| ET0028 | 禁止同时标记 Component 和 Child | 角色冲突 |
| ET0029 | 禁止泛型实体类 | 简化类型系统 |
| ET0030 | 消息类禁止实体字段 | 网络消息可序列化要求 |
| ET0031 | 禁止new构造禁用类型 | 强制走对象池 |
| ET0032 | Model 程序集禁止非实体类 | 除非加[EnableClass] |
| ET0034/ET0035 | CoroutineLock 生命周期 | 禁止using获取;禁止手动Dispose(锁可能超时自动释放,重复释放会出问题) |
| ET0036 | 测试用例命名规范 | 测试方法命名检查 |
| ET0037 | 禁止直接 GetComponent 被禁组件 | 需[EnableGetComponent(typeof(T))] |
| ET0038 | System 类目录位置一致 | Model/ModelView 中的 Entity,其 System 必须在 Hotfix/HotfixView 对应相对位置 |
| ET0039 | 禁止全局入口访问未授权 Singleton | 需[AllowInstance],改用Entity.GetSingleton<T>()/Fiber.GetSingleton<T>() |
| ET0040 | IPool 类型必须可清理 | 必须能生成或手写显式IPool.Clear() |
| ET1001 | ETSystem 函数必须位于静态分部类 | 生成前提 |
| ET1002–ET1004 | await 实体检查 / 字段访问 / 循环调用 | 异步与调用链安全 |
3.2 规则描述与分类:DiagnosticRules.cs
每条规则的 Title、MessageFormat、Description、所属分类与严重级别都定义在 Config/DiagnosticRules.cs 中。以ETTaskInSyncMethodAnalyzerRule为例:
private const string Title = "ETTask方法调用在非异步方法体内使用错误"; private const string MessageFormat = "方法: {0} 在非异步方法体内使用时需要添加.Coroutine()后缀"; private const string Description = "ETTask方法调用在非异步方法体内使用错误."; public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticIds.ETTaskInSyncMethodAnalyzerRuleId, // "ET0008" Title, MessageFormat, DiagnosticCategories.Hotfix, // 分类:Hotfix 程序集 DiagnosticSeverity.Error, // 级别:编译错误 true, Description);规则分类定义在 Config/DiagnosticCategories.cs,共四类:
public const string Generator = "ETGeneratorAnalyzers"; // 生成器相关 public const string Hotfix = "ETHotfixProjectAnalyzers"; // Hotfix/HotfixView public const string Model = "ETModelProjectAnalyzers"; // Model/ModelView public const string All = "ETAllProjectAnalyzers"; // 全部程序集而某条规则具体作用在哪些程序集,由 Config/AnalyzeAssembly.cs 定义:AllModel(Model、ModelView)、AllHotfix(Hotfix、HotfixView)、AllModelHotfix(四者)、All(Core、Loader、Model、Hotfix、ModelView、HotfixView)。例如EntityComponentAnalyzer在AnalyzeAssembly.AllModelHotfix上注册(见 EntityComponentAnalyzer.cs),意味着该约束同时作用于 Model 与 Hotfix 两侧代码。
3.3 分析器的统一开关与公共设施
所有 Analyzer 都受一个全局开关控制。在 AnalyzerGlobalSetting.cs 中:
/// <summary> /// 是否开启项目的所有分析器 /// </summary> public static bool EnableAnalyzer = true;Analyzer 在Initialize中首先判断AnalyzerGlobalSetting.EnableAnalyzer,为false时直接返回,从而可以整体关闭静态检查(例如在生成代码等特殊场景)。
公共语法/符号工具集中在 AnalyzerHelper.cs,提供了如GetFirstChild<T>()(获取首个指定类型子节点)、GetParentClassDeclaration()(向上查找所属类声明)、HasAttribute()(判断类型是否带指定 Attribute)等扩展方法,所有 Analyzer 复用。
四、Analyzer 实现原理:以 EntityComponentAnalyzer 为例
分析器的典型实现路径可以以 EntityComponentAnalyzer.cs 为样板进行拆解:
- 注册:
[DiagnosticAnalyzer(LanguageNames.CSharp)]标记类;SupportedDiagnostics声明产出EntityComponentAnalyzerRule.Rule(ET0007)与DisableAccessEntityChildAnalyzerRule.Rule(ET0014); - 装配:
Initialize中先判断AnalyzerGlobalSetting.EnableAnalyzer,再通过RegisterCompilationStartAction校验程序集是否在AnalyzeAssembly.AllModelHotfix中,命中后注册语义模型分析回调; - 语义分析:遍历语法树中所有成员访问表达式,筛选
AddComponent/GetComponent(即Definition.ComponentMethod),解析调用者类型与组件类型:- 若调用者是
Entity/LSEntity基类,则进入"禁止直接访问 Child/Component"检查(除非标记了EnableAccessEntiyChild); - 若调用者是 Entity 的直接子类,则进一步校验组件类型是否被允许:泛型调用从
TypeArgumentListSyntax提取组件类型,非泛型调用(AddComponent(typeOf(...))或变量形式)则从实参符号解析;
- 若调用者是
- 上报:校验失败即
context.ReportDiagnostic(...),提示开发者"若要允许该类型作为参数,请使用ComponentOfAttribute对组件类标记父级实体类型"。
这条链路完整展示了 ET 静态检查的核心思路:不依赖运行时,而是在编译期用 Roslyn 语义模型把"组件—父实体"关系契约固化成编译错误。类似地,AddChildTypeAnalyzer(ET0001)、EntitySystemAnalyzer(ET0024,检查生命周期函数是否已生成)、CoroutineLockAnalyzer(ET0034/ET0035)都遵循同一模式。
五、Source Generator:样板代码的"自动生产线"
Generator 目录下共有四个生成器:ETSystemGenerator、ETGetComponentGenerator、IPoolClearGenerator 与ETEntitySerializeFormatterGenerator(实体序列化 Formatter 生成)。
5.1 ETSystemGenerator:ECS 生命周期的核心生成器
这是 ET 框架"Entity 只管数据、System 只管逻辑"分层原则的编译期实现。其入口标注[Generator(LanguageNames.CSharp)](见 ETSystemGenerator.cs),通过SyntaxContextReceiver收集所有带模板 Attribute 的方法声明,再按"命名空间 + 类名"分组,为每个静态类生成一个{namespace}.{className}.EntitySystems.g.cs分部类文件。
生成器会先校验 System 类必须是静态分部类(否则上报 ET1001),随后从方法符号中解析出参数类型列表,按模板逐项替换占位符。模板定义在 AttributeTemplate.cs,共 6 套:
① EntitySystem(实体生命周期,如 Awake/Update/Destroy):
$attribute$ public class $argsTypesUnderLine$_$methodName$System: $methodName$System<$argsTypes$> { protected override $returnType$ $methodName$($argsTypesVars$) { $return$$argsVars0$.$methodName$($argsVarsWithout0$); } }② LSEntitySystem(锁定帧实体生命周期):与 EntitySystem 类似,但强制void返回,保证确定性。
③ MessageHandler:
$attribute$ public class $className$_$methodName$_Handler: MessageHandler<$argsTypesWithout0$> { protected override async ETTask Run($argsTypesVars$) { await $className$.$methodName$($argsVars$); } }④ ActorMessageHandler:生成继承ActorMessageHandler<$argsTypes$>的 Handler;⑤ ActorMessageLocationHandler:生成继承ActorMessageLocationHandler<$argsTypes$>的 Handler;⑥ Event:生成继承AEvent<$argsTypes$>的事件处理类,Run内await $className$.$methodName$(...)。
模板中$argsTypesUnderLine$是参数类型拼接(./<>/[]替换为_/Array)得到的唯一类名片段,$argsVarsWithout0$去掉首个参数后的实参列表。也就是说,开发者只需在一个静态类里写普通静态方法并打上标记,编译器就会自动产出对应的 System 子类、Handler 子类或 AEvent 子类,这正是 ET 代码量大幅缩减的根源。
5.2 ETGetComponentGenerator:组件访问的强类型扩展
该生成器扫描所有带ComponentOfAttribute且带类型参数的类(见 ETGetComponentGenerator.cs),为每个"父实体 + 组件"组合生成强类型扩展方法:
public static {{getComponentName}} Get{{getComponentName}} (this {{parentEntityName}} self) { return self.GetComponent<{{getComponentName}}>(); }生成结果按命名空间聚合到ETGetComponentGenerator.{nameSpace}.g.cs文件中,类名为{AssemblyName}_ETGetComponentExtension。这样业务代码可以写unit.GetMoveComponent()这样语义化的访问,而不必每次都写泛型GetComponent<MoveComponent>()。
5.3 IPoolClearGenerator:对象池回池清理的自动补齐
ET 的对象池要求实现IPool的类型提供显式void IPool.Clear(),用于回池前清理字段、避免与业务Clear()冲突。但手写这段样板既繁琐又易漏,于是有了 IPoolClearGenerator:
- 生成条件(
ShouldGenerate):类型直接实现ET.IPool、是partial类、未手写显式IPool.Clear(),且没有需要手工处理的成员; - 清理策略:可自动清理的集合字段/属性(实现了
ICollection/IDictionary/ISet等且有无参Clear())生成this.xxx?.Clear();;其他普通字段/属性生成this.xxx = default;;IsFromPool、InstanceId、IScene、ViewGO等基础设施成员跳过; - 基类处理:若基类链上有可清理的集合类型,会先追加
base.Clear();; - 兜底检查:若成员中存在
IDisposable/IEnumerable等需要手工管理清理的类型(NeedsManualClear),则不自动生成,转由 ET0040 规则提示开发者手写。
生成代码形态(见 IPoolClearGenerator.cs):
public partial class MyPoolObject { void global::ET.IPool.Clear() { this.bag?.Clear(); this.owner = default; } }对应的 ET0040 诊断(IPoolClearAnalyzerRule)在生成器无法完成时提示:"Type: {0} 实现了 IPool,但无法生成或找到显式 IPool.Clear() 方法。请将类改为 partial,或手写 void IPool.Clear()。" 生成器与分析器在此形成闭环。
六、CodeFixer:编译错误的一键修复
CodeFixer 目录下有 EntitySystemCodeFixProvider.cs、BracesCodeFixProvider.cs与ModuleCodeFixProvider.cs三个修复器。
以EntitySystemCodeFixProvider为例:它针对EntitySystemAnalyzerRuleId(ET0024,"Entity 类存在未生成的生命周期函数")提供名为"Generate Entity System"的代码操作。修复流程是:
- 从诊断位置向上定位到目标
ClassDeclarationSyntax; - 从诊断
Properties中读取生命周期接口序列(EntitySystemInterfaceSequence,以/分隔)及各方法的参数描述; - 逐个调用
CreateEntitySystemMethodSyntax生成对应的 System 方法语法节点(含[EntitySystem]标签); - 批量插入类成员开头,并套用格式化注解后替换语法树,完成一键修复。
这解释了 ET0024 为什么被设计为可修复诊断:开发者为 Entity 声明了生命周期接口(如IAwake)但忘了写 System 方法时,IDE 会直接提示并自动补齐。另外BracesCodeFixProvider处理花括号风格问题,ModuleCodeFixProvider处理模块声明问题,共同构成"报错—修复"的完整闭环。
七、包的构建与集成方式
7.1 工程配置要点
生成器工程 ET.SourceGenerator.csproj 的关键配置:
TargetFramework=netstandard2.0:Roslyn 分析器/生成器必须在 netstandard2.0 下编译才能被dotnet build与 IDE 加载;- 引用
Microsoft.CodeAnalysis.CSharp.Workspaces与Microsoft.CodeAnalysis.CSharp4.3.0、Microsoft.CodeAnalysis.Analyzers3.3.3(PrivateAssets=all,不向外传播); <Compile Include="$(SolutionDir)Packages\cn.etetet.*\DotNet~\SourceGenerator\**\*.cs">:把仓库内所有包的DotNet~/SourceGenerator目录下的源码统一编入本工程——即其他包也可以在自己的DotNet~/SourceGenerator中贡献生成器/分析器代码,实现按包分发的分布式扩展;- 自定义
CustomAfterBuild目标:构建完成后把生成的ET.SourceGenerator.dll拷贝到包根目录(即仓库中可见的ET.SourceGenerator.dll),供 Unity/IDE 作为 analyzer 加载。
7.2 标记程序集与包发布
- 包根目录下已附带编译产物
ET.SourceGenerator.dll(及.meta),是分析器在 Unity 中的实际加载入口; DotNet~/ET.SourceGeneratorAttribute/是独立的标记程序集工程(ET.SourceGeneratorAttribute.csproj),其中NoCut.cs提供空方法占位,防止链接器裁剪掉 Attribute 程序集;- package.json 声明包名为
cn.etetet.sourcegenerator、显示名ET.SourceGenerator、版本0.0.7、最低 Unity 版本2022.3,描述即"et框架的分析器跟代码生成器"。
八、开发约定:如何正确扩展这个包
包的 AGENTS.md 规定了四条硬性约定,这也是任何新增/修改生成器与分析器时必须遵守的流程:
8.1 通用代码规则以上层规范为准
通用代码规则以 Packages/cn.etetet.harness/AGENTS.md 和
et-codeskill 为准。
cn.etetet.harness是 ET 项目的 AI 技能分发包,其中et-codeskill 覆盖"新建或修改 Entity、Component、System、Helper""处理 ECS 分层、组件存在性契约、Module 分析器、分析器报错"等场景;同时该文件明确了项目唯一编译入口:"分析器编译要使用ET.sln",并且"项目只有一个编译命令,必须使用dotnet build ET.sln"。因此开发本包代码前,应先把et-code相关规则作为代码风格与契约的基线。
8.2 修改后必须用dotnet build ET.sln验证
分析器和生成器修改后必须使用
dotnet build ET.sln验证。
这是本包唯一的验证手段:分析器/生成器属于编译期设施,必须通过全量解决方案构建来确认(1)生成器工程自身可编译;(2)生成的代码能被解决方案中所有引用它的工程接受;(3)没有引入新的诊断误报。验证命令(在仓库根目录执行):
dotnet build ET.sln8.3 新增诊断时同步维护两个 Config 文件
新增诊断时同步维护
Config/DiagnosticIds.cs与Config/DiagnosticRules.cs。
也就是说,一条新规则需要三处配套改动才能合入:
- 在 Config/DiagnosticIds.cs 中登记新的规则 ID 常量(遵循
ET00xx/ET10xx编号体系); - 在 Config/DiagnosticRules.cs 中定义对应的
DiagnosticDescriptor(Title/MessageFormat/Description/分类/严重级别); - 在
Analyzer/下实现DiagnosticAnalyzer,并依据AnalyzeAssembly选择作用的程序集集合。
同时应保持 ID 常量与 Rule 描述一一对应,避免出现"有 ID 无规则"或"有规则无 ID"的半成品状态。
8.4 不手工生成.meta、不修改 Unity 工程文件
不手工生成
.meta或修改 Unity 工程文件。
.meta文件由 Unity 资源数据库维护(对应仓库中的*.cs.meta),手工编辑会破坏 GUID 引用;生成器/分析器代码位于DotNet~目录,属于纯 .NET 工程,也不需要改动任何 Unity 工程文件。需要时通过 Unity 刷新自动生成。
九、小结
cn.etetet.sourcegenerator是 ET 框架工程化的"编译期引擎",它把三类工作固化在构建阶段:
- 生成:
ETSystemGenerator依据标记把 System/Handler/Event 样板自动产出,ETGetComponentGenerator生成强类型组件访问扩展,IPoolClearGenerator自动补齐对象池清理; - 检查:以
ET0001–ET0040、ET1001–ET1004近 40 条诊断,在编译期锁定 ECS 契约(组件/子实体父级声明、EntityRef 替换实体字段、LSEntity 确定性、Fiber 日志、CoroutineLock 生命周期、Singleton 访问边界等); - 修复:
EntitySystemCodeFixProvider等 CodeFixer 对可修复诊断提供一键生成。
对框架使用者而言,只需记住"打标记、写静态方法、交给编译器"的开发范式,并理解dotnet build ET.sln是分析与生成生效的唯一入口;对框架贡献者而言,则需严格遵守 AGENTS.md 的开发约定——以et-codeskill 为代码基线、改后必须全量构建验证、新增诊断时同步维护DiagnosticIds.cs与DiagnosticRules.cs、绝不手工改动.meta与 Unity 工程文件。正是这套"标记驱动、编译期产出、规则约束"的体系,支撑起 ET 框架大规模 ECS 代码的整洁、一致与高性能。
【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考