NLDClient-yudde/ProjectNLD/AI_Gen_Doc/性能优化任务清单.md

22 KiB
Raw Blame History

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 数量预期 <=10IndexOf 开销可接受。

问题描述

_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 中的索引:

  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

var bt = go.GetComponent<BehaviorTree>();

位置2 — EnableSingleAI 行号 404

var bt = go.GetComponent<BehaviorTree>();

修改方案

GameUnit 或对应的单位数据结构中缓存 BehaviorTree 引用,在单位创建时获取一次,后续直接使用缓存引用。

具体步骤:

  1. 检查 GameUnit 或其视图组件是否有合适的字段存储 BehaviorTree
  2. 在单位初始化时(如 InitOnCreate)获取并缓存
  3. AIManager 中改为从缓存读取

注意事项

  • EnableSingleAIUnRegisterAI 不是每帧调用,但在大量单位进出战场时可能集中调用
  • 需确认 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

  • 行号 3417GameObjectUtils.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

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 的结果是否可能在单位静止时变化(如光照区域本身在变化)
  • 如果光照区域会动态变化,则需要事件通知或定时刷新

验证方法

  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. 在影响透明度的状态发生变化时(如 isForceHideneedSkillCoverbelongBuild 变化等)设置标记为 true
  3. _UpdateViewTransparent() 中检查标记,无变化时直接返回

注意事项

  • 需要找到所有修改 isForceHideneedSkillCoverbelongBuild 的代码位置,在其中设置脏标记
  • 此优化需要对透明度相关的状态变化有全面了解

验证方法

  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

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 是否在场景切换后仍需重新获取

验证方法

  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

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 则可暂不修改
  • 缓存无限增长问题可通过在关卡结束时清空来解决

验证方法

  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

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;
}

注意事项

  • 需确认调用方是否会向返回的列表中添加元素(如果会,则不能返回共享空列表)
  • 如果调用方需要向空列表中添加单位,应在单位注册到格子时创建列表

验证方法

  1. 编译通过
  2. 格子单位查询功能正常

P1-07: AIEventBusManager — 去除 ToArray 和 new 对象

优先级: P1
影响范围: GamePlay/Level/AIEventBus/AIEventBusManager.cs
问题类型: GC 分配

问题描述

  • AddCacheSkilllistCellIndex.ToArray() 分配新数组
  • CachePlayerSkill.PreExecutenew CommanderSkillParams() 每次创建新对象

问题代码位置

文件: Assets/Code/Scripts/GamePlay/Level/AIEventBus/AIEventBusManager.cs

  • 行号 317AddCacheSkill(skill, false, true, listCellIndex.ToArray());
  • 行号 86-90CommanderSkill.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.csPictorialItem.cs
问题类型: GetComponent 开销

问题描述

PictorialBackground.Update()PictorialItem.Update() 中多处 GetComponent<RectTransform>() / GetComponent<PictorialItem>() 调用。

问题代码位置

PictorialBackground.cs

  • 行号 128child.GetComponent<PictorialItem>() (循环内)
  • 行号 153rectTransform = GetComponent<RectTransform>()

PictorialItem.cs

  • 行号 127, 189, 229, 256, 282transform.parent?.GetComponent<RectTransform>() (多处)

修改方案

AwakeOnEnable 中缓存这些组件引用到字段中。

验证方法

  1. 编译通过
  2. 画报系统显示正常

P2-02: 画报系统 — Debug.Log 条件编译

优先级: P2
影响范围: Framework/UI/Pictorial/PictorialBackground.csPictorialItem.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

if (ConditionUtil.conditionNameCfg.Keys.Contains(type))

修改方案

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

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

验证方法

  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. 路径变更时线段正确更新