From 753d91e5e5fb09d69033694aaf86cc131eadd9f2 Mon Sep 17 00:00:00 2001 From: liutao <821580467@qq.com> Date: Tue, 10 Mar 2026 14:36:16 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90AI=E7=BC=96=E7=A0=81=E3=80=91=E3=80=90?= =?UTF-8?q?Buff/AI/=E6=88=98=E6=96=97=E3=80=91P0=20=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9ABuff=20=E7=B3=BB=E7=BB=9F=20GC=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E3=80=81AI=20=E5=88=97=E8=A1=A8=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E3=80=81DamageRecordor=20=E5=8E=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor # Conflicts: # ProjectNLD/Assets/Code/Scripts/GamePlay_Dll/GamePlay.dll --- .../Finish/P0-01_GetBuffsByType复用List.md | 51 ++ .../Finish/P0-02_RemoveBuffById去除lambda.md | 44 ++ .../Finish/P0-03_needRemoveList改用HashSet.md | 42 + ...P0-04_totalBuffList_Contains改用HashSet.md | 45 ++ .../P0-06_BuffAddSearchValue去除KeysToList.md | 56 ++ .../Finish/P0-07_UpdateBatchGroup复用List.md | 40 + .../Finish/P0-11_DamageRecordor去除ToArray.md | 45 ++ ProjectNLD/AI_Gen_Doc/性能优化任务清单.md | 726 ++++++++++++++++++ 8 files changed, 1049 insertions(+) create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-01_GetBuffsByType复用List.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-02_RemoveBuffById去除lambda.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-03_needRemoveList改用HashSet.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-04_totalBuffList_Contains改用HashSet.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-06_BuffAddSearchValue去除KeysToList.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-07_UpdateBatchGroup复用List.md create mode 100644 ProjectNLD/AI_Gen_Doc/Finish/P0-11_DamageRecordor去除ToArray.md create mode 100644 ProjectNLD/AI_Gen_Doc/性能优化任务清单.md diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-01_GetBuffsByType复用List.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-01_GetBuffsByType复用List.md new file mode 100644 index 00000000000..70ef0bf655a --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-01_GetBuffsByType复用List.md @@ -0,0 +1,51 @@ +## P0-01: Buff 系统 — GetBuffsByType 复用 List ✅ 已完成 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/UnitBuffManager.cs` +**问题类型**: GC 分配(热路径) +**状态**: ✅ 已完成 + +### 问题描述 + +`GetBuffsByType()` 每次调用都 `new List()`,如果在战斗中被频繁调用,会产生大量 GC。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs` +行号: 147-160 + +```csharp +public List GetBuffsByType(EBuffType buffType) +{ + var findList = new List(); // 每次分配新 List + for (var i = 0; i < totalBuffList.Count; ++i) + { + // ... + } + return findList; +} +``` + +### 修改方案 + +在类中增加一个可复用的 `List` 成员变量,每次调用时 `Clear()` 后重新填充。 + +**注意事项**: +- 调用方不应长期持有返回值引用,因为下次调用会清空内容 +- 如果存在嵌套调用(在遍历结果时再次调用此方法),需要特殊处理(调用方自行拷贝) +- 修改前需全局搜索 `GetBuffsByType` 的所有调用点,确认无嵌套使用场景 + +### 验证方法 + +1. 编译通过 +2. 战斗中 Buff 功能正常(添加、触发、移除) +3. 可通过 Unity Profiler 对比修改前后 GC Alloc + +### 完成记录 + +**修改内容**: +1. 新增 `_reusableBuffList` 成员变量(构造函数中初始化,容量 `EXPECTED_MAX_BUFFS`) +2. `GetBuffsByType()` 改为使用 `_reusableBuffList`,每次调用先 `Clear()` 再填充 +3. 方法添加了 XML 注释,提示调用方不应长期持有返回引用 + +**调用点分析**:全局搜索确认 `GetBuffsByType` 当前无外部调用者,无嵌套调用风险。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-02_RemoveBuffById去除lambda.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-02_RemoveBuffById去除lambda.md new file mode 100644 index 00000000000..fe509cff332 --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-02_RemoveBuffById去除lambda.md @@ -0,0 +1,44 @@ +## P0-02: Buff 系统 — RemoveBuffById 去除 lambda 分配 ✅ 已完成 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/UnitBuffManager.cs` +**问题类型**: GC 分配(lambda 委托) +**状态**: ✅ 已完成 + +### 问题描述 + +`RemoveBuffById()` 使用 `List.Find(buff => ...)` 查找 Buff,每次调用都会分配一个新的委托对象。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs` +行号: 214 + +```csharp +var findBuff = totalBuffList.Find(buff => buff.configData.ID == buffId); +``` + +### 修改方案 + +将 `List.Find` + lambda 替换为普通 `for` 循环查找: + +```csharp +BaseBuff findBuff = null; +for (int i = 0; i < totalBuffList.Count; i++) +{ + if (totalBuffList[i].configData.ID == buffId) + { + findBuff = totalBuffList[i]; + break; + } +} +``` + +### 验证方法 + +1. 编译通过 +2. 通过 Buff ID 移除 Buff 功能正常 + +### 完成记录 + +将 `totalBuffList.Find(buff => buff.configData.ID == buffId)` 替换为普通 `for` 循环查找,消除 lambda 委托分配。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-03_needRemoveList改用HashSet.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-03_needRemoveList改用HashSet.md new file mode 100644 index 00000000000..b5361d523de --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-03_needRemoveList改用HashSet.md @@ -0,0 +1,42 @@ +## P0-03: Buff 系统 — _needRemoveList 改用 HashSet ✅ 已完成 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/UnitBuffManager.cs` +**问题类型**: O(n) 查找优化 +**状态**: ✅ 已完成 + +### 问题描述 + +`AddNeedRemoveBuff()` 中 `_needRemoveList.Contains(buff)` 为 O(n) 操作,在 Buff 频繁增删时性能较差。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs` +行号: 411-415 + +```csharp +if (_needRemoveList.Contains(buff)) +{ + return; +} +_needRemoveList.Add(buff); +``` + +### 修改方案 + +增加一个 `HashSet _needRemoveSet` 用于快速判重,同时保留 `_needRemoveList` 用于顺序遍历(在 `LogicUpdate` 中使用): + +1. 新增字段 `private HashSet _needRemoveSet = new HashSet();` +2. `Contains` 检查改为 `_needRemoveSet.Contains(buff)` +3. `Add` 时同时添加到 `_needRemoveSet` 和 `_needRemoveList` +4. `Clear` 时同时清空两者 +5. 在 `LogicUpdate` 方法(行号 417-446)的 `_needRemoveList.Clear()` 后面加上 `_needRemoveSet.Clear()` + +### 验证方法 + +1. 编译通过 +2. Buff 移除逻辑正常,无遗漏或重复移除 + +### 完成记录 + +新增 `_needRemoveSet` (HashSet),`AddNeedRemoveBuff` 中用 `_needRemoveSet.Add(buff)` 返回值判重(O(1)),同时保留 `_needRemoveList` 用于顺序遍历。`LogicUpdate` 和 `ClearAllBuff` 中同步 Clear 两者。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-04_totalBuffList_Contains改用HashSet.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-04_totalBuffList_Contains改用HashSet.md new file mode 100644 index 00000000000..4f473496535 --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-04_totalBuffList_Contains改用HashSet.md @@ -0,0 +1,45 @@ +## P0-04: Buff 系统 — totalBuffList.Contains 改用 HashSet ✅ 已完成 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/UnitBuffManager.cs` +**问题类型**: O(n) 查找优化 +**状态**: ✅ 已完成 + +### 问题描述 + +`RemoveBuff()` 中 `totalBuffList.Contains(buff)` 为 O(n) 操作。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs` +行号: 188-189 + +```csharp +public void RemoveBuff(BaseBuff buff, bool imdRemove = false) +{ + var isInList = totalBuffList.Contains(buff); + // ... +} +``` + +### 修改方案 + +新增一个 `HashSet _totalBuffSet` 用于快速判断 Buff 是否在列表中: + +1. 新增字段 `private HashSet _totalBuffSet = new HashSet();` +2. 在 `totalBuffList` 添加 Buff 的地方,同时 `_totalBuffSet.Add(buff)` +3. 在 `totalBuffList` 移除 Buff 的地方,同时 `_totalBuffSet.Remove(buff)` +4. 将 `totalBuffList.Contains(buff)` 替换为 `_totalBuffSet.Contains(buff)` + +**注意事项**: +- 需要全局搜索所有对 `totalBuffList` 的 `Add` 和 `Remove`(包括 `RemoveAt`、`Insert` 等),确保 `_totalBuffSet` 同步 +- 在 `Clear` / `Dispose` 等清理方法中也要同步清理 + +### 验证方法 + +1. 编译通过 +2. Buff 添加、移除、查询功能正常 + +### 完成记录 + +新增 `_totalBuffSet` (HashSet),在 `_AddBuff`/`_RemoveBuff`/`ClearAllBuff` 中与 `totalBuffList` 同步增删清空。`RemoveBuff` 中 `totalBuffList.Contains` 替换为 `_totalBuffSet.Contains`(O(1))。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-06_BuffAddSearchValue去除KeysToList.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-06_BuffAddSearchValue去除KeysToList.md new file mode 100644 index 00000000000..aefb4babcc4 --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-06_BuffAddSearchValue去除KeysToList.md @@ -0,0 +1,56 @@ +## P0-06: Buff 系统 — BuffAddSearchValue 去除 Keys.ToList() ✅ 已完成 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/Impl/BuffAddSearchValue.cs` +**问题类型**: GC 分配 +**状态**: ✅ 已完成 + +### 问题描述 + +`_ChangeSearchValue()` 和 `_RevertSearchValue()` 中 `handleDict.Keys.ToList()` 每次调用都分配新 List。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/Impl/BuffAddSearchValue.cs` + +位置1 — 行号 30-31: +```csharp +var keys = handleDict.Keys.ToList(); +foreach (var key in keys) +``` + +位置2 — 行号 66-67: +```csharp +var keys = handleDict.Keys.ToList(); +foreach (var key in keys) +``` + +### 修改方案 + +**方案A**(如果循环内不修改 `handleDict`):直接遍历 `handleDict.Keys`,去掉 `.ToList()`。 + +**方案B**(如果循环内会修改 `handleDict`):使用类级别的可复用 List 缓存 keys: + +```csharp +private static readonly List _tempKeys = new List(); + +// 使用时: +_tempKeys.Clear(); +_tempKeys.AddRange(handleDict.Keys); +foreach (var key in _tempKeys) +{ + // ... +} +``` + +**注意事项**: +- 需要先检查循环体内是否有对 `handleDict` 的增删操作,决定使用方案A还是方案B + +### 验证方法 + +1. 编译通过 +2. Buff 的 SearchValue 加成和还原功能正常 + +### 完成记录 + +移除 `using System.Linq`,`handleDict.Keys.ToList()` 替换为 `static readonly List _tempKeys` 复用方案(`Clear()` + `AddRange(handleDict.Keys)` + `for` 循环)。`_ChangeSearchValue` 和 `_RevertSearchValue` 两处均已修改。使用方案B(缓存 keys)而非方案A,因为循环内修改字典值会导致枚举器失效。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-07_UpdateBatchGroup复用List.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-07_UpdateBatchGroup复用List.md new file mode 100644 index 00000000000..d40089371df --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-07_UpdateBatchGroup复用List.md @@ -0,0 +1,40 @@ +## P0-07: AI 系统 — UpdateBatchGroup 复用 List ✅ 已完成(部分) + +**优先级**: P0(高) +**影响范围**: `GamePlay/AI/AIManager.cs` +**问题类型**: GC 分配 +**状态**: ✅ 已完成(仅复用 `allTrees`,`group` 被长期持有不可复用) + +### 问题描述 + +`UpdateUnifiedBatchGroup()` 和 `UpdateBatchGroup()` 中频繁 `new List()`。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/AI/AIManager.cs` + +- 行号 460:`var allTrees = new List();` +- 行号 514:`var group = new List();` +- 行号 547:`var group = new List();` + +### 修改方案 + +将临时 List 改为类成员变量,调用前 `Clear()` 复用: + +1. 新增类成员: + - `private List _tempAllTrees = new List();` + - `private List> _tempBatchGroups`(根据分组逻辑的需要来定) +2. 每次调用 `UpdateUnifiedBatchGroup` / `UpdateBatchGroup` 时,先 Clear 再填充 + +**注意事项**: +- 分组的 `group` 列表如果最终被存入其他数据结构长期持有,则不能复用,需保持 new;仅对不被外部持有的临时变量做复用 +- 需确认 `allTrees` 和 `group` 的生命周期 + +### 验证方法 + +1. 编译通过 +2. AI 分批更新逻辑正常,行为树执行顺序不变 + +### 完成记录 + +`UpdateUnifiedBatchGroup` 中的 `var allTrees = new List()` 替换为类成员 `_tempAllTrees`(每次调用 `Clear()` 复用)。`group` 列表因被 `_unifiedBehaviorTreeGroups.Add(group)` 长期持有,保持 `new`。`UpdateBatchGroup` 中的 `group` 同理保持不变。 diff --git a/ProjectNLD/AI_Gen_Doc/Finish/P0-11_DamageRecordor去除ToArray.md b/ProjectNLD/AI_Gen_Doc/Finish/P0-11_DamageRecordor去除ToArray.md new file mode 100644 index 00000000000..69eb7226e75 --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/Finish/P0-11_DamageRecordor去除ToArray.md @@ -0,0 +1,45 @@ +## P0-11: DamageRecordor — 去除 ToArray 和 new double[0] ✅ 已完成(部分) + +**优先级**: P0(高) +**影响范围**: `GamePlay/Fight/Debug/DamageRecordor/DamageRecordor.cs` +**问题类型**: GC 分配 +**状态**: ✅ 已完成(`new double[0]` → `Array.Empty()`,`ToArray()` 保留因为赋给不同 log 对象无法缓存) + +### 问题描述 + +每次 Buff 触发/初始化时 `FloatParams.ToArray()` 分配新数组,空分支使用 `new double[0]`。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Fight/Debug/DamageRecordor/DamageRecordor.cs` + +位置1 — 行号 394-401(`FillBuffTriggerBeforData`): +```csharp +if (buff.configData.FloatParams != null && buff.configData.FloatParams.Count > 0) +{ + triggerLog.BuffAuxiliaryValues = buff.configData.FloatParams.ToArray(); +} +else +{ + triggerLog.BuffAuxiliaryValues = new double[0]; +} +``` + +位置2 — 行号 517-524(`FillBuffInitData`): +```csharp +// 同上结构 +``` + +### 修改方案 + +1. `new double[0]` → `Array.Empty()`(零分配) +2. 如果 `FloatParams` 内容不会变化,可在 `configData` 中缓存一次 `ToArray()` 的结果,避免重复转换;否则保持 `ToArray()` 不变(因为调试模块影响有限) + +### 验证方法 + +1. 编译通过 +2. 伤害记录功能正常 + +### 完成记录 + +`FillBuffTriggerBeforData` 和 `FillBuffInitData` 两处的 `new double[0]` 替换为 `Array.Empty()`(零 GC 分配)。`FloatParams.ToArray()` 保留不变,因为结果赋给不同的 log 对象实例,无法安全共享缓存。 diff --git a/ProjectNLD/AI_Gen_Doc/性能优化任务清单.md b/ProjectNLD/AI_Gen_Doc/性能优化任务清单.md new file mode 100644 index 00000000000..1ef2993ac21 --- /dev/null +++ b/ProjectNLD/AI_Gen_Doc/性能优化任务清单.md @@ -0,0 +1,726 @@ +# NLD 项目性能优化任务清单 + +> 本文档按"最小可独立执行单元"组织,每个任务可以独立完成、独立测试、独立提交。 +> AI 执行时请严格按照每个任务的描述进行修改,不扩展额外功能。 +> 修改 GamePlay 目录下的代码后需通过 Unity 菜单 `Tools/文件与路径/转换为Dll`(快捷键 F1)编译生成 DLL。 +> 已完成任务的详细内容见 `AI_Gen_Doc/Finish/` 文件夹。 + +### AI 自动执行规则 + +1. **风险评估优先**:执行每个任务前必须先评估改动风险(如跨类改动、生命周期管理、大型重构、遍历中修改集合等)。有风险的任务标记为 ⏭️ 跳过,并注明跳过原因,等待与用户确认后再决定是否执行。 +2. **完成后归档**:任务完成后,将该任务的完整详情(问题描述、修改方案、完成记录等)拆分到 `AI_Gen_Doc/Finish/` 文件夹下的独立文件,本清单中仅保留目录条目标记。 + +--- + +## 目录 + +- [P0-01] ✅ Buff 系统 — GetBuffsByType 复用 List +- [P0-02] ✅ Buff 系统 — RemoveBuffById 去除 lambda 分配 +- [P0-03] ✅ Buff 系统 — _needRemoveList 改用 HashSet +- [P0-04] ✅ Buff 系统 — totalBuffList.Contains 改用 HashSet +- [P0-05] ⏭️ Buff 系统 — totalBuffList.IndexOf 优化(跳过:Dictionary 索引维护复杂度高,易引入 bug) +- [P0-06] ✅ Buff 系统 — BuffAddSearchValue 去除 Keys.ToList() +- [P0-07] ✅ AI 系统 — UpdateBatchGroup 复用 List(仅 allTrees,group 被长期持有不可复用) +- [P0-08] ⏭️ AI 系统 — 缓存 BehaviorTree 引用(跳过:需跨多个类改动,需确认组件生命周期) +- [P0-09] ⏭️ 战斗 UI — UIFightSceneController 血条对象池(跳过:大型重构,需拆分子任务单独实施) +- [P0-10] ⏭️ 战斗 UI — UIFightSceneController Buff 图标对象池(跳过:大型重构,需拆分子任务单独实施) +- [P0-11] ✅ DamageRecordor — 去除 ToArray 和 new double[0] +- [P1-01] 每帧检查降频 — _UpdateBlackArea +- [P1-02] 每帧检查降频 — _UpdateViewTransparent 脏标记 +- [P1-03] 每帧检查降频 — BuffAroundCheck 空间查询优化 +- [P1-04] Camera.main 缓存 +- [P1-05] AreaManager — 距离缓存 key 碰撞修复 +- [P1-06] AreaManager — GetUnitsByCellIndex 去除无效 new List +- [P1-07] AIEventBusManager — 去除 ToArray 和 new 对象 +- [P2-01] 画报系统 — GetComponent 缓存 +- [P2-02] 画报系统 — Debug.Log 条件编译 +- [P2-03] ConditionModifier — Keys.Contains 改为 ContainsKey +- [P2-04] StorageMgr — Keys 遍历优化 +- [P2-05] 非战斗 UI — CurrencyItem 等对象池化 +- [P2-06] RoadLineView — 路径线对象池化 + +--- + +## P0-05: Buff 系统 — totalBuffList.IndexOf 优化 ⏭️ 跳过 + +**优先级**: P0(高) +**影响范围**: `GamePlay/Buff/UnitBuffManager.cs` +**问题类型**: O(n) 查找优化 +**状态**: ⏭️ 跳过 +**跳过原因**: 用 `Dictionary` 维护索引映射需要在每次 `RemoveAt` 后更新所有后续元素索引(仍为 O(n)),且 `_RelocateIndex` 在 Trigger 回调链中频繁调用,字典与 List 的中间状态难以保持一致,引入 bug 风险高。当前 buff 数量预期 <=10,`IndexOf` 开销可接受。 + +### 问题描述 + +`_RemoveBuff()` 和 `_RelocateIndex()` 中使用 `totalBuffList.IndexOf(buff)` 进行 O(n) 查找。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs` + +位置1 — `_RemoveBuff` 行号 376-378: +```csharp +var index = totalBuffList.IndexOf(buff); +if (index < 0) +{ +``` + +位置2 — `_RelocateIndex` 行号 798-801: +```csharp +var newIndex = totalBuffList.IndexOf(buff); +i = newIndex < 0 ? i - 1 : newIndex; +``` + +### 修改方案 + +维护一个 `Dictionary _buffIndexMap` 记录每个 Buff 在 `totalBuffList` 中的索引: + +1. 新增字段 `private Dictionary _buffIndexMap = new Dictionary();` +2. 在 Buff 添加时记录索引 +3. 在 Buff 移除后重建受影响的索引(仅更新被移除位置之后的元素) +4. 将 `IndexOf` 替换为字典查找 + +**注意事项**: +- 此优化与 P0-04 可合并实施 +- 需要仔细处理 `_RelocateIndex` 中的索引重排逻辑 +- 如果 `totalBuffList` 有排序操作,索引映射也需同步更新 + +### 验证方法 + +1. 编译通过 +2. Buff 优先级排序、移除、重定位功能正常 + +--- + +## P0-08: AI 系统 — 缓存 BehaviorTree 引用 ⏭️ 跳过 + +**优先级**: P0(高) +**影响范围**: `GamePlay/AI/AIManager.cs` +**问题类型**: GetComponent 开销 +**状态**: ⏭️ 跳过 +**跳过原因**: 需要在 `GameUnit` 或其视图组件中新增字段缓存 `BehaviorTree` 引用,涉及跨多个类的改动。且需确认 `BehaviorTree` 组件是否会被动态添加/移除,当前 `EnableSingleAI`/`UnRegisterAI` 不是每帧调用,风险收益比不高。 + +### 问题描述 + +`EnableSingleAI()` 和 `UnRegisterAI()` 中使用 `go.GetComponent()` 获取行为树组件。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/AI/AIManager.cs` + +位置1 — `UnRegisterAI` 行号 394: +```csharp +var bt = go.GetComponent(); +``` + +位置2 — `EnableSingleAI` 行号 404: +```csharp +var bt = go.GetComponent(); +``` + +### 修改方案 + +在 `GameUnit` 或对应的单位数据结构中缓存 `BehaviorTree` 引用,在单位创建时获取一次,后续直接使用缓存引用。 + +具体步骤: +1. 检查 `GameUnit` 或其视图组件是否有合适的字段存储 `BehaviorTree` +2. 在单位初始化时(如 `Init` 或 `OnCreate`)获取并缓存 +3. 在 `AIManager` 中改为从缓存读取 + +**注意事项**: +- `EnableSingleAI` 和 `UnRegisterAI` 不是每帧调用,但在大量单位进出战场时可能集中调用 +- 需确认 `BehaviorTree` 组件是否可能被动态添加/移除 + +### 验证方法 + +1. 编译通过 +2. AI 启用/禁用功能正常 + +--- + +## P0-09: 战斗 UI — UIFightSceneController 血条对象池 ⏭️ 跳过 + +**优先级**: P0(高) +**影响范围**: `GamePlay/UI/LevelFight/FightSceneUI/UIFightSceneController.cs` +**问题类型**: Instantiate/Destroy 开销 +**状态**: ⏭️ 跳过 +**跳过原因**: 大型重构任务,涉及 4 处不同类型血条的 Instantiate 和 Destroy 改造,需要按 prefab 类型分池、处理回收时的状态重置。任务清单也标注"此任务较大,可拆分为子任务逐步实施",不适合作为原子操作一次完成。 + +### 问题描述 + +战斗中单位血条频繁 `Instantiate` / `Destroy`,在大量单位进出战场时会产生明显的性能峰值。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/UI/LevelFight/FightSceneUI/UIFightSceneController.cs` + +**创建血条**(Instantiate): +- 行号 2939:角色/载具血条 +- 行号 3002:召唤物血条 +- 行号 3037:建筑血条 +- 行号 3090:NPC 血条 + +**销毁血条**(Destroy): +- 行号 3417:`GameObjectUtils.Destroy(dicGoBlood[uid])` + +### 修改方案 + +使用项目已有的 `ObjectPool`(`Framework/ObjectPool.cs`)或创建一个 `Dictionary>` 简单对象池: + +1. 创建血条对象池,按血条类型(角色、召唤物、建筑、NPC)分池 +2. 创建时从池中取出,设置 `SetActive(true)` 并重置数据 +3. 销毁时回收到池中,设置 `SetActive(false)` +4. 在战斗结束/场景切换时清空对象池 + +**注意事项**: +- 血条创建时有不同的 prefab,需按 prefab 类型分池 +- 回收时需重置血条状态(血量、颜色、Buff 显示等) +- 此任务较大,可拆分为子任务逐步实施 + +### 验证方法 + +1. 编译通过 +2. 单位出场/退场时血条显示正常 +3. 血条数据(血量、名称等)正确刷新 +4. 对比修改前后 Instantiate/Destroy 调用次数 + +--- + +## P0-10: 战斗 UI — UIFightSceneController Buff 图标对象池 ⏭️ 跳过 + +**优先级**: P0(高) +**影响范围**: `GamePlay/UI/LevelFight/FightSceneUI/UIFightSceneController.cs` +**问题类型**: Instantiate/Destroy 开销 +**状态**: ⏭️ 跳过 +**跳过原因**: 与 P0-09 类似,属于大型 UI 重构任务,需与血条对象池化统一规划实施。 + +### 问题描述 + +战斗中 Buff 图标频繁 `Instantiate` / `Destroy`。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/UI/LevelFight/FightSceneUI/UIFightSceneController.cs` + +- 行号 3542:创建 Buff 图标 `GameObjectUtils.Instantiate(prefab, _goMap.transform)` +- 行号 3534:销毁旧 Buff 图标 `GameObjectUtils.Destroy(curGo)` +- 行号 3423:销毁 Buff 图标 `GameObjectUtils.Destroy(dicGoShowBuff[uid])` + +### 修改方案 + +与 P0-09 类似,使用对象池管理 Buff 图标: + +1. 创建 Buff 图标对象池 +2. 创建时从池取出,销毁时回收 +3. 回收时重置图标状态 + +### 验证方法 + +1. 编译通过 +2. Buff 图标显示和隐藏正常 + +--- + +## P1-01: 每帧检查降频 — _UpdateBlackArea + +**优先级**: P1(中) +**影响范围**: `GamePlay/Unit/GameUnit.cs` +**问题类型**: 每帧重复计算 + +### 问题描述 + +`_UpdateBlackArea()` 每帧为**每个敌方单位**调用 `BlackAreaManager.instance.CheckIsInLightArea()`,开销与敌方单位数量成正比。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Unit/GameUnit.cs` +行号: 358-365 + +```csharp +private void _UpdateBlackArea() +{ + if (commonData.troopId == ETroopsId.Self) + { + return; + } + statusData.isInBlackArea = !BlackAreaManager.instance.CheckIsInLightArea(transData.cellIndex); +} +``` + +### 修改方案 + +降低检查频率,例如每 5-10 帧检查一次,或仅在单位移动(cellIndex 变化)时检查: + +```csharp +private int _lastBlackAreaCheckCell = -1; + +private void _UpdateBlackArea() +{ + if (commonData.troopId == ETroopsId.Self) return; + if (transData.cellIndex == _lastBlackAreaCheckCell) return; + _lastBlackAreaCheckCell = transData.cellIndex; + statusData.isInBlackArea = !BlackAreaManager.instance.CheckIsInLightArea(transData.cellIndex); +} +``` + +**注意事项**: +- 需要确认 `CheckIsInLightArea` 的结果是否可能在单位静止时变化(如光照区域本身在变化) +- 如果光照区域会动态变化,则需要事件通知或定时刷新 + +### 验证方法 + +1. 编译通过 +2. 敌方单位在黑区/光照区域的显隐切换正常 +3. 无明显延迟感 + +--- + +## P1-02: 每帧检查降频 — _UpdateViewTransparent 脏标记 + +**优先级**: P1(中) +**影响范围**: `GamePlay/Unit/GameUnit.cs` +**问题类型**: 每帧重复计算 + +### 问题描述 + +`_UpdateViewTransparent()` 每帧执行,含多层 if-else 条件判断和属性访问,但透明度状态不会每帧变化。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Unit/GameUnit.cs` +行号: 341-379 + +### 修改方案 + +引入脏标记机制,仅在相关状态变化时重新计算透明度: + +1. 新增 `_transparentDirty` 标记 +2. 在影响透明度的状态发生变化时(如 `isForceHide`、`needSkillCover`、`belongBuild` 变化等)设置标记为 true +3. `_UpdateViewTransparent()` 中检查标记,无变化时直接返回 + +**注意事项**: +- 需要找到所有修改 `isForceHide`、`needSkillCover`、`belongBuild` 的代码位置,在其中设置脏标记 +- 此优化需要对透明度相关的状态变化有全面了解 + +### 验证方法 + +1. 编译通过 +2. 单位透明度切换(技能覆盖、强制隐藏、建筑归属等)正常 + +--- + +## P1-03: 每帧检查降频 — BuffAroundCheck 空间查询优化 + +**优先级**: P1(中) +**影响范围**: `GamePlay/Buff/Impl/AroundCheck/BuffAroundCheckEnemyForDefense.cs` 及同目录下类似文件 +**问题类型**: 每帧重复计算 + 全场遍历 + +### 问题描述 + +`_Check()` 方法遍历 `GameUnitManager.instance.infightUnits.unitList` 全部单位来查找范围内的敌人,在大量单位场景下开销较大。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Buff/Impl/AroundCheck/BuffAroundCheckEnemyForDefense.cs` +行号: 56-96 + +同类文件: +- `BuffAroundCheckEnemyForMoraleDamageScale.cs` +- `BuffDamageScaleWithAroundCheck.cs` + +### 修改方案 + +**方案A**(简单):利用已有的 `AreaManager.GetUnitsByCellIndex()` 做近距离格子查询,而非遍历全场单位: + +1. 通过 `AreaManager.GetCellIndexsAround()` 获取周围格子 +2. 对每个格子调用 `GetUnitsByCellIndex()` 获取单位 +3. 仅对范围内格子上的单位做距离判断 + +**方案B**(保守):保持全场遍历,但提前过滤已死亡和同阵营单位,减少 `GetDistance` 调用。 + +**注意事项**: +- 需要确认 `_checkRange` 的范围与格子距离的换算关系 +- 类似的 AroundCheck 类需要统一修改 + +### 验证方法 + +1. 编译通过 +2. 防御加成/士气伤害倍率等 Buff 效果数值正确 + +--- + +## P1-04: Camera.main 缓存 + +**优先级**: P1(中) +**影响范围**: `GameWrapper/Camera/CameraStand.cs` +**问题类型**: Unity API 性能陷阱 + +### 问题描述 + +`DefaultCamera` 属性中使用 `Camera.main`,每次访问都会执行 `FindGameObjectsWithTag("MainCamera")`。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GameWrapper/Camera/CameraStand.cs` +行号: 48-49 + +```csharp +public Camera DefaultCamera => FrameWorkUtils.GetSceneRootComponent(gameObject.scene) ?? + Camera.main ?? CameraManager.Instance.MainCamera; +``` + +### 修改方案 + +将 `DefaultCamera` 从计算属性改为带缓存的属性: + +```csharp +private Camera _cachedCamera; + +public Camera DefaultCamera +{ + get + { + if (_cachedCamera == null) + { + _cachedCamera = FrameWorkUtils.GetSceneRootComponent(gameObject.scene) + ?? Camera.main + ?? CameraManager.Instance.MainCamera; + } + return _cachedCamera; + } +} +``` + +**注意事项**: +- 如果相机可能被销毁重建,需提供一个 `InvalidateCameraCache()` 方法 +- 检查 `DefaultCamera` 是否在场景切换后仍需重新获取 + +### 验证方法 + +1. 编译通过 +2. 相机功能正常 + +--- + +## P1-05: AreaManager — 距离缓存 key 碰撞修复 + +**优先级**: P1(中) +**影响范围**: `GamePlay/Area/AreaManager.cs` +**问题类型**: 逻辑缺陷 + 潜在性能问题 + +### 问题描述 + +`_distanceCache` 使用 `cellIndex1 * 10000 + cellIndex2` 作为 key,当 cellIndex 超过 10000 时会产生 key 碰撞,且缓存无限增长。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Area/AreaManager.cs` +行号: 298-310 + +```csharp +var key = cellIndex1 * 10000 + cellIndex2; +if (_distanceCache.TryGetValue(key, out var distance)) + return distance; +// ... +_distanceCache.Add(key, newDistance); +var invertedKey = cellIndex2 * 10000 + cellIndex1; +_distanceCache.Add(invertedKey, newDistance); +``` + +### 修改方案 + +将 key 改为 `long` 类型或 `(int, int)` 元组以避免碰撞: + +```csharp +private Dictionary _distanceCache; + +// 使用时: +long key = ((long)cellIndex1 << 32) | (uint)cellIndex2; +``` + +**注意事项**: +- 先确认项目中 cellIndex 的最大值,如果确定小于 10000 则可暂不修改 +- 缓存无限增长问题可通过在关卡结束时清空来解决 + +### 验证方法 + +1. 编译通过 +2. 距离计算结果正确 + +--- + +## P1-06: AreaManager — GetUnitsByCellIndex 去除无效 new List + +**优先级**: P1(中) +**影响范围**: `GamePlay/Area/AreaManager.cs` +**问题类型**: GC 分配 + +### 问题描述 + +`GetUnitsByCellIndex()` 在 `_cellIndex2Units` 无对应 key 时会 `new List()` 并加入字典,导致空格子也占用内存。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Area/AreaManager.cs` +行号: 421-425 + +```csharp +var newList = new List(); +_cellIndex2Units.Add(cellIndex, newList); +return newList; +``` + +### 修改方案 + +使用 `TryGetValue` 并返回一个静态空列表,不插入字典: + +```csharp +private static readonly List _emptyUnitList = new List(); + +public List GetUnitsByCellIndex(int cellIndex) +{ + if (_cellIndex2Units.TryGetValue(cellIndex, out var list)) + return list; + return _emptyUnitList; +} +``` + +**注意事项**: +- 需确认调用方是否会向返回的列表中添加元素(如果会,则不能返回共享空列表) +- 如果调用方需要向空列表中添加单位,应在单位注册到格子时创建列表 + +### 验证方法 + +1. 编译通过 +2. 格子单位查询功能正常 + +--- + +## P1-07: AIEventBusManager — 去除 ToArray 和 new 对象 + +**优先级**: P1(中) +**影响范围**: `GamePlay/Level/AIEventBus/AIEventBusManager.cs` +**问题类型**: GC 分配 + +### 问题描述 + +- `AddCacheSkill` 中 `listCellIndex.ToArray()` 分配新数组 +- `CachePlayerSkill.PreExecute` 中 `new CommanderSkillParams()` 每次创建新对象 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Level/AIEventBus/AIEventBusManager.cs` + +- 行号 317:`AddCacheSkill(skill, false, true, listCellIndex.ToArray());` +- 行号 86-90:`CommanderSkill.PreExecute(new CommanderSkillParams() { ... });` + +### 修改方案 + +1. `listCellIndex.ToArray()` → 如果接收方支持 `IList` 或 `IReadOnlyList`,直接传 `listCellIndex` +2. `CommanderSkillParams` → 如果是 class,考虑改为 struct 或使用对象池;如果已是 struct,则无需修改 + +**注意事项**: +- 需确认 `AddCacheSkill` 的参数类型和后续用法 +- 需确认 `CommanderSkillParams` 是 class 还是 struct + +### 验证方法 + +1. 编译通过 +2. AI 指挥官技能释放正常 + +--- + +## P2-01: 画报系统 — GetComponent 缓存 + +**优先级**: P2(低) +**影响范围**: `Framework/UI/Pictorial/PictorialBackground.cs`、`PictorialItem.cs` +**问题类型**: GetComponent 开销 + +### 问题描述 + +`PictorialBackground.Update()` 和 `PictorialItem.Update()` 中多处 `GetComponent()` / `GetComponent()` 调用。 + +### 问题代码位置 + +**PictorialBackground.cs**: +- 行号 128:`child.GetComponent()` (循环内) +- 行号 153:`rectTransform = GetComponent()` + +**PictorialItem.cs**: +- 行号 127, 189, 229, 256, 282:`transform.parent?.GetComponent()` (多处) + +### 修改方案 + +在 `Awake` 或 `OnEnable` 中缓存这些组件引用到字段中。 + +### 验证方法 + +1. 编译通过 +2. 画报系统显示正常 + +--- + +## P2-02: 画报系统 — Debug.Log 条件编译 + +**优先级**: P2(低) +**影响范围**: `Framework/UI/Pictorial/PictorialBackground.cs`、`PictorialItem.cs` +**问题类型**: 字符串 GC 分配 + +### 问题描述 + +`Update()` 中大量 `Debug.Log($"[画报系统] ...")` 调用,在发布版本中也会执行字符串拼接。 + +### 问题代码位置 + +**PictorialBackground.cs** — Update 中的 Debug.Log: +- 行号 95, 105, 139, 164, 182, 202, 211, 213 + +**PictorialItem.cs** — Update 中的 Debug.Log: +- 行号 117, 133, 148, 154, 192, 203, 212 + +### 修改方案 + +用 `#if UNITY_EDITOR` 包裹这些调试日志,或使用 `[Conditional("UNITY_EDITOR")]` 标记的日志方法。 + +### 验证方法 + +1. 编译通过 +2. 编辑器中日志仍然输出 +3. 发布版本中无此类日志 + +--- + +## P2-03: ConditionModifier — Keys.Contains 改为 ContainsKey + +**优先级**: P2(低) +**影响范围**: `Framework/Condition/Util/ConditionModifier.cs` +**问题类型**: 不必要的 GC 分配 + +### 问题描述 + +`conditionNameCfg.Keys.Contains(type)` 会分配枚举器,而 `ContainsKey` 是 O(1) 且无 GC。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/Framework/Condition/Util/ConditionModifier.cs` +行号: 155 + +```csharp +if (ConditionUtil.conditionNameCfg.Keys.Contains(type)) +``` + +### 修改方案 + +```csharp +if (ConditionUtil.conditionNameCfg.ContainsKey(type)) +``` + +### 验证方法 + +1. 编译通过 +2. 条件判断功能正常 + +--- + +## P2-04: StorageMgr — Keys 遍历优化 + +**优先级**: P2(低) +**影响范围**: `Framework/Storage/StorageMgr.cs` +**问题类型**: 不必要的 GC 分配 + +### 问题描述 + +`foreach (var key in localStorageMap.Keys)` 遍历 `Keys` 集合会分配枚举器。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/Framework/Storage/StorageMgr.cs` +行号: 193-196 + +```csharp +foreach (var key in localStorageMap.Keys) +{ + dataIOBase.LoadToDic(key, dataIOBase.Load(key), ref localStorageMap); +} +``` + +### 修改方案 + +改为遍历 Dictionary 本体: + +```csharp +foreach (var kv in localStorageMap) +{ + dataIOBase.LoadToDic(kv.Key, dataIOBase.Load(kv.Key), ref localStorageMap); +} +``` + +**注意事项**: +- 如果 `LoadToDic` 会修改 `localStorageMap`,则需要先将 keys 缓存到列表中再遍历(但应使用复用列表,非 new List) + +### 验证方法 + +1. 编译通过 +2. 存储加载功能正常 + +--- + +## P2-05: 非战斗 UI — CurrencyItem 等对象池化 + +**优先级**: P2(低) +**影响范围**: 多个 UI 控制器 +**问题类型**: Instantiate/Destroy 开销 + +### 问题描述 + +以下 UI 列表项频繁 `Instantiate` / `Destroy`,但未使用对象池: + +| 文件 | 说明 | +|------|------| +| `GamePlay/UI/Common/CurrencyItem.cs` 行号 60, 108 | 货币列表项 | +| `GamePlay/UI/Character/UIAdvanceNewController.cs` 行号 257-260, 443-460 | 属性/技能文本 | +| `GamePlay/UI/Character/UI_CultivateChangeController.cs` 行号 1153-1175 | 职业列表项 | +| `GamePlay/UI/UI_VehiclePartTreeDetailController.cs` 行号 802-803 | 列表项 | + +### 修改方案 + +使用 `Framework/ObjectPool.cs` 中的通用对象池对这些 UI 列表项进行池化管理。 + +每个文件可独立改造,建议按以下顺序: +1. `CurrencyItem.cs`(最简单,改造成本低) +2. `UIAdvanceNewController.cs` +3. 其他 + +### 验证方法 + +1. 编译通过 +2. 对应界面列表显示正常 +3. 界面开关时无明显内存增长 + +--- + +## P2-06: RoadLineView — 路径线对象池化 + +**优先级**: P2(低) +**影响范围**: `GamePlay/Area/Path/RoadLineView.cs` +**问题类型**: Instantiate/Destroy 开销 + +### 问题描述 + +路径线在创建/销毁时频繁 `Instantiate` / `Destroy`。 + +### 问题代码位置 + +文件: `Assets/Code/Scripts/GamePlay/Area/Path/RoadLineView.cs` + +**Instantiate**:行号 59, 69, 105 +**Destroy**:行号 80, 86, 92 + +### 修改方案 + +使用对象池管理路径线 GameObject,回收时 `SetActive(false)`,取出时 `SetActive(true)` 并重置位置/旋转。 + +### 验证方法 + +1. 编译通过 +2. 路径线显示和隐藏正常 +3. 路径变更时线段正确更新