【AI编码】【Buff/AI/战斗】P0 性能优化:Buff 系统 GC 优化、AI 列表复用、DamageRecordor 去除冗余分配

Made-with: Cursor

# Conflicts:
#	ProjectNLD/Assets/Code/Scripts/GamePlay_Dll/GamePlay.dll
main
刘涛 2026-03-10 14:36:16 +08:00
parent 4d74227f0b
commit 753d91e5e5
8 changed files with 1049 additions and 0 deletions

View File

@ -0,0 +1,51 @@
## P0-01: Buff 系统 — GetBuffsByType 复用 List ✅ 已完成
**优先级**: P0
**影响范围**: `GamePlay/Buff/UnitBuffManager.cs`
**问题类型**: GC 分配(热路径)
**状态**: ✅ 已完成
### 问题描述
`GetBuffsByType()` 每次调用都 `new List<BaseBuff>()`,如果在战斗中被频繁调用,会产生大量 GC。
### 问题代码位置
文件: `Assets/Code/Scripts/GamePlay/Buff/UnitBuffManager.cs`
行号: 147-160
```csharp
public List<BaseBuff> GetBuffsByType(EBuffType buffType)
{
var findList = new List<BaseBuff>(); // 每次分配新 List
for (var i = 0; i < totalBuffList.Count; ++i)
{
// ...
}
return findList;
}
```
### 修改方案
在类中增加一个可复用的 `List<BaseBuff>` 成员变量,每次调用时 `Clear()` 后重新填充。
**注意事项**
- 调用方不应长期持有返回值引用,因为下次调用会清空内容
- 如果存在嵌套调用(在遍历结果时再次调用此方法),需要特殊处理(调用方自行拷贝)
- 修改前需全局搜索 `GetBuffsByType` 的所有调用点,确认无嵌套使用场景
### 验证方法
1. 编译通过
2. 战斗中 Buff 功能正常(添加、触发、移除)
3. 可通过 Unity Profiler 对比修改前后 GC Alloc
### 完成记录
**修改内容**
1. 新增 `_reusableBuffList` 成员变量(构造函数中初始化,容量 `EXPECTED_MAX_BUFFS`
2. `GetBuffsByType()` 改为使用 `_reusableBuffList`,每次调用先 `Clear()` 再填充
3. 方法添加了 XML 注释,提示调用方不应长期持有返回引用
**调用点分析**:全局搜索确认 `GetBuffsByType` 当前无外部调用者,无嵌套调用风险。

View File

@ -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 委托分配。

View File

@ -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<BaseBuff> _needRemoveSet` 用于快速判重,同时保留 `_needRemoveList` 用于顺序遍历(在 `LogicUpdate` 中使用):
1. 新增字段 `private HashSet<BaseBuff> _needRemoveSet = new HashSet<BaseBuff>();`
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 两者。

View File

@ -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<BaseBuff> _totalBuffSet` 用于快速判断 Buff 是否在列表中:
1. 新增字段 `private HashSet<BaseBuff> _totalBuffSet = new HashSet<BaseBuff>();`
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))。

View File

@ -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<XXX> _tempKeys = new List<XXX>();
// 使用时:
_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<int> _tempKeys` 复用方案(`Clear()` + `AddRange(handleDict.Keys)` + `for` 循环)。`_ChangeSearchValue` 和 `_RevertSearchValue` 两处均已修改。使用方案B缓存 keys而非方案A因为循环内修改字典值会导致枚举器失效。

View File

@ -0,0 +1,40 @@
## P0-07: AI 系统 — UpdateBatchGroup 复用 List ✅ 已完成(部分)
**优先级**: P0
**影响范围**: `GamePlay/AI/AIManager.cs`
**问题类型**: GC 分配
**状态**: ✅ 已完成(仅复用 `allTrees``group` 被长期持有不可复用)
### 问题描述
`UpdateUnifiedBatchGroup()``UpdateBatchGroup()` 中频繁 `new List<BehaviorTree>()`
### 问题代码位置
文件: `Assets/Code/Scripts/GamePlay/AI/AIManager.cs`
- 行号 460`var allTrees = new List<BehaviorTree>();`
- 行号 514`var group = new List<BehaviorTree>();`
- 行号 547`var group = new List<BehaviorTree>();`
### 修改方案
将临时 List 改为类成员变量,调用前 `Clear()` 复用:
1. 新增类成员:
- `private List<BehaviorTree> _tempAllTrees = new List<BehaviorTree>();`
- `private List<List<BehaviorTree>> _tempBatchGroups`(根据分组逻辑的需要来定)
2. 每次调用 `UpdateUnifiedBatchGroup` / `UpdateBatchGroup` 时,先 Clear 再填充
**注意事项**
- 分组的 `group` 列表如果最终被存入其他数据结构长期持有,则不能复用,需保持 new仅对不被外部持有的临时变量做复用
- 需确认 `allTrees``group` 的生命周期
### 验证方法
1. 编译通过
2. AI 分批更新逻辑正常,行为树执行顺序不变
### 完成记录
`UpdateUnifiedBatchGroup` 中的 `var allTrees = new List<BehaviorTree>()` 替换为类成员 `_tempAllTrees`(每次调用 `Clear()` 复用)。`group` 列表因被 `_unifiedBehaviorTreeGroups.Add(group)` 长期持有,保持 `new`。`UpdateBatchGroup` 中的 `group` 同理保持不变。

View File

@ -0,0 +1,45 @@
## P0-11: DamageRecordor — 去除 ToArray 和 new double[0] ✅ 已完成(部分)
**优先级**: P0
**影响范围**: `GamePlay/Fight/Debug/DamageRecordor/DamageRecordor.cs`
**问题类型**: GC 分配
**状态**: ✅ 已完成(`new double[0]` → `Array.Empty<double>()``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<double>()`(零分配)
2. 如果 `FloatParams` 内容不会变化,可在 `configData` 中缓存一次 `ToArray()` 的结果,避免重复转换;否则保持 `ToArray()` 不变(因为调试模块影响有限)
### 验证方法
1. 编译通过
2. 伤害记录功能正常
### 完成记录
`FillBuffTriggerBeforData``FillBuffInitData` 两处的 `new double[0]` 替换为 `Array.Empty<double>()`(零 GC 分配)。`FloatParams.ToArray()` 保留不变,因为结果赋给不同的 log 对象实例,无法安全共享缓存。

View File

@ -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仅 allTreesgroup 被长期持有不可复用)
- [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<BaseBuff, int>` 维护索引映射需要在每次 `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<BaseBuff, int> _buffIndexMap` 记录每个 Buff 在 `totalBuffList` 中的索引:
1. 新增字段 `private Dictionary<BaseBuff, int> _buffIndexMap = new Dictionary<BaseBuff, int>();`
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<BehaviorTree>()` 获取行为树组件。
### 问题代码位置
文件: `Assets/Code/Scripts/GamePlay/AI/AIManager.cs`
位置1 — `UnRegisterAI` 行号 394
```csharp
var bt = go.GetComponent<BehaviorTree>();
```
位置2 — `EnableSingleAI` 行号 404
```csharp
var bt = go.GetComponent<BehaviorTree>();
```
### 修改方案
`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建筑血条
- 行号 3090NPC 血条
**销毁血条**Destroy
- 行号 3417`GameObjectUtils.Destroy(dicGoBlood[uid])`
### 修改方案
使用项目已有的 `ObjectPool<T>``Framework/ObjectPool.cs`)或创建一个 `Dictionary<prefab, Queue<GameObject>>` 简单对象池:
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<Camera>(gameObject.scene) ??
Camera.main ?? CameraManager.Instance.MainCamera;
```
### 修改方案
`DefaultCamera` 从计算属性改为带缓存的属性:
```csharp
private Camera _cachedCamera;
public Camera DefaultCamera
{
get
{
if (_cachedCamera == null)
{
_cachedCamera = FrameWorkUtils.GetSceneRootComponent<Camera>(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<long, int> _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<GameUnit>()` 并加入字典,导致空格子也占用内存。
### 问题代码位置
文件: `Assets/Code/Scripts/GamePlay/Area/AreaManager.cs`
行号: 421-425
```csharp
var newList = new List<GameUnit>();
_cellIndex2Units.Add(cellIndex, newList);
return newList;
```
### 修改方案
使用 `TryGetValue` 并返回一个静态空列表,不插入字典:
```csharp
private static readonly List<GameUnit> _emptyUnitList = new List<GameUnit>();
public List<GameUnit> 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<int>``IReadOnlyList<int>`,直接传 `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<RectTransform>()` / `GetComponent<PictorialItem>()` 调用。
### 问题代码位置
**PictorialBackground.cs**
- 行号 128`child.GetComponent<PictorialItem>()` (循环内)
- 行号 153`rectTransform = GetComponent<RectTransform>()`
**PictorialItem.cs**
- 行号 127, 189, 229, 256, 282`transform.parent?.GetComponent<RectTransform>()` (多处)
### 修改方案
`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. 路径变更时线段正确更新