22 KiB
NLD 项目性能优化任务清单
本文档按"最小可独立执行单元"组织,每个任务可以独立完成、独立测试、独立提交。 AI 执行时请严格按照每个任务的描述进行修改,不扩展额外功能。 修改 GamePlay 目录下的代码后需通过 Unity 菜单
Tools/文件与路径/转换为Dll(快捷键 F1)编译生成 DLL。 已完成任务的详细内容见AI_Gen_Doc/Finish/文件夹。
AI 自动执行规则
- 风险评估优先:执行每个任务前必须先评估改动风险(如跨类改动、生命周期管理、大型重构、遍历中修改集合等)。有风险的任务标记为 ⏭️ 跳过,并注明跳过原因,等待与用户确认后再决定是否执行。
- 完成后归档:任务完成后,将该任务的完整详情(问题描述、修改方案、完成记录等)拆分到
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<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:
var index = totalBuffList.IndexOf(buff);
if (index < 0)
{
位置2 — _RelocateIndex 行号 798-801:
var newIndex = totalBuffList.IndexOf(buff);
i = newIndex < 0 ? i - 1 : newIndex;
修改方案
维护一个 Dictionary<BaseBuff, int> _buffIndexMap 记录每个 Buff 在 totalBuffList 中的索引:
- 新增字段
private Dictionary<BaseBuff, int> _buffIndexMap = new Dictionary<BaseBuff, int>(); - 在 Buff 添加时记录索引
- 在 Buff 移除后重建受影响的索引(仅更新被移除位置之后的元素)
- 将
IndexOf替换为字典查找
注意事项:
- 此优化与 P0-04 可合并实施
- 需要仔细处理
_RelocateIndex中的索引重排逻辑 - 如果
totalBuffList有排序操作,索引映射也需同步更新
验证方法
- 编译通过
- 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:
var bt = go.GetComponent<BehaviorTree>();
位置2 — EnableSingleAI 行号 404:
var bt = go.GetComponent<BehaviorTree>();
修改方案
在 GameUnit 或对应的单位数据结构中缓存 BehaviorTree 引用,在单位创建时获取一次,后续直接使用缓存引用。
具体步骤:
- 检查
GameUnit或其视图组件是否有合适的字段存储BehaviorTree - 在单位初始化时(如
Init或OnCreate)获取并缓存 - 在
AIManager中改为从缓存读取
注意事项:
EnableSingleAI和UnRegisterAI不是每帧调用,但在大量单位进出战场时可能集中调用- 需确认
BehaviorTree组件是否可能被动态添加/移除
验证方法
- 编译通过
- 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<T>(Framework/ObjectPool.cs)或创建一个 Dictionary<prefab, Queue<GameObject>> 简单对象池:
- 创建血条对象池,按血条类型(角色、召唤物、建筑、NPC)分池
- 创建时从池中取出,设置
SetActive(true)并重置数据 - 销毁时回收到池中,设置
SetActive(false) - 在战斗结束/场景切换时清空对象池
注意事项:
- 血条创建时有不同的 prefab,需按 prefab 类型分池
- 回收时需重置血条状态(血量、颜色、Buff 显示等)
- 此任务较大,可拆分为子任务逐步实施
验证方法
- 编译通过
- 单位出场/退场时血条显示正常
- 血条数据(血量、名称等)正确刷新
- 对比修改前后 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 图标:
- 创建 Buff 图标对象池
- 创建时从池取出,销毁时回收
- 回收时重置图标状态
验证方法
- 编译通过
- Buff 图标显示和隐藏正常
P1-01: 每帧检查降频 — _UpdateBlackArea
优先级: P1(中)
影响范围: GamePlay/Unit/GameUnit.cs
问题类型: 每帧重复计算
问题描述
_UpdateBlackArea() 每帧为每个敌方单位调用 BlackAreaManager.instance.CheckIsInLightArea(),开销与敌方单位数量成正比。
问题代码位置
文件: Assets/Code/Scripts/GamePlay/Unit/GameUnit.cs
行号: 358-365
private void _UpdateBlackArea()
{
if (commonData.troopId == ETroopsId.Self)
{
return;
}
statusData.isInBlackArea = !BlackAreaManager.instance.CheckIsInLightArea(transData.cellIndex);
}
修改方案
降低检查频率,例如每 5-10 帧检查一次,或仅在单位移动(cellIndex 变化)时检查:
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的结果是否可能在单位静止时变化(如光照区域本身在变化) - 如果光照区域会动态变化,则需要事件通知或定时刷新
验证方法
- 编译通过
- 敌方单位在黑区/光照区域的显隐切换正常
- 无明显延迟感
P1-02: 每帧检查降频 — _UpdateViewTransparent 脏标记
优先级: P1(中)
影响范围: GamePlay/Unit/GameUnit.cs
问题类型: 每帧重复计算
问题描述
_UpdateViewTransparent() 每帧执行,含多层 if-else 条件判断和属性访问,但透明度状态不会每帧变化。
问题代码位置
文件: Assets/Code/Scripts/GamePlay/Unit/GameUnit.cs
行号: 341-379
修改方案
引入脏标记机制,仅在相关状态变化时重新计算透明度:
- 新增
_transparentDirty标记 - 在影响透明度的状态发生变化时(如
isForceHide、needSkillCover、belongBuild变化等)设置标记为 true _UpdateViewTransparent()中检查标记,无变化时直接返回
注意事项:
- 需要找到所有修改
isForceHide、needSkillCover、belongBuild的代码位置,在其中设置脏标记 - 此优化需要对透明度相关的状态变化有全面了解
验证方法
- 编译通过
- 单位透明度切换(技能覆盖、强制隐藏、建筑归属等)正常
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.csBuffDamageScaleWithAroundCheck.cs
修改方案
方案A(简单):利用已有的 AreaManager.GetUnitsByCellIndex() 做近距离格子查询,而非遍历全场单位:
- 通过
AreaManager.GetCellIndexsAround()获取周围格子 - 对每个格子调用
GetUnitsByCellIndex()获取单位 - 仅对范围内格子上的单位做距离判断
方案B(保守):保持全场遍历,但提前过滤已死亡和同阵营单位,减少 GetDistance 调用。
注意事项:
- 需要确认
_checkRange的范围与格子距离的换算关系 - 类似的 AroundCheck 类需要统一修改
验证方法
- 编译通过
- 防御加成/士气伤害倍率等 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
public Camera DefaultCamera => FrameWorkUtils.GetSceneRootComponent<Camera>(gameObject.scene) ??
Camera.main ?? CameraManager.Instance.MainCamera;
修改方案
将 DefaultCamera 从计算属性改为带缓存的属性:
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是否在场景切换后仍需重新获取
验证方法
- 编译通过
- 相机功能正常
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
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) 元组以避免碰撞:
private Dictionary<long, int> _distanceCache;
// 使用时:
long key = ((long)cellIndex1 << 32) | (uint)cellIndex2;
注意事项:
- 先确认项目中 cellIndex 的最大值,如果确定小于 10000 则可暂不修改
- 缓存无限增长问题可通过在关卡结束时清空来解决
验证方法
- 编译通过
- 距离计算结果正确
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
var newList = new List<GameUnit>();
_cellIndex2Units.Add(cellIndex, newList);
return newList;
修改方案
使用 TryGetValue 并返回一个静态空列表,不插入字典:
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;
}
注意事项:
- 需确认调用方是否会向返回的列表中添加元素(如果会,则不能返回共享空列表)
- 如果调用方需要向空列表中添加单位,应在单位注册到格子时创建列表
验证方法
- 编译通过
- 格子单位查询功能正常
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() { ... });
修改方案
listCellIndex.ToArray()→ 如果接收方支持IList<int>或IReadOnlyList<int>,直接传listCellIndexCommanderSkillParams→ 如果是 class,考虑改为 struct 或使用对象池;如果已是 struct,则无需修改
注意事项:
- 需确认
AddCacheSkill的参数类型和后续用法 - 需确认
CommanderSkillParams是 class 还是 struct
验证方法
- 编译通过
- 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 中缓存这些组件引用到字段中。
验证方法
- 编译通过
- 画报系统显示正常
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")] 标记的日志方法。
验证方法
- 编译通过
- 编辑器中日志仍然输出
- 发布版本中无此类日志
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
if (ConditionUtil.conditionNameCfg.Keys.Contains(type))
修改方案
if (ConditionUtil.conditionNameCfg.ContainsKey(type))
验证方法
- 编译通过
- 条件判断功能正常
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
foreach (var key in localStorageMap.Keys)
{
dataIOBase.LoadToDic(key, dataIOBase.Load(key), ref localStorageMap);
}
修改方案
改为遍历 Dictionary 本体:
foreach (var kv in localStorageMap)
{
dataIOBase.LoadToDic(kv.Key, dataIOBase.Load(kv.Key), ref localStorageMap);
}
注意事项:
- 如果
LoadToDic会修改localStorageMap,则需要先将 keys 缓存到列表中再遍历(但应使用复用列表,非 new List)
验证方法
- 编译通过
- 存储加载功能正常
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 列表项进行池化管理。
每个文件可独立改造,建议按以下顺序:
CurrencyItem.cs(最简单,改造成本低)UIAdvanceNewController.cs- 其他
验证方法
- 编译通过
- 对应界面列表显示正常
- 界面开关时无明显内存增长
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) 并重置位置/旋转。
验证方法
- 编译通过
- 路径线显示和隐藏正常
- 路径变更时线段正确更新