refactor : 代码重构

This commit is contained in:
何冠峰
2026-01-12 11:09:27 +08:00
committed by 何冠峰
parent d228e41df7
commit 5b81269090
1614 changed files with 44418 additions and 42154 deletions

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: bdbb4647038dcc842802f546c2fedc83
guid: b819eb7cc257867439445ce1a660ea0a
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,228 @@
# .NET 命名准则
> 参考来源:[Microsoft .NET 框架设计准则 - 命名准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/naming-guidelines)
>
> 核心原则:**可读性优先、语义清晰、风格一致、避免歧义**
---
## 一、大小写约定
### 1.1 两种大小写风格
| 风格 | 规则 | 示例 |
|------|------|------|
| **PascalCasing** | 每个单词首字母大写(含首个单词) | `PropertyDescriptor``HtmlTag` |
| **camelCasing** | 除第一个单词外,每个单词首字母大写 | `propertyDescriptor``htmlTag` |
### 1.2 各标识符的大小写规则
| 标识符 | 大小写 | 示例 |
|--------|--------|------|
| 命名空间 | PascalCasing | `System.Security` |
| 类型(类/结构) | PascalCasing | `StreamReader` |
| 接口 | PascalCasing | `IEnumerable` |
| 方法 | PascalCasing | `ToString()` |
| 属性 | PascalCasing | `Length` |
| 事件 | PascalCasing | `Exited` |
| 字段(公共/静态) | PascalCasing | `InfiniteTimeout` |
| 枚举值 | PascalCasing | `FileMode.Append` |
| 参数 | camelCasing | `string value` |
### 1.3 首字母缩写词规则
- **两个字母**的缩写词:全部大写,如 `IO``DB`
- **三个字母及以上**的缩写词:仅首字母大写,如 `Html``Xml`
- camelCasing 中,开头的两字母缩写词全部小写:`ioStream`
### 1.4 常见复合词的正确写法
| 正确 (Pascal) | 正确 (camel) | 错误 |
|---------|---------|------|
| `Callback` | `callback` | ~~CallBack~~ |
| `Endpoint` | `endpoint` | ~~EndPoint~~ |
| `FileName` | `fileName` | ~~Filename~~ |
| `Hashtable` | `hashtable` | ~~HashTable~~ |
| `Id` | `id` | ~~ID~~ |
| `Metadata` | `metadata` | ~~MetaData~~ |
| `Namespace` | `namespace` | ~~NameSpace~~ |
| `Ok` | `ok` | ~~OK~~ |
| `UserName` | `userName` | ~~Username~~ |
| `WhiteSpace` | `whiteSpace` | ~~Whitespace~~ |
| `SignIn` | `signIn` | ~~SignOn~~ |
| `LogOff` | `logOff` | ~~LogOut~~ |
| `Email` | `email` | ~~EMail~~ |
> ✔️ 在 Unity 项目中,`ID` 保持全大写以与 Unity API 风格一致(如 `GetInstanceID()`、`PropertyToID()`),项目内应保持风格统一。
### 1.5 区分大小写
- 不要假定所有语言都区分大小写
- 公开 API 中不应仅靠大小写来区分两个名称
---
## 二、常规命名约定
### 2.1 用词选择
- **可读性优先于简洁性**`CanScrollHorizontally` 优于 `ScrollableX`
- **语义清晰**`HorizontalAlignment` 优于 `AlignmentHorizontal`
- **禁止**使用下划线 `_`、连字符 `-` 或其他非字母字符
- **禁止**使用匈牙利命名法(如 `strName``iCount`
- **避免**与编程语言关键字冲突
### 2.2 缩写和首字母缩写词
- **禁止**使用缩写:用 `GetWindow` 而非 `GetWin`
- **禁止**使用未被广泛接受的首字母缩略词
### 2.3 避免语言特定名称
- 使用语义化名称:`GetLength` 优于 `GetInt`
- 需要表示类型时,使用 CLR 类型名称而非语言关键字:`ToInt64` 而非 `ToLong`
### 2.4 现有 API 的新版本命名
- 使用类似旧 API 的名称
- **优先添加后缀**而非前缀(便于 IntelliSense 排序)
- 用**数字后缀**标识版本(如 `64` 表示 64 位版本)
- **禁止**使用 `Ex` 后缀
---
## 三、程序集和 DLL 的名称
- 选择能体现**大范围功能**的名称
- 推荐格式:`<Company>.<Component>.dll`,如 `Litware.Controls.dll`
- 基于程序集所含命名空间的**公共前缀**来命名
---
## 四、命名空间的名称
- 推荐格式:`<Company>.(<Product>|<Technology>)[.<Feature>][.<Subnamespace>]`
- 使用 **PascalCasing**,用句点分隔:`Microsoft.Office.PowerPoint`
- 使用**与版本无关的稳定产品名称**
- 适当使用**复数**`System.Collections` 而非 `System.Collection`
- **禁止**命名空间与其内部类型同名
- **禁止**使用泛型名称如 `Element``Node``Log``Message`,应加限定:`FormElement``XmlNode``EventLog`
---
## 五、类、结构和接口的名称
### 5.1 类和结构
- 使用 PascalCasing**名词或名词短语**
- **禁止**添加 `C` 前缀
- 考虑以**基类名称结尾**`ArgumentOutOfRangeException`(派生自 `Exception`
### 5.2 接口
- 使用**形容词短语**,偶尔使用名词
- **必须**以 `I` 前缀开头:`IComponent``ICustomAttributeProvider``IPersistable`
### 5.3 泛型类型参数
- 单字母参数时用 `T``IComparer<T>`
- 描述性名称以 `T` 开头:`ISessionChannel<TSession>`
- 考虑在名称中体现约束:`TSession`(约束为 `ISession`
### 5.4 常见类型的后缀规则
| 基类/接口 | 后缀要求 |
|-----------|---------|
| `System.Attribute` | 添加 `Attribute` 后缀 |
| `System.Delegate`(事件用) | 添加 `EventHandler` 后缀 |
| `System.Delegate`(回调用) | 添加 `Callback` 后缀 |
| `System.EventArgs` | 添加 `EventArgs` 后缀 |
| `System.Exception` | 添加 `Exception` 后缀 |
| `IDictionary` / `IDictionary<TKey,TValue>` | 添加 `Dictionary` 后缀 |
| `IEnumerable` / `ICollection` / `IList` | 添加 `Collection` 后缀 |
| `System.IO.Stream` | 添加 `Stream` 后缀 |
### 5.5 枚举命名
- 普通枚举用**单数**名称:`FileMode`
- 位标志枚举用**复数**名称:`FileAttributes`
- **禁止**添加 `Enum``Flag``Flags` 后缀
- **禁止**在枚举值上使用前缀
- ✔️ 允许对枚举类型名称添加 `E` 前缀以提高可辨识度,项目内应保持风格一致
---
## 六、类型成员的名称
### 6.1 方法
- 使用**谓词或谓词短语**`CompareTo``Split``Trim`
### 6.2 属性
- 使用**名词、名词短语或形容词**
- 布尔属性使用肯定短语:`CanSeek` 而非 `CantSeek`
- 布尔属性可加 `Is``Can``Has` 前缀(仅在有意义时)
- 可以与类型同名:`public Color Color { get; set; }`
- 集合属性用**复数**`Items` 而非 `ItemList`
### 6.3 事件
- 使用**谓词或谓词短语**
- 用时态区分前后:`Closing`(关闭前)、`Closed`(关闭后)
- **禁止**用 `Before` / `After` 前后缀
- 事件处理程序委托添加 `EventHandler` 后缀
- 事件参数类添加 `EventArgs` 后缀
- 事件处理程序参数命名为 `sender``e`
### 6.4 字段
- 使用 PascalCasing
- 使用**名词、名词短语或形容词**
- **禁止**使用前缀(如 `g_``s_``m_`
---
## 七、参数命名
- 使用 **camelCasing**
- 使用**描述性名称**
- 根据**含义**而非类型命名
- 二元运算符重载:无特殊含义时用 `left` / `right`
- 一元运算符重载:无特殊含义时用 `value`
- **禁止**对运算符参数使用缩写或数字索引
---
## 八、资源命名
- 使用 **PascalCasing**
- 使用**描述性标识符**
- 仅使用字母数字字符和下划线
- 异常消息资源格式:`异常类型名 + 短标识符`,如 `ArgumentExceptionIllegalCharacters`
---
## 九、内部字段命名约定
以上命名准则主要适用于公开 API。对于内部实现代码以下前缀约定对代码审查者非常有价值
| 字段类型 | 前缀 | 示例 |
|---------|------|------|
| 私有/内部**实例**字段 | `_` | `_count``_name` |
| 私有/内部**静态**字段 | `s_` | `s_instance``s_defaultValue` |
| 私有/内部**线程静态**字段 | `t_` | `t_cachedBuffer` |
```csharp
public class ConnectionPool
{
private static readonly int s_maxPoolSize = 100;
[ThreadStatic]
private static Queue<Connection> t_localPool;
private readonly string _connectionString;
private int _activeCount;
}
```
> 注意:此约定仅适用于**私有和内部**字段,不适用于公共 API。公共字段如 `public static readonly` 字段)仍然使用 PascalCasing 且不带前缀,这与前文"六、类型成员的名称 - 6.4 字段"中"禁止使用前缀"的准则不矛盾。

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5fa2b66c20800124c8dd5cb77e854ce3
guid: b6b6bc56d8bfe6c4fa3776f98ab3f822
TextScriptImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,279 @@
# .NET 类型设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 类型设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/type)
>
> 核心原则:**每种类型都应是一组定义完善的相关成员,而非不相关功能的随机集合。**
---
## 一、类型概述
从 CLR 的角度看只有两类类型(引用类型和值类型),但在框架设计中可细分为以下逻辑组:
| 类型 | 说明 |
|------|------|
| **类Class** | 引用类型的一般情况,支持丰富的面向对象功能,构成框架中的大部分类型 |
| **接口Interface** | 可由引用类型和值类型实现,用作多态层次结构的根,模拟多重继承 |
| **结构Struct** | 值类型的一般情况,应保留为小型简单类型,类似语言基元 |
| **枚举Enum** | 特殊值类型,用于定义一组简短的命名常量 |
| **静态类Static Class** | 仅包含静态成员的容器,用于提供操作快捷方式 |
| **委托/异常/属性/数组/集合** | 用于特定用途的引用类型特殊情况 |
---
## 二、在类和结构之间进行选择
### 2.1 引用类型 vs 值类型的核心区别
| 特征 | 引用类型(类) | 值类型(结构) |
|------|--------------|--------------|
| **分配位置** | 堆上分配,由 GC 回收 | 栈上分配或内联在包含类型中 |
| **数组分配** | 元素是引用,实例在堆上 | 元素是实际实例,内联分配 |
| **赋值行为** | 复制引用 | 复制整个值 |
| **传递方式** | 按引用传递 | 按值传递(产生副本) |
| **装箱** | 无装箱开销 | 转换为接口或引用类型时会装箱 |
### 2.2 选择准则
✔️ 如果类型的实例**较小**且通常**生存期较短**或通常**嵌入在其他对象中**,请考虑定义结构而不是类。
❌ **避免**定义结构,除非类型**同时满足**以下所有特征:
- 逻辑上表示单个值,类似基元类型(`int``double`
- 实例大小**低于 24 字节**
- 是**不可变**的
- 不必**频繁装箱**
> 在所有其他情况下,应将类型定义为类。
---
## 三、抽象类设计
❌ **禁止**在抽象类型中定义 `public``protected internal` 构造函数。
> 抽象类型无法被实例化,公共构造函数会对用户产生误导。
✔️ **必须**在抽象类中定义 `protected``internal` 构造函数。
- `protected` 构造函数更常用,允许基类在创建子类型时执行初始化
- `internal` 构造函数可将具体实现限制在定义类的程序集内
✔️ **必须**提供至少一种从你交付的每个抽象类继承的具体类型。
> 例如 `FileStream` 是抽象类 `Stream` 的具体实现,这有助于验证抽象类的设计。
---
## 四、静态类设计
静态类仅包含静态成员(除继承自 `Object` 的实例成员和私有构造函数外)。在 C# 中,声明为 `static` 的类是密封的、抽象的,且无法重写或声明实例成员。
### 适用场景
- 提供其他操作的快捷方式(如 `System.IO.File`
- 扩展方法的持有者
- 无法确保完整面向对象包装器的功能(如 `System.Environment`
### 设计准则
✔️ **谨慎**使用静态类,仅作为框架面向对象核心的支持类。
❌ **禁止**将静态类视为杂项存储桶。
❌ **禁止**在静态类中声明或重写实例成员。
✔️ 如果语言没有对静态类的内置支持,则将其声明为 `sealed abstract` 并添加私有实例构造函数。
---
## 五、接口设计
### 5.1 适用场景
- 需要一组**包含值类型**的类型支持通用 API
- 需要在已从其他类型继承的类型上支持功能(模拟多重继承)
### 5.2 设计准则
✔️ 如果需要包含值类型的多种类型支持通用 API请定义接口。
✔️ 如果需要在已有基类继承的类型上支持功能,请考虑定义接口。
✔️ **必须**至少提供一种类型作为接口的实现。
> 例如 `List<T>` 是 `IList<T>` 的实现。
✔️ **必须**至少提供一个使用该接口的 API以接口为参数的方法或类型为接口的属性
> 例如 `List<T>.Sort` 使用 `IComparer<T>` 接口。
❌ **避免**使用标记接口没有成员的接口应改用自定义属性Attribute
❌ **禁止**向已发布的接口添加新成员。
> 这样做会破坏现有实现,应创建新接口以避免版本控制问题。
✔️ 优先定义**类**而非接口。基于类的 API 可以更容易地演进——向类添加成员不会破坏现有代码,而向接口添加成员则会。
✔️ 使用**抽象类**而非接口来将协定与实现解耦。
❌ **禁止**在公共 API 中使用 `ICloneable`
> `ICloneable` 的问题在于:有些实现是浅拷贝,有些是深拷贝,调用者永远不知道会得到什么。这使得该接口实际上毫无用处。如果需要克隆机制,请定义类型特定的 `Clone` 方法。
> **一般规则**:在设计可重用库时,通常应选择类而非接口。
---
## 六、结构设计
❌ **禁止**为结构提供无参数构造函数。
> 遵循此准则可以创建结构数组,而无需在每个元素上运行构造函数。
❌ **禁止**定义可变值类型。
> 可变值类型问题:属性 getter 返回副本,开发者可能不知道正在修改副本而非原始值。
✔️ **必须**确保所有实例数据设置为零/false/null 的状态有效。
> 防止创建结构数组时意外产生无效实例。
✔️ 如果值类型会参与相等比较(如用作字典键、放入集合、使用 `==` 运算符),**必须**实现 `IEquatable<T>`,并同时重写 `Equals(object)``GetHashCode()`
> `Object.Equals` 在值类型上会导致装箱且效率低下(使用反射),`IEquatable<T>.Equals` 性能更好且无装箱。
❌ 对于仅作为数据载体、不参与相等比较的值类型,**不必**实现 `IEquatable<T>`
❌ **禁止**显式扩展 `ValueType`
✔️ 声明不可变值类型时使用 **`readonly struct`** 修饰符。
> 编译器理解 `readonly` 修饰符,在对 `readonly` 字段调用方法等操作时可避免产生额外的防御性复制。
```csharp
public readonly struct ZipCode
{
public ZipCode(string value) => Value = value;
public string Value { get; }
public override string ToString() => Value;
}
```
✔️ 对可变值类型的非可变方法标记 **`readonly`** 修饰符。
> 允许编译器在调用时跳过值复制。
```csharp
public struct Point
{
public float X;
public float Y;
public readonly override string ToString() => $"({X}, {Y})";
}
```
❌ **避免**定义 `ref struct`ref-like 值类型),除非用于低级性能关键场景。
> `ref struct` 只能存在于栈上,不能被装箱到堆中。因此不能用作其他类型的字段(除了其他 `ref struct`),也不能在 `async` 方法中使用。
> **总结**:结构适用于小型、单个、不可变且不频繁装箱的值。
---
## 七、枚举设计
### 7.1 两种枚举类型
| 类型 | 说明 | 命名 | 示例 |
|------|------|------|------|
| **简单枚举** | 表示小型封闭式选项集 | 单数名词 | `FileMode` |
| **标志枚举** | 支持对枚举值执行位运算 | 复数名词 | `FileAttributes` |
### 7.2 通用准则
✔️ 使用枚举对表示值集的参数、属性和返回值进行**强类型化**。
✔️ **优先**使用枚举而不是静态常量。
✔️ 为简单枚举提供一个**零值**(通常命名为 `None`)。
✔️ 考虑使用 `Int32` 作为枚举的基础类型(除非有特殊需求)。
❌ **禁止**对开放集合使用枚举(如操作系统版本、好友姓名等)。
❌ **禁止**提供用于将来使用的保留枚举值。
❌ **避免**公开只有一个值的枚举。
❌ **禁止**在枚举中包含哨兵值。
❌ **禁止**直接扩展 `System.Enum`
### 7.3 标志枚举设计
✔️ **必须**将 `[Flags]` 特性应用于标志枚举,不要应用于简单枚举。
✔️ **必须**对标志枚举值使用 **2 的幂**,以便用按位 OR 自由组合。
✔️ 考虑为常用标志组合提供特殊枚举值(如 `ReadWrite = Read | Write`)。
✔️ **必须**将标志枚举的零值命名为 `None`,表示"所有标志均已清除"。
❌ **避免**在某些值组合无效的情况下创建标志枚举。
❌ **避免**使用标志枚举值零,除非它表示"所有标志已清除"且命名为 `None`
### 7.4 向枚举添加值
✔️ 考虑向已发布的枚举添加值(兼容性风险通常较小)。
> 如果有不兼容风险,可添加新 API 返回新旧值,并弃用旧 API。
---
## 八、嵌套类型
嵌套类型在另一个类型的作用域内定义,可访问封闭类型的所有成员(包括私有字段)。
### 8.1 适用场景
- 对封闭类型的**实现细节**进行建模
- 例如:集合的枚举器可以是该集合的嵌套类型
### 8.2 设计准则
✔️ 当嵌套类型与外部类型之间的关系使得**成员可访问性语义**可取时,使用嵌套类型。
❌ **禁止**将公共嵌套类型用作逻辑分组构造(应使用命名空间)。
❌ **避免**公开的嵌套类型(极少数子类化或高级自定义方案除外)。
❌ 如果某个类型可能在其包含类型**之外被引用**,不要使用嵌套类型。
> 例如,传递给类方法的枚举不应定义为该类的嵌套类型。
❌ **禁止**使用需要通过客户端代码实例化的嵌套类型。
> 如果类型有公共构造函数,通常不应被嵌套。
❌ **禁止**将嵌套类型定义为接口的成员。
> 多种语言不支持此类构造。
---
## 九、速查表
| 场景 | 推荐类型 |
|------|---------|
| 大部分通用情况 | 类Class |
| 小型、不可变、类似基元的值 | 结构Struct |
| 需要值类型参与多态 | 接口Interface |
| 需要模拟多重继承 | 接口Interface |
| 封闭的命名常量集 | 枚举Enum |
| 可组合的位标志 | 标志枚举([Flags] Enum |
| 纯静态工具方法容器 | 静态类Static Class |
| 提供可扩展的基础实现 | 抽象类Abstract Class |
| 封闭类型的内部实现细节 | 嵌套类型Nested Type |

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2c65d3ba8da65004b8d36dbeecc6c6be
guid: 982ed5c869a8a6c4d8adda239881d163
TextScriptImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,339 @@
# .NET 成员设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 成员设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/member)
>
> 方法、属性、事件、构造函数和字段统称为成员,是将框架功能展现给最终用户的手段。
---
## 一、成员重载
成员重载是指在同一类型上创建两个或多个仅参数数量或类型不同、但名称相同的成员。只有**方法、构造函数和索引属性**可以重载。
### 设计准则
✔️ 使用**描述性参数名称**来指示较短重载使用的默认值。
✔️ 仅使**最长的重载虚拟化**(如果需要扩展性),较短的重载应直接调用较长的重载。
✔️ 允许 `null` 作为可选参数传递。
✔️ 优先使用**成员重载**,而不是带默认参数的成员(默认参数不符合 CLS
❌ **避免**在重载中任意改变参数名称。相同输入的参数在所有重载中应保持相同名称。
❌ **避免**重载中参数顺序不一致。相同名称的参数应在所有重载中处于相同位置。
❌ **禁止**使用 `ref``out` 修饰符来区分重载。
❌ **禁止**使用位置相同、类型相似但语义不同的重载。
---
## 二、属性设计
属性在技术上与方法相似,但使用场景不同。它们应被视为"智能字段",兼具字段的调用语法和方法的灵活性。
### 2.1 通用准则
✔️ 如果调用方不应更改属性的值,请创建**只读属性**get-only
✔️ 为所有属性提供**合理的默认值**,确保不会导致安全漏洞或低效代码。
✔️ 允许按**任意顺序**设置属性,即使这会导致对象临时处于无效状态。
✔️ 如果属性 setter 引发异常,请**保留以前的值**。
❌ **禁止**提供仅有 setter 的属性,或 setter 比 getter 可访问性更广的属性。
> 如果无法提供 getter请改用以 `Set` 开头的方法,如 `SetCachePath`。
❌ **避免**在属性 getter 中引发异常。
> getter 应是简单操作,无前置条件。如果可能引发异常,应重新设计为方法。
### 2.2 索引器设计
✔️ 考虑使用索引器提供对内部数组中数据的访问。
✔️ 考虑为表示项集合的类型提供索引器。
✔️ 索引器默认使用 `Item` 作为名称(除非有明显更好的名称)。
❌ **避免**使用多个参数的索引属性(如需多参数,请改用 `Get`/`Set` 方法)。
❌ **避免**使用 `Int32``Int64``String``Object` 或枚举以外类型作为索引器参数。
❌ **禁止**同时提供索引器和语义等效的方法。
❌ **禁止**在同一类型中提供多个重载索引器系列。
### 2.3 属性更改通知事件
✔️ 考虑在修改高级 API通常是设计器组件中的属性值时引发**更改通知事件**。
✔️ 考虑在属性值因外部因素更改时引发更改通知事件。
---
## 三、构造函数设计
### 3.1 实例构造函数
✔️ 考虑提供**简单的**(理想情况下为默认)构造函数,参数少且都是基元或枚举类型。
✔️ 如果操作语义不直接映射到新实例的构造,考虑使用**静态工厂方法**代替构造函数。
✔️ 使用构造函数参数作为设置主属性的**快捷方式**。
✔️ 如果构造函数参数仅用于设置属性,则对参数和属性使用**相同的名称**(仅大小写不同)。
✔️ 在构造函数中执行**最少的工作**,其他处理应延迟到需要时。
✔️ 在适当时从实例构造函数**抛出异常**。
✔️ 如果需要无参数构造函数,请**显式声明**(避免添加参数化构造函数后隐式丢失)。
❌ **避免**在结构上显式定义无参数构造函数(使数组创建更快)。
**避免**在构造函数内对对象调用**虚拟成员**
> 调用虚拟成员会导致调用派生度最高的重写,即使该派生类的构造函数尚未完全执行。
### 3.2 类型构造函数(静态构造函数)
✔️ 将静态构造函数设为**私有**。
✔️ 考虑**内联初始化**静态字段,而非使用显式静态构造函数(运行时可优化性能)。
❌ **禁止**从静态构造函数引发异常。
> 如果类型构造函数抛出异常,该类型在当前应用程序域中将不可用。
---
## 四、事件设计
事件是最常见的回调形式,允许框架调用用户代码。
### 4.1 两种事件时机
| 类型 | 说明 | 示例 |
|------|------|------|
| **预事件** | 状态变更**之前**引发 | `Form.Closing` |
| **后事件** | 状态变更**之后**引发 | `Form.Closed` |
### 4.2 设计准则
✔️ 对事件使用"**raise**"(引发)一词,而非 "fire" 或 "trigger"。
✔️ 使用 `EventHandler<TEventArgs>` 而非手动创建新委托。
✔️ 在 Unity 项目中,可以使用 `Action<T>` 代替 `EventHandler<TEventArgs>` 作为事件委托类型,以保持与 Unity 生态惯例一致。
✔️ 考虑使用 `EventArgs` 的子类作为事件参数(即使最初为空,也便于将来扩展)。
✔️ 使用**受保护的虚拟方法**引发每个事件(仅适用于未密封类的非静态事件)。
> 方法命名约定:以 `On` 开头 + 事件名称,如 `OnClosing`。
✔️ 该方法接受一个参数,命名为 `e`,类型为事件参数类。
✔️ 考虑引发最终用户可**取消**的预事件(使用 `CancelEventArgs` 或其子类)。
❌ 引发非静态事件时,**禁止**将 `null` 作为 sender 传递。
✔️ 引发静态事件时,将 `null` 作为 sender 传递。
❌ 引发事件时,**禁止**将 `null` 作为事件数据参数传递(应使用 `EventArgs.Empty`)。
### 4.3 自定义事件处理程序
✔️ 事件处理程序返回类型为 `void`
✔️ 第一个参数类型为 `object`,命名为 `sender`
✔️ 第二个参数类型为 `EventArgs` 或其子类,命名为 `e`
❌ 事件处理程序**不超过两个参数**。
---
## 五、字段设计
封装原则要求存储在对象中的数据只能通过该对象访问。
### 设计准则
❌ **禁止**提供 `public``protected` 的实例字段(应使用属性代替)。
✔️ 在 Unity 项目中,需要通过 `JsonUtility` 序列化的 `[Serializable]` 类型,应使用公共字段而非属性(`JsonUtility` 不支持属性序列化)。
✔️ 对永远不会更改的常量使用 `const` 字段。
> 编译器将 const 值直接嵌入调用代码,更改 const 值会破坏兼容性。
✔️ 对预定义的对象实例使用 `public static readonly` 字段。
❌ **禁止**将可变类型的实例赋给 `readonly` 字段。
> `readonly` 仅阻止引用被替换,不阻止通过成员修改实例数据。例如数组、集合等可变类型的实例数据仍可被修改。
---
## 六、扩展方法
扩展方法允许使用实例方法调用语法调用静态方法,定义在静态"发起方"类中。
### 适用场景
✔️ 为接口的每个实现提供辅助功能(基于核心接口编写),如 LINQ 运算符。
✔️ 当实例方法会引入对某类型的依赖,且该依赖会破坏依赖管理规则时。
### 设计准则
❌ **避免**轻率地定义扩展方法,尤其是在你不拥有的类型上。
> 如果拥有类型源代码,考虑使用常规实例方法。
❌ **避免**在 `System.Object` 上定义扩展方法VB 用户无法使用扩展方法语法调用)。
❌ **禁止**将扩展方法放在与扩展类型相同的命名空间中(除非用于接口或依赖项管理)。
❌ **避免**使用相同签名定义多个扩展方法,即使在不同命名空间中。
❌ **避免**使用泛型命名空间名(如 "Extensions"),应使用描述性名称(如 "Routing")。
✔️ 如果类型是接口且扩展方法用于大多数场景,考虑放在与扩展类型相同的命名空间中。
---
## 七、运算符重载
运算符重载允许框架类型表现得像内置语言基元。
### 7.1 通用准则
✔️ 考虑在感觉像**基元类型**的类型中定义运算符重载。
✔️ 在表示**数字**的结构中定义运算符重载(如 `Decimal`)。
✔️ **对称**重载运算符:重载 `==` 则必须重载 `!=`,重载 `<` 则必须重载 `>`
✔️ 考虑为每个重载运算符提供具有**友好名称**的等效方法(因为某些语言不支持运算符重载)。
❌ **避免**定义运算符重载,除非类型感觉像基元类型。
❌ 定义运算符重载时**不要花哨**,操作结果应显而易见。
❌ 除非至少一个操作数属于定义重载的类型,否则**禁止**提供运算符重载。
### 7.2 常用运算符与友好方法名对照
| 运算符 | 友好方法名 |
|--------|----------|
| `+` (二元) | `Add` |
| `-` (二元) | `Subtract` |
| `*` (二元) | `Multiply` |
| `/` | `Divide` |
| `%` | `Mod` / `Remainder` |
| `==` | `Equals` |
| `!=` | `Equals` |
| `<` / `>` | `CompareTo` |
| `<=` / `>=` | `CompareTo` |
| `++` | `Increment` |
| `--` | `Decrement` |
| `- (一元)` | `Negate` |
### 7.3 转换运算符
❌ 如果最终用户**未明确预期**该转换,禁止提供转换运算符。
❌ **禁止**在类型域之外定义转换运算符(如不应将 `Double` 转换为 `DateTime`)。
❌ 如果转换**可能丢失数据**,禁止提供隐式转换运算符(可提供显式转换)。
❌ **禁止**从隐式强制转换引发异常。
✔️ 如果显式强制转换导致有损转换,且运算符契约不允许有损,请抛出 `InvalidCastException`
---
## 八、参数设计
### 8.1 通用准则
✔️ 使用提供成员所需功能的**最低派生参数类型**。
> 例如用 `IEnumerable` 而非 `ArrayList` 或 `IList` 作为枚举方法的参数。
✔️ 将所有 `out` 参数置于所有按值和 `ref` 参数**之后**。
✔️ 在重写成员或实现接口时,参数命名保持**一致**。
❌ **禁止**使用保留参数(将来需要更多输入时,添加新重载即可)。
❌ **禁止**公开采用指针、指针数组或多维数组作为参数的方法。
### 8.2 枚举 vs 布尔参数
✔️ 如果不使用枚举则成员会有两个或更多布尔参数,请改用**枚举**。
✔️ 考虑为构造函数参数使用布尔值(真正的双状态值,仅用于初始化布尔属性时)。
❌ **禁止**使用布尔值,除非绝对确定永远不需要两个以上的值。
```csharp
// 错误:布尔参数含义不明确
Stream stream = File.Open("foo.txt", true, false);
// 正确:使用枚举,语义清晰
Stream stream = File.Open("foo.txt", CasingOptions.CaseSensitive, FileMode.Open);
```
### 8.3 参数验证
✔️ 验证传递到 `public``protected` 或显式实现成员的参数,验证失败则抛出 `ArgumentException` 或其子类。
✔️ 如果传入 `null` 且成员不支持,抛出 `ArgumentNullException`
✔️ 验证枚举参数CLR 允许将任何整数值转换为枚举值)。
✔️ 注意可变参数在验证后可能已更改(安全敏感成员应先创建副本)。
❌ **禁止**使用 `Enum.IsDefined` 进行枚举范围检查。
### 8.4 参数传递方式
| 方式 | 说明 |
|------|------|
| **按值** (默认) | 成员收到参数副本 |
| **ref** | 成员收到参数引用,可修改调用方传递的参数 |
| **out** | 类似 ref但初始未赋值必须在返回前赋值 |
❌ **避免**使用 `out``ref` 参数(需要指针经验,且两者区别未被广泛理解)。
❌ **禁止**按引用传递引用类型(有限例外:如交换引用的方法)。
❌ **禁止**对值类型使用只读引用传递(`in`)。
> 结构本身应设计为小型,按值传递的开销很低。使用 `in` 反而增加了 API 的复杂性,收益甚微。
### 8.5 可变参数数params
✔️ 如果希望最终用户传递少量元素的数组,考虑添加 `params` 关键字。
✔️ 考虑在简单重载中使用 `params`,即使更复杂的重载不使用。
✔️ 尝试对参数排序以便使用 `params`
✔️ 考虑在性能敏感的 API 中为少量参数提供特殊重载(避免创建临时数组)。
✔️ 注意 `null` 可作为 params 数组传递,处理前应验证非 null。
❌ 如果调用方几乎总是已有数组作为输入,不要使用 `params`
❌ 如果成员会修改 params 数组,不要使用 `params`(临时数组的修改会丢失)。
❌ **禁止**使用 `varargs`(省略号)方法(不符合 CLS 标准)。

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7ec531875d1515b4496e0e9035e63661
guid: f6b12280cb6fd3c42bfc80626a6eaf54
TextScriptImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,268 @@
# .NET 扩展设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 针对扩展性进行设计](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/designing-for-extensibility)
>
> 核心原则:**选择满足需求的最低成本扩展机制。可以在将来增加扩展性,但永远无法在不引入重大变更的情况下去除它。**
---
## 一、扩展性机制概述
框架中可通过多种方式实现扩展性,从低成本到高成本依次为:
| 机制 | 成本 | 能力 | 说明 |
|------|------|------|------|
| **非密封类** | 低 | 低 | 允许继承添加便捷成员 |
| **受保护成员** | 低 | 低-中 | 为子类提供高级自定义选项 |
| **事件** | 中 | 中 | 便捷一致的回调语法,与 IDE 集成 |
| **回调(委托)** | 中 | 中 | 允许用户提供自定义执行代码 |
| **虚拟成员** | 中-高 | 中-高 | 允许子类替代行为,适合特化场景 |
| **抽象(抽象类/接口)** | 高 | 高 | 最强大的扩展性是插件、IoC、管道等模式的核心 |
---
## 二、非密封类
默认情况下,类在大多数编程语言中是非密封的,这也是框架中大多数类的推荐默认值。
### 设计准则
✔️ 考虑使用没有额外虚拟成员或受保护成员的**非密封类**,作为一种为框架提供既经济又受欢迎的扩展性方法。
> 开发人员通常希望继承自非密封类,以便添加自定义构造函数、新方法或方法重载。例如 `System.Messaging.MessageQueue` 未密封,允许用户创建默认为特定队列路径的自定义队列。
> 非密封类型的测试成本相对较低,框架用户非常赞赏其提供的扩展性。
---
## 三、受保护成员
受保护成员本身不提供扩展性,但可以通过子类化来增强扩展性,用于公开高级自定义选项而无需复杂化主公共接口。
### 设计准则
✔️ 考虑使用受保护成员进行**高级自定义**。
✔️ 出于安全、文档和兼容性分析的目的,应将未密封类中的受保护成员**视为公共成员**。
> 任何人都可以继承未密封类并访问其受保护成员,因此公共成员所用的所有防御性编码做法同样适用于受保护成员。
---
## 四、事件和回调
回调是允许框架通过委托回调到用户代码的扩展点,通常通过方法参数传递委托。事件是回调的特例,提供便捷一致的语法。
### 设计准则
✔️ 考虑使用**回调**来允许用户提供框架要执行的自定义代码。
✔️ 考虑使用**事件**来允许用户自定义框架行为,无需了解面向对象设计。
✔️ **优先使用事件**而非普通回调,因为事件对更广泛的开发人员更熟悉,且与 Visual Studio 语句完成集成。
✔️ 定义带回调的 API 时,使用 `Func<...>``Action<...>``Expression<...>` 类型,而非自定义委托。
✔️ 衡量并了解使用 `Expression<...>` 对性能的影响。
> `Expression<...>` 在逻辑上等效于 `Func<...>` 和 `Action<...>` 委托,主要区别在于表达式适用于远程进程或计算机中求值的场景。
✔️ 理解调用委托即在执行**任意代码**,这可能导致安全性、正确性和兼容性问题。
❌ **避免**在性能敏感的 API 中使用回调。
---
## 五、虚拟成员
虚拟成员可以被替代以更改子类行为,在扩展性方面类似回调,但执行性能和内存消耗更优。
### 优缺点
| 方面 | 说明 |
|------|------|
| **优势** | 性能优于回调和事件;在特化场景中感觉更自然 |
| **劣势** | 行为只能在编译时修改(回调可在运行时修改);设计、测试和维护成本高;任何调用都可能被不可预测地替代 |
### 设计准则
❌ **禁止**无充分理由地使成员成为虚拟成员,需充分了解设计、测试和维护虚拟成员的所有成本。
> 虚拟成员对可进行的更改不太宽容,且比非虚拟成员慢(无法内联)。
✔️ 将扩展性限制在**绝对必要的范围内**。
✔️ 对虚拟成员优先选择 **`protected`** 访问级别而非 `public`
> 公共成员应通过调用受保护的虚拟成员来提供扩展性。这样将虚拟扩展点的范围限定在可使用的位置。
```csharp
// 推荐模式:公共方法调用受保护的虚拟方法
public class Control
{
public void Draw()
{
// 公共 API 逻辑...
OnDraw();
}
protected virtual void OnDraw()
{
// 子类可替代此方法来自定义绘制行为
}
}
```
---
## 六、抽象(抽象类型和接口)
抽象是描述协定但不提供完整实现的类型通常作为抽象类或接口实现。它们提供最强大的扩展性是插件、IoC、管道等架构模式的核心。
### 设计注意事项
- 有意义且经得起时间考验的抽象**很难设计**
- 成员过多 → 难以实现;成员过少 → 许多场景中无用
- 框架中抽象过多会**负面影响可用性**
- 良好的抽象对框架的**可测试性**非常重要(便于单元测试中替代依赖)
### 设计准则
**禁止**提供抽象,除非已通过开发**多个具体实现和 API** 对其进行测试验证。
✔️ 设计抽象时,在**抽象类和接口**之间仔细选择。
✔️ 考虑为抽象的具体实现提供**参考测试**,允许用户验证其实现是否正确实现了协定。
---
## 七、实现抽象的基类
基类主要设计为提供通用抽象或其他类的默认实现,位于继承层次结构的中间——根部的抽象和底部的自定义实现之间。
### 典型场景
```
IList<T> ← 抽象(接口)
Collection<T> ← 基类(提供默认实现)
MyCustomCollection ← 具体实现
```
> 例如 `Collection<T>` 和 `KeyedCollection<TKey,TItem>` 是 `IList<T>` 的实现辅助基类,降低了实现自定义集合的难度。
### 设计准则
✔️ 考虑使基类为 **`abstract`**,即使不包含抽象成员。
> 清楚地向用户传达该类专为继承设计。
✔️ 考虑将基类放置在**独立于主线场景类型的命名空间**中。
> 基类适用于高级扩展性场景,对大多数用户不感兴趣。
❌ 如果类用于公共 API**避免**使用 `Base` 后缀命名基类。
> 仅当基类为框架用户提供重要价值时才应使用。如果只对框架实现者有价值,应考虑**委托内部实现**而非继承。
---
## 八、密封
密封是限制扩展性的强大机制,可以密封整个类或单个成员。
### 8.1 密封类
❌ **禁止**无充分理由地密封类。
> "想不出扩展性场景"不是充分理由。框架用户喜欢出于各种原因继承类(如添加便捷成员)。
**密封类的充分理由**
| 理由 | 说明 |
|------|------|
| 静态类 | 静态类应声明为密封 |
| 安全敏感的受保护成员 | 类在继承的受保护成员中存储安全敏感信息 |
| 大量虚拟成员 | 逐个密封的成本超过保持非密封的好处 |
| 需要快速运行时查找的属性类 | 密封属性的性能略高于非密封的 |
### 8.2 密封成员
✔️ 考虑**密封你替代override的成员**。
> 引入虚拟成员可能导致的问题同样适用于替代(尽管程度稍低),密封替代可从继承层次结构的该点开始避免这些问题。
### 8.3 密封类型的约束
❌ **禁止**在密封类型上声明 `protected` 成员或 `virtual` 成员。
> 密封类型不可被继承,受保护成员无法被调用,虚拟方法无法被替代。
---
## 九、模板方法模式
模板方法模式允许子类在保留算法整体结构的同时重新定义其中的特定步骤。其核心目标是**控制扩展性**——将扩展点集中到单个受保护的虚拟方法中。
### 设计准则
❌ **避免**将 `public` 成员设为 `virtual`
✔️ 考虑使用**模板方法模式**提供更可控的扩展性。
✔️ 为非虚公共成员提供扩展点的受保护虚拟方法,命名时使用 **`Core` 后缀**或 **`Internal` 前缀**。
✔️ 在非虚方法中完成**参数验证和状态检查**,然后再调用虚拟方法。
```csharp
// 示例1Core 后缀风格
public class Control
{
// 公共非虚方法:执行验证,调用虚拟扩展点
public void SetBounds(int x, int y, int width, int height)
{
if (width < 0) throw new ArgumentOutOfRangeException(nameof(width));
if (height < 0) throw new ArgumentOutOfRangeException(nameof(height));
SetBoundsCore(x, y, width, height);
}
// 受保护虚拟方法:子类可替代的扩展点
protected virtual void SetBoundsCore(int x, int y, int width, int height)
{
// 默认实现...
}
}
// 示例2Internal 前缀风格
public abstract class AsyncOperationBase
{
internal void StartOperation()
{
// 状态检查、异常隔离...
InternalStart();
}
protected abstract void InternalStart();
}
```
> 非虚公共方法可确保特定代码在虚拟成员调用前后执行,并确保虚拟成员按固定顺序执行。常见错误是将多个重载都设为虚拟方法——应将扩展性集中到单个方法。
---
## 十、扩展机制选择速查表
| 需求 | 推荐机制 |
|------|---------|
| 允许用户添加便捷方法 | 非密封类 |
| 提供高级自定义选项 | 受保护成员 |
| 允许用户自定义行为(不需要 OOP 知识) | 事件 |
| 允许用户提供自定义执行逻辑 | 回调Func/Action |
| 允许子类特化行为(编译时) | 虚拟成员 |
| 定义可由多种类型实现的协定 | 抽象类 / 接口 |
| 降低实现抽象的难度 | 基类 |
| 限制扩展性 | 密封sealed |
> **原则**:优先选择低成本机制。在不确定时保持非密封,因为增加扩展性容易,但移除扩展性会引入破坏性变更。

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8292f707fbf60854e852e1a75824d892
guid: 9db6d128ca66fee43a47eabdabe83070
TextScriptImporter:
externalObjects: {}
userData:

View File

@@ -0,0 +1,255 @@
# .NET 异常设计准则
> 参考来源:[Microsoft .NET 框架设计准则 - 异常设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/exceptions)
>
> 核心原则:**异常处理比基于返回值的错误报告具有更多优势。框架应使用异常报告所有错误条件,包括执行错误。**
---
## 一、异常引发
### 1.1 基本原则
✔️ **必须**通过引发异常来报告执行失败。
> 当成员无法完成其设计用途(成员名称所暗示的内容)时,就是执行失败。例如 `OpenFile` 无法返回打开的文件句柄。
✔️ 如果代码遇到进一步执行不安全的情况,考虑调用 `System.Environment.FailFast` 终止进程,而不是引发异常。
✔️ 考虑引发异常对性能的影响。
> **每秒抛出速率超过 100 次**可能会显著影响大多数应用程序的性能。
✔️ **必须**记录所有由公开成员因违反成员协定而引发的异常,并将其视为协定的一部分。
> 协定中的异常不应在版本间更改(异常类型不变、不添加新异常)。
❌ **禁止**返回错误代码。异常是框架中报告错误的主要方法。
❌ **禁止**对正常控制流使用异常(系统故障和争用条件除外)。
❌ **禁止**让公共成员根据某些选项决定是否引发异常。
❌ **禁止**让公共成员将异常作为返回值或 `out` 参数返回。
### 1.2 异常引发的代码组织
✔️ 考虑使用**异常生成器方法**Exception Builder
> 从不同位置引发相同异常是常见的。使用辅助方法创建并初始化异常,可避免代码膨胀,并允许引发异常的成员被内联。
```csharp
// 异常生成器方法示例
private static void ThrowArgumentNullException(string paramName)
{
throw new ArgumentNullException(paramName);
}
public void DoSomething(string value)
{
if (value == null)
ThrowArgumentNullException(nameof(value));
// ... 方法逻辑(可被内联)
}
```
❌ **禁止**从异常筛选器块exception filter block中引发异常。
> 筛选器中引发的异常会被 CLR 捕获,导致筛选器返回 false难以调试。
❌ **避免**从 `finally` 块显式引发异常(由方法调用隐式引发的可以接受)。
---
## 二、使用标准异常类型
### 2.1 Exception 和 SystemException
❌ **禁止**引发 `System.Exception``System.SystemException`
❌ **禁止**在框架代码中捕获 `System.Exception``System.SystemException`,除非打算重新引发。
❌ **避免**捕获 `System.Exception``System.SystemException`(顶级异常处理程序除外)。
### 2.2 ApplicationException
❌ **禁止**引发或派生自 `ApplicationException`
### 2.3 InvalidOperationException
✔️ 如果对象处于**不适当的状态**,引发 `InvalidOperationException`
```csharp
// 示例:对象状态不正确
public void Start()
{
if (_isRunning)
throw new InvalidOperationException("服务已在运行中。");
// ...
}
```
### 2.4 ArgumentException 系列
✔️ 如果传递给成员的参数错误,引发 `ArgumentException` 或其子类型之一,优先使用**派生程度最高的异常类型**。
✔️ 引发 `ArgumentException` 子类时,**必须**设置 `ParamName` 属性。
✔️ 使用 `value` 作为属性 setter 隐式值参数的名称。
| 异常类型 | 使用场景 |
|---------|---------|
| `ArgumentException` | 参数值不合法 |
| `ArgumentNullException` | 参数为 null 但不支持 null |
| `ArgumentOutOfRangeException` | 参数值超出有效范围 |
```csharp
// 推荐用法
public void SetAge(int age)
{
if (age < 0 || age > 150)
throw new ArgumentOutOfRangeException(nameof(age), age, "年龄必须在 0-150 之间。");
}
public void SetName(string name)
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (name.Length == 0)
throw new ArgumentException("名称不能为空。", nameof(name));
}
```
### 2.5 由 CLR 保留的异常(禁止显式引发)
以下异常仅由 CLR 基础设施引发,**禁止在用户代码中显式引发或捕获**
| 异常类型 | 说明 |
|---------|------|
| `NullReferenceException` | 空引用访问(应通过参数检查避免) |
| `IndexOutOfRangeException` | 数组越界(应通过参数检查避免) |
| `AccessViolationException` | 非法内存访问 |
| `StackOverflowException` | 栈溢出(禁止引发,禁止捕获) |
| `OutOfMemoryException` | 内存不足 |
| `COMException` | COM 互操作异常 |
| `SEHException` | 结构化异常处理 |
| `ExecutionEngineException` | 执行引擎异常 |
> 这些异常指示代码中的 bug 或 CLR 级别的故障,显式引发它们会暴露方法的实现细节。应通过参数检查来预防。
### 2.6 框架内部异常YooInternalException
`YooInternalException` 用于框架内部不变量断言,表示"理论上不可能到达的代码路径"。它与 `ArgumentException` 系列的区别在于**语义和受众**不同:
| 异常类型 | 语义 | 适用场景 |
|---------|------|---------|
| `ArgumentException` 系列 | 调用方传入了错误参数 | 公共 API 的参数校验 |
| `InvalidOperationException` | 对象处于不适当的状态 | 公共 API 的状态校验 |
| `YooInternalException` | 框架内部逻辑错误,理论不可达 | 内部方法的不变量断言 |
✔️ 在 `internal` 方法或框架内部流程中,对理论上不可能发生的状态使用 `YooInternalException`
✔️ 优先使用 `YooInternalException` 而非 `Debug.Assert`,确保在所有构建模式下均能捕获内部逻辑错误。
❌ **禁止**在公共 API 的参数校验中使用 `YooInternalException`,应使用 `ArgumentException` 系列。
---
## 三、异常和性能
### 3.1 性能问题
异常在引发时性能可能慢几个数量级。在严格遵循准则的同时,可通过两种设计模式实现良好性能。
❌ **禁止**因担心异常对性能的影响而使用错误代码。
### 3.2 Tester-Doer 模式(测试者-执行者)
将可能引发异常的成员拆分为两部分:先测试条件,再执行操作。
```csharp
// Tester-Doer 模式
ICollection<int> numbers = ...;
// Tester先检查前置条件
if (!numbers.IsReadOnly)
{
// Doer再执行操作
numbers.Add(1);
}
```
| 角色 | 说明 | 示例 |
|------|------|------|
| **Tester测试者** | 检查前置条件是否满足 | `IsReadOnly``CanSeek``ContainsKey` |
| **Doer执行者** | 执行可能引发异常的操作 | `Add``Seek``GetValue` |
✔️ 考虑对在**常见场景中可能引发异常**的成员使用 Tester-Doer 模式。
### 3.3 Try-Parse 模式(尝试-分析)
对于**极其性能敏感**的 API使用比 Tester-Doer 更快的 Try-Parse 模式。
```csharp
// Try-Parse 模式
public struct DateTime
{
// 失败时引发异常
public static DateTime Parse(string dateTime) { ... }
// 失败时返回 false成功通过 out 参数返回结果
public static bool TryParse(string dateTime, out DateTime result) { ... }
}
// 使用方式
if (DateTime.TryParse(input, out var date))
{
// 解析成功
}
else
{
// 解析失败,无异常开销
}
```
✔️ 对在常见场景中可能引发异常的成员使用 Try-Parse 模式。
✔️ 方法命名使用 **`Try` 前缀**和 **`bool` 返回类型**。
✔️ 为每个 Try-Parse 成员提供一个对应的**异常引发版本**。
> 例如 `Parse` / `TryParse`、`GetValue` / `TryGetValue`。
---
## 四、速查表
### 4.1 何时引发异常
| 场景 | 行为 |
|------|------|
| 成员无法完成其设计用途 | 引发异常 |
| 参数为 null不支持时 | 引发 `ArgumentNullException` |
| 参数超出有效范围 | 引发 `ArgumentOutOfRangeException` |
| 参数值不合法 | 引发 `ArgumentException` |
| 对象状态不正确 | 引发 `InvalidOperationException` |
| 操作不被支持 | 引发 `NotSupportedException` |
| 进一步执行不安全 | 调用 `Environment.FailFast` |
### 4.2 禁止做的事情
| 禁止 | 原因 |
|------|------|
| 返回错误代码 | 异常是唯一的错误报告方式 |
| 用异常做控制流 | 异常仅用于异常情况 |
| 引发 `Exception` / `SystemException` | 过于笼统 |
| 引发 CLR 保留异常 | 由运行时专用 |
| 在 finally 中显式引发 | 可能掩盖原始异常 |
| 在异常筛选器中引发 | 难以调试 |
### 4.3 性能优化模式选择
| 模式 | 适用场景 | 示例 |
|------|---------|------|
| **Tester-Doer** | 常见场景中的条件检查 | `if (!IsReadOnly) Add(item)` |
| **Try-Parse** | 极其性能敏感的 API | `TryParse(input, out result)` |

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d10f50d328fa47f44ad92a7b89f3d757
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,335 @@
# .NET 设计模式准则
> 参考来源:《框架设计准则:可重用 .NET 库的约定、惯例和模式第3版》第9章
>
> 核心原则:**框架设计者应提供完整的端到端解决方案,而非仅提供 API。高级 API 应当"开箱即用",用户无需了解底层复杂性。**
---
## 一、聚合组件设计
聚合组件将多个底层分解类型Factored Types绑定到一个更高级别的组件中为常见场景提供简化入口。它是开发者探索其命名空间的**起点**。
### 1.1 Create-Set-Call 使用模式
所有聚合组件都应支持此模式:
```csharp
// 1. Create使用默认或简单构造函数创建
var queue = new MessageQueue();
// 2. Set设置属性
queue.Path = @".\private$\orders";
// 3. Call调用方法
queue.Send("Hello World");
```
### 1.2 聚合组件 vs 分解类型
| 特征 | 聚合组件 | 分解类型 |
|------|---------|---------|
| **目的** | 简化常见场景 | 提供精细控制 |
| **状态** | 可以有模式和临时无效状态 | 不应有模式,生命周期清晰 |
| **构造** | 默认/简单构造函数 | 可能需要复杂初始化 |
| **可用性优先级** | 最高 | 较低 |
| **示例** | `SerialPort``MessageQueue` | `Stream``GZipStream` |
```csharp
// 聚合组件暴露内部分解类型以支持高级场景
var port = new SerialPort("COM1");
port.Open();
GZipStream compressed = new GZipStream(port.BaseStream, CompressionMode.Compress);
compressed.Write(data, 0, data.Length);
port.Close();
```
### 1.3 设计准则
✔️ 考虑为常用功能区域提供聚合组件。
✔️ 设计聚合组件以便**极简初始化后即可使用**。如果需要初始化,未初始化时引发的异常应清楚说明需要做什么。
✔️ **必须**支持 Create-Set-Call 使用模式。
✔️ 为所有构造函数参数提供对应的可读写属性,确保始终可以用默认构造函数 + 属性设置代替参数化构造函数。
✔️ 在聚合组件中使用**事件**代替委托 API 和需要重写的虚拟成员。
✔️ 选择最具"吸引力"的名称给聚合组件(如 `File`),将较不直观的名称留给分解类型(如 `StreamReader`)。
❌ **禁止**在常见场景中要求用户继承、重写方法或实现接口。
❌ **禁止**在常见场景中要求用户做编写代码以外的事(如配置文件、生成资源文件等)。
❌ **禁止**将分解类型设计为有模式的,它们应有明确的生命周期。
✔️ 当用户在无效状态下调用方法时,抛出 `InvalidOperationException`,异常消息应清楚说明需要更改哪些属性。
---
## 二、异步编程准则
### 2.1 异步方法返回类型
✔️ 考虑在方法**经常同步完成**时返回 `ValueTask<T>`
> 例如 `Stream.ReadAsync` 由于缓冲区中常有可用数据,经常同步完成。使用 `ValueTask<T>` 避免了每次调用都分配 `Task<T>` 对象的开销。
```csharp
// ValueTask<T> 适用于经常同步完成的场景
public virtual ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default);
```
> 注意:当方法确实异步完成时,`ValueTask<T>` 仍然需要创建 `Task` 实例。对于始终异步完成的操作,`ValueTask<T>` 会带来轻微的性能损失和可用性降低,此时应使用 `Task<T>`。
### 2.2 ConfigureAwait
✔️ 在 async 方法中等待异步操作时,使用 `await task.ConfigureAwait(false)`,除非在依赖同步上下文的应用模型中。
> 默认情况下,.NET 使用 `SynchronizationContext.Current` 决定如何继续处理 Task。控制台应用中Task 完成回调在后台线程上执行。WinForms/WPF 中,默认将所有回调调度到 UI 线程。`ConfigureAwait(false)` 避免了不必要的线程切换。
### 2.3 避免死锁
❌ **禁止**在 async 方法中调用 `Task.Wait()` 或读取 `Task.Result` 属性,应使用 `await`
> `await` 会让出当前 Task允许其他 Task 继续。`Task.Wait()` 和 `Task.Result` 执行阻塞式同步等待,效率低下且在某些场景下会导致死锁。
✔️ 在 async 方法的实现中,调用**异步方法变体**而非同步变体。
### 2.4 CancellationToken 取消准则
✔️ 因 `CancellationToken` 取消工作时,抛出 `OperationCanceledException`
```csharp
// 两种等效写法
if (cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);
// 或者
cancellationToken.ThrowIfCancellationRequested();
```
### 2.5 异步方法中的异常
✔️ 将用法错误(如参数验证)的异常直接从非 async 包装方法中抛出,以提高可调试性。
```csharp
// 正确:异常直接抛出,不包装在 Task 中
public Task SaveAsync(string filename)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename));
return SaveAsyncCore(filename);
}
private async Task SaveAsyncCore(string filename)
{
// 实际异步工作...
}
```
```csharp
// 错误:异常在 Task 内部抛出,调用栈不清晰
public async Task SaveAsync(string filename)
{
if (filename == null)
throw new ArgumentNullException(nameof(filename));
// ...
}
```
### 2.6 IAsyncEnumerable\<T\>
✔️ 在使用 `yield return``IAsyncEnumerable` 方法中,为 `CancellationToken` 参数添加 `[EnumeratorCancellation]` 特性。
> 不添加此特性时,`GetAsyncEnumerator` 传入的 `CancellationToken` 值会被编译器生成的实现忽略。
```csharp
public static async IAsyncEnumerable<int> GenerateValues(
int start, int count,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
int end = start + count;
for (int i = start; i < end; i++)
{
await Task.Delay(i, cancellationToken).ConfigureAwait(false);
yield return i;
}
}
```
✔️ 对 `IAsyncEnumerable` 参数使用 `await foreach` 时,使用 `WithCancellation` 传递 `CancellationToken`
```csharp
await foreach (int value in source.WithCancellation(cancellationToken).ConfigureAwait(false))
{
// ...
}
```
---
## 三、Dispose 模式与 IAsyncDisposable
### 3.1 基本 Dispose 模式
托管内存只是系统资源的一种。文件句柄、网络连接、数据库连接等非托管资源仍需显式释放。
终结器Finalizer的缺点
- GC 在**不确定的时间**才调用终结器,延迟可能不可接受
- 需要终结的对象的内存回收会被**推迟到下一轮 GC**
✔️ 考虑在本身不持有非托管资源的类上实现基本 Dispose 模式,如果其**子类型可能持有**。
> 例如 `System.IO.Stream` 是一个不持有资源的抽象基类,但其大多数子类持有资源。
✔️ 在对象被释放后,从任何不能使用的成员中抛出 `ObjectDisposedException`
✔️ 考虑提供 `Close()` 方法作为 `Dispose()` 的补充(如果"close"是该领域的标准术语)。
### 3.2 作用域操作
✔️ 考虑返回一个 disposable 值,代替让调用者手动配对 "begin" 和 "end" 方法。
```csharp
// 使用 disposable 作用域代替 begin/end 配对
using (var scope = logger.BeginScope("Processing"))
{
// 作用域内的操作...
} // 自动结束作用域
```
### 3.3 IAsyncDisposable
`IAsyncDisposable` 允许在 `Dispose()` 涉及 I/O 或其他阻塞操作时使用异步方法进行资源清理。
```csharp
public partial class SomeType : IDisposable, IAsyncDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) { /* ... */ }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
Dispose(false);
GC.SuppressFinalize(this);
}
protected virtual ValueTask DisposeAsyncCore() { /* ... */ }
}
```
> 同时实现 `IDisposable` 和 `IAsyncDisposable` 时,`DisposeAsync` 调用 `Dispose(false)` 以避免同步释放路径重复执行。
---
## 四、Span / Memory 缓冲区操作
### 4.1 Span\<T\> 和 ReadOnlySpan\<T\>
`Span<T>` 表示来自托管数组、stackalloc 数组、指针或字符串的连续内存,支持 O(1) 切片操作。
✔️ 考虑使用 `Span<T>` 作为缓冲区的表示。
✔️ 尽可能使用 `ReadOnlySpan<T>` 代替 `Span<T>`
❌ **避免**返回 Span 或 ReadOnlySpan除非返回值的生命周期非常明确。
✔️ 对返回的、非来自调用方的 Span 提供**非常清晰的所有权规则文档**。
✔️ 考虑从 get-only 属性或无参方法返回 `ReadOnlySpan<T>` 来表示固定数据(当 T 是不可变类型时)。
✔️ 考虑返回 `System.Range`(表示 Span 参数的边界)而非参数的切片。
> `Span<T>` 可以转换为 `ReadOnlySpan<T>`,但反过来不行。如果方法接受 `ReadOnlySpan` 并返回切片,调用方无法轻松确定其可写 Span 的等效切片。
### 4.2 Memory\<T\> 和 ReadOnlyMemory\<T\>
`Span<T>``ref struct`,不能在异步方法中使用。需要存储缓冲区时应使用 `Memory<T>`
✔️ 在异步方法中使用 `ReadOnlyMemory<T>` 代替 `ReadOnlySpan<T>`
✔️ 在异步方法中使用 `Memory<T>` 代替 `Span<T>`
✔️ 当构造函数或方法的目的是**保存缓冲区引用**时,使用 `Memory<T>` / `ReadOnlyMemory<T>` 作为参数。
### 4.3 命名和重载
✔️ 对接受输出缓冲区和单个输入缓冲区的方法,输入缓冲区参数命名为 **`source`**,输出缓冲区参数命名为 **`destination`**。
❌ **避免**在 `Span<T>``ReadOnlySpan<T>` 之间,或 `Memory<T>``ReadOnlyMemory<T>` 之间重载同一方法。
> 如果一个方法以只读方式操作缓冲区,另一个以读写方式操作,它们应有不同的方法名称。
---
## 五、工厂模式
工厂是抽象对象创建过程的操作或操作集合,允许专用语义和更细粒度的实例化控制。
### 设计准则
✔️ **优先使用构造函数**而非工厂,因为构造函数通常更可用、一致和方便。
✔️ 当工厂方法声明在独立的工厂类型上时,考虑使用 **`Create` + 类型名称** 命名。
> 例如创建按钮的工厂方法命名为 `CreateButton`。在某些情况下可使用领域特定名称,如 `File.Open`。
✔️ 考虑使用 **类型名称 + `Factory`** 命名工厂类型。
> 例如创建 `Control` 对象的工厂类型命名为 `ControlFactory`。
---
## 六、超时处理
✔️ **优先使用方法参数**作为用户指定超时时间的机制。
```csharp
server.PerformOperation(timeout);
```
> 备选方案是使用属性:
```csharp
server.Timeout = timeout;
server.PerformOperation();
```
✔️ 优先使用 `TimeSpan` 表示超时时间。
✔️ 在 Unity 项目中,可以使用 `long` 毫秒数代替 `TimeSpan` 表示超时或时间切片,以避免值类型转换开销。
✔️ 超时到期时抛出 `System.TimeoutException`
❌ **禁止**使用错误代码指示超时到期。
---
## 七、LINQ 支持
✔️ 实现 `IEnumerable<T>` 以启用基本 LINQ 支持。
```csharp
public class RangeOfInt32s : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator() { /* ... */ }
IEnumerator IEnumerable.GetEnumerator() { /* ... */ }
}
// 实现 IEnumerable<T> 后即可使用 LINQ
var result = new RangeOfInt32s().Where(x => x > 10);
```
✔️ 考虑实现 `ICollection<T>` 以提升查询运算符的性能。
✔️ 将查询扩展方法放在主命名空间的 **`Linq` 子命名空间**中。
> 例如 `System.Data` 功能的扩展方法应放在 `System.Data.Linq` 命名空间中。

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 1ddd0c64da4a16244bf65c56c9d403b2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,390 @@
# .NET 使用指南
> 参考来源:[Microsoft .NET 框架设计准则 - 使用指南](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/usage-guidelines)
>
> 核心原则:**在公开 API 中正确使用内置框架类型,优先选择抽象和接口而非具体实现类型。**
---
## 一、数组
✔️ 在公共 API 中优先使用**集合**而不是数组。
✔️ 考虑使用**交错数组**`int[][]`)而不是多维数组(`int[,]`)。
> 交错数组的元素大小可以不同,浪费空间更少。此外 CLR 优化了交错数组的索引操作,在某些场景中性能更好。
❌ **禁止**使用只读数组字段。
> 字段本身是只读的不能被替换,但数组中的元素仍可被修改。
```csharp
// 错误:数组元素仍可被外部修改
public static readonly int[] InvalidValues = { 1, 2, 3 };
// 正确:使用 ReadOnlyCollection 或 ImmutableArray
public static readonly ReadOnlyCollection<int> InvalidValues =
new ReadOnlyCollection<int>(new[] { 1, 2, 3 });
```
---
## 二、特性Attribute
特性是可添加到程序集、类型、成员和参数的元数据注释,通过反射 API 在运行时访问。
### 设计准则
✔️ 使用 `Attribute` 后缀命名自定义特性类。
✔️ **必须**对自定义特性应用 `[AttributeUsage]`
✔️ 为**必需参数**提供只读属性和对应的构造函数参数。
✔️ 为**可选参数**提供可读写属性(通过 setter 设置)。
✔️ 如果可能,请**密封**自定义特性类(使查找速度更快)。
❌ **避免**重载自定义特性的构造函数(单一构造函数更清晰)。
❌ **避免**提供构造函数参数来初始化可选属性(不要同时支持构造函数和 setter 两种方式设置同一属性)。
```csharp
// 推荐设计
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class MyCustomAttribute : Attribute
{
// 必需参数:通过构造函数 + 只读属性
public MyCustomAttribute(string name)
{
Name = name;
}
public string Name { get; }
// 可选参数:通过可读写属性
public int Priority { get; set; }
}
// 使用方式
[MyCustom("example", Priority = 5)]
public class MyClass { }
```
---
## 三、集合
### 3.1 公共 API 中的集合选择
❌ **禁止**在公共 API 中使用弱类型集合。
> 所有表示集合项的返回值和参数都应使用确切的项类型。
❌ **禁止**在公共 API 中使用 `ArrayList``List<T>`
> 这些是为内部实现设计的数据结构。`List<T>` 牺牲了 API 简洁性和灵活性来优化性能。
❌ **禁止**在公共 API 中使用 `Hashtable``Dictionary<TKey,TValue>`
> 应使用 `IDictionary`、`IDictionary<TKey,TValue>` 或实现这些接口的自定义类型。
❌ **禁止**在同一类型上同时实现 `IEnumerator<T>``IEnumerable<T>`
❌ **禁止**从非 `GetEnumerator` 方法返回枚举器类型。
### 3.2 集合参数
✔️ 尽可能使用**最不专用的类型**作为参数类型。
> 大多数以集合为参数的成员应使用 `IEnumerable<T>` 接口。
❌ **避免**仅为访问 `Count` 属性而使用 `ICollection<T>` 作为参数。
> 应使用 `IEnumerable<T>`,并动态检查是否实现了 `ICollection<T>`。
### 3.3 集合属性和返回值
❌ **禁止**提供可设置的集合属性。
> 用户可以通过先清除再添加来替换内容。如需替换整个集合,提供 `AddRange` 方法。
✔️ 对读写集合的属性或返回值使用 `Collection<T>` 或其子类。
✔️ 对只读集合使用 `ReadOnlyCollection<T>` 或其子类。
❌ **禁止**从集合属性或返回集合的方法中返回 `null`
> **应返回空集合或空数组**。一般规则null 和空集合应被视为相同。
### 3.4 快照集合 vs 实时集合
| 类型 | 说明 | 示例 |
|------|------|------|
| **快照集合** | 表示某个时间点的状态 | 数据库查询结果 |
| **实时集合** | 始终表示当前状态 | ComboBox 的 Items |
❌ **禁止**从属性返回快照集合getter 应轻量,快照需要 O(n) 复制)。
✔️ 使用快照集合或实时 `IEnumerable<T>` 表示可变集合(无需显式修改即可变化的集合)。
### 3.5 数组 vs 集合选择
✔️ 在大多数情况下**优先选择集合**——更好的内容控制、更易使用。
✔️ 在**低级 API** 中考虑使用数组以最小化内存消耗和最大化性能。
✔️ 使用**字节数组**`byte[]`)而非字节集合。
❌ 如果每次调用属性 getter 都需要返回新数组(如内部数组的副本),**禁止**对属性使用数组。
### 3.6 自定义集合设计
✔️ 设计新集合时考虑继承自 `Collection<T>``ReadOnlyCollection<T>``KeyedCollection<TKey,TItem>`
✔️ **必须**实现 `IEnumerable<T>`,按需考虑实现 `ICollection<T>``IList<T>`
✔️ 如果集合项具有唯一键,考虑使用**键控集合**(继承自 `KeyedCollection<TKey,TItem>`)。
❌ **禁止**继承自非泛型基集合(如 `CollectionBase`)。
❌ **避免**在与集合概念无关的复杂 API 类型上实现集合接口。
### 3.7 自定义集合命名
✔️ 实现 `IDictionary` / `IDictionary<TKey,TValue>` 的类型使用 **`Dictionary`** 后缀。
✔️ 实现 `IEnumerable` 且表示项列表的类型使用 **`Collection`** 后缀。
✔️ 考虑用项类型名称作为集合名称前缀:`AddressCollection`
✔️ 只读集合考虑使用 **`ReadOnly`** 前缀:`ReadOnlyStringCollection`
❌ **避免**使用表示具体实现的后缀(如 `LinkedList``Hashtable`)。
---
## 四、序列化
序列化是将对象转换为可持久保存或传输格式的过程。
### 4.1 三种序列化技术
| 技术 | 主要类型 | 适用场景 |
|------|---------|---------|
| **数据协定序列化** | `[DataContract]` / `[DataMember]` / `DataContractSerializer` | 一般持久性、Web 服务、JSON |
| **XML 序列化** | `XmlSerializer` | 需要完全控制 XML 形状 |
| **运行时序列化** | `[Serializable]` / `ISerializable` / `BinaryFormatter` | .NET 远程处理 |
### 4.2 选择准则
✔️ 设计新类型时,考虑序列化需求。
✔️ 如果类型需要持久保存或用于 Web 服务,考虑支持**数据协定序列化**。
✔️ 如果需要完全控制 XML 形状,考虑支持 **XML 序列化**
✔️ 如果类型需要跨 .NET 远程处理边界传输,考虑支持**运行时序列化**。
❌ **避免**仅出于一般持久性原因支持运行时序列化或 XML 序列化,应优先使用数据协定序列化。
### 4.3 数据协定序列化准则
✔️ 如果类型可在部分信任中使用,考虑将数据成员标记为 `public`
✔️ 具有 `[DataMember]` 的属性**必须**同时具有 getter 和 setter。
✔️ 考虑使用序列化回调初始化反序列化后的实例(反序列化时不调用构造函数)。
> 使用 `[OnDeserialized]`、`[OnDeserializing]`、`[OnSerializing]`、`[OnSerialized]` 特性。
✔️ 考虑使用 `[KnownType]` 指示反序列化复杂对象图时应使用的具体类型。
✔️ 创建或更改可序列化类型时,考虑**向后和向前兼容性**。
✔️ 考虑实现 `IExtensibleDataObject` 以允许不同版本间的往返序列化。
### 4.4 XML 序列化准则
❌ **避免**专门为 XML 序列化设计类型(已被数据协定序列化取代)。
✔️ 如果需要精细控制 XML 形状,考虑实现 `IXmlSerializable` 接口。
### 4.5 运行时序列化准则
✔️ 如果需要完全控制序列化过程,实现 `ISerializable` 并提供序列化构造函数。
✔️ 序列化构造函数应为 `protected`
✔️ **必须**显式实现 `ISerializable` 成员。
```csharp
[Serializable]
public class Person : ISerializable
{
public string Name { get; set; }
public Person() { }
// 序列化构造函数protected
protected Person(SerializationInfo info, StreamingContext context)
{
Name = info.GetString(nameof(Name));
}
// 显式实现
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(Name), Name);
}
}
```
---
## 五、System.Xml 用法
❌ **禁止**使用 `XmlNode``XmlDocument` 表示 XML 数据。
✔️ 使用 `XmlReader``XmlWriter``IXPathNavigable``XNode` 子类型作为接受或返回 XML 的成员的输入输出。
> 使用抽象类型可以将方法与内存中 XML 文档的具体实现分离,允许与虚拟 XML 数据源一起工作。
❌ 如果要创建表示基础对象模型的 XML 视图,**禁止**继承 `XmlDocument`
---
## 六、相等运算符
### 6.1 通用准则
❌ **禁止**只重载 `==``!=` 中的一个而忽略另一个(必须成对重载)。
✔️ 确保 `Object.Equals` 和相等运算符具有**相同的语义和相似的性能**。
> 重载相等运算符时,通常需要同时重写 `Object.Equals`。
❌ **避免**从相等运算符引发异常(参数为 null 时应返回 false
### 6.2 值类型的相等运算符
✔️ 如果相等有意义,**应该**对值类型重载相等运算符。
> 大多数语言没有为值类型提供默认的 `operator==` 实现。
### 6.3 引用类型的相等运算符
❌ **避免**在**可变引用类型**上重载相等运算符。
> 内置运算符实现引用相等性,改为值相等性会使许多开发人员困惑。不可变引用类型则问题较小。
❌ **避免**对引用类型重载相等运算符(如果实现速度远低于引用相等性)。
---
## 七、IEquatable / IComparable 准则
`IComparable<T>` 定义排序语义(小于、等于、大于),主要用于排序。`IEquatable<T>` 定义相等语义,主要用于查找。
✔️ **必须**在值类型上实现 `IEquatable<T>`
> `Object.Equals` 在值类型上会导致装箱,且默认实现使用反射,效率低下。
✔️ 实现 `IEquatable<T>` 时**必须**同时重写 `Object.Equals`
✔️ 实现 `IEquatable<T>` 时考虑重载 `operator==``operator!=`
✔️ 实现 `IComparable<T>` 时**必须**同时实现 `IEquatable<T>`
✔️ 重写 `Object.Equals` 时**必须**同时重写 `GetHashCode`
---
## 八、Object 准则
### 8.1 Object.Equals
- **值类型**默认实现:当所有字段相等时返回 true"值相等"),但实现**使用反射**,通常效率低下,**应当重写**
- **引用类型**默认实现:当两个引用指向同一对象时返回 true"引用相等"
❌ **禁止**在**可变引用类型**上实现值相等。
> 实现值相等的引用类型(如 `System.String`)应当是不可变的。
### 8.2 Object.GetHashCode
✔️ 重写 `Equals` 时**必须**重写 `GetHashCode`,确保被视为相等的两个对象具有相同的哈希码。
✔️ 尽力确保 `GetHashCode` 为类型的所有对象生成**均匀分布**的数字,以最小化哈希表冲突。
### 8.3 Object.ToString
`Object.ToString` 用于通用显示和调试目的,默认实现仅返回类型名称。
✔️ 建议**始终重写** `ToString`,提供有意义的字符串表示。
---
## 九、Nullable\<T\> 准则
❌ **禁止**使用 `Nullable<T>` 表示可选参数。
> 应使用方法重载来表示可选参数。
```csharp
// 正确:使用重载
public class Foo
{
public Foo(string name, int id) { }
public Foo(string name) { }
}
```
❌ **避免**使用 `Nullable<bool>` 表示通用三态值。
> `Nullable<bool>` 仅应用于真正可选的布尔值true、false、不可用。如果需要表示三种状态如 yes、no、cancel应使用枚举。
---
## 十、URI 准则
✔️ 使用 `System.Uri` 表示 URI 和 URL 数据,而非普通字符串。
> `System.Uri` 提供了更安全、更丰富的 URI 表示方式。使用普通字符串大量操作 URI 相关数据已被证明会导致许多安全和正确性问题。
---
## 十一、ICloneable 准则
❌ **禁止**在公共 API 中使用 `ICloneable`
> `ICloneable` 的问题在于协定未指定克隆是浅拷贝还是深拷贝——有些实现返回浅拷贝,有些返回深拷贝。调用者永远无法确定会得到什么,这使得该接口毫无用处。
✔️ 如果需要克隆机制,考虑在类型上定义自己的 `Clone` 方法,并明确文档说明是浅拷贝还是深拷贝。
---
## 十二、速查表
### 公共 API 中的类型选择
| 场景 | 推荐 | 避免 |
|------|------|------|
| 集合参数 | `IEnumerable<T>` | `List<T>``ArrayList` |
| 读写集合返回值 | `Collection<T>` 或子类 | `List<T>` |
| 只读集合返回值 | `ReadOnlyCollection<T>` | 数组(需复制) |
| 字典参数/返回值 | `IDictionary<TKey,TValue>` | `Dictionary<TKey,TValue>` |
| 表示 XML 数据 | `XmlReader``XNode` | `XmlNode``XmlDocument` |
| 序列化 | 数据协定序列化 | 运行时序列化(除非远程处理) |
| 低级高性能 API | 数组 | 集合 |
| 字节数据 | `byte[]` | `Collection<byte>` |
### 禁止事项
| 禁止 | 原因 |
|------|------|
| 公共 API 返回 `null` 集合 | 应返回空集合或空数组 |
| 可设置的集合属性 | 用户应通过 Clear+Add 替换内容 |
| 只读数组字段 | 元素仍可被修改 |
| 属性返回快照集合 | getter 应轻量,不应有 O(n) 开销 |
| 继承 `CollectionBase` | 应使用泛型基类 |

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 95ca2d569cd5c124892e8f6e0a01efea
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
# .NET 日志规范
> 参考来源:[Microsoft .NET 框架设计准则 - 异常消息](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/exceptions-and-performance) · [.NET Runtime 源码日志风格](https://github.com/dotnet/runtime)
>
> 核心原则:**措辞简洁准确、格式统一规范、使用地道英语、避免泄露敏感信息**
---
## 一、变量引用规则
### 1.1 字符串变量值加单引号
文件路径、名称、GUID、标识符等字符串变量值用单引号包裹便于在日志中区分变量边界。
```csharp
// ✔️
$"File cache entry not found: '{bundleGuid}'."
$"Invalid clear method: '{options.ClearMethod}'."
$"Could not find file '{filePath}'."
// ❌
$"File cache entry not found: {bundleGuid}."
```
### 1.2 数值变量不加引号
数值类型的变量(整数、浮点、枚举值)不加引号。
```csharp
// ✔️
$"MaxTimeSlice must be at least {MinTimeSlice} ms."
$"Does not support bundle type: {options.Bundle.BundleType}."
// ❌
$"Does not support bundle type: '{options.Bundle.BundleType}'."
```
### 1.3 类型名作句首主语不加引号
类型名(通常通过 `nameof()``GetType().Name` 获取)做句子主语时,不加引号。
```csharp
// ✔️
$"{nameof(SandboxBundleCache)} does not support synchronous loading."
$"AsyncOperationSystem is not initialized."
// ❌
$"'{nameof(SandboxBundleCache)}' does not support synchronous loading."
```
### 1.4 异常对象不加引号
异常对象(`{ex}``{ex.Message}`)直接拼接,不加引号。
```csharp
// ✔️
YooLogger.Error($"File verification exception: {ex.Message}.");
YooLogger.Warning($"Failed to delete cache bundle folder: {ex}.");
// ❌
YooLogger.Error($"File verification exception: '{ex.Message}'.");
```
---
## 二、消息格式规则
### 2.1 首字母大写,末尾加句号
所有日志和异常消息使用 Sentence case首字母大写末尾加句号。
```csharp
// ✔️
SetError("Unity engine load failed.");
throw new Exception("Catalog file data is null or empty.");
// ❌
SetError("unity engine load failed");
throw new Exception("catalog file data is null or empty");
```
### 2.2 使用完整句子
使用语法完整的句子,不省略主语或谓语。
> **来源**.NET 官方规范 — *"Do use complete sentences. 'Binding is too long.' rather than 'Binding too long.'"*
```csharp
// ✔️
"Decryptor is null."
"Loaded bundle handle is null."
// ❌
"Decryptor null."
"Bundle handle null."
```
> **例外**`not found` 等 .NET 生态中广泛使用的惯用短语,允许省略 `was`,以保持简洁和一致性。
```csharp
// ✔️ 惯用短语,省略 was
"File cache entry not found: '{bundleGuid}'."
"Asset not found: '{assetPath}'."
// ❌ 冗余
"File cache entry was not found: '{bundleGuid}'."
```
### 2.3 不要以冠词或变量开头
消息以关键实体词开头,不要以 `The` / `A` / `An` 或变量开头,便于日志搜索和排序。
> **来源**.NET 官方规范 — *"Do not start with an article or variable. 'Log file {0} is full.' is preferable to '{0} log file is full.'"*
```csharp
// ✔️
$"Cache entry already exists: '{bundleGuid}'."
$"Log file '{name}' is full."
// ❌
$"The cache entry already exists: '{bundleGuid}'."
$"'{name}' log file is full."
```
### 2.4 不要使用感叹号和问号
消息末尾只使用句号,禁止感叹号和问号。
> **来源**.NET 官方规范 — *"Do not use question marks or exclamation points."*
```csharp
// ✔️
"Command is unrecognizable."
// ❌
"Command is unrecognizable!!"
"Did the operation complete?"
```
---
## 三、语气和措辞规则
### 3.1 使用中性语气
使用中性客观的措辞,不归咎用户,不拟人化程序。
> **来源**.NET 官方规范 — *"Do use a neutral tone."* / *"Do not personify."*
```csharp
// ✔️
"Command is unrecognizable."
"Node parameter cannot use this protocol."
// ❌
"Bad input."
"Parameter node does not speak any of our protocols."
```
### 3.2 省略 `this.` 前缀
`this.GetType().Name` 中的 `this.` 可省略。
```csharp
// ✔️
$"Exception in {GetType().Name}.InternalStart: {ex}."
// ❌
$"Exception in {this.GetType().Name}.InternalStart: {ex}."
```
### 3.3 不要泄露敏感信息
异常和日志消息中避免包含密钥、密码、完整用户路径等敏感信息。
> **来源**.NET 官方规范 — *"Do not disclose sensitive information in exception messages."*
---
## 四、一致性规则
### 4.1 保持一致性
同一项目中相同语义使用相同句式,避免同一错误用不同表达方式。
```csharp
// ✔️ 统一格式
$"File cache entry not found: '{bundleGuid}'." // 所有缓存未命中统一使用此句式
$"Exception in {GetType().Name}.{MethodName}: {ex}." // 所有异常日志统一使用此句式
// ❌ 同一语义不同表达
"Cached bundle not found." // ← 与上面的 "File cache entry not found" 不一致
```
### 4.2 避免中式英语和语法错误
使用地道的英语表达,避免中文直译造成的不自然句式。
| 中式英语 | 地道表达 | 说明 |
|----------|----------|------|
| `"cannot match the file hash"` | `"File hash does not match."` | 主语应是名词而非动词短语 |
| `"Failed fallback load bundle"` | `"Failed to load bundle with fallback."` | 缺少介词和完整句法 |
| `"Not found xxx in yyy"` | `"Could not find xxx in yyy."` | `Not found` 不是完整句子 |
| `"The bundle file is lose"` | `"The bundle file is missing."` | 词性错误lose → missing |
| `"Begin to download"` | `"Starting download."` | `begin to` 是典型中式英语 |
---
## 五、速查表
| # | 规则 | 正确示例 | 错误示例 |
|---|------|---------|---------|
| 1 | 字符串变量值加单引号 | `'{bundleGuid}'` | `{bundleGuid}` |
| 2 | 数值变量不加引号 | `{count}` | `'{count}'` |
| 3 | 类型名作主语不加引号 | `TypeName is not ...` | `'TypeName' is not ...` |
| 4 | 异常对象不加引号 | `{ex.Message}` | `'{ex.Message}'` |
| 5 | 首字母大写,末尾加句号 | `"Load failed."` | `"load failed"` |
| 6 | 使用完整句子 | `"Decryptor is null."` | `"Decryptor null."` |
| 7 | 不以冠词或变量开头 | `"Cache entry ..."` | `"The cache entry ..."` |
| 8 | 只用句号 | `"Failed."` | `"Failed!!"` |
| 9 | 省略 `this.` | `GetType().Name` | `this.GetType().Name` |
| 10 | 使用中性语气 | `"Command is unrecognizable."` | `"Bad input."` |
| 11 | 不泄露敏感信息 | — | 密钥、密码、用户路径 |
| 12 | 保持一致性 | 同语义同句式 | 同错误不同表达 |
| 13 | 避免中式英语 | `"File hash does not match."` | `"cannot match the file hash"` |

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a8f2f1e57843e104f95aa6ad5ad3e1a9
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,531 @@
# .NET 注释规范
> 参考来源:[Microsoft C# XML 文档注释](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/xmldoc/) · [推荐的文档注释标签](https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/xmldoc/recommended-tags) · [框架设计准则](https://learn.microsoft.com/zh-cn/dotnet/standard/design-guidelines/)
>
> 核心原则:**公共 API 必须注释、说明意图而非复述代码、保持与代码同步**
---
## 一、概述
C# 使用 XML 文档注释为代码提供结构化文档。编译器从 `///`(三斜杠)注释中生成 XML 文件,供 IDE 智能提示和文档生成工具使用。
---
## 二、XML 注释标签
### 2.1 核心标签
| 标签 | 用途 | 适用场景 |
|------|------|----------|
| `<summary>` | 简要描述类型或成员的功能 | 所有公共类型和成员 |
| `<param>` | 描述方法参数 | 方法、构造函数 |
| `<returns>` | 描述方法返回值 | 有返回值的方法 |
| `<remarks>` | 补充说明,提供额外细节 | 需要详细解释的成员 |
| `<value>` | 描述属性所代表的值 | 属性 |
### 2.2 引用标签
| 标签 | 用途 | 示例 |
|------|------|------|
| `<see cref=""/>` | 行内引用其他类型或成员 | `<see cref="MyClass"/>` |
| `<seealso cref=""/>` | 在"另请参阅"区域添加引用 | `<seealso cref="OtherMethod"/>` |
| `<paramref name=""/>` | 引用参数名 | `<paramref name="count"/>` |
| `<typeparamref name=""/>` | 引用泛型类型参数名 | `<typeparamref name="T"/>` |
### 2.3 格式标签
| 标签 | 用途 |
|------|------|
| `<para>` | 在 `<remarks>` 等标签内分段 |
| `<list>` | 创建列表或表格 |
| `<code>` | 多行代码示例 |
| `<example>` | 包含使用示例 |
### 2.4 泛型与继承标签
| 标签 | 用途 |
|------|------|
| `<typeparam>` | 描述泛型类型参数 |
| `<inheritdoc/>` | 继承基类或接口的文档注释 |
---
## 三、注释范围要求
### 3.1 必须添加注释的成员
| 访问级别 | 要求 |
|----------|------|
| `public` | **必须**添加完整 XML 注释 |
| `protected` | **必须**添加完整 XML 注释 |
| `internal` | **建议**添加 XML 注释 |
| `private` | 仅在逻辑复杂时添加 |
### 3.2 各成员类型的注释要求
| 成员类型 | 必需标签 | 可选标签 |
|----------|---------|---------|
| 类 / 结构 | `<summary>` | `<remarks>``<typeparam>` |
| 接口 | `<summary>` | `<remarks>` |
| 方法 | `<summary>``<param>``<returns>` | `<remarks>``<example>` |
| 构造函数 | `<summary>``<param>` | — |
| 属性 | `<summary>` | `<value>` |
| 事件 | `<summary>` | `<remarks>` |
| 枚举类型 | `<summary>` | — |
| 枚举值 | `<summary>` | — |
| 委托 | `<summary>``<param>``<returns>` | — |
| 接口实现成员 | `<inheritdoc/>` | — |
| override 方法 | **不添加**注释 | — |
---
## 四、书写原则
### 4.1 `<summary>` 书写规范
✔️ **一句话**说明"做什么",而非"怎么做"。
✔️ 以**第三人称动词**开头。
✔️ 句号规则:注释内容含逗号、分号等分句标点时,末尾**加**句号;纯短语**不加**句号。
```csharp
// ✔️ 含逗号 → 加句号
/// <param name="timeout">超时时间0 表示不限制。</param>
/// <returns>下载结果,包含状态码和错误信息。</returns>
// ✔️ 纯短语 → 不加句号
/// <param name="url">资源包的完整下载地址</param>
/// <returns>初始化操作句柄</returns>
```
❌ **禁止**重复方法名或参数类型中已表达的信息。
❌ **禁止**在注释中重复方法命名后缀已表达的语义。常见后缀包括:
| 命名后缀 | 已表达的语义 | 注释中禁止使用 |
|----------|------------|--------------|
| `Async` | 异步执行 | "异步" |
| `Internal` | 内部实现 | "内部" |
❌ **禁止**以"这个方法"、"该类"等冗余前缀开头。
### 4.2 常见开头约定
#### 属性 —— 描述值本身,不加动词前缀
属性的 `{ get; }` / `{ get; set; }` 访问器已表达读写语义summary 应直接描述**值的含义**,避免用"获取"、"获取或设置"等动词前缀重复已知信息。
| 属性类型 | 推荐写法 | 反例 |
|----------|---------|------|
| 只读属性 | 名词短语,如 "错误信息" | ~~"获取错误信息"~~ |
| 可读写属性 | 名词短语,如 "当前下载进度" | ~~"获取或设置当前下载进度"~~ |
| 返回 bool 的属性 | "是否……",如 "是否为只读缓存" | ~~"判断是否为只读缓存"~~ |
#### 方法 —— 以动词开头
| 成员类型 | 推荐开头动词 |
|----------|-------------|
| 返回 bool 的方法 | "检查是否"、"判断是否" |
| 获取类方法 | "获取"、"查询" |
| 设置类方法 | "设置"、"更新" |
| 创建类方法 | "创建"、"构建"、"生成" |
| 事件 | "当……时触发" |
| 构造函数 | "创建 XXX 实例"XXX 为类型的中文描述) |
### 4.3 `<param>` 书写规范
✔️ 描述参数的**含义和用途**。
✔️ 说明有效范围和边界值(如为 `null` 时的行为、取值范围)。
❌ **禁止**仅重复参数名或类型名,如"url 参数"。
### 4.4 `<returns>` 书写规范
✔️ 说明返回值的**含义**。
✔️ 说明**特殊返回值**的含义(如返回 `null` 表示未找到)。
### 4.5 `<remarks>` 书写规范
✔️ 多行内容**必须**用 `<para>` 包裹每段,否则 IDE 会将多行合并显示为一行。
```csharp
// ✔️ 好:每段用 <para> 包裹
/// <remarks>
/// <para>下载并加载 Unity AssetBundle 资源包</para>
/// <para>支持 Unity 内置缓存机制和 CRC 校验</para>
/// </remarks>
// ❌ 差多行纯文本IDE 显示时会合并为一行
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包
/// 支持 Unity 内置缓存机制和 CRC 校验
/// </remarks>
```
✔️ 单行内容可直接书写,无需 `<para>`
---
## 五、注释示例
### 5.1 类注释
```csharp
/// <summary>
/// 资源包下载请求,负责管理单个资源包的下载生命周期。
/// </summary>
/// <remarks>
/// <para>支持断点续传和失败重试</para>
/// <para>通过 <see cref="DownloadRetryController"/> 控制重试策略</para>
/// </remarks>
public class BundleDownloadRequest
{
}
```
### 5.2 方法注释
```csharp
/// <summary>
/// 从指定 URL 下载资源包并保存到本地磁盘
/// </summary>
/// <param name="url">资源包的完整下载地址</param>
/// <param name="savePath">本地保存的目标路径</param>
/// <param name="timeout">超时时间0 表示不限制。</param>
/// <returns>下载结果,包含状态码和错误信息。</returns>
public async Task<DownloadResult> DownloadAsync(string url, string savePath, int timeout = 30)
{
}
```
### 5.3 属性注释
```csharp
/// <summary>
/// 当前下载进度
/// </summary>
/// <value>取值范围 0.0 ~ 1.0,其中 1.0 表示下载完成。</value>
public float Progress { get; private set; }
```
### 5.4 枚举注释
```csharp
/// <summary>
/// 下载任务的运行状态
/// </summary>
public enum DownloadStatus
{
/// <summary>
/// 等待中,尚未开始下载。
/// </summary>
Pending,
/// <summary>
/// 正在下载中
/// </summary>
Downloading,
/// <summary>
/// 下载已完成
/// </summary>
Completed,
/// <summary>
/// 下载失败
/// </summary>
Failed
}
```
### 5.5 泛型类注释
```csharp
/// <summary>
/// 通用对象池,提供对象的复用管理。
/// </summary>
/// <typeparam name="T">池化对象的类型,必须实现 <see cref="IDisposable"/>。</typeparam>
public class ObjectPool<T> where T : IDisposable
{
}
```
### 5.6 接口注释
```csharp
/// <summary>
/// 定义下载请求的标准行为
/// </summary>
public interface IDownloadRequest
{
/// <summary>
/// 请求的远程 URL
/// </summary>
string URL { get; }
/// <summary>
/// 取消当前下载请求
/// </summary>
void Abort();
}
```
### 5.7 使用 `<inheritdoc/>`
#### 接口实现 —— 使用 `<inheritdoc/>`
实现接口成员时,使用 `<inheritdoc/>` 继承接口中定义的文档:
```csharp
public class MyDownloadRequest : IDownloadRequest
{
/// <inheritdoc/>
public string URL { get; }
/// <inheritdoc/>
public void Abort()
{
}
}
```
#### 基类继承override —— 不添加任何注释
重写基类方法时,**不需要**添加任何 XML 文档注释(包括 `<inheritdoc/>`)。方法签名中的 `override` 关键字已明确表达继承关系IDE 会自动展示基类文档。
```csharp
// ✔️ 好override 方法不添加注释
protected override void InternalStart()
{
}
protected override void InternalUpdate()
{
}
// ❌ 差:多余的 inheritdoc
/// <inheritdoc/>
protected override void InternalStart()
{
}
```
### 5.8 使用 `<example>` 和 `<code>`
```csharp
/// <summary>
/// 根据资源路径加载资源对象
/// </summary>
/// <param name="assetPath">资源路径</param>
/// <returns>资源操作句柄,加载失败时资源对象为 null。</returns>
/// <example>
/// 加载一个预制体:
/// <code>
/// var handle = package.LoadAssetAsync&lt;GameObject&gt;("Assets/Prefabs/Player.prefab");
/// await handle.Task;
/// var prefab = handle.AssetObject as GameObject;
/// </code>
/// </example>
public AssetHandle LoadAssetAsync<T>(string assetPath)
{
}
```
---
## 六、反面示例
### 6.1 冗余注释
```csharp
// ❌ 差:重复方法签名中已有的信息
/// <summary>
/// 下载方法,参数是 url 和 savePath。
/// </summary>
public void Download(string url, string savePath) { }
// ✔️ 好:说明行为和目的
/// <summary>
/// 从远程服务器下载资源包并保存到本地磁盘
/// </summary>
/// <param name="url">资源包的完整下载地址</param>
/// <param name="savePath">本地保存的目标路径</param>
public void Download(string url, string savePath) { }
```
### 6.2 废话参数注释
```csharp
// ❌ 差:仅重复参数名
/// <param name="name">名称</param>
// ✔️ 好:说明含义和约束
/// <param name="name">资源包的唯一标识名称</param>
```
### 6.3 重复命名后缀
```csharp
// ❌ 差:方法名 Async 后缀已表达异步语义
/// <summary>
/// 异步加载资源对象
/// </summary>
public AssetHandle LoadAssetAsync(string location) { }
// ✔️ 好:描述功能意图,不重复后缀语义
/// <summary>
/// 加载资源对象
/// </summary>
public AssetHandle LoadAssetAsync(string location) { }
```
### 6.4 缺少边界说明
```csharp
// ❌ 差:未说明返回 null 的情况
/// <returns>资源对象</returns>
// ✔️ 好:说明特殊返回值
/// <returns>加载到的资源对象</returns>
```
---
## 七、行内注释规范
对于非 XML 文档注释(`//` 普通注释),遵循以下原则:
### 7.1 基本准则
✔️ 解释"**为什么**",而非"是什么"——代码本身应说明"做什么",注释解释不明显的意图或约束。
✔️ 标记待办事项使用 `// TODO:` 前缀。
✔️ 标记临时方案使用 `// HACK:` 前缀。
✔️ 修改代码时**同步更新**相关注释,过时的注释比没有注释更有害。
❌ **禁止**注释显而易见的代码,如 `// 递增计数器``// 返回结果`
❌ **禁止**用注释替代清晰的命名——如果需要注释来解释变量含义,应优先改善变量名。
### 7.2 分段注释(例外)
在较长的方法体中,允许使用**简短的分段注释**充当逻辑区域标题,帮助快速定位代码段落。这类注释虽然在形式上可能"复述"了下方代码的行为,但其核心价值是**导航与分区**,不属于上述"禁止复述"的范畴。
```csharp
// ✔️ 好:在长方法中用分段注释划分逻辑区域
public void InitViewer()
{
// 加载布局文件
_visualAsset = UxmlLoader.LoadWindowUxml<DebuggerBundleListViewer>();
...
// 资源包列表
_bundleTableView = _root.Q<TableViewer>("BundleTableView");
...
// 使用列表
_usingTableView = _root.Q<TableViewer>("UsingTableView");
...
// 面板分屏
var topGroup = _root.Q<VisualElement>("TopGroup");
...
}
```
判断标准:如果移除该注释后,读者需要逐行阅读才能理解方法内的结构,则应保留。
### 7.3 示例
```csharp
// ❌ 差:复述代码
// 如果计数大于最大值,重置为零
if (count > maxCount)
count = 0;
// ✔️ 好:解释约束
// 环形缓冲区写满后从头覆盖,避免无限增长
if (count > maxCount)
count = 0;
```
---
## 八、术语翻译映射表
> 为保持注释用语统一,以下列出项目中常用英文类型/概念与其中文翻译的对应关系。
> 撰写或审核注释时,请以此表为准。
### 8.1 核心领域类型
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `PackageBundle` | 资源包描述 | 清单中的静态元数据(`Package*` = 描述) |
| `PackageAsset` | 资源描述 | 清单中的静态元数据(`Package*` = 描述) |
| `AssetInfo` | 资源信息 | 运行时上下文(`*Info` = 信息) |
| `BundleInfo` | 资源包信息 | 运行时上下文(`*Info` = 信息) |
| `ResourcePackage` | 资源包裹 | 顶层包裹容器 |
| `PackageManifest` | 包裹清单 / 资源清单 | — |
| `AssetBundle` | AssetBundle | Unity 引擎类型,保留英文 |
| `RawBundle` | 原生资源包 | 非 AssetBundle 的原始文件 |
### 8.2 文件系统与缓存
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IFileSystem` | 文件系统 | — |
| `IBundleCache` | 缓存系统 | — |
| `ICacheEntry` | 缓存条目 | — |
| `BundleGuid` | 资源包 GUID / Bundle 唯一标识 | — |
| `ICacheEvictionPolicy` | 淘汰策略 | — |
| `EvictionResult` | 淘汰结果 | — |
### 8.3 下载与网络
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IDownloadRequest` | 下载请求 | — |
| `IDownloadBackend` | 下载后台 | — |
| `IRemoteService` | 远端资源服务 | — |
| `IDownloadRetryPolicy` | 下载重试策略 | — |
| `IDownloadUrlPolicy` | URL 选择策略 | — |
### 8.4 加密与解密
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IBundleEncryptor` | 资源包加密器 | — |
| `IBundleDecryptor` | 资源包解密器 | 基接口,需实现派生接口 |
| `IManifestEncryptor` | 资源清单加密器 | — |
| `IManifestDecryptor` | 资源清单解密器 | — |
| `BundleEncryptArgs` | 加密操作的输入参数 | — |
| `BundleDecryptArgs` | 解密操作的输入参数 | — |
### 8.5 资源加载与句柄
| 英文类型 / 概念 | 中文翻译 | 备注 |
|-----------------|---------|------|
| `IBundleHandle` | 资源包句柄 | — |
| `AssetHandle` | 资源句柄 | — |
| `RawFileHandle` | 原生文件句柄 | — |
| `SubAssetsHandle` | 子资源句柄 | — |
### 8.6 通用术语
| 英文术语 | 中文翻译 | 备注 |
|---------|---------|------|
| Package | 包裹 | 项目级容器 |
| Bundle | 资源包 | 构建产物单元 |
| Asset | 资源 | 单个资源文件 |
| Manifest | 清单 | — |
| Handle | 句柄 | — |
| Operation | 操作 / 异步操作 | — |
| Scheduler | 调度器 | — |
| Provider | 提供者 | — |

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5673a09d2666eb847a78066794039d23
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,200 @@
# 逻辑检测
> **总则**:检查运行时代码的关键逻辑路径,确保宏分支、异步操作生命周期、参数完整性和资源释放均无遗漏。
## 1. 宏包裹代码逻辑
检查所有 `#if` / `#elif` / `#else` / `#endif` 包裹的代码:
- 每个分支的变量赋值路径是否完整(不存在未赋值分支)
- `#else` 兜底分支是否覆盖所有未列出的平台
- 各分支之间的语义是否一致(同一变量在不同分支中的类型和用途是否相同)
- 分支中引用的 API 是否与对应宏版本匹配(如 `UNITY_2020_3_OR_NEWER` 对应的 API
## 2. 子异步操作检查
**定义**:通过 `AddChildOperation()` 添加的操作为子异步操作。
### 2.1 同步等待传播
如果类实现了 `InternalWaitForCompletion()` 方法,所有子异步操作必须在该方法中主动调用 `WaitForCompletion()`
```
❌ 父操作实现了 InternalWaitForCompletion但未对子操作调用 WaitForCompletion
✅ 父操作在 InternalWaitForCompletion 中对每个子操作调用 WaitForCompletion()
```
### 2.2 更新驱动
所有子异步操作必须在父操作的 `InternalUpdate()` 中主动调用 `UpdateOperation()`,确保子操作能被正常驱动。
```
❌ 子操作被 AddChildOperation 添加后,未在 InternalUpdate 中调用 UpdateOperation
✅ 每个子操作在 InternalUpdate 中被调用 UpdateOperation()
```
## 3. 异步操作参数完整性
检查所有 Operation 的 Options 在 `new` 的位置是否完整填充了所有成员字段。
- 找到 Options 结构体的所有字段定义
- 找到所有 `new XxxOptions(...)` 的调用点
- 逐一比对构造参数是否覆盖了所有必填字段
- 可选参数(有默认值)可以跳过
```
❌ new DownloadFileRequestOptions(url, savePath, timeout) // 遗漏了 watchdogTimeout
✅ new DownloadFileRequestOptions(url, savePath, timeout, watchdogTimeout)
```
## 4. 整数溢出与取模安全
检查所有对 `int` 计数器执行 `%`(取模)运算的位置,确保不会因溢出产生负索引。
- 只增不减的 `int` 计数器(如失败计数、轮转索引),溢出 `int.MaxValue` 后变为负数
- 负数 `%` 正数在 C# 中结果为负数,用作数组/列表索引会触发 `ArgumentOutOfRangeException`
- 使用 `& 0x7FFFFFFF` 清除符号位,保证结果始终为非负整数(.NET 中处理哈希取模的惯用手法)
```
❌ int index = _counter % list.Count; // _counter 溢出后 index 为负
✅ int index = (_counter & 0x7FFFFFFF) % list.Count; // 始终非负
```
## 5. 下载请求资源释放
### 5.1 IDownloadRequest 释放
检查所有持有 `IDownloadRequest`(及其子接口)实例的 Operation是否在 `InternalDispose()` 中调用了 `Dispose()` 释放资源。
```
❌ Operation 持有 IDownloadFileRequest 但未在 InternalDispose 中释放
✅ protected override void InternalDispose() { _request?.Dispose(); }
```
### 5.2 下载数据完整性验证
检查文件数据下载完成后,是否有容错或数据完整性验证:
- 是否校验文件大小(下载字节数与预期是否一致)
- 是否校验文件哈希CRC / MD5 / SHA 等)
- 如果无校验,是否有其他容错机制(如重试、回退)
- 无任何验证且无容错机制的,标记为需要关注
## 6. 计时方式一致性
检查所有需要"测量经过时间"的逻辑,确保计时方式不会因帧率波动或 App 后台恢复而失真。
- 使用 `Time.unscaledDeltaTime` 累加计时的方式,在 App 从后台恢复时会产生巨大 spike一帧跨越几十秒导致等待逻辑在单帧内被直接跳过
- 同一系统内应保持计时方式统一,避免一处用绝对时间戳、另一处用帧增量
- 推荐使用 `TimeUtility.RealtimeSinceStartup` 记录起止时间戳做差值判断
```
❌ _elapsed += Time.unscaledDeltaTime; return _elapsed >= delay; // 后台恢复时 spike 跳过等待
✅ return TimeUtility.RealtimeSinceStartup - _startTime >= delay; // 绝对时间戳,不受 spike 影响
```
## 7. 子类回调不得覆写基类已确定的状态
检查所有基类定义的"成功/失败回调"(如 `OnRequestSucceeded``OnRequestFailed`),子类在重写时不应直接修改基类已确定的 `Status`
- 基类在调用回调前已设置 `Status = Succeeded`,若子类在回调中将其改为 `Failed`,后续基类逻辑可能与实际状态不一致
- 回调应仅负责提取结果或设置错误信息,状态转换应由基类统一控制
- 推荐将回调签名改为返回 `bool`(或通过 out 参数传递错误),基类根据返回值决定最终状态
```
❌ protected override void OnRequestSucceeded()
{
if (result == null) Status = Failed; // 子类直接覆写基类已确定的 Succeeded 状态
}
✅ protected override bool OnRequestSucceeded()
{
if (result == null) { Error = "..."; return false; } // 返回 false由基类设置 Failed
return true;
}
```
## 8. 结构体不可变性检测
> 依据:[类型设计准则](2.类型设计准则.md) — "❌ **禁止**定义可变值类型" / "✔️ 声明不可变值类型时使用 `readonly struct` 修饰符"
检查所有 `struct` 定义,**必须**声明为 `readonly struct`。存在可变 struct 即为违规。
检测要点:
- `struct` 声明缺少 `readonly` 修饰符
- 属性使用 `{ get; set; }` 而非 `{ get; }`
- 公共字段未声明为 `readonly`
- 缺少构造函数(依赖默认值逐字段赋值的模式)
```
❌ struct MutableConfig
{
public int Timeout { get; set; } // 可变属性
public string Name { get; set; }
}
✅ readonly struct ImmutableConfig
{
public int Timeout { get; } // 只读属性
public string Name { get; }
public ImmutableConfig(int timeout, string name)
{
Timeout = timeout;
Name = name;
}
}
```
**禁止可变 struct 的原因**
- 值类型按值复制,可变成员修改作用于副本而非原值,极易产生静默 bug
- `readonly` 字段持有可变 struct 时,编译器每次访问都会产生防御性复制,造成不必要的性能开销
- `readonly struct` 从根源上杜绝以上两类问题
## 9. 文件系统禁止重写 GetHashCode
检查所有 `IFileSystem` 实现类,确认未重写 `GetHashCode()``Equals()`
`BundleInfo.GetCombineKey()` 依赖 `RuntimeHelpers.GetHashCode(_fileSystem)` 获取基于引用标识的哈希值,用于下载器合并时的去重判断。如果文件系统实现类重写了 `GetHashCode()`,虽然 `RuntimeHelpers.GetHashCode` 本身不受影响,但会暗示该类型有自定义相等性语义,可能导致其他使用 `GetHashCode()` 的位置产生非预期的碰撞。
检测要点:
- `IFileSystem` 实现类中存在 `override int GetHashCode()` 声明
- `IFileSystem` 实现类中存在 `override bool Equals(object)` 声明
```
❌ class SandboxFileSystem : IFileSystem
{
public override int GetHashCode() => PackageName.GetHashCode(); // 破坏引用标识语义
}
✅ class SandboxFileSystem : IFileSystem
{
// 不重写 GetHashCode保持 object 默认的引用标识行为
}
```
## 10. 不可变结构体命名参数检测
检测所有 `readonly struct` 构造函数调用中**位置参数 > 3 个**的位置,确认是否使用命名参数提高可读性。
`readonly struct` 只能通过构造函数一次性传入所有字段,参数数量通常较多,且参数顺序是唯一区分手段。同类型参数相邻时,顺序错误不会产生编译错误,容易引入静默 bug。
重点检查以下场景:
- 构造函数参数 > 3 个
- 多个同类型参数相邻(如连续多个 `bool``int`、接口类型)
- 参数名与属性名高度对应,使用命名参数可消除歧义
```
❌ new Configuration(60, true, level, decryptor, backend, retryPolicy, urlPolicy);
✅ new Configuration(
watchdogTimeout: 60,
disableUnityWebCache: true,
downloadVerifyLevel: level,
assetBundleDecryptor: decryptor,
downloadBackend: backend,
downloadRetryPolicy: retryPolicy,
downloadUrlPolicy: urlPolicy);
```

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 200087470392cf24ebdd0efac8e44210
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
// 外部友元
[assembly: InternalsVisibleTo("YooAsset.EditorExtension")]

View File

@@ -1,670 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
public class AssetArtReporterWindow : EditorWindow
{
[MenuItem("YooAsset/AssetArt Reporter", false, 302)]
public static AssetArtReporterWindow OpenWindow()
{
AssetArtReporterWindow window = GetWindow<AssetArtReporterWindow>("AssetArt Reporter", true, WindowsDefine.DockedWindowTypes);
window.minSize = new Vector2(800, 600);
return window;
}
private class ElementTableData : DefaultTableData
{
public ReportElement Element;
}
private class PassesBtnCell : ITableCell, IComparable
{
public object CellValue { set; get; }
public string SearchTag { private set; get; }
public ReportElement Element
{
get
{
return (ReportElement)CellValue;
}
}
public PassesBtnCell(string searchTag, ReportElement element)
{
SearchTag = searchTag;
CellValue = element;
}
public object GetDisplayObject()
{
return string.Empty;
}
public int CompareTo(object other)
{
if (other is PassesBtnCell cell)
{
return this.Element.Passes.CompareTo(cell.Element.Passes);
}
else
{
return 0;
}
}
}
private class WhiteListBtnCell : ITableCell, IComparable
{
public object CellValue { set; get; }
public string SearchTag { private set; get; }
public ReportElement Element
{
get
{
return (ReportElement)CellValue;
}
}
public WhiteListBtnCell(string searchTag, ReportElement element)
{
SearchTag = searchTag;
CellValue = element;
}
public object GetDisplayObject()
{
return string.Empty;
}
public int CompareTo(object other)
{
if (other is WhiteListBtnCell cell)
{
return this.Element.IsWhiteList.CompareTo(cell.Element.IsWhiteList);
}
else
{
return 0;
}
}
}
private ToolbarSearchField _searchField;
private Button _showHiddenBtn;
private Button _whiteListVisibleBtn;
private Button _passesVisibleBtn;
private Label _titleLabel;
private Label _descLabel;
private TableViewer _elementTableView;
private ScanReportCombiner _reportCombiner;
private string _lastestOpenFolder;
private List<ITableData> _sourceDatas;
private bool _elementVisibleState = true;
private bool _whiteListVisibleState = true;
private bool _passesVisibleState = true;
public void CreateGUI()
{
try
{
VisualElement root = this.rootVisualElement;
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUXML<AssetArtReporterWindow>();
if (visualAsset == null)
return;
visualAsset.CloneTree(root);
// 导入按钮
var importSingleBtn = root.Q<Button>("SingleImportButton");
importSingleBtn.clicked += ImportSingleBtn_clicked;
var importMultiBtn = root.Q<Button>("MultiImportButton");
importMultiBtn.clicked += ImportMultiBtn_clicked;
// 修复按钮
var fixAllBtn = root.Q<Button>("FixAllButton");
fixAllBtn.clicked += FixAllBtn_clicked;
var fixSelectBtn = root.Q<Button>("FixSelectButton");
fixSelectBtn.clicked += FixSelectBtn_clicked;
// 可见性按钮
_showHiddenBtn = root.Q<Button>("ShowHiddenButton");
_showHiddenBtn.clicked += ShowHiddenBtn_clicked;
_whiteListVisibleBtn = root.Q<Button>("WhiteListVisibleButton");
_whiteListVisibleBtn.clicked += WhiteListVisibleBtn_clicked;
_passesVisibleBtn = root.Q<Button>("PassesVisibleButton");
_passesVisibleBtn.clicked += PassesVsibleBtn_clicked;
// 文件导出按钮
var exportFilesBtn = root.Q<Button>("ExportFilesButton");
exportFilesBtn.clicked += ExportFilesBtn_clicked;
// 搜索过滤
_searchField = root.Q<ToolbarSearchField>("SearchField");
_searchField.RegisterValueChangedCallback(OnSearchKeyWordChange);
// 标题和备注
_titleLabel = root.Q<Label>("ReportTitle");
_descLabel = root.Q<Label>("ReportDesc");
// 列表相关
_elementTableView = root.Q<TableViewer>("TopTableView");
_elementTableView.ClickTableDataEvent = OnClickTableViewItem;
_lastestOpenFolder = EditorTools.GetProjectPath();
}
catch (System.Exception e)
{
Debug.LogError(e.ToString());
}
}
public void OnDestroy()
{
if (_reportCombiner != null)
_reportCombiner.SaveChange();
}
/// <summary>
/// 导入单个报告文件
/// </summary>
public void ImportSingleReprotFile(string filePath)
{
// 记录本次打开目录
_lastestOpenFolder = Path.GetDirectoryName(filePath);
_reportCombiner = new ScanReportCombiner();
try
{
var scanReport = ScanReportConfig.ImportJsonConfig(filePath);
_reportCombiner.Combine(scanReport);
// 刷新页面
RefreshToolbar();
FillTableView();
RebuildView();
}
catch (System.Exception e)
{
_reportCombiner = null;
_titleLabel.text = "导入报告失败!";
_descLabel.text = e.Message;
UnityEngine.Debug.LogError(e.StackTrace);
}
}
/// <summary>
/// 导入单个报告对象
/// </summary>
public void ImportSingleReprotFile(ScanReport report)
{
_reportCombiner = new ScanReportCombiner();
try
{
_reportCombiner.Combine(report);
// 刷新页面
RefreshToolbar();
FillTableView();
RebuildView();
}
catch (System.Exception e)
{
_reportCombiner = null;
_titleLabel.text = "导入报告失败!";
_descLabel.text = e.Message;
UnityEngine.Debug.LogError(e.StackTrace);
}
}
private void ImportSingleBtn_clicked()
{
string selectFilePath = EditorUtility.OpenFilePanel("导入报告", _lastestOpenFolder, "json");
if (string.IsNullOrEmpty(selectFilePath))
return;
ImportSingleReprotFile(selectFilePath);
}
private void ImportMultiBtn_clicked()
{
string selectFolderPath = EditorUtility.OpenFolderPanel("导入报告", _lastestOpenFolder, null);
if (string.IsNullOrEmpty(selectFolderPath))
return;
// 记录本次打开目录
_lastestOpenFolder = selectFolderPath;
_reportCombiner = new ScanReportCombiner();
try
{
string[] files = Directory.GetFiles(selectFolderPath);
foreach (string filePath in files)
{
string extension = System.IO.Path.GetExtension(filePath);
if (extension == ".json")
{
var scanReport = ScanReportConfig.ImportJsonConfig(filePath);
_reportCombiner.Combine(scanReport);
}
}
// 刷新页面
RefreshToolbar();
FillTableView();
RebuildView();
}
catch (System.Exception e)
{
_reportCombiner = null;
_titleLabel.text = "Failed to import report!";
_descLabel.text = e.Message;
UnityEngine.Debug.LogError(e.StackTrace);
}
}
private void FixAllBtn_clicked()
{
if (EditorUtility.DisplayDialog("Info", "Fix all resources (excluding whitelist and hidden elements)", "Yes", "No"))
{
if (_reportCombiner != null)
_reportCombiner.FixAll();
}
}
private void FixSelectBtn_clicked()
{
if (EditorUtility.DisplayDialog("Info", "Fix selected resources (including whitelist and hidden elements)", "Yes", "No"))
{
if (_reportCombiner != null)
_reportCombiner.FixSelect();
}
}
private void ShowHiddenBtn_clicked()
{
_elementVisibleState = !_elementVisibleState;
RefreshToolbar();
RebuildView();
}
private void WhiteListVisibleBtn_clicked()
{
_whiteListVisibleState = !_whiteListVisibleState;
RefreshToolbar();
RebuildView();
}
private void PassesVsibleBtn_clicked()
{
_passesVisibleState = !_passesVisibleState;
RefreshToolbar();
RebuildView();
}
private void ExportFilesBtn_clicked()
{
string selectFolderPath = EditorUtility.OpenFolderPanel("Export all selected resources", EditorTools.GetProjectPath(), string.Empty);
if (string.IsNullOrEmpty(selectFolderPath) == false)
{
if (_reportCombiner != null)
_reportCombiner.ExportFiles(selectFolderPath);
}
}
private void RefreshToolbar()
{
if (_reportCombiner == null)
return;
_titleLabel.text = _reportCombiner.ReportTitle;
_descLabel.text = _reportCombiner.ReportDesc;
var enableColor = new Color32(18, 100, 18, 255);
var disableColor = new Color32(100, 100, 100, 255);
if (_elementVisibleState)
_showHiddenBtn.style.backgroundColor = new StyleColor(enableColor);
else
_showHiddenBtn.style.backgroundColor = new StyleColor(disableColor);
if (_whiteListVisibleState)
_whiteListVisibleBtn.style.backgroundColor = new StyleColor(enableColor);
else
_whiteListVisibleBtn.style.backgroundColor = new StyleColor(disableColor);
if (_passesVisibleState)
_passesVisibleBtn.style.backgroundColor = new StyleColor(enableColor);
else
_passesVisibleBtn.style.backgroundColor = new StyleColor(disableColor);
}
private void FillTableView()
{
if (_reportCombiner == null)
return;
_elementTableView.ClearAll(true, true);
// 眼睛标题
{
var columnStyle = new ColumnStyle(20);
columnStyle.Stretchable = false;
columnStyle.Searchable = false;
columnStyle.Sortable = false;
var column = new TableColumn("眼睛框", string.Empty, columnStyle);
column.MakeCell = () =>
{
var toggle = new ToggleDisplay();
toggle.text = string.Empty;
toggle.style.unityTextAlign = TextAnchor.MiddleCenter;
toggle.RegisterValueChangedCallback((evt) => { OnDisplayToggleValueChange(toggle, evt); });
return toggle;
};
column.BindCell = (VisualElement element, ITableData data, ITableCell cell) =>
{
var toggle = element as ToggleDisplay;
toggle.userData = data;
var tableData = data as ElementTableData;
toggle.SetValueWithoutNotify(tableData.Element.Hidden);
};
_elementTableView.AddColumn(column);
var headerElement = _elementTableView.GetHeaderElement("眼睛框");
headerElement.style.unityTextAlign = TextAnchor.MiddleCenter;
}
// 通过标题
{
var columnStyle = new ColumnStyle(70);
columnStyle.Stretchable = false;
columnStyle.Searchable = false;
columnStyle.Sortable = true;
var column = new TableColumn("通过", "通过", columnStyle);
column.MakeCell = () =>
{
var button = new Button();
button.text = "通过";
button.style.unityTextAlign = TextAnchor.MiddleCenter;
button.SetEnabled(false);
return button;
};
column.BindCell = (VisualElement element, ITableData data, ITableCell cell) =>
{
Button button = element as Button;
var elementTableData = data as ElementTableData;
if (elementTableData.Element.Passes)
{
button.style.backgroundColor = new StyleColor(new Color32(56, 147, 58, 255));
button.text = "通过";
}
else
{
button.style.backgroundColor = new StyleColor(new Color32(137, 0, 0, 255));
button.text = "失败";
}
};
_elementTableView.AddColumn(column);
var headerElement = _elementTableView.GetHeaderElement("通过");
headerElement.style.unityTextAlign = TextAnchor.MiddleCenter;
}
// 白名单标题
{
var columnStyle = new ColumnStyle(70);
columnStyle.Stretchable = false;
columnStyle.Searchable = false;
columnStyle.Sortable = true;
var column = new TableColumn("白名单", "白名单", columnStyle);
column.MakeCell = () =>
{
Button button = new Button();
button.text = "白名单";
button.style.unityTextAlign = TextAnchor.MiddleCenter;
button.clickable.clickedWithEventInfo += OnClickWhitListButton;
return button;
};
column.BindCell = (VisualElement element, ITableData data, ITableCell cell) =>
{
Button button = element as Button;
button.userData = data;
var elementTableData = data as ElementTableData;
if (elementTableData.Element.IsWhiteList)
button.style.backgroundColor = new StyleColor(new Color32(56, 147, 58, 255));
else
button.style.backgroundColor = new StyleColor(new Color32(100, 100, 100, 255));
};
_elementTableView.AddColumn(column);
var headerElement = _elementTableView.GetHeaderElement("白名单");
headerElement.style.unityTextAlign = TextAnchor.MiddleCenter;
}
// 选中标题
{
var columnStyle = new ColumnStyle(20);
columnStyle.Stretchable = false;
columnStyle.Searchable = false;
columnStyle.Sortable = false;
var column = new TableColumn("选中框", string.Empty, columnStyle);
column.MakeCell = () =>
{
var toggle = new Toggle();
toggle.text = string.Empty;
toggle.style.unityTextAlign = TextAnchor.MiddleCenter;
toggle.RegisterValueChangedCallback((evt) => { OnSelectToggleValueChange(toggle, evt); });
return toggle;
};
column.BindCell = (VisualElement element, ITableData data, ITableCell cell) =>
{
var toggle = element as Toggle;
toggle.userData = data;
var tableData = data as ElementTableData;
toggle.SetValueWithoutNotify(tableData.Element.IsSelected);
};
_elementTableView.AddColumn(column);
}
// 自定义标题栏
foreach (var header in _reportCombiner.Headers)
{
var columnStyle = new ColumnStyle(header.Width, header.MinWidth, header.MaxWidth);
columnStyle.Stretchable = header.Stretchable;
columnStyle.Searchable = header.Searchable;
columnStyle.Sortable = header.Sortable;
columnStyle.Counter = header.Counter;
columnStyle.Units = header.Units;
var column = new TableColumn(header.HeaderTitle, header.HeaderTitle, columnStyle);
column.MakeCell = () =>
{
if (header.HeaderType == EHeaderType.AssetObject)
{
var objectFiled = new ObjectField();
return objectFiled;
}
else
{
var label = new Label();
label.style.marginLeft = 3f;
label.style.unityTextAlign = TextAnchor.MiddleLeft;
return label;
}
};
column.BindCell = (VisualElement element, ITableData data, ITableCell cell) =>
{
if (header.HeaderType == EHeaderType.AssetObject)
{
var objectFiled = element as ObjectField;
objectFiled.value = (UnityEngine.Object)cell.GetDisplayObject();
}
else
{
var infoLabel = element as Label;
infoLabel.text = (string)cell.GetDisplayObject();
}
};
_elementTableView.AddColumn(column);
}
// 填充数据源
_sourceDatas = new List<ITableData>(_reportCombiner.Elements.Count);
foreach (var element in _reportCombiner.Elements)
{
var tableData = new ElementTableData();
tableData.Element = element;
// 固定标题
tableData.AddButtonCell("眼睛框");
tableData.AddCell(new PassesBtnCell("通过", element));
tableData.AddCell(new WhiteListBtnCell("白名单", element));
tableData.AddButtonCell("选中框");
// 自定义标题
foreach (var scanInfo in element.ScanInfos)
{
var header = _reportCombiner.GetHeader(scanInfo.HeaderTitle);
if (header.HeaderType == EHeaderType.AssetPath)
{
tableData.AddAssetPathCell(scanInfo.HeaderTitle, scanInfo.ScanInfo);
}
else if (header.HeaderType == EHeaderType.AssetObject)
{
tableData.AddAssetObjectCell(scanInfo.HeaderTitle, scanInfo.ScanInfo);
}
else if (header.HeaderType == EHeaderType.StringValue)
{
tableData.AddStringValueCell(scanInfo.HeaderTitle, scanInfo.ScanInfo);
}
else if (header.HeaderType == EHeaderType.LongValue)
{
long value = Convert.ToInt64(scanInfo.ScanInfo);
tableData.AddLongValueCell(scanInfo.HeaderTitle, value);
}
else if (header.HeaderType == EHeaderType.DoubleValue)
{
double value = Convert.ToDouble(scanInfo.ScanInfo);
tableData.AddDoubleValueCell(scanInfo.HeaderTitle, value);
}
else
{
throw new NotImplementedException(header.HeaderType.ToString());
}
}
_sourceDatas.Add(tableData);
}
_elementTableView.itemsSource = _sourceDatas;
}
private void RebuildView()
{
if (_reportCombiner == null)
return;
string searchKeyword = _searchField.value;
// 搜索匹配
DefaultSearchSystem.Search(_sourceDatas, searchKeyword);
// 开关匹配
foreach (var tableData in _sourceDatas)
{
var elementTableData = tableData as ElementTableData;
if (_elementVisibleState == false && elementTableData.Element.Hidden)
{
tableData.Visible = false;
continue;
}
if (_passesVisibleState == false && elementTableData.Element.Passes)
{
tableData.Visible = false;
continue;
}
if (_whiteListVisibleState == false && elementTableData.Element.IsWhiteList)
{
tableData.Visible = false;
continue;
}
}
// 重建视图
_elementTableView.RebuildView();
}
private void OnClickTableViewItem(PointerDownEvent evt, ITableData tableData)
{
// 双击后检视对应的资源
if (evt.clickCount == 2)
{
foreach (var cell in tableData.Cells)
{
if (cell is AssetPathCell assetPathCell)
{
if (assetPathCell.PingAssetObject())
break;
}
}
}
}
private void OnSearchKeyWordChange(ChangeEvent<string> e)
{
RebuildView();
}
private void OnSelectToggleValueChange(Toggle toggle, ChangeEvent<bool> e)
{
// 处理自身
toggle.SetValueWithoutNotify(e.newValue);
// 记录数据
var elementTableData = toggle.userData as ElementTableData;
elementTableData.Element.IsSelected = e.newValue;
// 处理多选目标
var selectedItems = _elementTableView.selectedItems;
foreach (var selectedItem in selectedItems)
{
var selectElement = selectedItem as ElementTableData;
selectElement.Element.IsSelected = e.newValue;
}
// 重绘视图
RebuildView();
}
private void OnDisplayToggleValueChange(ToggleDisplay toggle, ChangeEvent<bool> e)
{
// 处理自身
toggle.SetValueWithoutNotify(e.newValue);
// 记录数据
var elementTableData = toggle.userData as ElementTableData;
elementTableData.Element.Hidden = e.newValue;
// 处理多选目标
var selectedItems = _elementTableView.selectedItems;
foreach (var selectedItem in selectedItems)
{
var selectElement = selectedItem as ElementTableData;
if (selectElement != null)
selectElement.Element.Hidden = e.newValue;
}
// 重绘视图
RebuildView();
}
private void OnClickWhitListButton(EventBase evt)
{
// 刷新点击的按钮
Button button = evt.target as Button;
var elementTableData = button.userData as ElementTableData;
elementTableData.Element.IsWhiteList = !elementTableData.Element.IsWhiteList;
// 刷新框选的按钮
var selectedItems = _elementTableView.selectedItems;
if (selectedItems.Count() > 1)
{
foreach (var selectedItem in selectedItems)
{
var selectElement = selectedItem as ElementTableData;
selectElement.Element.IsWhiteList = selectElement.Element.IsWhiteList;
}
}
RebuildView();
}
}
}
#endif

View File

@@ -1,22 +0,0 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<uie:Toolbar name="Toolbar" style="display: flex; flex-direction: row;">
<uie:ToolbarSearchField focusable="true" name="SearchField" style="flex-grow: 1;" />
<ui:Button text="Fix Select" display-tooltip-when-elided="true" name="FixSelectButton" style="width: 80px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Fix All" display-tooltip-when-elided="true" name="FixAllButton" style="width: 80px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Export Select" display-tooltip-when-elided="true" name="ExportFilesButton" style="width: 100px; background-color: rgb(56, 147, 58);" />
<ui:Button text=" Multi-Import" display-tooltip-when-elided="true" name="MultiImportButton" style="width: 100px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Import" display-tooltip-when-elided="true" name="SingleImportButton" style="width: 100px; background-color: rgb(56, 147, 58);" />
</uie:Toolbar>
<ui:VisualElement name="PublicContainer" style="background-color: rgb(79, 79, 79); flex-direction: column; border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px;">
<ui:Label text="标题" display-tooltip-when-elided="true" name="ReportTitle" style="height: 16px; -unity-font-style: bold; -unity-text-align: middle-center;" />
<ui:Label text="说明" display-tooltip-when-elided="true" name="ReportDesc" style="-unity-text-align: upper-left; -unity-font-style: bold; background-color: rgb(42, 42, 42); min-height: 50px; border-top-left-radius: 3px; border-bottom-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; white-space: normal;" />
</ui:VisualElement>
<uie:Toolbar name="Toolbar" style="display: flex; flex-direction: row;">
<ui:Button text="显示隐藏元素" display-tooltip-when-elided="true" name="ShowHiddenButton" style="width: 100px;" />
<ui:Button text="显示通过元素" display-tooltip-when-elided="true" name="PassesVisibleButton" style="width: 100px;" />
<ui:Button text="显示白名单元素" display-tooltip-when-elided="true" name="WhiteListVisibleButton" style="width: 100px;" />
</uie:Toolbar>
<ui:VisualElement name="AssetGroup" style="flex-grow: 1; border-left-width: 1px; border-right-width: 1px; border-top-width: 1px; border-bottom-width: 1px; border-left-color: rgb(0, 0, 0); border-right-color: rgb(0, 0, 0); border-top-color: rgb(0, 0, 0); border-bottom-color: rgb(0, 0, 0); margin-left: 0; margin-right: 0; margin-top: 2px; margin-bottom: 1px; display: flex;">
<YooAsset.Editor.TableViewer name="TopTableView" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -1,10 +0,0 @@
fileFormatVersion: 2
guid: b87fc70b750616849942173af3bdfd90
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -1,31 +0,0 @@

namespace YooAsset.Editor
{
public enum EHeaderType
{
/// <summary>
/// 资源路径
/// </summary>
AssetPath,
/// <summary>
/// 资源对象
/// </summary>
AssetObject,
/// <summary>
/// 字符串
/// </summary>
StringValue,
/// <summary>
/// 整数数值
/// </summary>
LongValue,
/// <summary>
/// 浮点数数值
/// </summary>
DoubleValue,
}
}

View File

@@ -1,88 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
[Serializable]
public class ReportElement
{
/// <summary>
/// GUID白名单存储对象
/// </summary>
public string GUID;
/// <summary>
/// 扫描是否通过
/// </summary>
public bool Passes = true;
/// <summary>
/// 反馈的信息列表
/// </summary>
public List<ReportScanInfo> ScanInfos = new List<ReportScanInfo>();
public ReportElement(string guid)
{
GUID = guid;
}
/// <summary>
/// 添加扫描信息
/// </summary>
public void AddScanInfo(string headerTitle, string value)
{
var reportScanInfo = new ReportScanInfo(headerTitle, value);
ScanInfos.Add(reportScanInfo);
}
/// <summary>
/// 添加扫描信息
/// </summary>
public void AddScanInfo(string headerTitle, long value)
{
AddScanInfo(headerTitle, value.ToString());
}
/// <summary>
/// 添加扫描信息
/// </summary>
public void AddScanInfo(string headerTitle, double value)
{
AddScanInfo(headerTitle, value.ToString());
}
/// <summary>
/// 获取扫描信息
/// </summary>
public ReportScanInfo GetScanInfo(string headerTitle)
{
foreach (var scanInfo in ScanInfos)
{
if (scanInfo.HeaderTitle == headerTitle)
return scanInfo;
}
UnityEngine.Debug.LogWarning($"Not found {nameof(ReportScanInfo)} : {headerTitle}");
return null;
}
#region
/// <summary>
/// 是否在列表里选中
/// </summary>
public bool IsSelected { set; get; }
/// <summary>
/// 是否在白名单里
/// </summary>
public bool IsWhiteList { set; get; }
/// <summary>
/// 是否隐藏元素
/// </summary>
public bool Hidden { set; get; }
#endregion
}
}

View File

@@ -1,154 +0,0 @@
using System;
using UnityEditor;
namespace YooAsset.Editor
{
[Serializable]
public class ReportHeader
{
public const int MaxValue = 8388608;
/// <summary>
/// 标题
/// </summary>
public string HeaderTitle;
/// <summary>
/// 标题宽度
/// </summary>
public int Width;
/// <summary>
/// 单元列最小宽度
/// </summary>
public int MinWidth = 50;
/// <summary>
/// 单元列最大宽度
/// </summary>
public int MaxWidth = MaxValue;
/// <summary>
/// 可伸缩选项
/// </summary>
public bool Stretchable = false;
/// <summary>
/// 可搜索选项
/// </summary>
public bool Searchable = false;
/// <summary>
/// 可排序选项
/// </summary>
public bool Sortable = false;
/// <summary>
/// 统计数量
/// </summary>
public bool Counter = false;
/// <summary>
/// 展示单位
/// </summary>
public string Units = string.Empty;
/// <summary>
/// 数值类型
/// </summary>
public EHeaderType HeaderType = EHeaderType.StringValue;
public ReportHeader(string headerTitle, int width)
{
HeaderTitle = headerTitle;
Width = width;
MinWidth = width;
MaxWidth = width;
}
public ReportHeader(string headerTitle, int width, int minWidth, int maxWidth)
{
HeaderTitle = headerTitle;
Width = width;
MinWidth = minWidth;
MaxWidth = maxWidth;
}
public ReportHeader SetMinWidth(int value)
{
MinWidth = value;
return this;
}
public ReportHeader SetMaxWidth(int value)
{
MaxWidth = value;
return this;
}
public ReportHeader SetStretchable()
{
Stretchable = true;
return this;
}
public ReportHeader SetSearchable()
{
Searchable = true;
return this;
}
public ReportHeader SetSortable()
{
Sortable = true;
return this;
}
public ReportHeader SetCounter()
{
Counter = true;
return this;
}
public ReportHeader SetUnits(string units)
{
Units = units;
return this;
}
public ReportHeader SetHeaderType(EHeaderType value)
{
HeaderType = value;
return this;
}
/// <summary>
/// 检测数值有效性
/// </summary>
public void CheckValueValid(string value)
{
if (HeaderType == EHeaderType.AssetPath)
{
string guid = AssetDatabase.AssetPathToGUID(value);
if (string.IsNullOrEmpty(guid))
throw new Exception($"{HeaderTitle} value is invalid asset path : {value}");
}
else if (HeaderType == EHeaderType.AssetObject)
{
string guid = AssetDatabase.AssetPathToGUID(value);
if (string.IsNullOrEmpty(guid))
throw new Exception($"{HeaderTitle} value is invalid asset object : {value}");
}
else if (HeaderType == EHeaderType.DoubleValue)
{
if (double.TryParse(value, out double doubleValue) == false)
throw new Exception($"{HeaderTitle} value is invalid double value : {value}");
}
else if (HeaderType == EHeaderType.LongValue)
{
if (long.TryParse(value, out long longValue) == false)
throw new Exception($"{HeaderTitle} value is invalid long value : {value}");
}
else if (HeaderType == EHeaderType.StringValue)
{
}
else
{
throw new System.NotImplementedException(HeaderType.ToString());
}
}
}
}

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
[Serializable]
public class ReportScanInfo
{
/// <summary>
/// 标题
/// </summary>
public string HeaderTitle;
/// <summary>
/// 扫描反馈的信息
/// </summary>
public string ScanInfo;
public ReportScanInfo(string headerTitle, string scanInfo)
{
HeaderTitle = headerTitle;
ScanInfo = scanInfo;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 02caa7ae84ee8294a8904a5aaed420ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,118 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
[Serializable]
public class ScanReport
{
/// <summary>
/// 文件签名(自动填写)
/// </summary>
public string FileSign;
/// <summary>
/// 文件版本(自动填写)
/// </summary>
public string FileVersion;
/// <summary>
/// 模式类型(自动填写)
/// </summary>
public string SchemaType;
/// <summary>
/// 扫描器GUID自动填写
/// </summary>
public string ScannerGUID;
/// <summary>
/// 报告名称
/// </summary>
public string ReportName;
/// <summary>
/// 报告介绍
/// </summary>
public string ReportDesc;
/// <summary>
/// 报告的标题列表
/// </summary>
public List<ReportHeader> ReportHeaders = new List<ReportHeader>();
/// <summary>
/// 扫描的元素列表
/// </summary>
public List<ReportElement> ReportElements = new List<ReportElement>();
public ScanReport(string reportName, string reportDesc)
{
ReportName = reportName;
ReportDesc = reportDesc;
}
/// <summary>
/// 添加标题
/// </summary>
public ReportHeader AddHeader(string headerTitle, int width)
{
var reportHeader = new ReportHeader(headerTitle, width);
ReportHeaders.Add(reportHeader);
return reportHeader;
}
/// <summary>
/// 添加标题
/// </summary>
public ReportHeader AddHeader(string headerTitle, int width, int minWidth, int maxWidth)
{
var reportHeader = new ReportHeader(headerTitle, width, minWidth, maxWidth);
ReportHeaders.Add(reportHeader);
return reportHeader;
}
/// <summary>
/// 检测错误
/// </summary>
public void CheckError()
{
// 检测标题
Dictionary<string, ReportHeader> headerMap = new Dictionary<string, ReportHeader>();
foreach (var header in ReportHeaders)
{
string headerTitle = header.HeaderTitle;
if (headerMap.ContainsKey(headerTitle))
throw new Exception($"The header title {headerTitle} already exists !");
else
headerMap.Add(headerTitle, header);
}
// 检测扫描元素
HashSet<string> elementMap = new HashSet<string>();
foreach (var element in ReportElements)
{
if (string.IsNullOrEmpty(element.GUID))
throw new Exception($"The report element GUID is null or empty !");
if (elementMap.Contains(element.GUID))
throw new Exception($"The report element GUID already exists ! {element.GUID}");
else
elementMap.Add(element.GUID);
foreach (var scanInfo in element.ScanInfos)
{
if (headerMap.ContainsKey(scanInfo.HeaderTitle) == false)
throw new Exception($"The report element header {scanInfo.HeaderTitle} is missing !");
// 检测数值有效性
var header = headerMap[scanInfo.HeaderTitle];
header.CheckValueValid(scanInfo.ScanInfo);
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 650e3e4af4ede2a4eb2471c30e7820bb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,221 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 资源扫描报告合并器
/// 说明:相同类型的报告可以合并查看
/// </summary>
public class ScanReportCombiner
{
/// <summary>
/// 模式类型
/// </summary>
public string SchemaType { private set; get; }
/// <summary>
/// 报告标题
/// </summary>
public string ReportTitle { private set; get; }
/// <summary>
/// 报告介绍
/// </summary>
public string ReportDesc { private set; get; }
/// <summary>
/// 标题列表
/// </summary>
public List<ReportHeader> Headers = new List<ReportHeader>();
/// <summary>
/// 扫描结果
/// </summary>
public readonly List<ReportElement> Elements = new List<ReportElement>(10000);
private readonly Dictionary<string, ScanReport> _combines = new Dictionary<string, ScanReport>(100);
/// <summary>
/// 合并报告文件
/// 注意:模式不同的报告文件会合并失败!
/// </summary>
public bool Combine(ScanReport scanReport)
{
if (string.IsNullOrEmpty(scanReport.SchemaType))
{
Debug.LogError("Scan report schema type is null or empty !");
return false;
}
if (string.IsNullOrEmpty(SchemaType))
{
SchemaType = scanReport.SchemaType;
ReportTitle = scanReport.ReportName;
ReportDesc = scanReport.ReportDesc;
Headers = scanReport.ReportHeaders;
}
if (SchemaType != scanReport.SchemaType)
{
Debug.LogWarning($"Scan report has different schema type{scanReport.SchemaType} != {SchemaType}");
return false;
}
if (_combines.ContainsKey(scanReport.ScannerGUID))
{
Debug.LogWarning($"Scan report has already existed : {scanReport.ScannerGUID}");
return false;
}
_combines.Add(scanReport.ScannerGUID, scanReport);
CombineInternal(scanReport);
return true;
}
private void CombineInternal(ScanReport scanReport)
{
string scannerGUID = scanReport.ScannerGUID;
List<ReportElement> elements = scanReport.ReportElements;
// 设置白名单
var scanner = AssetArtScannerSettingData.Setting.GetScanner(scannerGUID);
if (scanner != null)
{
foreach (var element in elements)
{
if (scanner.CheckWhiteList(element.GUID))
element.IsWhiteList = true;
}
}
// 添加到集合
Elements.AddRange(elements);
}
/// <summary>
/// 获取指定的标题类
/// </summary>
public ReportHeader GetHeader(string headerTitle)
{
foreach (var header in Headers)
{
if (header.HeaderTitle == headerTitle)
return header;
}
UnityEngine.Debug.LogWarning($"Not found {nameof(ReportHeader)} : {headerTitle}");
return null;
}
/// <summary>
/// 导出选中文件
/// </summary>
public void ExportFiles(string exportFolderPath)
{
if (string.IsNullOrEmpty(exportFolderPath))
return;
foreach (var element in Elements)
{
if (element.IsSelected)
{
string assetPath = AssetDatabase.GUIDToAssetPath(element.GUID);
if (string.IsNullOrEmpty(assetPath) == false)
{
string destPath = Path.Combine(exportFolderPath, assetPath);
EditorTools.CopyFile(assetPath, destPath, true);
}
}
}
}
/// <summary>
/// 保存改变数据
/// </summary>
public void SaveChange()
{
// 存储白名单
foreach (var scanReport in _combines.Values)
{
string scannerGUID = scanReport.ScannerGUID;
var elements = scanReport.ReportElements;
var scanner = AssetArtScannerSettingData.Setting.GetScanner(scannerGUID);
if (scanner != null)
{
List<string> whiteList = new List<string>(elements.Count);
foreach (var element in elements)
{
if (element.IsWhiteList)
whiteList.Add(element.GUID);
}
whiteList.Sort();
scanner.WhiteList = whiteList;
AssetArtScannerSettingData.SaveFile();
}
}
}
/// <summary>
/// 修复所有元素
/// 注意:排除白名单和隐藏元素
/// </summary>
public void FixAll()
{
foreach (var scanReport in _combines.Values)
{
string scannerGUID = scanReport.ScannerGUID;
var elements = scanReport.ReportElements;
List<ReportElement> fixList = new List<ReportElement>(elements.Count);
foreach (var element in elements)
{
if (element.Passes || element.IsWhiteList || element.Hidden)
continue;
fixList.Add(element);
}
FixInternal(scannerGUID, fixList);
}
}
/// <summary>
/// 修复选定元素
/// 注意:包含白名单和隐藏元素
/// </summary>
public void FixSelect()
{
foreach (var scanReport in _combines.Values)
{
string scannerGUID = scanReport.ScannerGUID;
var elements = scanReport.ReportElements;
List<ReportElement> fixList = new List<ReportElement>(elements.Count);
foreach (var element in elements)
{
if (element.Passes)
continue;
if (element.IsSelected)
fixList.Add(element);
}
FixInternal(scannerGUID, fixList);
}
}
private void FixInternal(string scannerGUID, List<ReportElement> fixList)
{
AssetArtScanner scanner = AssetArtScannerSettingData.Setting.GetScanner(scannerGUID);
if (scanner != null)
{
var schema = scanner.LoadSchema();
if (schema != null)
{
schema.FixResult(fixList);
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8524c3deb9b27fe4e8e63f15b9ffaaa3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,54 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using UnityEngine;
namespace YooAsset.Editor
{
public class ScanReportConfig
{
/// <summary>
/// 导入JSON报告文件
/// </summary>
public static ScanReport ImportJsonConfig(string filePath)
{
if (File.Exists(filePath) == false)
throw new FileNotFoundException(filePath);
string jsonData = FileUtility.ReadAllText(filePath);
ScanReport report = JsonUtility.FromJson<ScanReport>(jsonData);
// 检测配置文件的签名
if (report.FileSign != ScannerDefine.ReportFileSign)
throw new Exception($"导入的报告文件无法识别 : {filePath}");
// 检测报告文件的版本
if (report.FileVersion != ScannerDefine.ReportFileVersion)
throw new Exception($"报告文件的版本不匹配 : {report.FileVersion} != {ScannerDefine.ReportFileVersion}");
// 检测标题数和内容是否匹配
foreach (var element in report.ReportElements)
{
if (element.ScanInfos.Count != report.ReportHeaders.Count)
{
throw new Exception($"报告的标题数和内容不匹配!");
}
}
return report;
}
/// <summary>
/// 导出JSON报告文件
/// </summary>
public static void ExportJsonConfig(string savePath, ScanReport scanReport)
{
if (File.Exists(savePath))
File.Delete(savePath);
string json = JsonUtility.ToJson(scanReport, true);
FileUtility.WriteAllText(savePath, json);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 694cf47ade54f2b4fa6e618c1310c476
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,13 +0,0 @@
using System;
namespace YooAsset.Editor
{
[Serializable]
public class AssetArtCollector
{
/// <summary>
/// 扫描目录
/// </summary>
public string CollectPath = string.Empty;
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 6e7252b59455e5c45af0041ccd24b234
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,132 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor
{
[Serializable]
public class AssetArtScanner
{
/// <summary>
/// 扫描器GUID
/// </summary>
public string ScannerGUID = string.Empty;
/// <summary>
/// 扫描器名称
/// </summary>
public string ScannerName = string.Empty;
/// <summary>
/// 扫描器描述
/// </summary>
public string ScannerDesc = string.Empty;
/// <summary>
/// 扫描模式
/// 注意文件路径或文件GUID
/// </summary>
public string ScannerSchema = string.Empty;
/// <summary>
/// 存储目录
/// </summary>
public string SaveDirectory = string.Empty;
/// <summary>
/// 收集列表
/// </summary>
public List<AssetArtCollector> Collectors = new List<AssetArtCollector>();
/// <summary>
/// 白名单
/// </summary>
public List<string> WhiteList = new List<string>();
/// <summary>
/// 检测关键字匹配
/// </summary>
public bool CheckKeyword(string keyword)
{
if (ScannerName.Contains(keyword) || ScannerDesc.Contains(keyword))
return true;
else
return false;
}
/// <summary>
/// 是否在白名单里
/// </summary>
public bool CheckWhiteList(string guid)
{
return WhiteList.Contains(guid);
}
/// <summary>
/// 检测配置错误
/// </summary>
public void CheckConfigError()
{
if (string.IsNullOrEmpty(ScannerName))
throw new Exception($"Scanner name is null or empty !");
if (string.IsNullOrEmpty(ScannerSchema))
throw new Exception($"Scanner {ScannerName} schema is null !");
if (string.IsNullOrEmpty(SaveDirectory) == false)
{
if (Directory.Exists(SaveDirectory) == false)
throw new Exception($"Scanner {ScannerName} save directory is invalid : {SaveDirectory}");
}
}
/// <summary>
/// 加载扫描模式实例
/// </summary>
public ScannerSchema LoadSchema()
{
if (string.IsNullOrEmpty(ScannerSchema))
return null;
string filePath;
if (ScannerSchema.StartsWith("Assets/"))
{
filePath = ScannerSchema;
}
else
{
string guid = ScannerSchema;
filePath = AssetDatabase.GUIDToAssetPath(guid);
}
var schema = AssetDatabase.LoadMainAssetAtPath(filePath) as ScannerSchema;
if (schema == null)
Debug.LogWarning($"Failed load scanner schema : {filePath}");
return schema;
}
/// <summary>
/// 运行扫描器生成报告类
/// </summary>
public ScanReport RunScanner()
{
if (Collectors.Count == 0)
Debug.LogWarning($"Scanner {ScannerName} collector is empty !");
ScannerSchema schema = LoadSchema();
if (schema == null)
throw new Exception($"Failed to load schema : {ScannerSchema}");
var report = schema.RunScanner(this);
report.FileSign = ScannerDefine.ReportFileSign;
report.FileVersion = ScannerDefine.ReportFileVersion;
report.SchemaType = schema.GetType().FullName;
report.ScannerGUID = ScannerGUID;
return report;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c63683b07b7a2454b93539ae6b9f32ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,85 +0,0 @@
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor
{
public class AssetArtScannerConfig
{
public class ConfigWrapper
{
/// <summary>
/// 文件签名
/// </summary>
public string FileSign;
/// <summary>
/// 文件版本
/// </summary>
public string FileVersion;
/// <summary>
/// 扫描器列表
/// </summary>
public List<AssetArtScanner> Scanners = new List<AssetArtScanner>();
}
/// <summary>
/// 导入JSON配置文件
/// </summary>
public static void ImportJsonConfig(string filePath)
{
if (File.Exists(filePath) == false)
throw new FileNotFoundException(filePath);
string json = FileUtility.ReadAllText(filePath);
ConfigWrapper setting = JsonUtility.FromJson<ConfigWrapper>(json);
// 检测配置文件的签名
if (setting.FileSign != ScannerDefine.SettingFileSign)
throw new Exception($"导入的配置文件无法识别 : {filePath}");
// 检测配置文件的版本
if (setting.FileVersion != ScannerDefine.SettingFileVersion)
throw new Exception($"配置文件的版本不匹配 : {setting.FileVersion} != {ScannerDefine.SettingFileVersion}");
// 检测配置合法性
HashSet<string> scanGUIDs = new HashSet<string>();
foreach (var sacnner in setting.Scanners)
{
if (scanGUIDs.Contains(sacnner.ScannerGUID))
{
throw new Exception($"Scanner {sacnner.ScannerName} GUID is existed : {sacnner.ScannerGUID} ");
}
else
{
scanGUIDs.Add(sacnner.ScannerGUID);
}
}
AssetArtScannerSettingData.Setting.Scanners = setting.Scanners;
AssetArtScannerSettingData.SaveFile();
}
/// <summary>
/// 导出JSON配置文件
/// </summary>
public static void ExportJsonConfig(string savePath)
{
if (File.Exists(savePath))
File.Delete(savePath);
ConfigWrapper wrapper = new ConfigWrapper();
wrapper.FileSign = ScannerDefine.SettingFileSign;
wrapper.FileVersion = ScannerDefine.SettingFileVersion;
wrapper.Scanners = AssetArtScannerSettingData.Setting.Scanners;
string json = JsonUtility.ToJson(wrapper, true);
FileUtility.WriteAllText(savePath, json);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: bed1ef72d1c03e848a41d5ea115e9870
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,61 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEngine;
using NUnit.Framework.Constraints;
namespace YooAsset.Editor
{
public class AssetArtScannerSetting : ScriptableObject
{
/// <summary>
/// 扫描器列表
/// </summary>
public List<AssetArtScanner> Scanners = new List<AssetArtScanner>();
/// <summary>
/// 开始扫描
/// </summary>
public ScannerResult BeginScan(string scannerGUID)
{
try
{
// 获取扫描器配置
var scanner = GetScanner(scannerGUID);
if (scanner == null)
throw new Exception($"Invalid scanner GUID : {scannerGUID}");
// 检测配置合法性
scanner.CheckConfigError();
// 开始扫描工作
ScanReport report = scanner.RunScanner();
report.CheckError();
// 返回扫描结果
return new ScannerResult(report);
}
catch (Exception e)
{
return new ScannerResult(e.Message, e.StackTrace);
}
}
/// <summary>
/// 获取指定的扫描器
/// </summary>
public AssetArtScanner GetScanner(string scannerGUID)
{
foreach (var scanner in Scanners)
{
if (scanner.ScannerGUID == scannerGUID)
return scanner;
}
Debug.LogWarning($"Not found scanner : {scannerGUID}");
return null;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 84df5e62e3f1b6746a1263e076b003e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,161 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
public class AssetArtScannerSettingData
{
/// <summary>
/// 配置数据是否被修改
/// </summary>
public static bool IsDirty { private set; get; } = false;
static AssetArtScannerSettingData()
{
}
private static AssetArtScannerSetting _setting = null;
public static AssetArtScannerSetting Setting
{
get
{
if (_setting == null)
_setting = SettingLoader.LoadSettingData<AssetArtScannerSetting>();
return _setting;
}
}
/// <summary>
/// 存储配置文件
/// </summary>
public static void SaveFile()
{
if (Setting != null)
{
IsDirty = false;
EditorUtility.SetDirty(Setting);
AssetDatabase.SaveAssets();
Debug.Log($"{nameof(AssetArtScannerSetting)}.asset is saved!");
}
}
/// <summary>
/// 清空所有数据
/// </summary>
public static void ClearAll()
{
Setting.Scanners.Clear();
SaveFile();
}
/// <summary>
/// 扫描所有项
/// </summary>
public static void ScanAll()
{
foreach (var scanner in Setting.Scanners)
{
var scanResult = Setting.BeginScan(scanner.ScannerGUID);
if (scanResult.Succeed == false)
{
Debug.LogError($"{scanner.ScannerName} failed : {scanResult.ErrorInfo}");
}
}
}
/// <summary>
/// 扫描所有项
/// </summary>
public static void ScanAll(string keyword)
{
foreach (var scanner in Setting.Scanners)
{
if (string.IsNullOrEmpty(keyword) == false)
{
if (scanner.CheckKeyword(keyword) == false)
continue;
}
var scanResult = Setting.BeginScan(scanner.ScannerGUID);
if (scanResult.Succeed == false)
{
Debug.LogError($"{scanner.ScannerName} failed : {scanResult.ErrorInfo}");
}
}
}
/// <summary>
/// 扫描单项
/// </summary>
public static ScannerResult Scan(string scannerGUID)
{
var scanResult = Setting.BeginScan(scannerGUID);
if (scanResult.Succeed == false)
{
Debug.LogError(scanResult.ErrorInfo);
}
return scanResult;
}
// 扫描器编辑相关
public static AssetArtScanner CreateScanner(string name, string desc)
{
AssetArtScanner scanner = new AssetArtScanner();
scanner.ScannerGUID = System.Guid.NewGuid().ToString();
scanner.ScannerName = name;
scanner.ScannerDesc = desc;
Setting.Scanners.Add(scanner);
IsDirty = true;
return scanner;
}
public static void RemoveScanner(AssetArtScanner scanner)
{
if (Setting.Scanners.Remove(scanner))
{
IsDirty = true;
}
else
{
Debug.LogWarning($"Failed remove scanner : {scanner.ScannerName}");
}
}
public static void ModifyScanner(AssetArtScanner scanner)
{
if (scanner != null)
{
IsDirty = true;
}
}
// 资源收集编辑相关
public static void CreateCollector(AssetArtScanner scanner, AssetArtCollector collector)
{
scanner.Collectors.Add(collector);
IsDirty = true;
}
public static void RemoveCollector(AssetArtScanner scanner, AssetArtCollector collector)
{
if (scanner.Collectors.Remove(collector))
{
IsDirty = true;
}
else
{
Debug.LogWarning($"Failed remove collector : {collector.CollectPath}");
}
}
public static void ModifyCollector(AssetArtScanner scanner, AssetArtCollector collector)
{
if (scanner != null && collector != null)
{
IsDirty = true;
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: fda10f23f6f36bf498b54323fe4f680b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,549 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
public class AssetArtScannerWindow : EditorWindow
{
[MenuItem("YooAsset/AssetArt Scanner", false, 301)]
public static void OpenWindow()
{
AssetArtScannerWindow window = GetWindow<AssetArtScannerWindow>("AssetArt Scanner", true, WindowsDefine.DockedWindowTypes);
window.minSize = new Vector2(800, 600);
}
private Button _saveButton;
private ListView _scannerListView;
private ToolbarSearchField _scannerSearchField;
private VisualElement _scannerContentContainer;
private VisualElement _inspectorContainer;
private Label _schemaGuideTxt;
private TextField _scannerNameTxt;
private TextField _scannerDescTxt;
private ObjectField _scannerSchemaField;
private ObjectField _outputFolderField;
private ScrollView _collectorScrollView;
private int _lastModifyScannerIndex = 0;
public void CreateGUI()
{
Undo.undoRedoPerformed -= RefreshWindow;
Undo.undoRedoPerformed += RefreshWindow;
try
{
VisualElement root = this.rootVisualElement;
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUXML<AssetArtScannerWindow>();
if (visualAsset == null)
return;
visualAsset.CloneTree(root);
// 导入导出按钮
var exportBtn = root.Q<Button>("ExportButton");
exportBtn.clicked += ExportBtn_clicked;
var importBtn = root.Q<Button>("ImportButton");
importBtn.clicked += ImportBtn_clicked;
// 配置保存按钮
_saveButton = root.Q<Button>("SaveButton");
_saveButton.clicked += SaveBtn_clicked;
// 扫描按钮
var scanAllBtn = root.Q<Button>("ScanAllButton");
scanAllBtn.clicked += ScanAllBtn_clicked;
var scanBtn = root.Q<Button>("ScanBtn");
scanBtn.clicked += ScanBtn_clicked;
// 扫描列表相关
_scannerListView = root.Q<ListView>("ScannerListView");
_scannerListView.makeItem = MakeScannerListViewItem;
_scannerListView.bindItem = BindScannerListViewItem;
#if UNITY_2022_3_OR_NEWER
_scannerListView.selectionChanged += ScannerListView_onSelectionChange;
#elif UNITY_2020_1_OR_NEWER
_scannerListView.onSelectionChange += ScannerListView_onSelectionChange;
#else
_scannerListView.onSelectionChanged += ScannerListView_onSelectionChange;
#endif
// 扫描列表过滤
_scannerSearchField = root.Q<ToolbarSearchField>("ScannerSearchField");
_scannerSearchField.RegisterValueChangedCallback(OnSearchKeyWordChange);
// 扫描器添加删除按钮
var scannerAddContainer = root.Q("ScannerAddContainer");
{
var addBtn = scannerAddContainer.Q<Button>("AddBtn");
addBtn.clicked += AddScannerBtn_clicked;
var removeBtn = scannerAddContainer.Q<Button>("RemoveBtn");
removeBtn.clicked += RemoveScannerBtn_clicked;
}
// 扫描器容器
_scannerContentContainer = root.Q("ScannerContentContainer");
// 检视界面容器
_inspectorContainer = root.Q("InspectorContainer");
// 扫描器指南
_schemaGuideTxt = root.Q<Label>("SchemaUserGuide");
// 扫描器名称
_scannerNameTxt = root.Q<TextField>("ScannerName");
_scannerNameTxt.RegisterValueChangedCallback(evt =>
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner != null)
{
selectScanner.ScannerName = evt.newValue;
AssetArtScannerSettingData.ModifyScanner(selectScanner);
FillScannerListViewData();
}
});
// 扫描器备注
_scannerDescTxt = root.Q<TextField>("ScannerDesc");
_scannerDescTxt.RegisterValueChangedCallback(evt =>
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner != null)
{
selectScanner.ScannerDesc = evt.newValue;
AssetArtScannerSettingData.ModifyScanner(selectScanner);
FillScannerListViewData();
}
});
// 扫描模式
_scannerSchemaField = root.Q<ObjectField>("ScanSchema");
_scannerSchemaField.RegisterValueChangedCallback(evt =>
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner != null)
{
string assetPath = AssetDatabase.GetAssetPath(evt.newValue);
selectScanner.ScannerSchema = AssetDatabase.AssetPathToGUID(assetPath);
AssetArtScannerSettingData.ModifyScanner(selectScanner);
}
});
// 存储目录
_outputFolderField = root.Q<ObjectField>("OutputFolder");
_outputFolderField.RegisterValueChangedCallback(evt =>
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner != null)
{
if (evt.newValue == null)
{
selectScanner.SaveDirectory = string.Empty;
AssetArtScannerSettingData.ModifyScanner(selectScanner);
}
else
{
string assetPath = AssetDatabase.GetAssetPath(evt.newValue);
if (AssetDatabase.IsValidFolder(assetPath))
{
selectScanner.SaveDirectory = assetPath;
AssetArtScannerSettingData.ModifyScanner(selectScanner);
}
else
{
Debug.LogWarning($"Select asset object not folder ! {assetPath}");
}
}
}
});
// 收集列表相关
_collectorScrollView = root.Q<ScrollView>("CollectorScrollView");
_collectorScrollView.style.height = new Length(100, LengthUnit.Percent);
_collectorScrollView.viewDataKey = "scrollView";
// 收集器创建按钮
var collectorAddContainer = root.Q("CollectorAddContainer");
{
var addBtn = collectorAddContainer.Q<Button>("AddBtn");
addBtn.clicked += AddCollectorBtn_clicked;
}
// 刷新窗体
RefreshWindow();
}
catch (System.Exception e)
{
Debug.LogError(e.ToString());
}
}
public void OnDestroy()
{
// 注意:清空所有撤销操作
Undo.ClearAll();
if (AssetArtScannerSettingData.IsDirty)
AssetArtScannerSettingData.SaveFile();
}
public void Update()
{
if (_saveButton != null)
{
if (AssetArtScannerSettingData.IsDirty)
{
if (_saveButton.enabledSelf == false)
_saveButton.SetEnabled(true);
}
else
{
if (_saveButton.enabledSelf)
_saveButton.SetEnabled(false);
}
}
}
private void RefreshWindow()
{
_scannerContentContainer.visible = false;
FillScannerListViewData();
}
private void ExportBtn_clicked()
{
string resultPath = EditorTools.OpenFolderPanel("Export JSON", "Assets/");
if (resultPath != null)
{
AssetArtScannerConfig.ExportJsonConfig($"{resultPath}/AssetArtScannerConfig.json");
}
}
private void ImportBtn_clicked()
{
string resultPath = EditorTools.OpenFilePath("Import JSON", "Assets/", "json");
if (resultPath != null)
{
AssetArtScannerConfig.ImportJsonConfig(resultPath);
RefreshWindow();
}
}
private void SaveBtn_clicked()
{
AssetArtScannerSettingData.SaveFile();
}
private void ScanAllBtn_clicked()
{
if (EditorUtility.DisplayDialog("Info", $"Start full scan!", "Yes", "No"))
{
string searchKeyWord = _scannerSearchField.value;
AssetArtScannerSettingData.ScanAll(searchKeyWord);
AssetDatabase.Refresh();
}
else
{
Debug.LogWarning("Full scan has been canceled.");
}
}
private void ScanBtn_clicked()
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner == null)
return;
ScannerResult scannerResult = AssetArtScannerSettingData.Scan(selectScanner.ScannerGUID);
if (scannerResult.Succeed)
{
// 自动打开报告界面
scannerResult.OpenReportWindow();
AssetDatabase.Refresh();
}
}
private void OnSearchKeyWordChange(ChangeEvent<string> e)
{
_lastModifyScannerIndex = 0;
RefreshWindow();
}
// 分组列表相关
private void FillScannerListViewData()
{
_scannerListView.Clear();
_scannerListView.ClearSelection();
var filterItems = FilterScanners();
if (AssetArtScannerSettingData.Setting.Scanners.Count == filterItems.Count)
{
#if UNITY_2020_3_OR_NEWER
_scannerListView.reorderable = true;
#endif
_scannerListView.itemsSource = AssetArtScannerSettingData.Setting.Scanners;
_scannerListView.Rebuild();
}
else
{
#if UNITY_2020_3_OR_NEWER
_scannerListView.reorderable = false;
#endif
_scannerListView.itemsSource = filterItems;
_scannerListView.Rebuild();
}
if (_lastModifyScannerIndex >= 0 && _lastModifyScannerIndex < _scannerListView.itemsSource.Count)
{
_scannerListView.selectedIndex = _lastModifyScannerIndex;
}
}
private List<AssetArtScanner> FilterScanners()
{
string searchKeyWord = _scannerSearchField.value;
List<AssetArtScanner> result = new List<AssetArtScanner>(AssetArtScannerSettingData.Setting.Scanners.Count);
// 过滤列表
foreach (var scanner in AssetArtScannerSettingData.Setting.Scanners)
{
if (string.IsNullOrEmpty(searchKeyWord) == false)
{
if (scanner.CheckKeyword(searchKeyWord) == false)
continue;
}
result.Add(scanner);
}
return result;
}
private VisualElement MakeScannerListViewItem()
{
VisualElement element = new VisualElement();
{
var label = new Label();
label.name = "Label1";
label.style.unityTextAlign = TextAnchor.MiddleLeft;
label.style.flexGrow = 1f;
label.style.height = 20f;
element.Add(label);
}
return element;
}
private void BindScannerListViewItem(VisualElement element, int index)
{
List<AssetArtScanner> sourceList = _scannerListView.itemsSource as List<AssetArtScanner>;
var scanner = sourceList[index];
var textField1 = element.Q<Label>("Label1");
if (string.IsNullOrEmpty(scanner.ScannerDesc))
textField1.text = scanner.ScannerName;
else
textField1.text = $"{scanner.ScannerName} ({scanner.ScannerDesc})";
}
private void ScannerListView_onSelectionChange(IEnumerable<object> objs)
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner == null)
{
_scannerContentContainer.visible = false;
return;
}
_scannerContentContainer.visible = true;
_lastModifyScannerIndex = _scannerListView.selectedIndex;
_scannerNameTxt.SetValueWithoutNotify(selectScanner.ScannerName);
_scannerDescTxt.SetValueWithoutNotify(selectScanner.ScannerDesc);
// 显示检视面板
var scanSchema = selectScanner.LoadSchema();
RefreshInspector(scanSchema);
// 设置Schema对象
if (scanSchema == null)
{
_scannerSchemaField.SetValueWithoutNotify(null);
_schemaGuideTxt.text = string.Empty;
}
else
{
_scannerSchemaField.SetValueWithoutNotify(scanSchema);
_schemaGuideTxt.text = scanSchema.GetUserGuide();
}
// 显示存储目录
DefaultAsset saveFolder = AssetDatabase.LoadAssetAtPath<DefaultAsset>(selectScanner.SaveDirectory);
if (saveFolder == null)
{
_outputFolderField.SetValueWithoutNotify(null);
}
else
{
_outputFolderField.SetValueWithoutNotify(saveFolder);
}
FillCollectorViewData();
}
private void AddScannerBtn_clicked()
{
Undo.RecordObject(AssetArtScannerSettingData.Setting, "YooAsset.AssetArtScannerWindow AddScanner");
AssetArtScannerSettingData.CreateScanner("Default Scanner", string.Empty);
FillScannerListViewData();
}
private void RemoveScannerBtn_clicked()
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner == null)
return;
Undo.RecordObject(AssetArtScannerSettingData.Setting, "YooAsset.AssetArtScannerWindow RemoveScanner");
AssetArtScannerSettingData.RemoveScanner(selectScanner);
FillScannerListViewData();
}
// 收集列表相关
private void FillCollectorViewData()
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner == null)
return;
// 填充数据
_collectorScrollView.Clear();
for (int i = 0; i < selectScanner.Collectors.Count; i++)
{
VisualElement element = MakeCollectorListViewItem();
BindCollectorListViewItem(element, i);
_collectorScrollView.Add(element);
}
}
private VisualElement MakeCollectorListViewItem()
{
VisualElement element = new VisualElement();
VisualElement elementTop = new VisualElement();
elementTop.style.flexDirection = FlexDirection.Row;
element.Add(elementTop);
VisualElement elementSpace = new VisualElement();
elementSpace.style.flexDirection = FlexDirection.Column;
element.Add(elementSpace);
// Top VisualElement
{
var button = new Button();
button.name = "Button1";
button.text = "-";
button.style.unityTextAlign = TextAnchor.MiddleCenter;
button.style.flexGrow = 0f;
elementTop.Add(button);
}
{
var objectField = new ObjectField();
objectField.name = "ObjectField1";
objectField.label = "Collector";
objectField.objectType = typeof(UnityEngine.Object);
objectField.style.unityTextAlign = TextAnchor.MiddleLeft;
objectField.style.flexGrow = 1f;
elementTop.Add(objectField);
var label = objectField.Q<Label>();
label.style.minWidth = 63;
}
// Space VisualElement
{
var label = new Label();
label.style.height = 10;
elementSpace.Add(label);
}
return element;
}
private void BindCollectorListViewItem(VisualElement element, int index)
{
var selectScanner = _scannerListView.selectedItem as AssetArtScanner;
if (selectScanner == null)
return;
var collector = selectScanner.Collectors[index];
var collectObject = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(collector.CollectPath);
if (collectObject != null)
collectObject.name = collector.CollectPath;
// Remove Button
var removeBtn = element.Q<Button>("Button1");
removeBtn.clicked += () =>
{
RemoveCollectorBtn_clicked(collector);
};
// Collector Path
var objectField1 = element.Q<ObjectField>("ObjectField1");
objectField1.SetValueWithoutNotify(collectObject);
objectField1.RegisterValueChangedCallback(evt =>
{
collector.CollectPath = AssetDatabase.GetAssetPath(evt.newValue);
objectField1.value.name = collector.CollectPath;
AssetArtScannerSettingData.ModifyCollector(selectScanner, collector);
});
}
private void AddCollectorBtn_clicked()
{
var selectSacnner = _scannerListView.selectedItem as AssetArtScanner;
if (selectSacnner == null)
return;
Undo.RecordObject(AssetArtScannerSettingData.Setting, "YooAsset.AssetArtScannerWindow AddCollector");
AssetArtCollector collector = new AssetArtCollector();
AssetArtScannerSettingData.CreateCollector(selectSacnner, collector);
FillCollectorViewData();
}
private void RemoveCollectorBtn_clicked(AssetArtCollector selectCollector)
{
var selectSacnner = _scannerListView.selectedItem as AssetArtScanner;
if (selectSacnner == null)
return;
if (selectCollector == null)
return;
Undo.RecordObject(AssetArtScannerSettingData.Setting, "YooAsset.AssetArtScannerWindow RemoveCollector");
AssetArtScannerSettingData.RemoveCollector(selectSacnner, selectCollector);
FillCollectorViewData();
}
// 属性面板相关
private void RefreshInspector(ScannerSchema scanSchema)
{
if (scanSchema == null)
{
UIElementsTools.SetElementVisible(_inspectorContainer, false);
return;
}
var inspector = scanSchema.CreateInspector();
if (inspector == null)
{
UIElementsTools.SetElementVisible(_inspectorContainer, false);
return;
}
if (inspector.Containner is VisualElement container)
{
UIElementsTools.SetElementVisible(_inspectorContainer, true);
_inspectorContainer.Clear();
_inspectorContainer.Add(container);
_inspectorContainer.style.width = inspector.Width;
_inspectorContainer.style.minWidth = inspector.MinWidth;
_inspectorContainer.style.maxWidth = inspector.MaxWidth;
}
else
{
Debug.LogWarning($"{nameof(ScannerSchema)} inspector container is invalid !");
UIElementsTools.SetElementVisible(_inspectorContainer, false);
}
}
}
}
#endif

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: bff583b32bbeb7e498920bfdc84dba90
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,33 +0,0 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<uie:Toolbar name="Toolbar" style="display: flex; flex-direction: row-reverse;">
<ui:Button text="Save" display-tooltip-when-elided="true" name="SaveButton" style="width: 50px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Export" display-tooltip-when-elided="true" name="ExportButton" style="width: 50px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Import" display-tooltip-when-elided="true" name="ImportButton" style="width: 50px; background-color: rgb(56, 147, 58);" />
<ui:Button text="Scan All" display-tooltip-when-elided="true" name="ScanAllButton" style="width: 80px; background-color: rgb(56, 147, 58);" />
</uie:Toolbar>
<ui:VisualElement name="ContentContainer" style="flex-grow: 1; flex-direction: row;">
<ui:VisualElement name="ScannerListContainer" style="width: 250px; flex-grow: 0; background-color: rgb(67, 67, 67); border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px;">
<ui:Label text="Scanner List" display-tooltip-when-elided="true" name="ScannerListTitle" style="background-color: rgb(89, 89, 89); -unity-text-align: upper-center; -unity-font-style: bold; border-left-width: 3px; border-right-width: 3px; border-top-width: 3px; border-bottom-width: 3px; font-size: 12px;" />
<uie:ToolbarSearchField focusable="true" name="ScannerSearchField" style="width: 230px;" />
<ui:ListView focusable="true" name="ScannerListView" item-height="20" virtualization-method="DynamicHeight" reorder-mode="Animated" reorderable="true" style="flex-grow: 1;" />
<ui:VisualElement name="ScannerAddContainer" style="justify-content: center; flex-direction: row; flex-shrink: 0;">
<ui:Button text=" - " display-tooltip-when-elided="true" name="RemoveBtn" />
<ui:Button text=" + " display-tooltip-when-elided="true" name="AddBtn" />
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="ScannerContentContainer" style="flex-grow: 1; border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px; min-width: 400px;">
<ui:Label text="Scanner" display-tooltip-when-elided="true" name="ScannerContentTitle" style="-unity-text-align: upper-center; -unity-font-style: bold; font-size: 12px; border-top-width: 3px; border-right-width: 3px; border-bottom-width: 3px; border-left-width: 3px; background-color: rgb(89, 89, 89);" />
<ui:Label display-tooltip-when-elided="true" name="SchemaUserGuide" style="-unity-text-align: upper-center; -unity-font-style: bold; border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px; font-size: 12px; height: 40px;" />
<ui:TextField picking-mode="Ignore" label="Scanner Name" name="ScannerName" />
<ui:TextField picking-mode="Ignore" label="Scanner Desc" name="ScannerDesc" />
<uie:ObjectField label="Scanner Schema" name="ScanSchema" type="YooAsset.Editor.ScannerSchema, YooAsset.Editor" allow-scene-objects="false" />
<uie:ObjectField label="Output Folder" name="OutputFolder" type="UnityEditor.DefaultAsset, UnityEditor.CoreModule" allow-scene-objects="false" />
<ui:VisualElement name="CollectorAddContainer" style="height: 20px; flex-direction: row-reverse;">
<ui:Button text="[ + ]" display-tooltip-when-elided="true" name="AddBtn" />
<ui:Button text="Scan" display-tooltip-when-elided="true" name="ScanBtn" style="width: 60px;" />
</ui:VisualElement>
<ui:ScrollView name="CollectorScrollView" style="flex-grow: 1;" />
</ui:VisualElement>
<ui:VisualElement name="InspectorContainer" style="flex-grow: 1; border-top-width: 5px; border-right-width: 5px; border-bottom-width: 5px; border-left-width: 5px; background-color: rgb(67, 67, 67);" />
</ui:VisualElement>
</ui:UXML>

View File

@@ -1,10 +0,0 @@
fileFormatVersion: 2
guid: 5bbb873a7bee2924a86c876b67bb2cb4
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -1,26 +0,0 @@

namespace YooAsset.Editor
{
public class ScannerDefine
{
/// <summary>
/// 报告文件签名
/// </summary>
public const string ReportFileSign = "596f6f4172745265706f7274";
/// <summary>
/// 配置文件签名
/// </summary>
public const string SettingFileSign = "596f6f41727453657474696e67";
/// <summary>
/// 报告文件的版本
/// </summary>
public const string ReportFileVersion = "1.0";
/// <summary>
/// 配置文件的版本
/// </summary>
public const string SettingFileVersion = "1.0";
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: ed658bfc32cbfc44caf262a741a7c387
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,72 +0,0 @@

namespace YooAsset.Editor
{
public class ScannerResult
{
/// <summary>
/// 报告对象
/// </summary>
public ScanReport Report { private set; get; }
/// <summary>
/// 错误信息
/// </summary>
public string ErrorInfo { private set; get; }
/// <summary>
/// 错误堆栈
/// </summary>
public string ErrorStack { private set; get; }
/// <summary>
/// 是否成功
/// </summary>
public bool Succeed
{
get
{
if (string.IsNullOrEmpty(ErrorInfo))
return true;
else
return false;
}
}
public ScannerResult(string error, string stack)
{
ErrorInfo = error;
ErrorStack = stack;
}
public ScannerResult(ScanReport report)
{
Report = report;
}
/// <summary>
/// 打开报告窗口
/// </summary>
public void OpenReportWindow()
{
if (Succeed)
{
var reproterWindow = AssetArtReporterWindow.OpenWindow();
reproterWindow.ImportSingleReprotFile(Report);
}
}
/// <summary>
/// 保存报告文件
/// </summary>
public void SaveReportFile(string saveDirectory)
{
if (Report == null)
throw new System.Exception("Scan report is invalid !");
if (string.IsNullOrEmpty(saveDirectory))
saveDirectory = "Assets/";
string filePath = $"{saveDirectory}/{Report.ReportName}_{Report.ReportDesc}.json";
ScanReportConfig.ExportJsonConfig(filePath, Report);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e10cdab189d80b142ad5903d12956c59
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,32 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace YooAsset.Editor
{
public abstract class ScannerSchema : ScriptableObject
{
/// <summary>
/// 获取用户指南信息
/// </summary>
public abstract string GetUserGuide();
/// <summary>
/// 运行生成扫描报告
/// </summary>
public abstract ScanReport RunScanner(AssetArtScanner scanner);
/// <summary>
/// 修复扫描结果
/// </summary>
public abstract void FixResult(List<ReportElement> fixList);
/// <summary>
/// 创建检视面板
/// </summary>
public virtual SchemaInspector CreateInspector()
{
return null;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: eb6a587c72ccecc4ab6d386063cf0736
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,45 +0,0 @@

namespace YooAsset.Editor
{
public class SchemaInspector
{
/// <summary>
/// 检视界面的UI元素容器UIElements元素
/// </summary>
public object Containner { private set; get; }
/// <summary>
/// 检视界面宽度
/// </summary>
public int Width = 250;
/// <summary>
/// 检视界面最小宽度
/// </summary>
public int MinWidth = 250;
/// <summary>
/// 检视界面最大宽度
/// </summary>
public int MaxWidth = 250;
public SchemaInspector(object containner)
{
Containner = containner;
}
public SchemaInspector(object containner, int width)
{
Containner = containner;
Width = width;
MinWidth = width;
MaxWidth = width;
}
public SchemaInspector(object containner, int width, int minWidth, int maxWidth)
{
Containner = containner;
Width = width;
MinWidth = minWidth;
MaxWidth = maxWidth;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3440549fcb36bbf4c8c6da17fb858947
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,28 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
public static class AssetBundleBuilderHelper
{
/// <summary>
/// 获取默认的输出根目录
/// </summary>
public static string GetDefaultBuildOutputRoot()
{
string projectPath = EditorTools.GetProjectPath();
return $"{projectPath}/Bundles";
}
/// <summary>
/// 获取流文件夹路径
/// </summary>
public static string GetStreamingAssetsRoot()
{
return YooAssetSettingsData.GetYooDefaultBuildinRoot();
}
}
}

View File

@@ -1,130 +0,0 @@
using System;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
public static class AssetBundleBuilderSetting
{
// BuildPipelineName
public static string GetPackageBuildPipeline(string packageName)
{
string key = $"{Application.productName}_{packageName}_BuildPipelineName";
string defaultValue = EBuildPipeline.ScriptableBuildPipeline.ToString();
return EditorPrefs.GetString(key, defaultValue);
}
public static void SetPackageBuildPipeline(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_BuildPipelineName";
EditorPrefs.SetString(key, buildPipeline);
}
// ECompressOption
public static ECompressOption GetPackageCompressOption(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(ECompressOption)}";
return (ECompressOption)EditorPrefs.GetInt(key, (int)ECompressOption.LZ4);
}
public static void SetPackageCompressOption(string packageName, string buildPipeline, ECompressOption compressOption)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(ECompressOption)}";
EditorPrefs.SetInt(key, (int)compressOption);
}
// EFileNameStyle
public static EFileNameStyle GetPackageFileNameStyle(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(EFileNameStyle)}";
return (EFileNameStyle)EditorPrefs.GetInt(key, (int)EFileNameStyle.HashName);
}
public static void SetPackageFileNameStyle(string packageName, string buildPipeline, EFileNameStyle fileNameStyle)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(EFileNameStyle)}";
EditorPrefs.SetInt(key, (int)fileNameStyle);
}
// EBuildinFileCopyOption
public static EBuildinFileCopyOption GetPackageBuildinFileCopyOption(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(EBuildinFileCopyOption)}";
return (EBuildinFileCopyOption)EditorPrefs.GetInt(key, (int)EBuildinFileCopyOption.None);
}
public static void SetPackageBuildinFileCopyOption(string packageName, string buildPipeline, EBuildinFileCopyOption buildinFileCopyOption)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_{nameof(EBuildinFileCopyOption)}";
EditorPrefs.SetInt(key, (int)buildinFileCopyOption);
}
// BuildFileCopyParams
public static string GetPackageBuildinFileCopyParams(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_BuildFileCopyParams";
return EditorPrefs.GetString(key, string.Empty);
}
public static void SetPackageBuildinFileCopyParams(string packageName, string buildPipeline, string buildinFileCopyParams)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_BuildFileCopyParams";
EditorPrefs.SetString(key, buildinFileCopyParams);
}
// EncyptionServicesClassName
public static string GetPackageEncyptionServicesClassName(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_EncyptionServicesClassName";
return EditorPrefs.GetString(key, $"{typeof(EncryptionNone).FullName}");
}
public static void SetPackageEncyptionServicesClassName(string packageName, string buildPipeline, string encyptionClassName)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_EncyptionServicesClassName";
EditorPrefs.SetString(key, encyptionClassName);
}
// ManifestProcessServicesClassName
public static string GetPackageManifestProcessServicesClassName(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ManifestProcessServicesClassName";
return EditorPrefs.GetString(key, $"{typeof(ManifestProcessNone).FullName}");
}
public static void SetPackageManifestProcessServicesClassName(string packageName, string buildPipeline, string encyptionClassName)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ManifestProcessServicesClassName";
EditorPrefs.SetString(key, encyptionClassName);
}
// ManifestRestoreServicesClassName
public static string GetPackageManifestRestoreServicesClassName(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ManifestRestoreServicesClassName";
return EditorPrefs.GetString(key, $"{typeof(ManifestRestoreNone).FullName}");
}
public static void SetPackageManifestRestoreServicesClassName(string packageName, string buildPipeline, string encyptionClassName)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ManifestRestoreServicesClassName";
EditorPrefs.SetString(key, encyptionClassName);
}
// ClearBuildCache
public static bool GetPackageClearBuildCache(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ClearBuildCache";
return EditorPrefs.GetInt(key, 0) > 0;
}
public static void SetPackageClearBuildCache(string packageName, string buildPipeline, bool clearBuildCache)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_ClearBuildCache";
EditorPrefs.SetInt(key, clearBuildCache ? 1 : 0);
}
// UseAssetDependencyDB
public static bool GetPackageUseAssetDependencyDB(string packageName, string buildPipeline)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_UseAssetDependencyDB";
return EditorPrefs.GetInt(key, 0) > 0;
}
public static void SetPackageUseAssetDependencyDB(string packageName, string buildPipeline, bool useAssetDependencyDB)
{
string key = $"{Application.productName}_{packageName}_{buildPipeline}_UseAssetDependencyDB";
EditorPrefs.SetInt(key, useAssetDependencyDB ? 1 : 0);
}
}
}

View File

@@ -1,121 +0,0 @@
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
public class BuildMapContext : IContextObject
{
/// <summary>
/// 资源包集合
/// </summary>
private readonly Dictionary<string, BuildBundleInfo> _bundleInfoDic = new Dictionary<string, BuildBundleInfo>(10000);
/// <summary>
/// 图集资源集合
/// </summary>
public readonly List<BuildAssetInfo> SpriteAtlasAssetList = new List<BuildAssetInfo>(10000);
/// <summary>
/// 未被依赖的资源列表
/// </summary>
public readonly List<ReportIndependAsset> IndependAssets = new List<ReportIndependAsset>(1000);
/// <summary>
/// 参与构建的资源总数
/// 说明:包括主动收集的资源以及其依赖的所有资源
/// </summary>
public int AssetFileCount;
/// <summary>
/// 资源收集命令
/// </summary>
public CollectCommand Command { set; get; }
/// <summary>
/// 资源包信息列表
/// </summary>
public Dictionary<string, BuildBundleInfo>.ValueCollection Collection
{
get
{
return _bundleInfoDic.Values;
}
}
/// <summary>
/// 添加一个打包资源
/// </summary>
public void PackAsset(BuildAssetInfo assetInfo)
{
string bundleName = assetInfo.BundleName;
if (string.IsNullOrEmpty(bundleName))
throw new Exception("Should never get here !");
if (_bundleInfoDic.TryGetValue(bundleName, out BuildBundleInfo bundleInfo))
{
bundleInfo.PackAsset(assetInfo);
}
else
{
BuildBundleInfo newBundleInfo = new BuildBundleInfo(bundleName);
newBundleInfo.PackAsset(assetInfo);
_bundleInfoDic.Add(bundleName, newBundleInfo);
}
// 统计所有的精灵图集
if (assetInfo.AssetInfo.IsSpriteAtlas())
{
SpriteAtlasAssetList.Add(assetInfo);
}
}
/// <summary>
/// 是否包含资源包
/// </summary>
public bool IsContainsBundle(string bundleName)
{
return _bundleInfoDic.ContainsKey(bundleName);
}
/// <summary>
/// 获取资源包信息如果没找到返回NULL
/// </summary>
public BuildBundleInfo GetBundleInfo(string bundleName)
{
if (_bundleInfoDic.TryGetValue(bundleName, out BuildBundleInfo result))
{
return result;
}
throw new Exception($"Should never get here ! Not found bundle : {bundleName}");
}
/// <summary>
/// 获取构建管线里需要的数据
/// </summary>
public UnityEditor.AssetBundleBuild[] GetPipelineBuilds(bool replaceAssetPathWithAddres)
{
List<UnityEditor.AssetBundleBuild> builds = new List<UnityEditor.AssetBundleBuild>(_bundleInfoDic.Count);
foreach (var bundleInfo in _bundleInfoDic.Values)
{
builds.Add(bundleInfo.CreatePipelineBuild(replaceAssetPathWithAddres));
}
return builds.ToArray();
}
/// <summary>
/// 创建空的资源包
/// </summary>
public void CreateEmptyBundleInfo(string bundleName)
{
if (IsContainsBundle(bundleName) == false)
{
var bundleInfo = new BuildBundleInfo(bundleName);
_bundleInfoDic.Add(bundleName, bundleInfo);
}
}
}
}

View File

@@ -1,222 +0,0 @@
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
/// <summary>
/// 构建参数
/// </summary>
public abstract class BuildParameters
{
/// <summary>
/// 构建输出的根目录
/// </summary>
public string BuildOutputRoot;
/// <summary>
/// 内置文件的根目录
/// </summary>
public string BuildinFileRoot;
/// <summary>
/// 构建管线名称
/// </summary>
public string BuildPipeline;
/// <summary>
/// 构建资源包类型
/// </summary>
public int BuildBundleType;
/// <summary>
/// 构建的平台
/// </summary>
public BuildTarget BuildTarget;
/// <summary>
/// 构建的包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 构建的包裹版本
/// </summary>
public string PackageVersion;
/// <summary>
/// 构建的包裹备注
/// </summary>
public string PackageNote;
/// <summary>
/// 清空构建缓存文件
/// </summary>
public bool ClearBuildCacheFiles = false;
/// <summary>
/// 使用资源依赖缓存数据库
/// 说明:开启此项可以极大提高资源收集速度!
/// </summary>
public bool UseAssetDependencyDB = false;
/// <summary>
/// 启用共享资源打包
/// </summary>
public bool EnableSharePackRule = false;
/// <summary>
/// 对单独引用的共享资源进行独立打包
/// 说明:关闭该选项单独引用的共享资源将会构建到引用它的资源包内!
/// </summary>
public bool SingleReferencedPackAlone = true;
/// <summary>
/// 验证构建结果
/// </summary>
public bool VerifyBuildingResult = false;
/// <summary>
/// 资源包名称样式
/// </summary>
public EFileNameStyle FileNameStyle = EFileNameStyle.HashName;
/// <summary>
/// 内置文件的拷贝选项
/// </summary>
public EBuildinFileCopyOption BuildinFileCopyOption = EBuildinFileCopyOption.None;
/// <summary>
/// 内置文件的拷贝参数
/// </summary>
public string BuildinFileCopyParams;
/// <summary>
/// 资源包加密服务类
/// </summary>
public IEncryptionServices EncryptionServices;
/// <summary>
/// 资源清单加密服务类
/// </summary>
public IManifestProcessServices ManifestProcessServices;
/// <summary>
/// 资源清单解密服务类
/// </summary>
public IManifestRestoreServices ManifestRestoreServices;
private string _pipelineOutputDirectory = string.Empty;
private string _packageOutputDirectory = string.Empty;
private string _packageRootDirectory = string.Empty;
private string _buildinRootDirectory = string.Empty;
/// <summary>
/// 检测构建参数是否合法
/// </summary>
public virtual void CheckBuildParameters()
{
// 检测当前是否正在构建资源包
if (UnityEditor.BuildPipeline.isBuildingPlayer)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.ThePipelineIsBuiding, "The pipeline is buiding, please try again after finish !");
throw new Exception(message);
}
// 检测构建参数合法性
if (BuildTarget == BuildTarget.NoTarget)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.NoBuildTarget, "Please select the build target platform !");
throw new Exception(message);
}
if (string.IsNullOrEmpty(BuildOutputRoot))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildOutputRootIsNullOrEmpty, "Build output root is null or empty !");
throw new Exception(message);
}
if (string.IsNullOrEmpty(BuildinFileRoot))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildinFileRootIsNullOrEmpty, "Buildin file root is null or empty !");
throw new Exception(message);
}
if (string.IsNullOrEmpty(BuildPipeline))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildPipelineIsNullOrEmpty, "Build pipeline is null or empty !");
throw new Exception(message);
}
if (BuildBundleType == (int)EBuildBundleType.Unknown)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildBundleTypeIsUnknown, $"Build bundle type is unknown {BuildBundleType} !");
throw new Exception(message);
}
if (string.IsNullOrEmpty(PackageName))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.PackageNameIsNullOrEmpty, "Package name is null or empty !");
throw new Exception(message);
}
if (string.IsNullOrEmpty(PackageVersion))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.PackageVersionIsNullOrEmpty, "Package version is null or empty !");
throw new Exception(message);
}
// 设置默认备注信息
if (string.IsNullOrEmpty(PackageNote))
{
PackageNote = DateTime.Now.ToString();
}
}
/// <summary>
/// 获取构建管线的输出目录
/// </summary>
/// <returns></returns>
public virtual string GetPipelineOutputDirectory()
{
if (string.IsNullOrEmpty(_pipelineOutputDirectory))
{
_pipelineOutputDirectory = $"{BuildOutputRoot}/{BuildTarget}/{PackageName}/{YooAssetSettings.OutputFolderName}";
}
return _pipelineOutputDirectory;
}
/// <summary>
/// 获取本次构建的补丁输出目录
/// </summary>
public virtual string GetPackageOutputDirectory()
{
if (string.IsNullOrEmpty(_packageOutputDirectory))
{
_packageOutputDirectory = $"{BuildOutputRoot}/{BuildTarget}/{PackageName}/{PackageVersion}";
}
return _packageOutputDirectory;
}
/// <summary>
/// 获取本次构建的补丁根目录
/// </summary>
public virtual string GetPackageRootDirectory()
{
if (string.IsNullOrEmpty(_packageRootDirectory))
{
_packageRootDirectory = $"{BuildOutputRoot}/{BuildTarget}/{PackageName}";
}
return _packageRootDirectory;
}
/// <summary>
/// 获取内置资源的根目录
/// </summary>
public virtual string GetBuildinRootDirectory()
{
if (string.IsNullOrEmpty(_buildinRootDirectory))
{
_buildinRootDirectory = $"{BuildinFileRoot}/{PackageName}";
}
return _buildinRootDirectory;
}
}
}

View File

@@ -1,82 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor
{
public class TaskCopyBuildinFiles
{
/// <summary>
/// 拷贝首包资源文件
/// </summary>
internal void CopyBuildinFilesToStreaming(BuildParametersContext buildParametersContext, PackageManifest manifest)
{
EBuildinFileCopyOption copyOption = buildParametersContext.Parameters.BuildinFileCopyOption;
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
string buildinRootDirectory = buildParametersContext.GetBuildinRootDirectory();
string buildPackageName = buildParametersContext.Parameters.PackageName;
string buildPackageVersion = buildParametersContext.Parameters.PackageVersion;
// 清空内置文件的目录
if (copyOption == EBuildinFileCopyOption.ClearAndCopyAll || copyOption == EBuildinFileCopyOption.ClearAndCopyByTags)
{
EditorTools.ClearFolder(buildinRootDirectory);
}
// 拷贝补丁清单文件
{
string fileName = YooAssetSettingsData.GetManifestBinaryFileName(buildPackageName, buildPackageVersion);
string sourcePath = $"{packageOutputDirectory}/{fileName}";
string destPath = $"{buildinRootDirectory}/{fileName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝补丁清单哈希文件
{
string fileName = YooAssetSettingsData.GetPackageHashFileName(buildPackageName, buildPackageVersion);
string sourcePath = $"{packageOutputDirectory}/{fileName}";
string destPath = $"{buildinRootDirectory}/{fileName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝补丁清单版本文件
{
string fileName = YooAssetSettingsData.GetPackageVersionFileName(buildPackageName);
string sourcePath = $"{packageOutputDirectory}/{fileName}";
string destPath = $"{buildinRootDirectory}/{fileName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝文件列表(所有文件)
if (copyOption == EBuildinFileCopyOption.ClearAndCopyAll || copyOption == EBuildinFileCopyOption.OnlyCopyAll)
{
foreach (var packageBundle in manifest.BundleList)
{
string sourcePath = $"{packageOutputDirectory}/{packageBundle.FileName}";
string destPath = $"{buildinRootDirectory}/{packageBundle.FileName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
}
// 拷贝文件列表(带标签的文件)
if (copyOption == EBuildinFileCopyOption.ClearAndCopyByTags || copyOption == EBuildinFileCopyOption.OnlyCopyByTags)
{
string[] tags = buildParametersContext.Parameters.BuildinFileCopyParams.Split(';');
foreach (var packageBundle in manifest.BundleList)
{
if (packageBundle.HasTag(tags) == false)
continue;
string sourcePath = $"{packageOutputDirectory}/{packageBundle.FileName}";
string destPath = $"{buildinRootDirectory}/{packageBundle.FileName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
}
// 刷新目录
AssetDatabase.Refresh();
BuildLogger.Log($"Buildin files copy complete: {buildinRootDirectory}");
}
}
}

View File

@@ -1,50 +0,0 @@
using System;
using System.Linq;
using System.IO;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class TaskEncryption
{
/// <summary>
/// 加密文件
/// </summary>
public void EncryptingBundleFiles(BuildParametersContext buildParametersContext, BuildMapContext buildMapContext)
{
var encryptionServices = buildParametersContext.Parameters.EncryptionServices;
if (encryptionServices == null)
return;
if (encryptionServices.GetType() == typeof(EncryptionNone))
return;
int progressValue = 0;
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
foreach (var bundleInfo in buildMapContext.Collection)
{
EncryptFileInfo fileInfo = new EncryptFileInfo();
fileInfo.BundleName = bundleInfo.BundleName;
fileInfo.FileLoadPath = $"{pipelineOutputDirectory}/{bundleInfo.BundleName}";
var encryptResult = encryptionServices.Encrypt(fileInfo);
if (encryptResult.Encrypted)
{
string filePath = $"{pipelineOutputDirectory}/{bundleInfo.BundleName}.encrypt";
FileUtility.WriteAllBytes(filePath, encryptResult.EncryptedData);
bundleInfo.EncryptedFilePath = filePath;
bundleInfo.Encrypted = true;
BuildLogger.Log($"Bundle file encryption complete: {filePath}");
}
else
{
bundleInfo.Encrypted = false;
}
// 进度条
EditorTools.DisplayProgressBar("Encrypting bundle", ++progressValue, buildMapContext.Collection.Count);
}
EditorTools.ClearProgressBar();
}
}
}

View File

@@ -1,28 +0,0 @@
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class TaskCreateManifest_BBP : TaskCreateManifest, IBuildTask
{
private TaskBuilding_BBP.BuildResultContext _buildResultContext = null;
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var builtinBuildParameters = buildParametersContext.Parameters as BuiltinBuildParameters;
bool replaceAssetPathWithAddress = builtinBuildParameters.ReplaceAssetPathWithAddress;
CreateManifestFile(true, true, replaceAssetPathWithAddress, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)
{
if (_buildResultContext == null)
_buildResultContext = context.GetContextObject<TaskBuilding_BBP.BuildResultContext>();
return _buildResultContext.UnityManifest.GetAllDependencies(bundleName);
}
}
}

View File

@@ -1,49 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class TaskCreatePackage_BBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
CreatePackagePatch(buildParameters, buildMapContext);
}
/// <summary>
/// 拷贝补丁文件到补丁包目录
/// </summary>
private void CreatePackagePatch(BuildParametersContext buildParametersContext, BuildMapContext buildMapContext)
{
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
BuildLogger.Log($"Start making patch package: {packageOutputDirectory}");
// 拷贝UnityManifest序列化文件
{
string sourcePath = $"{pipelineOutputDirectory}/{YooAssetSettings.OutputFolderName}";
string destPath = $"{packageOutputDirectory}/{YooAssetSettings.OutputFolderName}";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝UnityManifest文本文件
{
string sourcePath = $"{pipelineOutputDirectory}/{YooAssetSettings.OutputFolderName}.manifest";
string destPath = $"{packageOutputDirectory}/{YooAssetSettings.OutputFolderName}.manifest";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝所有补丁文件
int progressValue = 0;
int fileTotalCount = buildMapContext.Collection.Count;
foreach (var bundleInfo in buildMapContext.Collection)
{
EditorTools.CopyFile(bundleInfo.PackageSourceFilePath, bundleInfo.PackageDestFilePath, true);
EditorTools.DisplayProgressBar("Copy patch file", ++progressValue, fileTotalCount);
}
EditorTools.ClearProgressBar();
}
}
}

View File

@@ -1,59 +0,0 @@
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
public class TaskPrepare_BBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters;
var builtinBuildParameters = buildParameters as BuiltinBuildParameters;
// 检测基础构建参数
buildParametersContext.CheckBuildParameters();
// 检测是否有未保存场景
if (EditorTools.HasDirtyScenes())
{
string message = BuildLogger.GetErrorMessage(ErrorCode.FoundUnsavedScene, "Found unsaved scene !");
throw new Exception(message);
}
// 删除包裹目录
if (buildParameters.ClearBuildCacheFiles)
{
string packageRootDirectory = buildParameters.GetPackageRootDirectory();
if (EditorTools.DeleteDirectory(packageRootDirectory))
{
BuildLogger.Log($"Delete package root directory: {packageRootDirectory}");
}
}
// 检测包裹输出目录是否存在
string packageOutputDirectory = buildParameters.GetPackageOutputDirectory();
if (Directory.Exists(packageOutputDirectory))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.PackageOutputDirectoryExists, $"Package outout directory exists: {packageOutputDirectory}");
throw new Exception(message);
}
// 如果输出目录不存在
string pipelineOutputDirectory = buildParameters.GetPipelineOutputDirectory();
if (EditorTools.CreateDirectory(pipelineOutputDirectory))
{
BuildLogger.Log($"Create pipeline output directory: {pipelineOutputDirectory}");
}
// 检测Unity版本
#if UNITY_2021_3_OR_NEWER
string warning = BuildLogger.GetErrorMessage(ErrorCode.RecommendScriptBuildPipeline, $"Starting with UnityEngine2021, recommend use script build pipeline (SBP) !");
BuildLogger.Warning(warning);
#endif
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class BuiltinBuildPipeline : IBuildPipeline
{
public BuildResult Run(BuildParameters buildParameters, bool enableLog)
{
if (buildParameters is BuiltinBuildParameters)
{
AssetBundleBuilder builder = new AssetBundleBuilder();
return builder.Run(buildParameters, GetDefaultBuildPipeline(), enableLog);
}
else
{
throw new Exception($"Invalid build parameter type : {buildParameters.GetType().Name}");
}
}
/// <summary>
/// 获取默认的构建流程
/// </summary>
private List<IBuildTask> GetDefaultBuildPipeline()
{
List<IBuildTask> pipeline = new List<IBuildTask>
{
new TaskPrepare_BBP(),
new TaskGetBuildMap_BBP(),
new TaskBuilding_BBP(),
new TaskVerifyBuildResult_BBP(),
new TaskEncryption_BBP(),
new TaskUpdateBundleInfo_BBP(),
new TaskCreateManifest_BBP(),
new TaskCreateReport_BBP(),
new TaskCreatePackage_BBP(),
new TaskCopyBuildinFiles_BBP(),
new TaskCreateCatalog_BBP()
};
return pipeline;
}
}
}

View File

@@ -1,34 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class TaskCreatePackage_RFBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
CreatePackagePatch(buildParameters, buildMapContext);
}
/// <summary>
/// 拷贝补丁文件到补丁包目录
/// </summary>
private void CreatePackagePatch(BuildParametersContext buildParametersContext, BuildMapContext buildMapContext)
{
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
BuildLogger.Log($"Start making patch package: {packageOutputDirectory}");
// 拷贝所有补丁文件
int progressValue = 0;
int fileTotalCount = buildMapContext.Collection.Count;
foreach (var bundleInfo in buildMapContext.Collection)
{
EditorTools.CopyFile(bundleInfo.PackageSourceFilePath, bundleInfo.PackageDestFilePath, true);
EditorTools.DisplayProgressBar("Copy patch file", ++progressValue, fileTotalCount);
}
EditorTools.ClearProgressBar();
}
}
}

View File

@@ -1,38 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
public class TaskGetBuildMap_RFBP : TaskGetBuildMap, IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildMapContext = CreateBuildMap(true, buildParametersContext.Parameters);
context.SetContextObject(buildMapContext);
// 检测构建结果
CheckBuildMapContent(buildMapContext);
}
/// <summary>
/// 检测资源构建上下文
/// </summary>
private void CheckBuildMapContent(BuildMapContext buildMapContext)
{
// 注意:原生文件资源包只能包含一个原生文件
foreach (var bundleInfo in buildMapContext.Collection)
{
if (bundleInfo.AllPackAssets.Count != 1)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.NotSupportMultipleRawAsset, $"The bundle does not support multiple raw asset : {bundleInfo.BundleName}");
throw new Exception(message);
}
}
}
}
}

View File

@@ -1,45 +0,0 @@
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
public class TaskPrepare_RFBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters;
// 检测基础构建参数
buildParametersContext.CheckBuildParameters();
// 删除包裹目录
if (buildParameters.ClearBuildCacheFiles)
{
string packageRootDirectory = buildParameters.GetPackageRootDirectory();
if (EditorTools.DeleteDirectory(packageRootDirectory))
{
BuildLogger.Log($"Delete package root directory: {packageRootDirectory}");
}
}
// 检测包裹输出目录是否存在
string packageOutputDirectory = buildParameters.GetPackageOutputDirectory();
if (Directory.Exists(packageOutputDirectory))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.PackageOutputDirectoryExists, $"Package outout directory exists: {packageOutputDirectory}");
throw new Exception(message);
}
// 如果输出目录不存在
string pipelineOutputDirectory = buildParameters.GetPipelineOutputDirectory();
if (EditorTools.CreateDirectory(pipelineOutputDirectory))
{
BuildLogger.Log($"Create pipeline output directory: {pipelineOutputDirectory}");
}
}
}
}

View File

@@ -1,51 +0,0 @@
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class TaskCreatePackage_SBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParameters = context.GetContextObject<BuildParametersContext>();
var buildMapContext = context.GetContextObject<BuildMapContext>();
CreatePackagePatch(buildParameters, buildMapContext);
}
/// <summary>
/// 拷贝补丁文件到补丁包目录
/// </summary>
private void CreatePackagePatch(BuildParametersContext buildParametersContext, BuildMapContext buildMapContext)
{
var scriptableBuildParameters = buildParametersContext.Parameters as ScriptableBuildParameters;
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
string packageOutputDirectory = buildParametersContext.GetPackageOutputDirectory();
BuildLogger.Log($"Start making patch package: {packageOutputDirectory}");
// 拷贝构建日志
{
string sourcePath = $"{pipelineOutputDirectory}/buildlogtep.json";
string destPath = $"{packageOutputDirectory}/buildlogtep.json";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝代码防裁剪配置
if (scriptableBuildParameters.WriteLinkXML)
{
string sourcePath = $"{pipelineOutputDirectory}/link.xml";
string destPath = $"{packageOutputDirectory}/link.xml";
EditorTools.CopyFile(sourcePath, destPath, true);
}
// 拷贝所有补丁文件
int progressValue = 0;
int fileTotalCount = buildMapContext.Collection.Count;
foreach (var bundleInfo in buildMapContext.Collection)
{
EditorTools.CopyFile(bundleInfo.PackageSourceFilePath, bundleInfo.PackageDestFilePath, true);
EditorTools.DisplayProgressBar("Copy patch file", ++progressValue, fileTotalCount);
}
EditorTools.ClearProgressBar();
}
}
}

View File

@@ -1,62 +0,0 @@
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
namespace YooAsset.Editor
{
public class TaskPrepare_SBP : IBuildTask
{
void IBuildTask.Run(BuildContext context)
{
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var buildParameters = buildParametersContext.Parameters as ScriptableBuildParameters;
// 检测基础构建参数
buildParametersContext.CheckBuildParameters();
// 检测是否有未保存场景
if (EditorTools.HasDirtyScenes())
{
string message = BuildLogger.GetErrorMessage(ErrorCode.FoundUnsavedScene, "Found unsaved scene !");
throw new Exception(message);
}
// 删除包裹目录
if (buildParameters.ClearBuildCacheFiles)
{
// Deletes the build cache directory.
UnityEditor.Build.Pipeline.Utilities.BuildCache.PurgeCache(false);
string packageRootDirectory = buildParameters.GetPackageRootDirectory();
if (EditorTools.DeleteDirectory(packageRootDirectory))
{
BuildLogger.Log($"Delete package root directory: {packageRootDirectory}");
}
}
// 检测包裹输出目录是否存在
string packageOutputDirectory = buildParameters.GetPackageOutputDirectory();
if (Directory.Exists(packageOutputDirectory))
{
string message = BuildLogger.GetErrorMessage(ErrorCode.PackageOutputDirectoryExists, $"Package outout directory exists: {packageOutputDirectory}");
throw new Exception(message);
}
// 如果输出目录不存在
string pipelineOutputDirectory = buildParameters.GetPipelineOutputDirectory();
if (EditorTools.CreateDirectory(pipelineOutputDirectory))
{
BuildLogger.Log($"Create pipeline output directory: {pipelineOutputDirectory}");
}
// 检测内置着色器资源包名称
if (string.IsNullOrEmpty(buildParameters.BuiltinShadersBundleName))
{
string warning = BuildLogger.GetErrorMessage(ErrorCode.BuiltinShadersBundleNameIsNull, $"Builtin shaders bundle name is null. It will cause resource redundancy !");
BuildLogger.Warning(warning);
}
}
}
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset.Editor
{
public class BuildContext
{
private readonly Dictionary<System.Type, IContextObject> _contextObjects = new Dictionary<System.Type, IContextObject>();
/// <summary>
/// 清空所有上下文对象
/// </summary>
public void ClearAllContext()
{
_contextObjects.Clear();
}
/// <summary>
/// 设置上下文对象
/// </summary>
public void SetContextObject(IContextObject contextObject)
{
if (contextObject == null)
throw new ArgumentNullException("contextObject");
var type = contextObject.GetType();
if (_contextObjects.ContainsKey(type))
throw new Exception($"Context object {type} is already existed.");
_contextObjects.Add(type, contextObject);
}
/// <summary>
/// 获取上下文对象
/// </summary>
public T GetContextObject<T>() where T : IContextObject
{
var type = typeof(T);
if (_contextObjects.TryGetValue(type, out IContextObject contextObject))
{
return (T)contextObject;
}
else
{
throw new Exception($"Not found context object : {type}");
}
}
/// <summary>
/// 获取上下文对象
/// </summary>
public T TryGetContextObject<T>() where T : IContextObject
{
var type = typeof(T);
if (_contextObjects.TryGetValue(type, out IContextObject contextObject))
{
return (T)contextObject;
}
else
{
return default;
}
}
}
}

View File

@@ -1,101 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using System.Text;
namespace YooAsset.Editor
{
internal static class BuildLogger
{
private const int MAX_LOG_BUFFER_SIZE = 1024 * 1024 * 2; //2MB
private static bool _enableLog = true;
private static string _logFilePath;
private static readonly object _lockObj = new object();
private static readonly StringBuilder _logBuilder = new StringBuilder(MAX_LOG_BUFFER_SIZE);
/// <summary>
/// 初始化日志系统
/// </summary>
public static void InitLogger(bool enableLog, string logFilePath)
{
_enableLog = enableLog;
_logFilePath = logFilePath;
_logBuilder.Clear();
if (_enableLog)
{
if (string.IsNullOrEmpty(_logFilePath))
throw new Exception("Log file path is null or empty !");
Debug.Log($"Logger initialized at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
}
/// <summary>
/// 关闭日志系统
/// </summary>
public static void Shuntdown()
{
if (_enableLog)
{
lock (_lockObj)
{
try
{
if (File.Exists(_logFilePath))
File.Delete(_logFilePath);
FileUtility.CreateFileDirectory(_logFilePath);
File.WriteAllText(_logFilePath, _logBuilder.ToString(), Encoding.UTF8);
_logBuilder.Clear();
}
catch (Exception ex)
{
Debug.LogError($"Failed to write log file: {ex.Message}");
}
}
}
}
public static void Log(string message)
{
if (_enableLog)
{
WriteLog("INFO", message);
Debug.Log(message);
}
}
public static void Warning(string message)
{
if (_enableLog)
{
WriteLog("WARN", message);
Debug.LogWarning(message);
}
}
public static void Error(string message)
{
if (_enableLog)
{
WriteLog("ERROR", message);
Debug.LogError(message);
}
}
public static string GetErrorMessage(ErrorCode code, string message)
{
return $"[ErrorCode{(int)code}] {message}";
}
private static void WriteLog(string level, string message)
{
lock (_lockObj)
{
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";
_logBuilder.AppendLine(logEntry);
}
}
}
}

View File

@@ -1,45 +0,0 @@

namespace YooAsset.Editor
{
internal enum ErrorCode
{
// TaskPrepare
ThePipelineIsBuiding = 100,
FoundUnsavedScene = 101,
NoBuildTarget = 110,
PackageNameIsNullOrEmpty = 111,
PackageVersionIsNullOrEmpty = 112,
BuildOutputRootIsNullOrEmpty = 113,
BuildinFileRootIsNullOrEmpty = 114,
PackageOutputDirectoryExists = 115,
BuildPipelineIsNullOrEmpty = 116,
BuildBundleTypeIsUnknown = 117,
RecommendScriptBuildPipeline = 130,
BuiltinShadersBundleNameIsNull = 131,
// TaskGetBuildMap
RemoveInvalidTags = 200,
FoundUndependedAsset = 201,
PackAssetListIsEmpty = 202,
NotSupportMultipleRawAsset = 210,
// TaskBuilding
UnityEngineBuildFailed = 300,
UnityEngineBuildFatal = 301,
// TaskUpdateBundleInfo
CharactersOverTheLimit = 400,
NotFoundUnityBundleHash = 401,
NotFoundUnityBundleCRC = 402,
BundleTempSizeIsZero = 403,
// TaskVerifyBuildResult
UnintendedBuildBundle = 500,
UnintendedBuildResult = 501,
// TaskCreateManifest
NotFoundUnityBundleInBuildResult = 600,
FoundStrayBundle = 601,
BundleHashConflict = 602,
}
}

View File

@@ -1,8 +0,0 @@

namespace YooAsset.Editor
{
public interface IBuildTask
{
void Run(BuildContext context);
}
}

View File

@@ -1,7 +0,0 @@

namespace YooAsset.Editor
{
public interface IContextObject
{
}
}

View File

@@ -1,11 +0,0 @@

namespace YooAsset.Editor
{
public class EncryptionNone : IEncryptionServices
{
public EncryptResult Encrypt(EncryptFileInfo fileInfo)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 46b8b200b841799498896403d9d427c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,19 +0,0 @@

namespace YooAsset.Editor
{
public class ManifestProcessNone : IManifestProcessServices
{
byte[] IManifestProcessServices.ProcessManifest(byte[] fileData)
{
return fileData;
}
}
public class ManifestRestoreNone : IManifestRestoreServices
{
byte[] IManifestRestoreServices.RestoreManifest(byte[] fileData)
{
return fileData;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 446513b0ea9f5d445ade0cfb09c5073b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 4defd475b635cdf4b87108140d3a0ad1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,13 +0,0 @@

namespace YooAsset.Editor
{
/// <summary>
/// AssetBundle压缩选项
/// </summary>
public enum ECompressOption
{
Uncompressed = 0,
LZMA,
LZ4,
}
}

View File

@@ -1,13 +0,0 @@
using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor
{
public interface IBuildPipeline
{
/// <summary>
/// 运行构建任务
/// </summary>
BuildResult Run(BuildParameters buildParameters, bool enableLog);
}
}

View File

@@ -1,14 +0,0 @@
using System;
namespace YooAsset.Editor
{
public class BuildPipelineAttribute : Attribute
{
public string PipelineName;
public BuildPipelineAttribute(string name)
{
this.PipelineName = name;
}
}
}

View File

@@ -1,280 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
internal abstract class BuildPipelineViewerBase
{
protected const int StyleWidth = 400;
protected const int LabelMinWidth = 190;
protected string PackageName { private set; get; }
protected string PipelineName { private set; get; }
protected BuildTarget BuildTarget { private set; get; }
/// <summary>
/// 初始化视图
/// </summary>
public void InitView(string packageName, string pipelineName, BuildTarget buildTarget)
{
PackageName = packageName;
PipelineName = pipelineName;
BuildTarget = buildTarget;
}
/// <summary>
/// 创建视图
/// </summary>
public abstract void CreateView(VisualElement parent);
/// <summary>
/// 获取默认版本
/// </summary>
protected virtual string GetDefaultPackageVersion()
{
int totalMinutes = DateTime.Now.Hour * 60 + DateTime.Now.Minute;
return DateTime.Now.ToString("yyyy-MM-dd") + "-" + totalMinutes;
}
/// <summary>
/// 创建资源包加密服务类实例
/// </summary>
protected IEncryptionServices CreateEncryptionServicesInstance()
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
var classType = classTypes.Find(x => x.FullName.Equals(className));
if (classType != null)
return (IEncryptionServices)Activator.CreateInstance(classType);
else
return null;
}
/// <summary>
/// 创建资源清单加密服务类实例
/// </summary>
protected IManifestProcessServices CreateManifestProcessServicesInstance()
{
var className = AssetBundleBuilderSetting.GetPackageManifestProcessServicesClassName(PackageName, PipelineName);
var classTypes = EditorTools.GetAssignableTypes(typeof(IManifestProcessServices));
var classType = classTypes.Find(x => x.FullName.Equals(className));
if (classType != null)
return (IManifestProcessServices)Activator.CreateInstance(classType);
else
return null;
}
/// <summary>
/// 创建资源清单解密服务类实例
/// </summary>
protected IManifestRestoreServices CreateManifestRestoreServicesInstance()
{
var className = AssetBundleBuilderSetting.GetPackageManifestRestoreServicesClassName(PackageName, PipelineName);
var classTypes = EditorTools.GetAssignableTypes(typeof(IManifestRestoreServices));
var classType = classTypes.Find(x => x.FullName.Equals(className));
if (classType != null)
return (IManifestRestoreServices)Activator.CreateInstance(classType);
else
return null;
}
#region UI元素通用处理方法
protected void SetBuildOutputField(TextField textField)
{
// 输出目录
string defaultOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
textField.SetValueWithoutNotify(defaultOutputRoot);
textField.SetEnabled(false);
UIElementsTools.SetElementLabelMinWidth(textField, LabelMinWidth);
}
protected void SetBuildVersionField(TextField textField)
{
// 构建版本
textField.style.width = StyleWidth;
textField.SetValueWithoutNotify(GetDefaultPackageVersion());
UIElementsTools.SetElementLabelMinWidth(textField, LabelMinWidth);
}
protected void SetCompressionField(EnumField enumField)
{
// 压缩方式选项
var compressOption = AssetBundleBuilderSetting.GetPackageCompressOption(PackageName, PipelineName);
enumField.Init(compressOption);
enumField.SetValueWithoutNotify(compressOption);
enumField.style.width = StyleWidth;
enumField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageCompressOption(PackageName, PipelineName, (ECompressOption)enumField.value);
});
UIElementsTools.SetElementLabelMinWidth(enumField, LabelMinWidth);
}
protected void SetOutputNameStyleField(EnumField enumField)
{
// 输出文件名称样式
var fileNameStyle = AssetBundleBuilderSetting.GetPackageFileNameStyle(PackageName, PipelineName);
enumField.Init(fileNameStyle);
enumField.SetValueWithoutNotify(fileNameStyle);
enumField.style.width = StyleWidth;
enumField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageFileNameStyle(PackageName, PipelineName, (EFileNameStyle)enumField.value);
});
UIElementsTools.SetElementLabelMinWidth(enumField, LabelMinWidth);
}
protected void SetCopyBuildinFileOptionField(EnumField enumField, TextField tagField)
{
// 首包文件拷贝选项
var buildinFileCopyOption = AssetBundleBuilderSetting.GetPackageBuildinFileCopyOption(PackageName, PipelineName);
enumField.Init(buildinFileCopyOption);
enumField.SetValueWithoutNotify(buildinFileCopyOption);
enumField.style.width = StyleWidth;
enumField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageBuildinFileCopyOption(PackageName, PipelineName, (EBuildinFileCopyOption)enumField.value);
// 设置内置资源标签显隐
SetCopyBuildinFileTagsVisible(tagField);
});
UIElementsTools.SetElementLabelMinWidth(enumField, LabelMinWidth);
}
protected void SetCopyBuildinFileTagsVisible(TextField tagField)
{
var option = AssetBundleBuilderSetting.GetPackageBuildinFileCopyOption(PackageName, PipelineName);
tagField.visible = option == EBuildinFileCopyOption.ClearAndCopyByTags || option == EBuildinFileCopyOption.OnlyCopyByTags;
}
protected void SetCopyBuildinFileTagsField(TextField textField)
{
// 首包文件拷贝参数
var buildinFileCopyParams = AssetBundleBuilderSetting.GetPackageBuildinFileCopyParams(PackageName, PipelineName);
textField.SetValueWithoutNotify(buildinFileCopyParams);
textField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageBuildinFileCopyParams(PackageName, PipelineName, textField.value);
});
UIElementsTools.SetElementLabelMinWidth(textField, LabelMinWidth);
}
protected void SetClearBuildCacheToggle(Toggle toggle)
{
// 清理构建缓存
bool clearBuildCache = AssetBundleBuilderSetting.GetPackageClearBuildCache(PackageName, PipelineName);
toggle.SetValueWithoutNotify(clearBuildCache);
toggle.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageClearBuildCache(PackageName, PipelineName, toggle.value);
});
UIElementsTools.SetElementLabelMinWidth(toggle, LabelMinWidth);
}
protected void SetUseAssetDependencyDBToggle(Toggle toggle)
{
// 使用资源依赖数据库
bool useAssetDependencyDB = AssetBundleBuilderSetting.GetPackageUseAssetDependencyDB(PackageName, PipelineName);
toggle.SetValueWithoutNotify(useAssetDependencyDB);
toggle.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageUseAssetDependencyDB(PackageName, PipelineName, toggle.value);
});
UIElementsTools.SetElementLabelMinWidth(toggle, LabelMinWidth);
}
protected PopupField<Type> CreateEncryptionServicesField(VisualElement container)
{
// 资源包加密服务类
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
if (classTypes.Count > 0)
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);
int defaultIndex = classTypes.FindIndex(x => x.FullName.Equals(className));
if (defaultIndex < 0)
defaultIndex = 0;
var popupField = new PopupField<Type>(classTypes, defaultIndex);
popupField.label = "Encryption Services";
popupField.style.width = StyleWidth;
popupField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageEncyptionServicesClassName(PackageName, PipelineName, popupField.value.FullName);
});
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
else
{
var popupField = new PopupField<Type>();
popupField.label = "Encryption Services";
popupField.style.width = StyleWidth;
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
}
protected PopupField<Type> CreateManifestProcessServicesField(VisualElement container)
{
// 资源清单加密服务类
var classTypes = EditorTools.GetAssignableTypes(typeof(IManifestProcessServices));
if (classTypes.Count > 0)
{
var className = AssetBundleBuilderSetting.GetPackageManifestProcessServicesClassName(PackageName, PipelineName);
int defaultIndex = classTypes.FindIndex(x => x.FullName.Equals(className));
if (defaultIndex < 0)
defaultIndex = 0;
var popupField = new PopupField<Type>(classTypes, defaultIndex);
popupField.label = "Manifest Process Services";
popupField.style.width = StyleWidth;
popupField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageManifestProcessServicesClassName(PackageName, PipelineName, popupField.value.FullName);
});
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
else
{
var popupField = new PopupField<Type>();
popupField.label = "Manifest Process Services";
popupField.style.width = StyleWidth;
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
}
protected PopupField<Type> CreateManifestRestoreServicesField(VisualElement container)
{
// 资源清单加密服务类
var classTypes = EditorTools.GetAssignableTypes(typeof(IManifestRestoreServices));
if (classTypes.Count > 0)
{
var className = AssetBundleBuilderSetting.GetPackageManifestRestoreServicesClassName(PackageName, PipelineName);
int defaultIndex = classTypes.FindIndex(x => x.FullName.Equals(className));
if (defaultIndex < 0)
defaultIndex = 0;
var popupField = new PopupField<Type>(classTypes, defaultIndex);
popupField.label = "Manifest Restore Services";
popupField.style.width = StyleWidth;
popupField.RegisterValueChangedCallback(evt =>
{
AssetBundleBuilderSetting.SetPackageManifestRestoreServicesClassName(PackageName, PipelineName, popupField.value.FullName);
});
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
else
{
var popupField = new PopupField<Type>();
popupField.label = "Manifest Restore Services";
popupField.style.width = StyleWidth;
container.Add(popupField);
UIElementsTools.SetElementLabelMinWidth(popupField, LabelMinWidth);
return popupField;
}
}
#endregion
}
}
#endif

View File

@@ -1,135 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
[BuildPipelineAttribute(nameof(EBuildPipeline.BuiltinBuildPipeline))]
internal class BuiltinBuildPipelineViewer : BuildPipelineViewerBase
{
protected TemplateContainer Root;
protected TextField _buildOutputField;
protected TextField _buildVersionField;
protected PopupField<Type> _encryptionServicesField;
protected PopupField<Type> _manifestProcessServicesField;
protected PopupField<Type> _manifestRestoreServicesField;
protected EnumField _compressionField;
protected EnumField _outputNameStyleField;
protected EnumField _copyBuildinFileOptionField;
protected TextField _copyBuildinFileTagsField;
protected Toggle _clearBuildCacheToggle;
protected Toggle _useAssetDependencyDBToggle;
public override void CreateView(VisualElement parent)
{
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUXML<BuiltinBuildPipelineViewer>();
if (visualAsset == null)
return;
Root = visualAsset.CloneTree();
Root.style.flexGrow = 1f;
parent.Add(Root);
// 输出目录
_buildOutputField = Root.Q<TextField>("BuildOutput");
SetBuildOutputField(_buildOutputField);
// 构建版本
_buildVersionField = Root.Q<TextField>("BuildVersion");
SetBuildVersionField(_buildVersionField);
// 服务类
var popupContainer = Root.Q("PopupContainer");
_encryptionServicesField = CreateEncryptionServicesField(popupContainer);
_manifestProcessServicesField = CreateManifestProcessServicesField(popupContainer);
_manifestRestoreServicesField = CreateManifestRestoreServicesField(popupContainer);
// 压缩方式选项
_compressionField = Root.Q<EnumField>("Compression");
SetCompressionField(_compressionField);
// 输出文件名称样式
_outputNameStyleField = Root.Q<EnumField>("FileNameStyle");
SetOutputNameStyleField(_outputNameStyleField);
// 首包文件拷贝参数
_copyBuildinFileTagsField = Root.Q<TextField>("CopyBuildinFileParam");
SetCopyBuildinFileTagsField(_copyBuildinFileTagsField);
SetCopyBuildinFileTagsVisible(_copyBuildinFileTagsField);
// 首包文件拷贝选项
_copyBuildinFileOptionField = Root.Q<EnumField>("CopyBuildinFileOption");
SetCopyBuildinFileOptionField(_copyBuildinFileOptionField, _copyBuildinFileTagsField);
// 清理构建缓存
_clearBuildCacheToggle = Root.Q<Toggle>("ClearBuildCache");
SetClearBuildCacheToggle(_clearBuildCacheToggle);
// 使用资源依赖数据库
_useAssetDependencyDBToggle = Root.Q<Toggle>("UseAssetDependency");
SetUseAssetDependencyDBToggle(_useAssetDependencyDBToggle);
// 构建按钮
var buildButton = Root.Q<Button>("Build");
buildButton.clicked += BuildButton_clicked;
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
/// <summary>
/// 执行构建
/// </summary>
protected virtual void ExecuteBuild()
{
var fileNameStyle = AssetBundleBuilderSetting.GetPackageFileNameStyle(PackageName, PipelineName);
var buildinFileCopyOption = AssetBundleBuilderSetting.GetPackageBuildinFileCopyOption(PackageName, PipelineName);
var buildinFileCopyParams = AssetBundleBuilderSetting.GetPackageBuildinFileCopyParams(PackageName, PipelineName);
var compressOption = AssetBundleBuilderSetting.GetPackageCompressOption(PackageName, PipelineName);
var clearBuildCache = AssetBundleBuilderSetting.GetPackageClearBuildCache(PackageName, PipelineName);
var useAssetDependencyDB = AssetBundleBuilderSetting.GetPackageUseAssetDependencyDB(PackageName, PipelineName);
BuiltinBuildParameters buildParameters = new BuiltinBuildParameters();
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;
buildParameters.EnableSharePackRule = true;
buildParameters.VerifyBuildingResult = true;
buildParameters.FileNameStyle = fileNameStyle;
buildParameters.BuildinFileCopyOption = buildinFileCopyOption;
buildParameters.BuildinFileCopyParams = buildinFileCopyParams;
buildParameters.CompressOption = compressOption;
buildParameters.ClearBuildCacheFiles = clearBuildCache;
buildParameters.UseAssetDependencyDB = useAssetDependencyDB;
buildParameters.EncryptionServices = CreateEncryptionServicesInstance();
buildParameters.ManifestProcessServices = CreateManifestProcessServicesInstance();
buildParameters.ManifestRestoreServices = CreateManifestRestoreServicesInstance();
BuiltinBuildPipeline pipeline = new BuiltinBuildPipeline();
var buildResult = pipeline.Run(buildParameters, true);
if (buildResult.Success)
EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
}
}
}
#endif

View File

@@ -1,127 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
[BuildPipelineAttribute(nameof(EBuildPipeline.RawFileBuildPipeline))]
internal class RawfileBuildpipelineViewer : BuildPipelineViewerBase
{
protected TemplateContainer Root;
protected TextField _buildOutputField;
protected TextField _buildVersionField;
protected PopupField<Type> _encryptionServicesField;
protected PopupField<Type> _manifestProcessServicesField;
protected PopupField<Type> _manifestRestoreServicesField;
protected EnumField _outputNameStyleField;
protected EnumField _copyBuildinFileOptionField;
protected TextField _copyBuildinFileTagsField;
protected Toggle _clearBuildCacheToggle;
protected Toggle _useAssetDependencyDBToggle;
public override void CreateView(VisualElement parent)
{
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUXML<RawfileBuildpipelineViewer>();
if (visualAsset == null)
return;
Root = visualAsset.CloneTree();
Root.style.flexGrow = 1f;
parent.Add(Root);
// 输出目录
_buildOutputField = Root.Q<TextField>("BuildOutput");
SetBuildOutputField(_buildOutputField);
// 构建版本
_buildVersionField = Root.Q<TextField>("BuildVersion");
SetBuildVersionField(_buildVersionField);
// 加密方法
var popupContainer = Root.Q("PopupContainer");
_encryptionServicesField = CreateEncryptionServicesField(popupContainer);
_manifestProcessServicesField = CreateManifestProcessServicesField(popupContainer);
_manifestRestoreServicesField = CreateManifestRestoreServicesField(popupContainer);
// 输出文件名称样式
_outputNameStyleField = Root.Q<EnumField>("FileNameStyle");
SetOutputNameStyleField(_outputNameStyleField);
// 首包文件拷贝参数
_copyBuildinFileTagsField = Root.Q<TextField>("CopyBuildinFileParam");
SetCopyBuildinFileTagsField(_copyBuildinFileTagsField);
SetCopyBuildinFileTagsVisible(_copyBuildinFileTagsField);
// 首包文件拷贝选项
_copyBuildinFileOptionField = Root.Q<EnumField>("CopyBuildinFileOption");
SetCopyBuildinFileOptionField(_copyBuildinFileOptionField, _copyBuildinFileTagsField);
// 清理构建缓存
_clearBuildCacheToggle = Root.Q<Toggle>("ClearBuildCache");
SetClearBuildCacheToggle(_clearBuildCacheToggle);
// 使用资源依赖数据库
_useAssetDependencyDBToggle = Root.Q<Toggle>("UseAssetDependency");
SetUseAssetDependencyDBToggle(_useAssetDependencyDBToggle);
// 构建按钮
var buildButton = Root.Q<Button>("Build");
buildButton.clicked += BuildButton_clicked;
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
/// <summary>
/// 执行构建
/// </summary>
protected virtual void ExecuteBuild()
{
var fileNameStyle = AssetBundleBuilderSetting.GetPackageFileNameStyle(PackageName, PipelineName);
var buildinFileCopyOption = AssetBundleBuilderSetting.GetPackageBuildinFileCopyOption(PackageName, PipelineName);
var buildinFileCopyParams = AssetBundleBuilderSetting.GetPackageBuildinFileCopyParams(PackageName, PipelineName);
var clearBuildCache = AssetBundleBuilderSetting.GetPackageClearBuildCache(PackageName, PipelineName);
var useAssetDependencyDB = AssetBundleBuilderSetting.GetPackageUseAssetDependencyDB(PackageName, PipelineName);
RawFileBuildParameters buildParameters = new RawFileBuildParameters();
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.RawBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;
buildParameters.VerifyBuildingResult = true;
buildParameters.FileNameStyle = fileNameStyle;
buildParameters.BuildinFileCopyOption = buildinFileCopyOption;
buildParameters.BuildinFileCopyParams = buildinFileCopyParams;
buildParameters.ClearBuildCacheFiles = clearBuildCache;
buildParameters.UseAssetDependencyDB = useAssetDependencyDB;
buildParameters.EncryptionServices = CreateEncryptionServicesInstance();
buildParameters.ManifestProcessServices = CreateManifestProcessServicesInstance();
buildParameters.ManifestRestoreServices = CreateManifestRestoreServicesInstance();
RawFileBuildPipeline pipeline = new RawFileBuildPipeline();
var buildResult = pipeline.Run(buildParameters, true);
if (buildResult.Success)
EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
}
}
}
#endif

View File

@@ -1,157 +0,0 @@
#if UNITY_2019_4_OR_NEWER
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
namespace YooAsset.Editor
{
[BuildPipelineAttribute(nameof(EBuildPipeline.ScriptableBuildPipeline))]
internal class ScriptableBuildPipelineViewer : BuildPipelineViewerBase
{
protected TemplateContainer Root;
protected TextField _buildOutputField;
protected TextField _buildVersionField;
protected PopupField<Type> _encryptionServicesField;
protected PopupField<Type> _manifestProcessServicesField;
protected PopupField<Type> _manifestRestoreServicesField;
protected EnumField _compressionField;
protected EnumField _outputNameStyleField;
protected EnumField _copyBuildinFileOptionField;
protected TextField _copyBuildinFileTagsField;
protected Toggle _clearBuildCacheToggle;
protected Toggle _useAssetDependencyDBToggle;
public override void CreateView(VisualElement parent)
{
// 加载布局文件
var visualAsset = UxmlLoader.LoadWindowUXML<ScriptableBuildPipelineViewer>();
if (visualAsset == null)
return;
Root = visualAsset.CloneTree();
Root.style.flexGrow = 1f;
parent.Add(Root);
// 输出目录
_buildOutputField = Root.Q<TextField>("BuildOutput");
SetBuildOutputField(_buildOutputField);
// 构建版本
_buildVersionField = Root.Q<TextField>("BuildVersion");
SetBuildVersionField(_buildVersionField);
// 加密方法
var popupContainer = Root.Q("PopupContainer");
_encryptionServicesField = CreateEncryptionServicesField(popupContainer);
_manifestProcessServicesField = CreateManifestProcessServicesField(popupContainer);
_manifestRestoreServicesField = CreateManifestRestoreServicesField(popupContainer);
// 压缩方式选项
_compressionField = Root.Q<EnumField>("Compression");
SetCompressionField(_compressionField);
// 输出文件名称样式
_outputNameStyleField = Root.Q<EnumField>("FileNameStyle");
SetOutputNameStyleField(_outputNameStyleField);
// 首包文件拷贝参数
_copyBuildinFileTagsField = Root.Q<TextField>("CopyBuildinFileParam");
SetCopyBuildinFileTagsField(_copyBuildinFileTagsField);
SetCopyBuildinFileTagsVisible(_copyBuildinFileTagsField);
// 首包文件拷贝选项
_copyBuildinFileOptionField = Root.Q<EnumField>("CopyBuildinFileOption");
SetCopyBuildinFileOptionField(_copyBuildinFileOptionField, _copyBuildinFileTagsField);
// 清理构建缓存
_clearBuildCacheToggle = Root.Q<Toggle>("ClearBuildCache");
SetClearBuildCacheToggle(_clearBuildCacheToggle);
// 使用资源依赖数据库
_useAssetDependencyDBToggle = Root.Q<Toggle>("UseAssetDependency");
SetUseAssetDependencyDBToggle(_useAssetDependencyDBToggle);
// 构建按钮
var buildButton = Root.Q<Button>("Build");
buildButton.clicked += BuildButton_clicked;
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
/// <summary>
/// 执行构建
/// </summary>
protected virtual void ExecuteBuild()
{
var fileNameStyle = AssetBundleBuilderSetting.GetPackageFileNameStyle(PackageName, PipelineName);
var buildinFileCopyOption = AssetBundleBuilderSetting.GetPackageBuildinFileCopyOption(PackageName, PipelineName);
var buildinFileCopyParams = AssetBundleBuilderSetting.GetPackageBuildinFileCopyParams(PackageName, PipelineName);
var compressOption = AssetBundleBuilderSetting.GetPackageCompressOption(PackageName, PipelineName);
var clearBuildCache = AssetBundleBuilderSetting.GetPackageClearBuildCache(PackageName, PipelineName);
var useAssetDependencyDB = AssetBundleBuilderSetting.GetPackageUseAssetDependencyDB(PackageName, PipelineName);
ScriptableBuildParameters buildParameters = new ScriptableBuildParameters();
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;
buildParameters.EnableSharePackRule = true;
buildParameters.VerifyBuildingResult = true;
buildParameters.FileNameStyle = fileNameStyle;
buildParameters.BuildinFileCopyOption = buildinFileCopyOption;
buildParameters.BuildinFileCopyParams = buildinFileCopyParams;
buildParameters.CompressOption = compressOption;
buildParameters.ClearBuildCacheFiles = clearBuildCache;
buildParameters.UseAssetDependencyDB = useAssetDependencyDB;
buildParameters.EncryptionServices = CreateEncryptionServicesInstance();
buildParameters.ManifestProcessServices = CreateManifestProcessServicesInstance();
buildParameters.ManifestRestoreServices = CreateManifestRestoreServicesInstance();
buildParameters.BuiltinShadersBundleName = GetBuiltinShaderBundleName();
ScriptableBuildPipeline pipeline = new ScriptableBuildPipeline();
var buildResult = pipeline.Run(buildParameters, true);
if (buildResult.Success)
EditorUtility.RevealInFinder(buildResult.OutputPackageDirectory);
}
/// <summary>
/// 内置着色器资源包名称
/// 注意:和自动收集的着色器资源包名保持一致!
/// </summary>
protected string GetBuiltinShaderBundleName()
{
var uniqueBundleName = AssetBundleCollectorSettingData.Setting.UniqueBundleName;
var packRuleResult = DefaultPackRule.CreateShadersPackRuleResult();
return packRuleResult.GetBundleName(PackageName, uniqueBundleName);
}
/// <summary>
/// Mono脚本的资源包名称
/// </summary>
protected string GetMonoScriptsBundleName()
{
var uniqueBundleName = AssetBundleCollectorSettingData.Setting.UniqueBundleName;
var packRuleResult = DefaultPackRule.CreateMonosPackRuleResult();
return packRuleResult.GetBundleName(PackageName, uniqueBundleName);
}
}
}
#endif

View File

@@ -1,301 +0,0 @@
using System;
using System.IO;
using System.Xml;
using System.Text;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace YooAsset.Editor
{
public class AssetBundleCollectorConfig
{
public const string ConfigVersion = "v2025.8.28";
public const string XmlVersion = "Version";
public const string XmlCommon = "Common";
public const string XmlShowPackageView = "ShowPackageView";
public const string XmlShowEditorAlias = "ShowEditorAlias";
public const string XmlUniqueBundleName = "UniqueBundleName";
public const string XmlPackage = "Package";
public const string XmlPackageName = "PackageName";
public const string XmlPackageDesc = "PackageDesc";
public const string XmlEnableAddressable = "AutoAddressable";
public const string XmlSupportExtensionless = "SupportExtensionless";
public const string XmlLocationToLower = "LocationToLower";
public const string XmlIncludeAssetGUID = "IncludeAssetGUID";
public const string XmlIgnoreRuleName = "IgnoreRuleName";
public const string XmlGroup = "Group";
public const string XmlGroupActiveRule = "GroupActiveRule";
public const string XmlGroupName = "GroupName";
public const string XmlGroupDesc = "GroupDesc";
public const string XmlCollector = "Collector";
public const string XmlCollectPath = "CollectPath";
public const string XmlCollectorGUID = "CollectGUID";
public const string XmlCollectorType = "CollectType";
public const string XmlAddressRule = "AddressRule";
public const string XmlPackRule = "PackRule";
public const string XmlFilterRule = "FilterRule";
public const string XmlUserData = "UserData";
public const string XmlAssetTags = "AssetTags";
/// <summary>
/// 导入XML配置表
/// </summary>
public static void ImportXmlConfig(string filePath)
{
if (File.Exists(filePath) == false)
throw new FileNotFoundException(filePath);
if (Path.GetExtension(filePath) != ".xml")
throw new Exception($"Only support xml : {filePath}");
// 加载配置文件
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(filePath);
XmlElement root = xmlDoc.DocumentElement;
// 读取配置版本
string configVersion = root.GetAttribute(XmlVersion);
if (configVersion != ConfigVersion)
{
if (UpdateXmlConfig(xmlDoc) == false)
throw new Exception($"The config version update failed : {configVersion} -> {ConfigVersion}");
else
Debug.Log($"The config version update succeed : {configVersion} -> {ConfigVersion}");
}
// 读取公共配置
bool uniqueBundleName = false;
bool showPackageView = false;
bool showEditorAlias = false;
var commonNodeList = root.GetElementsByTagName(XmlCommon);
if (commonNodeList.Count > 0)
{
XmlElement commonElement = commonNodeList[0] as XmlElement;
if (commonElement.HasAttribute(XmlShowPackageView))
showPackageView = commonElement.GetAttribute(XmlShowPackageView) == "True" ? true : false;
if (commonElement.HasAttribute(XmlShowEditorAlias))
showEditorAlias = commonElement.GetAttribute(XmlShowEditorAlias) == "True" ? true : false;
if (commonElement.HasAttribute(XmlUniqueBundleName))
uniqueBundleName = commonElement.GetAttribute(XmlUniqueBundleName) == "True" ? true : false;
}
// 读取包裹配置
List<AssetBundleCollectorPackage> packages = new List<AssetBundleCollectorPackage>();
var packageNodeList = root.GetElementsByTagName(XmlPackage);
foreach (var packageNode in packageNodeList)
{
XmlElement packageElement = packageNode as XmlElement;
if (packageElement.HasAttribute(XmlPackageName) == false)
throw new Exception($"Not found attribute {XmlPackageName} in {XmlPackage}");
if (packageElement.HasAttribute(XmlPackageDesc) == false)
throw new Exception($"Not found attribute {XmlPackageDesc} in {XmlPackage}");
AssetBundleCollectorPackage package = new AssetBundleCollectorPackage();
package.PackageName = packageElement.GetAttribute(XmlPackageName);
package.PackageDesc = packageElement.GetAttribute(XmlPackageDesc);
package.EnableAddressable = packageElement.GetAttribute(XmlEnableAddressable) == "True" ? true : false;
package.SupportExtensionless = packageElement.GetAttribute(XmlSupportExtensionless) == "True" ? true : false;
package.LocationToLower = packageElement.GetAttribute(XmlLocationToLower) == "True" ? true : false;
package.IncludeAssetGUID = packageElement.GetAttribute(XmlIncludeAssetGUID) == "True" ? true : false;
package.IgnoreRuleName = packageElement.GetAttribute(XmlIgnoreRuleName);
packages.Add(package);
// 读取分组配置
var groupNodeList = packageElement.GetElementsByTagName(XmlGroup);
foreach (var groupNode in groupNodeList)
{
XmlElement groupElement = groupNode as XmlElement;
if (groupElement.HasAttribute(XmlGroupActiveRule) == false)
throw new Exception($"Not found attribute {XmlGroupActiveRule} in {XmlGroup}");
if (groupElement.HasAttribute(XmlGroupName) == false)
throw new Exception($"Not found attribute {XmlGroupName} in {XmlGroup}");
if (groupElement.HasAttribute(XmlGroupDesc) == false)
throw new Exception($"Not found attribute {XmlGroupDesc} in {XmlGroup}");
if (groupElement.HasAttribute(XmlAssetTags) == false)
throw new Exception($"Not found attribute {XmlAssetTags} in {XmlGroup}");
AssetBundleCollectorGroup group = new AssetBundleCollectorGroup();
group.ActiveRuleName = groupElement.GetAttribute(XmlGroupActiveRule);
group.GroupName = groupElement.GetAttribute(XmlGroupName);
group.GroupDesc = groupElement.GetAttribute(XmlGroupDesc);
group.AssetTags = groupElement.GetAttribute(XmlAssetTags);
package.Groups.Add(group);
// 读取收集器配置
var collectorNodeList = groupElement.GetElementsByTagName(XmlCollector);
foreach (var collectorNode in collectorNodeList)
{
XmlElement collectorElement = collectorNode as XmlElement;
if (collectorElement.HasAttribute(XmlCollectPath) == false)
throw new Exception($"Not found attribute {XmlCollectPath} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlCollectorGUID) == false)
throw new Exception($"Not found attribute {XmlCollectorGUID} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlCollectorType) == false)
throw new Exception($"Not found attribute {XmlCollectorType} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlAddressRule) == false)
throw new Exception($"Not found attribute {XmlAddressRule} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlPackRule) == false)
throw new Exception($"Not found attribute {XmlPackRule} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlFilterRule) == false)
throw new Exception($"Not found attribute {XmlFilterRule} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlUserData) == false)
throw new Exception($"Not found attribute {XmlUserData} in {XmlCollector}");
if (collectorElement.HasAttribute(XmlAssetTags) == false)
throw new Exception($"Not found attribute {XmlAssetTags} in {XmlCollector}");
AssetBundleCollector collector = new AssetBundleCollector();
collector.CollectPath = collectorElement.GetAttribute(XmlCollectPath);
collector.CollectorGUID = collectorElement.GetAttribute(XmlCollectorGUID);
collector.CollectorType = EditorTools.NameToEnum<ECollectorType>(collectorElement.GetAttribute(XmlCollectorType));
collector.AddressRuleName = collectorElement.GetAttribute(XmlAddressRule);
collector.PackRuleName = collectorElement.GetAttribute(XmlPackRule);
collector.FilterRuleName = collectorElement.GetAttribute(XmlFilterRule);
collector.UserData = collectorElement.GetAttribute(XmlUserData);
collector.AssetTags = collectorElement.GetAttribute(XmlAssetTags);
group.Collectors.Add(collector);
}
}
}
// 检测配置错误
foreach (var package in packages)
{
package.CheckConfigError();
}
// 保存配置数据
AssetBundleCollectorSettingData.ClearAll();
AssetBundleCollectorSettingData.Setting.ShowPackageView = showPackageView;
AssetBundleCollectorSettingData.Setting.ShowEditorAlias = showEditorAlias;
AssetBundleCollectorSettingData.Setting.UniqueBundleName = uniqueBundleName;
AssetBundleCollectorSettingData.Setting.Packages.AddRange(packages);
AssetBundleCollectorSettingData.SaveFile();
Debug.Log($"Asset bundle collector config import complete");
}
/// <summary>
/// 导出XML配置表
/// </summary>
public static void ExportXmlConfig(string savePath)
{
if (File.Exists(savePath))
File.Delete(savePath);
StringBuilder sb = new StringBuilder();
sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
sb.AppendLine("<root>");
sb.AppendLine("</root>");
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(sb.ToString());
XmlElement root = xmlDoc.DocumentElement;
// 设置配置版本
root.SetAttribute(XmlVersion, ConfigVersion);
// 设置公共配置
var commonElement = xmlDoc.CreateElement(XmlCommon);
commonElement.SetAttribute(XmlShowPackageView, AssetBundleCollectorSettingData.Setting.ShowPackageView.ToString());
commonElement.SetAttribute(XmlShowEditorAlias, AssetBundleCollectorSettingData.Setting.ShowEditorAlias.ToString());
commonElement.SetAttribute(XmlUniqueBundleName, AssetBundleCollectorSettingData.Setting.UniqueBundleName.ToString());
root.AppendChild(commonElement);
// 设置Package配置
foreach (var package in AssetBundleCollectorSettingData.Setting.Packages)
{
var packageElement = xmlDoc.CreateElement(XmlPackage);
packageElement.SetAttribute(XmlPackageName, package.PackageName);
packageElement.SetAttribute(XmlPackageDesc, package.PackageDesc);
packageElement.SetAttribute(XmlEnableAddressable, package.EnableAddressable.ToString());
packageElement.SetAttribute(XmlSupportExtensionless, package.SupportExtensionless.ToString());
packageElement.SetAttribute(XmlLocationToLower, package.LocationToLower.ToString());
packageElement.SetAttribute(XmlIncludeAssetGUID, package.IncludeAssetGUID.ToString());
packageElement.SetAttribute(XmlIgnoreRuleName, package.IgnoreRuleName);
root.AppendChild(packageElement);
// 设置分组配置
foreach (var group in package.Groups)
{
var groupElement = xmlDoc.CreateElement(XmlGroup);
groupElement.SetAttribute(XmlGroupActiveRule, group.ActiveRuleName);
groupElement.SetAttribute(XmlGroupName, group.GroupName);
groupElement.SetAttribute(XmlGroupDesc, group.GroupDesc);
groupElement.SetAttribute(XmlAssetTags, group.AssetTags);
packageElement.AppendChild(groupElement);
// 设置收集器配置
foreach (var collector in group.Collectors)
{
var collectorElement = xmlDoc.CreateElement(XmlCollector);
collectorElement.SetAttribute(XmlCollectPath, collector.CollectPath);
collectorElement.SetAttribute(XmlCollectorGUID, collector.CollectorGUID);
collectorElement.SetAttribute(XmlCollectorType, collector.CollectorType.ToString());
collectorElement.SetAttribute(XmlAddressRule, collector.AddressRuleName);
collectorElement.SetAttribute(XmlPackRule, collector.PackRuleName);
collectorElement.SetAttribute(XmlFilterRule, collector.FilterRuleName);
collectorElement.SetAttribute(XmlUserData, collector.UserData);
collectorElement.SetAttribute(XmlAssetTags, collector.AssetTags);
groupElement.AppendChild(collectorElement);
}
}
}
// 生成配置文件
xmlDoc.Save(savePath);
Debug.Log($"Asset bundle collector config export complete");
}
/// <summary>
/// 升级XML配置表
/// </summary>
private static bool UpdateXmlConfig(XmlDocument xmlDoc)
{
XmlElement root = xmlDoc.DocumentElement;
string configVersion = root.GetAttribute(XmlVersion);
if (configVersion == ConfigVersion)
return true;
// v2.0.0 -> v2.1
if (configVersion == "v2.0.0")
{
// 读取包裹配置
var packageNodeList = root.GetElementsByTagName(XmlPackage);
foreach (var packageNode in packageNodeList)
{
XmlElement packageElement = packageNode as XmlElement;
if (packageElement.HasAttribute(XmlIgnoreRuleName) == false)
packageElement.SetAttribute(XmlIgnoreRuleName, nameof(NormalIgnoreRule));
}
// 更新版本
root.SetAttribute(XmlVersion, "v2.1");
return UpdateXmlConfig(xmlDoc);
}
// v2.1 -> v2025.8.28
if (configVersion == "v2.1")
{
// 读取包裹配置
var packageNodeList = root.GetElementsByTagName(XmlPackage);
foreach (var packageNode in packageNodeList)
{
XmlElement packageElement = packageNode as XmlElement;
if (packageElement.HasAttribute(XmlSupportExtensionless) == false)
packageElement.SetAttribute(XmlSupportExtensionless, "True");
}
// 更新版本
root.SetAttribute(XmlVersion, "v2025.8.28");
return UpdateXmlConfig(xmlDoc);
}
return false;
}
}
}

View File

@@ -1,489 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
public class AssetBundleCollectorSettingData
{
private static readonly Dictionary<string, System.Type> _cacheActiveRuleTypes = new Dictionary<string, Type>();
private static readonly Dictionary<string, IActiveRule> _cacheActiveRuleInstance = new Dictionary<string, IActiveRule>();
private static readonly Dictionary<string, System.Type> _cacheAddressRuleTypes = new Dictionary<string, System.Type>();
private static readonly Dictionary<string, IAddressRule> _cacheAddressRuleInstance = new Dictionary<string, IAddressRule>();
private static readonly Dictionary<string, System.Type> _cachePackRuleTypes = new Dictionary<string, System.Type>();
private static readonly Dictionary<string, IPackRule> _cachePackRuleInstance = new Dictionary<string, IPackRule>();
private static readonly Dictionary<string, System.Type> _cacheFilterRuleTypes = new Dictionary<string, System.Type>();
private static readonly Dictionary<string, IFilterRule> _cacheFilterRuleInstance = new Dictionary<string, IFilterRule>();
private static readonly Dictionary<string, System.Type> _cacheIgnoreRuleTypes = new Dictionary<string, System.Type>();
private static readonly Dictionary<string, IIgnoreRule> _cacheIgnoreRuleInstance = new Dictionary<string, IIgnoreRule>();
/// <summary>
/// 配置数据是否被修改
/// </summary>
public static bool IsDirty { private set; get; } = false;
static AssetBundleCollectorSettingData()
{
// IPackRule
{
// 清空缓存集合
_cachePackRuleTypes.Clear();
_cachePackRuleInstance.Clear();
// 获取所有类型
List<Type> types = new List<Type>(100)
{
typeof(PackSeparately),
typeof(PackDirectory),
typeof(PackTopDirectory),
typeof(PackCollector),
typeof(PackGroup),
typeof(PackRawFile),
typeof(PackShaderVariants)
};
var customTypes = EditorTools.GetAssignableTypes(typeof(IPackRule));
types.AddRange(customTypes);
for (int i = 0; i < types.Count; i++)
{
Type type = types[i];
if (_cachePackRuleTypes.ContainsKey(type.Name) == false)
_cachePackRuleTypes.Add(type.Name, type);
}
}
// IFilterRule
{
// 清空缓存集合
_cacheFilterRuleTypes.Clear();
_cacheFilterRuleInstance.Clear();
// 获取所有类型
List<Type> types = new List<Type>(100)
{
typeof(CollectAll),
typeof(CollectScene),
typeof(CollectPrefab),
typeof(CollectSprite)
};
var customTypes = EditorTools.GetAssignableTypes(typeof(IFilterRule));
types.AddRange(customTypes);
for (int i = 0; i < types.Count; i++)
{
Type type = types[i];
if (_cacheFilterRuleTypes.ContainsKey(type.Name) == false)
_cacheFilterRuleTypes.Add(type.Name, type);
}
}
// IAddressRule
{
// 清空缓存集合
_cacheAddressRuleTypes.Clear();
_cacheAddressRuleInstance.Clear();
// 获取所有类型
List<Type> types = new List<Type>(100)
{
typeof(AddressByFileName),
typeof(AddressByFolderAndFileName),
typeof(AddressByGroupAndFileName),
typeof(AddressDisable)
};
var customTypes = EditorTools.GetAssignableTypes(typeof(IAddressRule));
types.AddRange(customTypes);
for (int i = 0; i < types.Count; i++)
{
Type type = types[i];
if (_cacheAddressRuleTypes.ContainsKey(type.Name) == false)
_cacheAddressRuleTypes.Add(type.Name, type);
}
}
// IActiveRule
{
// 清空缓存集合
_cacheActiveRuleTypes.Clear();
_cacheActiveRuleInstance.Clear();
// 获取所有类型
List<Type> types = new List<Type>(100)
{
typeof(EnableGroup),
typeof(DisableGroup),
};
var customTypes = EditorTools.GetAssignableTypes(typeof(IActiveRule));
types.AddRange(customTypes);
for (int i = 0; i < types.Count; i++)
{
Type type = types[i];
if (_cacheActiveRuleTypes.ContainsKey(type.Name) == false)
_cacheActiveRuleTypes.Add(type.Name, type);
}
}
// IIgnoreRule
{
// 清空缓存集合
_cacheIgnoreRuleTypes.Clear();
_cacheIgnoreRuleInstance.Clear();
// 获取所有类型
List<Type> types = new List<Type>(100)
{
typeof(NormalIgnoreRule),
typeof(RawFileIgnoreRule),
};
var customTypes = EditorTools.GetAssignableTypes(typeof(IIgnoreRule));
types.AddRange(customTypes);
for (int i = 0; i < types.Count; i++)
{
Type type = types[i];
if (_cacheIgnoreRuleTypes.ContainsKey(type.Name) == false)
_cacheIgnoreRuleTypes.Add(type.Name, type);
}
}
}
private static AssetBundleCollectorSetting _setting = null;
public static AssetBundleCollectorSetting Setting
{
get
{
if (_setting == null)
_setting = SettingLoader.LoadSettingData<AssetBundleCollectorSetting>();
return _setting;
}
}
/// <summary>
/// 存储配置文件
/// </summary>
public static void SaveFile()
{
if (Setting != null)
{
IsDirty = false;
EditorUtility.SetDirty(Setting);
AssetDatabase.SaveAssets();
Debug.Log($"{nameof(AssetBundleCollectorSetting)}.asset is saved!");
}
}
/// <summary>
/// 修复配置文件
/// </summary>
public static void FixFile()
{
bool isFixed = Setting.FixAllPackageConfigError();
if (isFixed)
{
IsDirty = true;
Debug.Log("Fix package config error done !");
}
}
/// <summary>
/// 清空所有数据
/// </summary>
public static void ClearAll()
{
Setting.ClearAll();
}
public static List<RuleDisplayName> GetActiveRuleNames()
{
List<RuleDisplayName> names = new List<RuleDisplayName>();
foreach (var pair in _cacheActiveRuleTypes)
{
RuleDisplayName ruleName = new RuleDisplayName();
ruleName.ClassName = pair.Key;
ruleName.DisplayName = GetRuleDisplayName(pair.Key, pair.Value);
names.Add(ruleName);
}
return names;
}
public static List<RuleDisplayName> GetAddressRuleNames()
{
List<RuleDisplayName> names = new List<RuleDisplayName>();
foreach (var pair in _cacheAddressRuleTypes)
{
RuleDisplayName ruleName = new RuleDisplayName();
ruleName.ClassName = pair.Key;
ruleName.DisplayName = GetRuleDisplayName(pair.Key, pair.Value);
names.Add(ruleName);
}
return names;
}
public static List<RuleDisplayName> GetPackRuleNames()
{
List<RuleDisplayName> names = new List<RuleDisplayName>();
foreach (var pair in _cachePackRuleTypes)
{
RuleDisplayName ruleName = new RuleDisplayName();
ruleName.ClassName = pair.Key;
ruleName.DisplayName = GetRuleDisplayName(pair.Key, pair.Value);
names.Add(ruleName);
}
return names;
}
public static List<RuleDisplayName> GetFilterRuleNames()
{
List<RuleDisplayName> names = new List<RuleDisplayName>();
foreach (var pair in _cacheFilterRuleTypes)
{
RuleDisplayName ruleName = new RuleDisplayName();
ruleName.ClassName = pair.Key;
ruleName.DisplayName = GetRuleDisplayName(pair.Key, pair.Value);
names.Add(ruleName);
}
return names;
}
public static List<RuleDisplayName> GetIgnoreRuleNames()
{
List<RuleDisplayName> names = new List<RuleDisplayName>();
foreach (var pair in _cacheIgnoreRuleTypes)
{
RuleDisplayName ruleName = new RuleDisplayName();
ruleName.ClassName = pair.Key;
ruleName.DisplayName = GetRuleDisplayName(pair.Key, pair.Value);
names.Add(ruleName);
}
return names;
}
private static string GetRuleDisplayName(string name, Type type)
{
var attribute = EditorTools.GetAttribute<DisplayNameAttribute>(type);
if (attribute != null && string.IsNullOrEmpty(attribute.DisplayName) == false)
return attribute.DisplayName;
else
return name;
}
public static bool HasActiveRuleName(string ruleName)
{
return _cacheActiveRuleTypes.ContainsKey(ruleName);
}
public static bool HasAddressRuleName(string ruleName)
{
return _cacheAddressRuleTypes.ContainsKey(ruleName);
}
public static bool HasPackRuleName(string ruleName)
{
return _cachePackRuleTypes.ContainsKey(ruleName);
}
public static bool HasFilterRuleName(string ruleName)
{
return _cacheFilterRuleTypes.ContainsKey(ruleName);
}
public static bool HasIgnoreRuleName(string ruleName)
{
return _cacheIgnoreRuleTypes.ContainsKey(ruleName);
}
public static IActiveRule GetActiveRuleInstance(string ruleName)
{
if (_cacheActiveRuleInstance.TryGetValue(ruleName, out IActiveRule instance))
return instance;
// 如果不存在创建类的实例
if (_cacheActiveRuleTypes.TryGetValue(ruleName, out Type type))
{
instance = (IActiveRule)Activator.CreateInstance(type);
_cacheActiveRuleInstance.Add(ruleName, instance);
return instance;
}
else
{
throw new Exception($"{nameof(IActiveRule)} is invalid{ruleName}");
}
}
public static IAddressRule GetAddressRuleInstance(string ruleName)
{
if (_cacheAddressRuleInstance.TryGetValue(ruleName, out IAddressRule instance))
return instance;
// 如果不存在创建类的实例
if (_cacheAddressRuleTypes.TryGetValue(ruleName, out Type type))
{
instance = (IAddressRule)Activator.CreateInstance(type);
_cacheAddressRuleInstance.Add(ruleName, instance);
return instance;
}
else
{
throw new Exception($"{nameof(IAddressRule)} is invalid{ruleName}");
}
}
public static IPackRule GetPackRuleInstance(string ruleName)
{
if (_cachePackRuleInstance.TryGetValue(ruleName, out IPackRule instance))
return instance;
// 如果不存在创建类的实例
if (_cachePackRuleTypes.TryGetValue(ruleName, out Type type))
{
instance = (IPackRule)Activator.CreateInstance(type);
_cachePackRuleInstance.Add(ruleName, instance);
return instance;
}
else
{
throw new Exception($"{nameof(IPackRule)} is invalid{ruleName}");
}
}
public static IFilterRule GetFilterRuleInstance(string ruleName)
{
if (_cacheFilterRuleInstance.TryGetValue(ruleName, out IFilterRule instance))
return instance;
// 如果不存在创建类的实例
if (_cacheFilterRuleTypes.TryGetValue(ruleName, out Type type))
{
instance = (IFilterRule)Activator.CreateInstance(type);
_cacheFilterRuleInstance.Add(ruleName, instance);
return instance;
}
else
{
throw new Exception($"{nameof(IFilterRule)} is invalid{ruleName}");
}
}
public static IIgnoreRule GetIgnoreRuleInstance(string ruleName)
{
if (_cacheIgnoreRuleInstance.TryGetValue(ruleName, out IIgnoreRule instance))
return instance;
// 如果不存在创建类的实例
if (_cacheIgnoreRuleTypes.TryGetValue(ruleName, out Type type))
{
instance = (IIgnoreRule)Activator.CreateInstance(type);
_cacheIgnoreRuleInstance.Add(ruleName, instance);
return instance;
}
else
{
throw new Exception($"{nameof(IIgnoreRule)} is invalid{ruleName}");
}
}
// 公共参数编辑相关
public static void ModifyShowPackageView(bool showPackageView)
{
Setting.ShowPackageView = showPackageView;
IsDirty = true;
}
public static void ModifyShowEditorAlias(bool showAlias)
{
Setting.ShowEditorAlias = showAlias;
IsDirty = true;
}
public static void ModifyUniqueBundleName(bool uniqueBundleName)
{
Setting.UniqueBundleName = uniqueBundleName;
IsDirty = true;
}
// 资源包裹编辑相关
public static AssetBundleCollectorPackage CreatePackage(string packageName)
{
AssetBundleCollectorPackage package = new AssetBundleCollectorPackage();
package.PackageName = packageName;
Setting.Packages.Add(package);
IsDirty = true;
return package;
}
public static void RemovePackage(AssetBundleCollectorPackage package)
{
if (Setting.Packages.Remove(package))
{
IsDirty = true;
}
else
{
Debug.LogWarning($"Failed remove package : {package.PackageName}");
}
}
public static void ModifyPackage(AssetBundleCollectorPackage package)
{
if (package != null)
{
IsDirty = true;
}
}
// 资源分组编辑相关
public static AssetBundleCollectorGroup CreateGroup(AssetBundleCollectorPackage package, string groupName)
{
AssetBundleCollectorGroup group = new AssetBundleCollectorGroup();
group.GroupName = groupName;
package.Groups.Add(group);
IsDirty = true;
return group;
}
public static void RemoveGroup(AssetBundleCollectorPackage package, AssetBundleCollectorGroup group)
{
if (package.Groups.Remove(group))
{
IsDirty = true;
}
else
{
Debug.LogWarning($"Failed remove group : {group.GroupName}");
}
}
public static void ModifyGroup(AssetBundleCollectorPackage package, AssetBundleCollectorGroup group)
{
if (package != null && group != null)
{
IsDirty = true;
}
}
// 资源收集器编辑相关
public static void CreateCollector(AssetBundleCollectorGroup group, AssetBundleCollector collector)
{
group.Collectors.Add(collector);
IsDirty = true;
}
public static void RemoveCollector(AssetBundleCollectorGroup group, AssetBundleCollector collector)
{
if (group.Collectors.Remove(collector))
{
IsDirty = true;
}
else
{
Debug.LogWarning($"Failed remove collector : {collector.CollectPath}");
}
}
public static void ModifyCollector(AssetBundleCollectorGroup group, AssetBundleCollector collector)
{
if (group != null && collector != null)
{
IsDirty = true;
}
}
/// <summary>
/// 获取所有的资源标签
/// </summary>
public static string GetPackageAllTags(string packageName)
{
var allTags = Setting.GetPackageAllTags(packageName);
return string.Join(";", allTags);
}
}
}

View File

@@ -1,24 +0,0 @@

namespace YooAsset.Editor
{
public struct GroupData
{
public string GroupName;
public GroupData(string groupName)
{
GroupName = groupName;
}
}
/// <summary>
/// 资源分组激活规则接口
/// </summary>
public interface IActiveRule
{
/// <summary>
/// 是否激活分组
/// </summary>
bool IsActiveGroup(GroupData data);
}
}

View File

@@ -1,27 +0,0 @@

namespace YooAsset.Editor
{
public struct AddressRuleData
{
public string AssetPath;
public string CollectPath;
public string GroupName;
public string UserData;
public AddressRuleData(string assetPath, string collectPath, string groupName, string userData)
{
AssetPath = assetPath;
CollectPath = collectPath;
GroupName = groupName;
UserData = userData;
}
}
/// <summary>
/// 寻址规则接口
/// </summary>
public interface IAddressRule
{
string GetAssetAddress(AddressRuleData data);
}
}

View File

@@ -1,37 +0,0 @@

namespace YooAsset.Editor
{
public struct FilterRuleData
{
public string AssetPath;
public string CollectPath;
public string GroupName;
public string UserData;
public FilterRuleData(string assetPath, string collectPath, string groupName, string userData)
{
AssetPath = assetPath;
CollectPath = collectPath;
GroupName = groupName;
UserData = userData;
}
}
/// <summary>
/// 资源过滤规则接口
/// </summary>
public interface IFilterRule
{
/// <summary>
/// 搜寻的资源类型
/// 说明:使用引擎方法搜索获取所有资源列表
/// </summary>
string FindAssetType { get; }
/// <summary>
/// 验证搜寻的资源是否为收集资源
/// </summary>
/// <returns>如果收集该资源返回TRUE</returns>
bool IsCollectAsset(FilterRuleData data);
}
}

View File

@@ -1,11 +0,0 @@

namespace YooAsset.Editor
{
/// <summary>
/// 资源忽略规则接口
/// </summary>
public interface IIgnoreRule
{
bool IsIgnore(AssetInfo assetInfo);
}
}

View File

@@ -1,78 +0,0 @@

namespace YooAsset.Editor
{
public struct PackRuleData
{
public string AssetPath;
public string CollectPath;
public string GroupName;
public string UserData;
public PackRuleData(string assetPath, string collectPath, string groupName, string userData)
{
AssetPath = assetPath;
CollectPath = collectPath;
GroupName = groupName;
UserData = userData;
}
}
public struct PackRuleResult
{
private readonly string _bundleName;
private readonly string _bundleExtension;
public PackRuleResult(string bundleName, string bundleExtension)
{
_bundleName = bundleName;
_bundleExtension = bundleExtension;
}
/// <summary>
/// 结果是否有效
/// </summary>
public bool IsValid()
{
return string.IsNullOrEmpty(_bundleName) == false && string.IsNullOrEmpty(_bundleExtension) == false;
}
/// <summary>
/// 获取资源包全名称
/// </summary>
public string GetBundleName(string packageName, bool uniqueBundleName)
{
string fullName;
string bundleName = EditorTools.GetRegularPath(_bundleName).Replace('/', '_').Replace('.', '_').Replace(" ", "_").ToLower();
if (uniqueBundleName)
fullName = $"{packageName}_{bundleName}.{_bundleExtension}";
else
fullName = $"{bundleName}.{_bundleExtension}";
return fullName.ToLower();
}
/// <summary>
/// 获取共享资源包全名称
/// </summary>
public string GetShareBundleName(string packageName, bool uniqueBundleName)
{
string fullName;
string bundleName = EditorTools.GetRegularPath(_bundleName).Replace('/', '_').Replace('.', '_').Replace(" ", "_").ToLower();
if (uniqueBundleName)
fullName = $"{packageName}_share_{bundleName}.{_bundleExtension}";
else
fullName = $"share_{bundleName}.{_bundleExtension}";
return fullName.ToLower();
}
}
/// <summary>
/// 资源打包规则接口
/// </summary>
public interface IPackRule
{
/// <summary>
/// 获取打包规则结果
/// </summary>
PackRuleResult GetPackRuleResult(PackRuleData data);
}
}

View File

@@ -1,21 +0,0 @@

namespace YooAsset.Editor
{
[DisplayName("启用分组")]
public class EnableGroup : IActiveRule
{
public bool IsActiveGroup(GroupData data)
{
return true;
}
}
[DisplayName("禁用分组")]
public class DisableGroup : IActiveRule
{
public bool IsActiveGroup(GroupData data)
{
return false;
}
}
}

View File

@@ -1,105 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEditor;
namespace YooAsset.Editor
{
[DisplayName("收集所有资源")]
public class CollectAll : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.All.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return true;
}
}
[DisplayName("收集场景")]
public class CollectScene : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Scene.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
string extension = Path.GetExtension(data.AssetPath);
return extension == ".unity" || extension == ".scene";
}
}
[DisplayName("收集预制体")]
public class CollectPrefab : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Prefab.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".prefab";
}
}
[DisplayName("收集精灵类型的纹理")]
public class CollectSprite : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Sprite.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
var mainAssetType = AssetDatabase.GetMainAssetTypeAtPath(data.AssetPath);
if (mainAssetType == typeof(Texture2D))
{
var texImporter = AssetImporter.GetAtPath(data.AssetPath) as TextureImporter;
if (texImporter != null && texImporter.textureType == TextureImporterType.Sprite)
return true;
else
return false;
}
else
{
return false;
}
}
}
[DisplayName("收集着色器")]
public class CollectShader : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Shader.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".shader";
}
}
[DisplayName("收集着色器变种集合")]
public class CollectShaderVariants : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.All.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".shadervariants";
}
}
}

View File

@@ -1,198 +0,0 @@
using System;
using System.IO;
using UnityEditor;
namespace YooAsset.Editor
{
public class DefaultPackRule
{
/// <summary>
/// AssetBundle文件的后缀名
/// </summary>
public const string AssetBundleFileExtension = "bundle";
/// <summary>
/// 原生文件的后缀名
/// </summary>
public const string RawFileExtension = "rawfile";
/// <summary>
/// 默认的Unity着色器资源包名称
/// </summary>
public const string ShadersBundleName = "unityshaders";
/// <summary>
/// 默认的Unity脚本资源包名称
/// </summary>
public const string MonosBundleName = "unitymonos";
public static PackRuleResult CreateShadersPackRuleResult()
{
PackRuleResult result = new PackRuleResult(ShadersBundleName, AssetBundleFileExtension);
return result;
}
public static PackRuleResult CreateMonosPackRuleResult()
{
PackRuleResult result = new PackRuleResult(MonosBundleName, AssetBundleFileExtension);
return result;
}
}
/// <summary>
/// 以文件路径作为资源包名
/// 注意:每个文件独自打资源包
/// 例如:"Assets/UIPanel/Shop/Image/backgroud.png" --> "assets_uipanel_shop_image_backgroud.bundle"
/// 例如:"Assets/UIPanel/Shop/View/main.prefab" --> "assets_uipanel_shop_view_main.bundle"
/// </summary>
[DisplayName("资源包名: 文件路径")]
public class PackSeparately : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName = PathUtility.RemoveExtension(data.AssetPath);
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
return result;
}
}
/// <summary>
/// 以父类文件夹路径作为资源包名
/// 注意:文件夹下所有文件打进一个资源包
/// 例如:"Assets/UIPanel/Shop/Image/backgroud.png" --> "assets_uipanel_shop_image.bundle"
/// 例如:"Assets/UIPanel/Shop/View/main.prefab" --> "assets_uipanel_shop_view.bundle"
/// </summary>
[DisplayName("资源包名: 父类文件夹路径")]
public class PackDirectory : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName = Path.GetDirectoryName(data.AssetPath);
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
return result;
}
}
/// <summary>
/// 以收集器路径下顶级文件夹为资源包名
/// 注意:文件夹下所有文件打进一个资源包
/// 例如:收集器路径为 "Assets/UIPanel"
/// 例如:"Assets/UIPanel/Shop/Image/backgroud.png" --> "assets_uipanel_shop.bundle"
/// 例如:"Assets/UIPanel/Shop/View/main.prefab" --> "assets_uipanel_shop.bundle"
/// </summary>
[DisplayName("资源包名: 收集器下顶级文件夹路径")]
public class PackTopDirectory : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string assetPath = data.AssetPath.Replace(data.CollectPath, string.Empty);
assetPath = assetPath.TrimStart('/');
string[] splits = assetPath.Split('/');
if (splits.Length > 0)
{
if (Path.HasExtension(splits[0]))
throw new Exception($"Not found root directory : {assetPath}");
string bundleName = $"{data.CollectPath}/{splits[0]}";
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
return result;
}
else
{
throw new Exception($"Not found root directory : {assetPath}");
}
}
}
/// <summary>
/// 以收集器路径作为资源包名
/// 注意:收集的所有文件打进一个资源包
/// </summary>
[DisplayName("资源包名: 收集器路径")]
public class PackCollector : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName;
string collectPath = data.CollectPath;
if (AssetDatabase.IsValidFolder(collectPath))
{
bundleName = collectPath;
}
else
{
bundleName = PathUtility.RemoveExtension(collectPath);
}
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
return result;
}
}
/// <summary>
/// 以分组名称作为资源包名
/// 注意:收集的所有文件打进一个资源包
/// </summary>
[DisplayName("资源包名: 分组名称")]
public class PackGroup : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName = data.GroupName;
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.AssetBundleFileExtension);
return result;
}
}
/// <summary>
/// 打包原生文件
/// </summary>
[DisplayName("打包原生文件")]
public class PackRawFile : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName = data.AssetPath;
PackRuleResult result = new PackRuleResult(bundleName, DefaultPackRule.RawFileExtension);
return result;
}
}
/// <summary>
/// 打包视频文件
/// </summary>
[DisplayName("打包视频文件")]
public class PackVideoFile : IPackRule
{
PackRuleResult IPackRule.GetPackRuleResult(PackRuleData data)
{
string bundleName = data.AssetPath;
string fileExtension = Path.GetExtension(data.AssetPath);
fileExtension = fileExtension.Remove(0, 1);
PackRuleResult result = new PackRuleResult(bundleName, fileExtension);
return result;
}
}
/// <summary>
/// 打包着色器
/// </summary>
[DisplayName("打包着色器文件")]
public class PackShader : IPackRule
{
public PackRuleResult GetPackRuleResult(PackRuleData data)
{
return DefaultPackRule.CreateShadersPackRuleResult();
}
}
/// <summary>
/// 打包着色器变种集合
/// </summary>
[DisplayName("打包着色器变种集合文件")]
public class PackShaderVariants : IPackRule
{
public PackRuleResult GetPackRuleResult(PackRuleData data)
{
return DefaultPackRule.CreateShadersPackRuleResult();
}
}
}

Some files were not shown because too many files have changed in this diff Show More