diff --git a/MD/代码优化标准.md b/MD/代码优化标准.md deleted file mode 100644 index b741e73e6..000000000 --- a/MD/代码优化标准.md +++ /dev/null @@ -1,859 +0,0 @@ -# 🔧 TH1 代码优化标准 - -> **适用范围**: TH1 回合制4X策略游戏全模块 -> **核心目标**: 减少GC分配 · 加快计算速度 · 保证输入输出不变 -> **约束条件**: 不改变整体逻辑 · 兼容 MemoryPack 本地/网络序列化 · 保持多人同步正确性 - ---- - -## 目录 - -- [第一章 总则与核心原则](#第一章-总则与核心原则) -- [第二章 序列化安全规范](#第二章-序列化安全规范) -- [第三章 GC优化标准](#第三章-gc优化标准) -- [第四章 计算优化标准](#第四章-计算优化标准) -- [第五章 模块专项优化指南](#第五章-模块专项优化指南) -- [第六章 优化验证清单](#第六章-优化验证清单) -- [附录A 已知GC热点清单](#附录a-已知gc热点清单) -- [附录B 禁止与允许操作速查表](#附录b-禁止与允许操作速查表) - ---- - -## 第一章 总则与核心原则 - -### 1.1 优化铁律 - -| 编号 | 规则 | 说明 | -|------|------|------| -| **R-01** | **输入输出不变** | 任何优化不得改变方法的参数签名、返回值语义和副作用。对外行为必须与优化前完全一致 | -| **R-02** | **逻辑等价** | 优化只能改变"怎么做",不能改变"做什么"。所有分支路径、计算结果、状态变更必须等价 | -| **R-03** | **序列化安全** | 任何新增缓存/查找表字段必须加 `[MemoryPackIgnore]`,不得影响存档文件和网络消息的二进制格式 | -| **R-04** | **确定性一致** | 多人联网场景下,优化后的计算必须保持确定性(相同输入产生相同输出),不得引入浮点精度差异 | -| **R-05** | **渐进式优化** | 每次只优化一个点,优化后必须验证,确认无误后再优化下一个 | - -### 1.2 优化优先级 - -``` -优先级从高到低: -P0 - 消除热路径中的 GC 分配(每帧/每回合执行的代码) -P1 - 缓存重复计算结果(空间换时间,需考虑序列化) -P2 - 算法复杂度优化(O(n²) → O(n) 或 O(n log n)) -P3 - 减少非热路径的 GC 分配(初始化/低频调用) -P4 - 微优化(方法内联、分支预测等) -``` - -### 1.3 不优化原则 - -以下情况**不应**进行优化: - -- 仅在游戏初始化时执行一次的代码(如 `SkillFactory` 首次反射扫描) -- 仅在加载/存档时执行的代码(除非造成明显卡顿) -- UI 层的低频更新代码 -- 已经足够高效且无 GC 问题的代码 - ---- - -## 第二章 序列化安全规范 - -### 2.1 TH1 序列化架构概览 - -TH1 存在三种序列化场景,任何"空间换时间"优化必须同时考虑: - -| 场景 | 序列化方式 | 频率 | 敏感度 | -|------|-----------|------|--------| -| **本地存档** | MemoryPack → `.dat` 文件 | 每回合一次 | 向后兼容(旧存档能读取) | -| **网络同步** | MemoryPack → Steam P2P | 每个Action、每2秒心跳 | 双向兼容(多客户端版本) | -| **训练数据** | MemoryPack / JSON → `.jsonl` | `#if ENABLE_TRAIN` 时每步一次 | 格式稳定 | - -### 2.2 核心 MemoryPackable 类清单 - -以下类直接参与序列化,新增字段时**必须**遵守规范: - -``` -MapData ─ 游戏世界根节点(包含以下所有子结构) -├── MapConfig ─ 对局配置 -├── GridMapData ─ 所有格子 -│ └── GridData ─ 单个格子(附带 Skills) -├── PlayerMapData─ 所有玩家 -│ └── PlayerData─ 单个玩家(附带 Skills, DiplomacyData, TechTreeData) -├── CityMapData ─ 所有城市 -│ └── CityData ─ 单个城市(附带 TerritoryData, Skills) -├── UnitMapData ─ 所有部队 -│ └── UnitData ─ 单个部队(附带 Skills, ActionPoint) -└── NetData ─ 网络状态(含 Actions 历史, 随机种子) - -CommonActionParams ─ Action参数(网络传输) -BaseMessage (16种) ─ 网络消息(含 GameStartMessage 携带完整 MapData) -SkillBase (242种子类)─ 技能多态序列化 -``` - -### 2.3 新增缓存字段的标准做法 - -#### ✅ 正确做法:`[MemoryPackIgnore]` + 重建逻辑 - -```csharp -[MemoryPackable] -public partial class UnitData : IdentifierBase -{ - // 已有的序列化字段 - public List Skills; - - // ===== 新增的缓存字段 ===== - [MemoryPackIgnore] - private Dictionary _skillLookup; - - /// - /// O(1) 技能查找,替代 Skills 列表的 O(n) 线性搜索。 - /// 缓存在反序列化后自动重建。 - /// - public SkillBase GetSkillFast(SkillType type) - { - if (_skillLookup == null) RebuildSkillLookup(); - return _skillLookup.TryGetValue(type, out var skill) ? skill : null; - } - - // 当 Skills 列表发生变更时调用 - public void InvalidateSkillLookup() - { - _skillLookup = null; // 延迟重建,下次查找时自动构建 - } - - private void RebuildSkillLookup() - { - _skillLookup ??= new Dictionary(Skills.Count); - _skillLookup.Clear(); - foreach (var skill in Skills) - _skillLookup[skill.GetSkillType()] = skill; - } - - [MemoryPackOnDeserialized] - public void OnAfterDeserialize() - { - _skillLookup = null; // 反序列化后标记为需要重建 - // 不在此处立即重建,延迟到首次使用 - } -} -``` - -#### ❌ 错误做法 - -```csharp -// 错误1:缓存字段没有加 [MemoryPackIgnore] -[MemoryPackable] -public partial class UnitData -{ - public Dictionary _skillLookup; // ❌ 会被序列化,增大存档/网络包体积 -} - -// 错误2:在 [MemoryPackOnDeserialized] 中执行重量级初始化 -[MemoryPackOnDeserialized] -public void OnAfterDeserialize() -{ - RebuildSkillLookup(); // ❌ 网络同步的每次反序列化都会触发 - RecalcAllTerritoryBonuses(); // ❌ 高频反序列化时造成卡顿 -} - -// 错误3:修改序列化字段的类型或移除字段 -public uint LegionId; // ❌ 如果改名或移除,旧存档无法反序列化 -``` - -### 2.4 序列化兼容性检查清单 - -每次涉及 `[MemoryPackable]` 类的修改,必须检查: - -- [ ] 新增字段是否加了 `[MemoryPackIgnore]`? -- [ ] 是否修改/删除/重排了已有的序列化字段?(**禁止**) -- [ ] 缓存重建逻辑是否在 `[MemoryPackOnDeserialized]` 中以**延迟方式**处理? -- [ ] 旧存档能否正常加载?(MemoryPack 对新增 Ignore 字段透明) -- [ ] 多人游戏中不同客户端版本能否互通? - -### 2.5 网络消息的特殊约束 - -对于 `CommonActionParams` 和 `BaseMessage` 子类: - -- **绝不**新增序列化字段(增加每次 Action 的网络开销) -- 如需传递额外数据,使用已有字段编码(如 `Value1`-`Value4` 通用数值字段) -- 对于高频消息(心跳、Action确认),任何字节增长都会累积 - -### 2.6 反序列化重建的性能规范 - -``` -[MemoryPackOnDeserialized] 回调中: -├── ✅ 可以做:设置标志位、置空缓存引用 -├── ✅ 可以做:O(1) 的简单赋值 -├── ⚠️ 谨慎做:O(n) 的字典/集合重建(仅在必要时) -└── ❌ 禁止做:O(n²) 或更复杂的计算、涉及其他对象的查询 -``` - -理由:网络同步时,`ForceUpdateMessage` 和 `GameStartMessage` 会反序列化完整 `MapData`,触发所有子对象的 `[MemoryPackOnDeserialized]` 回调。如果每个回调都做重量级初始化,会造成瞬间卡顿。 - ---- - -## 第三章 GC优化标准 - -### 3.1 集合分配优化 - -#### 3.1.1 复用集合替代重复创建 - -**问题模式**:在频繁调用的方法中 `new List()` / `new HashSet()` / `new Dictionary()` - -```csharp -// ❌ 每次调用都分配新集合 -public List GetAroundGridData(GridData center, int radius) -{ - List result = new List(); // GC 分配 - // ... 填充 result - return result; -} - -// ✅ 方案A:调用方传入缓冲区 -public void GetAroundGridData(GridData center, int radius, List buffer) -{ - buffer.Clear(); - // ... 填充 buffer -} - -// ✅ 方案B:类级别复用集合(非线程安全,单线程游戏逻辑可用) -private static readonly List _sharedGridBuffer = new List(64); - -public List GetAroundGridData(GridData center, int radius) -{ - _sharedGridBuffer.Clear(); - // ... 填充 _sharedGridBuffer - return _sharedGridBuffer; // 注意:调用方不应持有引用或修改 -} -``` - -**适用范围**: - -| 方法 | 当前问题 | 建议方案 | -|------|---------|---------| -| `GridMapData.GetAroundGridData()` (4个重载) | 每次 new List/HashSet | 传入缓冲区 | -| `GridMapData.GetAroundGridIdList()` | 每次 new List\ | 传入缓冲区 | -| `MapData.GetPlayerTerritoryGridIdSet()` | 每次 new HashSet + new List | 玩家级缓存 + 脏标记 | -| `MapData.GetUnitDataListByPlayerId()` | null 参数时 new List | 必须传入缓冲区 | -| `MapData.GetUnitDataListByCityId()` | null 参数时 new List | 必须传入缓冲区 | -| `MapData.GetCityDataListByPlayerId()` | null 参数时 new List | 必须传入缓冲区 | - -#### 3.1.2 集合预分配容量 - -```csharp -// ❌ 默认容量,后续频繁扩容 -var list = new List(); - -// ✅ 预分配合理容量 -var list = new List(expectedCount); - -// ✅ 字典也需要预分配 -var dict = new Dictionary(CityList.Count); -``` - -**预分配容量参考**: - -| 集合用途 | 建议初始容量 | -|---------|-------------| -| 格子周围列表(radius=1) | 9 | -| 格子周围列表(radius=2) | 25 | -| 玩家城市列表 | 8 | -| 玩家部队列表 | 32 | -| 城市领土格子集合 | 64 | -| 技能列表 | 8 | - -#### 3.1.3 消除默认参数创建集合 - -```csharp -// ❌ 可选参数导致隐式分配 -public void GetUnitDataListByPlayerId(uint pid, List list = null) -{ - if (list == null) list = new List(); // 隐式 GC 分配 - // ... -} - -// ✅ 强制调用方提供缓冲区(消除隐式分配) -public void GetUnitDataListByPlayerId(uint pid, List buffer) -{ - buffer.Clear(); - // ... -} -``` - -### 3.2 字符串优化 - -#### 3.2.1 避免热路径中的字符串操作 - -```csharp -// ❌ 循环中拼接字符串 -foreach (var unit in units) -{ - string key = "Unit_" + unit.Id.ToString(); // 每次循环产生 2 次分配 - Debug.Log($"Processing {key}"); // 插值字符串再分配 -} - -// ✅ 调试日志使用条件编译 -#if UNITY_EDITOR -foreach (var unit in units) -{ - Debug.Log($"Processing Unit_{unit.Id}"); -} -#endif - -// ✅ 需要运行时字符串时使用 StringBuilder 复用 -private static readonly StringBuilder _sb = new StringBuilder(256); - -public string GetDebugInfo() -{ - _sb.Clear(); - _sb.Append("Unit_").Append(unit.Id); - return _sb.ToString(); // 仅产生一次分配 -} -``` - -#### 3.2.2 枚举转字符串缓存 - -```csharp -// ❌ 每次调用都装箱 + 反射 -string name = skillType.ToString(); // 装箱 + 内部反射查找 - -// ✅ 预建枚举字符串映射表 -private static readonly Dictionary _skillTypeNames; - -static SkillHelper() -{ - var values = (SkillType[])Enum.GetValues(typeof(SkillType)); - _skillTypeNames = new Dictionary(values.Length); - foreach (var v in values) _skillTypeNames[v] = v.ToString(); -} - -public static string GetSkillTypeName(SkillType type) => _skillTypeNames[type]; -``` - -### 3.3 对象创建优化 - -#### 3.3.1 技能工厂优化(SkillFactory) - -```csharp -// ❌ 当前:每次创建技能都用 Activator.CreateInstance(反射开销大) -public static SkillBase GetSkillBySkillType(SkillType type) -{ - return Activator.CreateInstance(_skillDict[type]) as SkillBase; // 反射 + 装箱 -} - -// ✅ 使用编译时委托替代反射 -private static Dictionary> _skillCreators; - -static void InitCreators() -{ - _skillCreators = new Dictionary>(); - foreach (var kv in _skillDict) - { - var type = kv.Value; - // 编译一次,后续调用零反射 - var constructor = type.GetConstructor(Type.EmptyTypes); - var newExp = Expression.New(constructor); - var lambda = Expression.Lambda>(newExp).Compile(); - _skillCreators[kv.Key] = lambda; - } -} - -public static SkillBase GetSkillBySkillType(SkillType type) -{ - return _skillCreators[type](); // 无反射,接近 new 的性能 -} -``` - -#### 3.3.2 避免闭包和委托分配 - -```csharp -// ❌ Lambda 捕获外部变量产生闭包对象 -int threshold = 5; -var filtered = units.Where(u => u.Level > threshold).ToList(); // 闭包 + List 分配 - -// ✅ 手动循环,零分配 -private static readonly List _tempFilteredUnits = new List(32); - -public List GetUnitsAboveLevel(List units, int threshold) -{ - _tempFilteredUnits.Clear(); - for (int i = 0; i < units.Count; i++) - { - if (units[i].Level > threshold) - _tempFilteredUnits.Add(units[i]); - } - return _tempFilteredUnits; -} -``` - -### 3.4 LINQ 禁用规范 - -**在热路径中完全禁止使用 LINQ**,包括但不限于: - -| 禁止操作 | 原因 | 替代方案 | -|---------|------|---------| -| `.Where().ToList()` | 分配迭代器 + 新 List | 手动 for 循环 + 预分配 List | -| `.Select().ToArray()` | 分配迭代器 + 新 Array | 手动 for 循环 + 预分配 Array | -| `.Any(predicate)` | 分配闭包对象 | 手动 for 循环 + bool 标志 | -| `.Count(predicate)` | 分配闭包对象 | 手动 for 循环 + int 计数 | -| `.First() / .FirstOrDefault()` | 分配迭代器 | 手动 for 循环 + break | -| `.OrderBy()` | 分配排序缓冲区 | 原地排序或插入排序 | -| `.ToDictionary()` | 分配新 Dictionary | 预分配 + 手动填充 | -| `.Sum() / .Max() / .Min()` | 分配迭代器 | 手动累加 | - -**热路径定义**:每帧、每回合、每个 Action、AI 决策循环中执行的代码。 - -**非热路径**(允许使用 LINQ):初始化、UI 事件响应(低频点击)、加载/存档。 - -### 3.5 装箱避免 - -```csharp -// ❌ 值类型传给 object 参数 -void Log(object value) { } // 调用 Log(42) 会装箱 int - -// ❌ 枚举作为字典键(如果没有实现 IEqualityComparer) -Dictionary dict; // SkillType 是 enum,默认比较器会装箱 - -// ✅ 为枚举字典提供专用比较器 -public struct SkillTypeComparer : IEqualityComparer -{ - public bool Equals(SkillType x, SkillType y) => x == y; - public int GetHashCode(SkillType obj) => (int)obj; -} - -var dict = new Dictionary(new SkillTypeComparer()); -``` - -### 3.6 struct 使用规范 - -```csharp -// ✅ 短期使用、小数据量、值语义的数据适合 struct -public struct AttackResult // 战斗结果传递,无需 GC -{ - public int Damage; - public bool IsKill; - public bool IsCritical; -} - -// ✅ 事件系统已经使用 struct(UIEvents),保持不变 -public struct ShowUINotifyMoment // 零 GC 事件分发 -{ - public MomentType MomentType; - public CityData CityData; // 引用类型成员不影响 struct 本身 -} -``` - -**struct 使用条件**: -- 大小不超过 64 字节 -- 不需要继承 -- 创建后不会被修改(或修改成本可接受) -- 不会被装箱(不存入 object 或接口引用) - ---- - -## 第四章 计算优化标准 - -### 4.1 缓存 + 脏标记模式 - -适用于结果稳定、查询频繁但修改不频繁的计算。 - -```csharp -/// -/// 玩家领土格子集合缓存。 -/// 仅在领土变更时置脏,查询时按需重建。 -/// -[MemoryPackable] -public partial class PlayerData -{ - // ===== 已有序列化字段(不动)===== - - // ===== 缓存字段 ===== - [MemoryPackIgnore] private HashSet _territoryCache; - [MemoryPackIgnore] private bool _territoryDirty = true; - - public HashSet GetTerritoryGridIdSet(MapData map) - { - if (_territoryDirty || _territoryCache == null) - { - _territoryCache ??= new HashSet(); - _territoryCache.Clear(); - // 从城市列表中收集领土 - foreach (var city in map.CityMap.CityList) - { - if (map.CityToPlayerDict.TryGetValue(city.Id, out var pid) && pid == Id) - city.Territory.GetAllTerritoryArea(_territoryCache); - } - _territoryDirty = false; - } - return _territoryCache; - } - - /// 领土变更时调用(城市升级、征服、丢失城市) - public void InvalidateTerritoryCache() => _territoryDirty = true; -} -``` - -**适用场景**: - -| 缓存对象 | 失效条件 | 查询频率 | -|---------|---------|---------| -| 玩家领土集合 | 城市升级 / 征服 / 丢失 | 每回合多次(AI评分、视野、建筑升级) | -| 玩家部队列表 | 部队创建 / 阵亡 / 城市易主 | 每回合多次(AI决策) | -| 技能查找字典 | 技能添加 / 移除 | 战斗计算、回合结算 | -| 城市驻军列表 | 部队移动 / 阵亡 | 每次攻击判定 | - -### 4.2 查找表优化 - -#### 4.2.1 O(n) 线性搜索 → O(1) 字典查找 - -```csharp -// ❌ 当前:遍历所有城市查找领土归属 -public bool GetCityDataByTerritoryGridId(uint gid, out CityData cityData) -{ - foreach (var city in CityList) // O(城市数) - { - if (city.CheckIsInTerritory(gid)) // O(领土格子数) - { - cityData = city; - return true; - } - } - // ... -} - -// ✅ 维护 格子ID → 城市 的反向映射 -[MemoryPackIgnore] -private Dictionary _gridToCityLookup; - -public bool GetCityDataByTerritoryGridId_Fast(uint gid, out CityData cityData) -{ - if (_gridToCityLookup == null) RebuildGridToCityLookup(); - return _gridToCityLookup.TryGetValue(gid, out cityData); // O(1) -} - -// 城市领土变更时调用 -public void InvalidateGridToCityLookup() => _gridToCityLookup = null; -``` - -#### 4.2.2 MapData 中已有的映射表利用 - -MapData 中已维护以下映射关系,优化时**优先使用已有映射**,避免重复建表: - -``` -CityToPlayerDict 城市ID → 玩家ID -UnitToCityDict 部队ID → 城市ID -UnitToGridDict 部队ID → 格子ID -CityToGridDict 城市ID → 格子ID -GridToCityDict 格子ID → 城市ID(中心格子) -``` - -### 4.3 算法复杂度优化 - -#### 4.3.1 BFS/邻居搜索优化 - -`GetAroundGridData` 是最高频的空间查询方法,当前实现为三重嵌套循环: - -```csharp -// ❌ 当前:triple nested loop + 距离判断冗余 -for (int r = 0; r <= radius; r++) - for (int x = center.X - offset; x <= center.X + offset; x++) - for (int y = center.Y - offset; y <= center.Y + offset; y++) - if (Mathf.Max(Mathf.Abs(x-cx), Mathf.Abs(y-cy)) != r) continue; - -// ✅ 优化:直接按距离环遍历,消除冗余条件判断 -// 对于小 radius(1-3),使用预计算偏移表 -private static readonly Vector2Int[][] _neighborOffsets = PrecomputeOffsets(maxRadius: 5); - -public void GetAroundGridData(GridData center, int radius, List buffer) -{ - buffer.Clear(); - buffer.Add(center); // 中心格子 - for (int r = 1; r <= radius; r++) - { - var offsets = _neighborOffsets[r]; - for (int i = 0; i < offsets.Length; i++) - { - int x = center.Pos.x + offsets[i].x; - int y = center.Pos.y + offsets[i].y; - if (GetGridDataByPos(x, y, out var grid)) - buffer.Add(grid); - } - } -} -``` - -#### 4.3.2 O(n²) 外交计算优化 - -```csharp -// ❌ 当前:O(玩家数²) 的外交关系计算 -foreach (var p1 in players) - foreach (var p2 in players) - foreach (var strategy in p1.GetFeelingStrategies(p2)) - Calculate(strategy); - -// ✅ 优化:增量计算 + 仅在外交事件触发时更新 -// 方案:事件驱动的增量更新 -public void OnDiplomacyEvent(uint player1Id, uint player2Id) -{ - // 只重算涉及的两个玩家的外交关系 - RecalcFeelingBetween(player1Id, player2Id); -} -``` - -#### 4.3.3 视野计算优化 - -```csharp -// ❌ 当前:每次移动都完整重算所有部队+城市的视野集合 -UpdateSight_LogicView(player); // 遍历所有部队、所有城市领土 - -// ✅ 增量视野更新 -public void UpdateSightIncremental(MapData map, PlayerData player, - UnitData movedUnit, Vector2Int oldPos, Vector2Int newPos) -{ - // 1. 移除旧位置视野贡献 - RemoveSightContribution(movedUnit, oldPos); - // 2. 添加新位置视野贡献 - AddSightContribution(movedUnit, newPos); - // 比完整重算快 O(部队数 × 视野范围²) → O(视野范围²) -} -``` - -### 4.4 循环优化 - -#### 4.4.1 for 替代 foreach(仅对 List\) - -```csharp -// ✅ List 使用 for 循环(避免 enumerator 分配,虽然 List 的 enumerator 是 struct) -for (int i = 0; i < list.Count; i++) -{ - var item = list[i]; - // ... -} - -// ✅ Dictionary 的 foreach 是安全的(struct enumerator) -foreach (var kv in dict) { } // OK - -// ✅ HashSet 的 foreach 也是安全的(struct enumerator) -foreach (var item in set) { } // OK -``` - -#### 4.4.2 提前退出优化 - -```csharp -// ❌ 遍历完整列表仅为找到一个元素 -foreach (var unit in allUnits) -{ - if (unit.IsAlive() && unit.OwnerId == playerId) - { - result = unit; - // 没有 break,继续遍历... - } -} - -// ✅ 找到即退出 -foreach (var unit in allUnits) -{ - if (unit.IsAlive() && unit.OwnerId == playerId) - { - result = unit; - break; - } -} -``` - -#### 4.4.3 条件外提 - -```csharp -// ❌ 循环内重复计算不变量 -for (int i = 0; i < units.Count; i++) -{ - var grid = map.GridMap.GetGridDataByGid(units[i].GridId); - var player = map.PlayerMap.GetPlayerById(playerId); // 每次循环都查找同一个 player - // ... -} - -// ✅ 循环不变量外提 -var player = map.PlayerMap.GetPlayerById(playerId); -for (int i = 0; i < units.Count; i++) -{ - var grid = map.GridMap.GetGridDataByGid(units[i].GridId); - // ... -} -``` - -### 4.5 数学计算优化 - -```csharp -// ❌ 使用 Mathf(浮点 + Unity 函数调用开销) -int dist = Mathf.Max(Mathf.Abs(x1 - x2), Mathf.Abs(y1 - y2)); - -// ✅ 使用纯整数数学 -int dist = Math.Max(Math.Abs(x1 - x2), Math.Abs(y1 - y2)); - -// ❌ 重复计算距离 -for (int i = 0; i < targets.Count; i++) -{ - float dist = Vector2.Distance(pos, targets[i].Pos); // sqrt 开销 - if (dist < minDist) { ... } -} - -// ✅ 使用距离平方比较(避免 sqrt) -for (int i = 0; i < targets.Count; i++) -{ - float distSq = (pos - targets[i].Pos).sqrMagnitude; - if (distSq < minDistSq) { ... } -} -``` - ---- - -## 第五章 模块专项优化指南 - -### 5.1 数据层(TH1_Data) - -| 优化项 | 当前问题 | 优化方案 | 序列化影响 | 优先级 | -|-------|---------|---------|-----------|--------| -| `GetPlayerTerritoryGridIdSet` | 每次调用 new HashSet + new List | 玩家级缓存 + 脏标记 | `[MemoryPackIgnore]` 无影响 | P0 | -| `GetCityDataByTerritoryGridId` | O(城市数×领土数) 线性搜索,代码中标注 TODO | 维护格子→城市反向映射 | `[MemoryPackIgnore]` 无影响 | P0 | -| `GetAroundGridData` (4个重载) | 每次调用 new List/HashSet | 传入缓冲区模式 | 无 | P0 | -| `GetAroundGridIdList` | 每次调用 new List | 传入缓冲区模式 | 无 | P0 | -| `GetUnitDataListByPlayerId` | null 参数时隐式分配 | 强制传入缓冲区 | 无 | P1 | -| `GetCityDataListByPlayerId` | null 参数时隐式分配 | 强制传入缓冲区 | 无 | P1 | -| OnAfterMemoryPackDeserialize | 每次反序列化重建全部字典 | 延迟初始化(首次访问时重建) | 无 | P1 | - -### 5.2 技能系统(TH1_Logic/Skill) - -| 优化项 | 当前问题 | 优化方案 | 序列化影响 | 优先级 | -|-------|---------|---------|-----------|--------| -| SkillFactory | 每次创建技能用 Activator.CreateInstance | 编译时委托缓存 | 无 | P1 | -| 技能查找 | Skills 列表 O(n) 线性搜索 | `[MemoryPackIgnore]` 字典缓存 + 脏标记 | 无影响 | P1 | -| GetAttackAdditionParam | 遍历所有技能聚合加成值 | 缓存加成总值 + 技能变更时置脏 | `[MemoryPackIgnore]` 无影响 | P2 | -| OnTurnStart/End 技能回调 | 逐个遍历调用虚方法 | 仅遍历注册了该回调的技能(分类桶) | `[MemoryPackIgnore]` 无影响 | P2 | - -### 5.3 AI系统(TH1_Logic/AI) - -| 优化项 | 当前问题 | 优化方案 | 序列化影响 | 优先级 | -|-------|---------|---------|-----------|--------| -| AIActionGenerator.Init | 复制领土到新 List,冗余遍历 | 直接引用已有缓存,减少拷贝 | 无 | P0 | -| AILogic 主循环 | 循环内 new List\() | 预分配 NodeRecords 池 | 无 | P1 | -| 评分计算器 | 7个集合每次 Init 重建 | 复用集合 + Clear | 无 | P1 | -| AI 合法 Action 生成 | 生成大量临时 CommonActionParams | 对象池复用 | 无 | P2 | -| `validActions.Select().ToArray()` | LINQ 链式分配 | 预分配数组 + 手动填充 | 无 | P1 | - -### 5.4 逻辑模块(City/Unit/Player Logic) - -| 优化项 | 当前问题 | 优化方案 | 序列化影响 | 优先级 | -|-------|---------|---------|-----------|--------| -| 视野完整重算 | 每次移动重算全部视野 | 增量视野更新 | 无 | P1 | -| 外交好感度 O(n²) | 每帧/每回合全量重算 | 事件驱动增量更新 | 无 | P2 | -| 领土建筑升级遍历 | 遍历领土两遍(普通+学院/市场) | 单次遍历 + 分类处理 | 无 | P2 | -| BFS 城市连通性 | 每回合完整 BFS | 仅在道路/领土变化时重算 | 无 | P2 | -| 每回合资源计算 | 遍历所有领土格子 | 缓存产出总值 + 建筑变更时增量更新 | `[MemoryPackIgnore]` 无影响 | P3 | - -### 5.5 渲染与动画(TH1_Renderer / TH1_Anim) - -| 优化项 | 当前问题 | 优化方案 | 优先级 | -|-------|---------|---------|--------| -| Fragment 创建 | FragmentFactory 每次创建新对象 | 对象池复用 Fragment 实例 | P2 | -| UnitAtomAnim | 每次创建新动画对象 | 对象池复用 | P2 | -| 投射物 | ProjectileRenderer 每次实例化 | 已有 PrefabPool,确保使用 | P3 | - -### 5.6 事件系统(TH1_Core/Events) - -事件系统已使用 struct 事件,零 GC 分配,**保持现有设计,不需优化**。 - ---- - -## 第六章 优化验证清单 - -### 6.1 每次优化必须验证 - -- [ ] **功能等价**:优化前后方法的输入参数和返回结果完全一致 -- [ ] **单人游戏**:完整对局流程无报错、无异常行为 -- [ ] **多人游戏**:两个客户端对局,Action 同步正确、MapConfirm 校验通过 -- [ ] **存档兼容**:用优化前版本创建的存档能被优化后版本正常加载 -- [ ] **AI 行为**:AI 对局结果分布无异常偏移(相同种子应产生相同决策路径) - -### 6.2 性能验证方法 - -``` -1. Unity Profiler → 对比优化前后: - - GC Alloc (bytes/frame) - - 目标方法的 CPU 时间 (ms) - -2. Deep Profile 模式 → 确认热路径 GC 分配降为 0 - -3. 多人对局 → 确认 MapConfirm 哈希始终一致 - -4. 存档测试 → 加载旧存档 → 进行 10 回合 → 再次存档 → 加载验证 -``` - -### 6.3 回归测试范围 - -| 优化区域 | 最小测试范围 | -|---------|------------| -| 数据层(MapData/GridData) | 单人完整对局 + 多人2人对局 | -| 技能系统 | 包含全部8文明的对局 + 英雄技能触发 | -| AI系统 | 8人AI全自动对局(200回合) | -| 逻辑模块 | 征服他国 + 联盟 + 奇迹建造 | -| 视野/领土 | 大地图 + 地形多样 | - ---- - -## 附录A 已知GC热点清单 - -### 按严重程度排序 - -| # | 热点 | 位置 | 调用频率 | 分配类型 | 严重度 | -|---|------|------|---------|---------|--------| -| 1 | `GetAroundGridData` (4个重载) | GridData.cs | 每回合 100+ 次 | new List / new HashSet | 🔴 严重 | -| 2 | `GetPlayerTerritoryGridIdSet` | MapData.cs | 每回合 3-10 次 | new HashSet + new List | 🔴 严重 | -| 3 | `OnAfterMemoryPackDeserialize` (4处) | CityData/GridData/UnitData | 每次网络同步 | Dictionary 重建 | 🔴 严重 | -| 4 | `AIActionGenerator.Init` | AIActionGenerator.cs | 每个AI回合 | 列表拷贝 + HashSet | 🟡 中等 | -| 5 | `SkillFactory.GetSkillBySkillType` | SkillFactory.cs | 技能创建时 | Activator.CreateInstance | 🟡 中等 | -| 6 | `GetUnitDataListByPlayerId/ByCityId` | MapData.cs | 每回合多次 | null 参数时 new List | 🟡 中等 | -| 7 | AI主循环 NodeRecords | AILogic.cs | 每AI步 | new List\() | 🟡 中等 | -| 8 | `validActions.Select().ToArray()` | AILogic.cs | AI 推理时 | LINQ 链分配 | 🟡 中等 | -| 9 | `UpdateTerritoryAllBuildingLevel` | PlayerLogic.cs | 每回合 | 领土集合分配 x2 | 🟢 轻度 | -| 10 | `GetCityDataByTerritoryGridId` | MapData.cs | 按需调用 | 仅 CPU 开销(O(n²)) | 🟢 轻度 | - ---- - -## 附录B 禁止与允许操作速查表 - -### 🟢 允许的优化操作 - -| 操作 | 条件 | -|------|------| -| 新增 `[MemoryPackIgnore]` 字段作为缓存 | 配合延迟初始化和脏标记 | -| 将 `new List()` 改为传入缓冲区模式 | 调用方和被调用方同步修改 | -| 用 for 循环替代 LINQ | 保证结果等价 | -| 新增 static readonly 共享缓冲区 | 仅限单线程游戏逻辑 | -| 预计算偏移表/查找表 | 只读数据,初始化时构建 | -| 对象池复用 | 正确的获取/归还生命周期 | -| 增量计算替代全量计算 | 脏标记在所有修改路径上正确设置 | - -### 🔴 禁止的操作 - -| 操作 | 原因 | -|------|------| -| 向 `[MemoryPackable]` 类新增序列化字段 | 破坏存档兼容性和网络协议 | -| 删除或重命名已有的序列化字段 | 旧存档无法加载 | -| 修改方法的返回值类型或语义 | 违反 R-01 规则 | -| 在缓存中引入浮点累积误差 | 多人同步会出现不一致 | -| 使用多线程/异步优化游戏逻辑计算 | 破坏确定性 | -| 跳过 null 检查以"加速" | 引入崩溃风险 | -| 改变遍历顺序导致不同的执行结果 | 影响确定性(AI/战斗/随机数消费) | - -### 🟡 需审慎评估的操作 - -| 操作 | 评估点 | -|------|--------| -| 修改 `[MemoryPackOnDeserialized]` 逻辑 | 确保所有缓存在反序列化后正确重建 | -| static 共享缓冲区 | 确保单线程下无递归/重入问题 | -| 缓存失效策略 | 确保所有修改路径都调用 Invalidate | -| 移除冗余集合 | 确认没有外部引用依赖该集合 | -| 改变内部数据结构(如 List → Array) | 确认所有增删操作都适配 | - ---- - -> **文档版本**: 1.0 -> **最后更新**: 2026-04-16 -> **适用项目**: TH1 回合制4X策略游戏 diff --git a/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs b/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs new file mode 100644 index 000000000..a7ee4d1c9 --- /dev/null +++ b/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using UnityEditor; +using UnityEngine; + +public class ProfilerAutoInstrumentWindow : EditorWindow +{ + private const string DefaultFolder = "Assets/Scripts/TH1_Logic"; + private const string MarkerBegin = "// TH1_AUTO_PROFILE_BEGIN"; + private const string MarkerEnd = "// TH1_AUTO_PROFILE_END"; + private const string IndentUnit = " "; + + private static readonly HashSet NonMethodKeywords = new HashSet(StringComparer.Ordinal) + { + "if", "for", "foreach", "while", "switch", "catch", "lock", "using", "nameof", "typeof", "sizeof", "default" + }; + + private string _targetFolder = DefaultFolder; + private Vector2 _scrollPos; + private string _lastReport = string.Empty; + + private enum BlockKind + { + Other, + Type, + Method + } + + private struct BlockInfo + { + public BlockKind Kind; + public int MethodIndex; + } + + private struct MethodBlock + { + public int OpenBraceIndex; + public int CloseBraceIndex; + public string MethodName; + } + + private enum InstrumentResult + { + Changed, + AlreadyInstrumented, + NoMethod, + Failed + } + + [MenuItem("Tools/TH1/Profiler Auto Instrument")] + public static void ShowWindow() + { + var window = GetWindow("Profiler Auto Instrument"); + window.minSize = new Vector2(720, 420); + } + + private void OnGUI() + { + EditorGUILayout.LabelField("批量自动打点(Profiler Begin/End)", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "会在目录下所有 .cs 文件的方法体内自动插入 Profiler.BeginSample/EndSample。\n" + + "默认目录:Assets/Scripts/TH1_Logic\n" + + $"已插入的代码会带标记:{MarkerBegin}", + MessageType.Info); + + EditorGUILayout.Space(6); + EditorGUILayout.BeginHorizontal(); + _targetFolder = EditorGUILayout.TextField("目标目录", _targetFolder); + if (GUILayout.Button("选择...", GUILayout.Width(80))) + { + SelectFolder(); + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(8); + if (GUILayout.Button("一键自动打点", GUILayout.Height(34))) + { + RunAutoInstrument(); + } + + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("执行结果", EditorStyles.boldLabel); + _scrollPos = EditorGUILayout.BeginScrollView(_scrollPos); + EditorGUILayout.TextArea(string.IsNullOrEmpty(_lastReport) ? "尚未执行。" : _lastReport, GUILayout.ExpandHeight(true)); + EditorGUILayout.EndScrollView(); + } + + private void SelectFolder() + { + var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + var selected = EditorUtility.OpenFolderPanel("选择要自动打点的目录", projectRoot, string.Empty); + if (string.IsNullOrEmpty(selected)) + { + return; + } + + selected = Path.GetFullPath(selected); + if (!selected.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase)) + { + EditorUtility.DisplayDialog("目录无效", "请选择项目目录内的文件夹。", "确定"); + return; + } + + var relative = selected.Substring(projectRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + _targetFolder = relative.Replace("\\", "/"); + } + + private void RunAutoInstrument() + { + var folder = NormalizeFolderPath(_targetFolder); + if (!AssetDatabase.IsValidFolder(folder)) + { + EditorUtility.DisplayDialog("目录不存在", $"找不到目录:{folder}", "确定"); + return; + } + + var folderAbsolute = AssetPathToAbsolutePath(folder); + var files = Directory.GetFiles(folderAbsolute, "*.cs", SearchOption.AllDirectories); + + int changed = 0; + int already = 0; + int noMethod = 0; + int failed = 0; + var details = new StringBuilder(); + + for (int i = 0; i < files.Length; i++) + { + var fullPath = files[i]; + var assetPath = AbsolutePathToAssetPath(fullPath); + var result = InstrumentSingleFile(fullPath, assetPath, out var detail); + + switch (result) + { + case InstrumentResult.Changed: + changed++; + details.AppendLine($"[Changed] {assetPath}"); + break; + case InstrumentResult.AlreadyInstrumented: + already++; + break; + case InstrumentResult.NoMethod: + noMethod++; + break; + case InstrumentResult.Failed: + failed++; + details.AppendLine($"[Failed] {assetPath} -> {detail}"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + AssetDatabase.Refresh(); + + _lastReport = + $"目录:{folder}\n" + + $"扫描文件:{files.Length}\n" + + $"已打点:{changed}\n" + + $"已存在打点:{already}\n" + + $"无可打点方法:{noMethod}\n" + + $"失败:{failed}\n\n" + + details; + + Debug.Log($"[ProfilerAutoInstrument] 完成。Changed={changed}, Already={already}, NoMethod={noMethod}, Failed={failed}"); + } + + private static InstrumentResult InstrumentSingleFile(string fullPath, string assetPath, out string detail) + { + detail = string.Empty; + try + { + var rawBytes = File.ReadAllBytes(fullPath); + bool hasUtf8Bom = rawBytes.Length >= 3 && + rawBytes[0] == 0xEF && + rawBytes[1] == 0xBB && + rawBytes[2] == 0xBF; + + var original = File.ReadAllText(fullPath); + if (original.Contains(MarkerBegin)) + { + return InstrumentResult.AlreadyInstrumented; + } + + var methods = FindMethodBlocks(original); + if (methods.Count == 0) + { + return InstrumentResult.NoMethod; + } + + var instrumented = BuildInstrumentedContent(original, methods, assetPath); + if (instrumented == original) + { + return InstrumentResult.NoMethod; + } + + File.WriteAllText(fullPath, instrumented, new UTF8Encoding(hasUtf8Bom)); + return InstrumentResult.Changed; + } + catch (Exception ex) + { + detail = ex.Message; + return InstrumentResult.Failed; + } + } + + private static List FindMethodBlocks(string content) + { + var codeOnly = BuildCodeOnlyBuffer(content); + var methods = new List(); + var stack = new Stack(); + int typeDepth = 0; + int methodDepth = 0; + + for (int i = 0; i < codeOnly.Length; i++) + { + char c = codeOnly[i]; + if (c == '{') + { + var block = new BlockInfo { Kind = BlockKind.Other, MethodIndex = -1 }; + + if (LooksLikeTypeBlock(codeOnly, i)) + { + block.Kind = BlockKind.Type; + typeDepth++; + } + else if (typeDepth > 0 && methodDepth == 0 && TryResolveMethodName(codeOnly, i, out var methodName)) + { + var method = new MethodBlock + { + OpenBraceIndex = i, + CloseBraceIndex = -1, + MethodName = methodName + }; + methods.Add(method); + block.Kind = BlockKind.Method; + block.MethodIndex = methods.Count - 1; + methodDepth++; + } + + stack.Push(block); + continue; + } + + if (c != '}' || stack.Count == 0) + { + continue; + } + + var closedBlock = stack.Pop(); + if (closedBlock.Kind == BlockKind.Type) + { + typeDepth = Math.Max(0, typeDepth - 1); + } + else if (closedBlock.Kind == BlockKind.Method) + { + methodDepth = Math.Max(0, methodDepth - 1); + if (closedBlock.MethodIndex >= 0 && closedBlock.MethodIndex < methods.Count) + { + var method = methods[closedBlock.MethodIndex]; + method.CloseBraceIndex = i; + methods[closedBlock.MethodIndex] = method; + } + } + } + + methods.RemoveAll(m => m.CloseBraceIndex <= m.OpenBraceIndex); + return methods; + } + + private static string BuildInstrumentedContent(string content, List methods, string assetPath) + { + string newline = DetectNewLine(content); + var output = new StringBuilder(content.Length + methods.Count * 220); + int cursor = 0; + + for (int i = 0; i < methods.Count; i++) + { + var method = methods[i]; + if (method.OpenBraceIndex < cursor || method.CloseBraceIndex <= method.OpenBraceIndex) + { + continue; + } + + output.Append(content, cursor, method.OpenBraceIndex + 1 - cursor); + + string braceIndent = GetLineIndent(content, method.OpenBraceIndex); + string innerIndent = braceIndent + IndentUnit; + string sampleName = BuildSampleName(assetPath, method.MethodName); + + output.Append(newline); + output.Append(innerIndent).Append(MarkerBegin).Append(newline); + output.Append(innerIndent).Append("UnityEngine.Profiling.Profiler.BeginSample(\"").Append(sampleName).Append("\");").Append(newline); + output.Append(innerIndent).Append("try").Append(newline); + output.Append(innerIndent).Append("{").Append(newline); + + int bodyStart = method.OpenBraceIndex + 1; + output.Append(content, bodyStart, method.CloseBraceIndex - bodyStart); + + output.Append(newline); + output.Append(innerIndent).Append("}").Append(newline); + output.Append(innerIndent).Append("finally").Append(newline); + output.Append(innerIndent).Append("{").Append(newline); + output.Append(innerIndent).Append(IndentUnit).Append("UnityEngine.Profiling.Profiler.EndSample();").Append(newline); + output.Append(innerIndent).Append("}").Append(newline); + output.Append(innerIndent).Append(MarkerEnd).Append(newline); + output.Append(braceIndent); + + cursor = method.CloseBraceIndex; + } + + if (cursor < content.Length) + { + output.Append(content, cursor, content.Length - cursor); + } + + return output.ToString(); + } + + private static string BuildSampleName(string assetPath, string methodName) + { + var normalizedPath = assetPath.Replace("\\", "/"); + var sample = $"TH1Auto/{normalizedPath}::{methodName}"; + return sample.Replace("\\", "\\\\").Replace("\"", "\\\""); + } + + private static bool LooksLikeTypeBlock(string codeOnly, int braceIndex) + { + int headerStart = FindHeaderStart(codeOnly, braceIndex); + int length = braceIndex - headerStart; + if (length <= 0) + { + return false; + } + + string header = codeOnly.Substring(headerStart, length); + if (Regex.IsMatch(header, @"\bnamespace\b")) + { + return false; + } + + return Regex.IsMatch(header, @"\b(class|struct|interface|enum|record)\b"); + } + + private static bool TryResolveMethodName(string codeOnly, int braceIndex, out string methodName) + { + methodName = null; + + int segmentStart = FindHeaderStart(codeOnly, braceIndex); + int closeParen = FindPreviousChar(codeOnly, ')', braceIndex - 1); + while (closeParen >= 0 && closeParen >= segmentStart) + { + int openParen = FindMatchingOpenParen(codeOnly, closeParen); + if (openParen < 0 || openParen < segmentStart) + { + return false; + } + + int nameEnd = PreviousNonWhitespace(codeOnly, openParen - 1); + if (nameEnd < 0) + { + return false; + } + + int nameStart = nameEnd; + while (nameStart >= 0 && IsIdentifierChar(codeOnly[nameStart])) + { + nameStart--; + } + + nameStart++; + if (nameStart > nameEnd) + { + return false; + } + + methodName = codeOnly.Substring(nameStart, nameEnd - nameStart + 1); + if (string.IsNullOrEmpty(methodName)) + { + return false; + } + + if (methodName[0] == '@') + { + methodName = methodName.Substring(1); + } + + if (methodName == "base" || methodName == "this") + { + closeParen = FindPreviousChar(codeOnly, ')', openParen - 1); + continue; + } + + if (NonMethodKeywords.Contains(methodName)) + { + return false; + } + + int checkLength = braceIndex - segmentStart; + if (checkLength <= 0) + { + return false; + } + + string header = codeOnly.Substring(segmentStart, checkLength); + if (header.Contains("=>") || header.Contains(";") || header.Contains("=")) + { + return false; + } + + if (Regex.IsMatch(header, @"\b(return|throw)\b")) + { + return false; + } + + return true; + } + + return false; + } + + private static string BuildCodeOnlyBuffer(string content) + { + var chars = content.ToCharArray(); + bool inSingleLineComment = false; + bool inMultiLineComment = false; + bool inString = false; + bool inChar = false; + bool verbatimString = false; + + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + char next = i + 1 < chars.Length ? chars[i + 1] : '\0'; + + if (inSingleLineComment) + { + if (c != '\r' && c != '\n') + { + chars[i] = ' '; + } + else + { + inSingleLineComment = false; + } + + continue; + } + + if (inMultiLineComment) + { + chars[i] = ' '; + if (c == '*' && next == '/') + { + chars[i + 1] = ' '; + i++; + inMultiLineComment = false; + } + + continue; + } + + if (inString) + { + chars[i] = ' '; + if (verbatimString) + { + if (c == '"' && next == '"') + { + chars[i + 1] = ' '; + i++; + } + else if (c == '"') + { + inString = false; + verbatimString = false; + } + } + else + { + if (c == '\\' && i + 1 < chars.Length) + { + chars[i + 1] = ' '; + i++; + } + else if (c == '"') + { + inString = false; + } + } + + continue; + } + + if (inChar) + { + chars[i] = ' '; + if (c == '\\' && i + 1 < chars.Length) + { + chars[i + 1] = ' '; + i++; + } + else if (c == '\'') + { + inChar = false; + } + + continue; + } + + if (c == '/' && next == '/') + { + chars[i] = ' '; + chars[i + 1] = ' '; + i++; + inSingleLineComment = true; + continue; + } + + if (c == '/' && next == '*') + { + chars[i] = ' '; + chars[i + 1] = ' '; + i++; + inMultiLineComment = true; + continue; + } + + if (c == '"') + { + chars[i] = ' '; + inString = true; + verbatimString = + (i > 0 && chars[i - 1] == '@') || + (i > 1 && chars[i - 1] == '$' && chars[i - 2] == '@') || + (i > 1 && chars[i - 1] == '@' && chars[i - 2] == '$'); + continue; + } + + if (c == '\'') + { + chars[i] = ' '; + inChar = true; + } + } + + return new string(chars); + } + + private static int FindMatchingOpenParen(string codeOnly, int closeParenIndex) + { + int depth = 0; + for (int i = closeParenIndex; i >= 0; i--) + { + char c = codeOnly[i]; + if (c == ')') + { + depth++; + continue; + } + + if (c != '(') + { + continue; + } + + depth--; + if (depth == 0) + { + return i; + } + } + + return -1; + } + + private static int FindHeaderStart(string codeOnly, int index) + { + for (int i = index - 1; i >= 0; i--) + { + char c = codeOnly[i]; + if (c == ';' || c == '{' || c == '}') + { + return i + 1; + } + } + + return 0; + } + + private static int PreviousNonWhitespace(string text, int index) + { + for (int i = index; i >= 0; i--) + { + if (!char.IsWhiteSpace(text[i])) + { + return i; + } + } + + return -1; + } + + private static int FindPreviousChar(string text, char target, int startIndex) + { + for (int i = startIndex; i >= 0; i--) + { + char c = text[i]; + if (c == target) + { + return i; + } + + if (c == ';' || c == '{' || c == '}') + { + return -1; + } + } + + return -1; + } + + private static bool IsIdentifierChar(char c) + { + return char.IsLetterOrDigit(c) || c == '_' || c == '@'; + } + + private static string DetectNewLine(string content) + { + return content.Contains("\r\n") ? "\r\n" : "\n"; + } + + private static string GetLineIndent(string content, int index) + { + int lineStart = content.LastIndexOf('\n', Mathf.Clamp(index, 0, Math.Max(0, content.Length - 1))); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + int i = lineStart; + while (i < content.Length && (content[i] == ' ' || content[i] == '\t')) + { + i++; + } + + return content.Substring(lineStart, i - lineStart); + } + + private static string NormalizeFolderPath(string folder) + { + if (string.IsNullOrWhiteSpace(folder)) + { + return DefaultFolder; + } + + var normalized = folder.Replace("\\", "/").Trim(); + if (!normalized.StartsWith("Assets", StringComparison.OrdinalIgnoreCase)) + { + normalized = "Assets/" + normalized.TrimStart('/'); + } + + return normalized.TrimEnd('/'); + } + + private static string AssetPathToAbsolutePath(string assetPath) + { + return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath)); + } + + private static string AbsolutePathToAssetPath(string absolutePath) + { + var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + var full = Path.GetFullPath(absolutePath); + var relative = full.Substring(projectRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return relative.Replace("\\", "/"); + } +} diff --git a/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs.meta b/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs.meta new file mode 100644 index 000000000..1f825fcae --- /dev/null +++ b/Unity/Assets/Editor/ProfilerAutoInstrumentWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f2d8ec8a8d147f5abdf1f4b7f940a11 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: