EnemyBase.OnSpawn 里 InterruptAll 那行的注释写着「重置能力冷却」, 三处都不成立: 1. EnemyAbilityRegistry.InterruptAll 有 `if (ab.IsRunning)` 门; 2. EnemyAbilityBase.Interrupt 里还有一道 `if (!_isRunning) return`; 出生时没有能力在跑,循环体一次都不执行; 3. 即便执行到,末行是 `_cooldownEndTime = Time.time + cooldown * 0.5f`—— 那是「中断后计半程冷却」的写入语义,本就不是清零。 冷却以绝对 Time.time 记时,于是上一条命的剩余冷却原样活到下一条命。 活路径是 EnemyRespawner.SpawnEnemy():敌人死亡归池、延迟复活后取出复用, 按 1.5–10 秒的冷却量级,新生的敌人有数秒出不了招。 修法是补一条真正的清除路径而非改动中断语义: EnemyAbilityBase.ResetCooldown() 把 _cooldownEndTime 清回出生态的 -1, EnemyAbilityRegistry.ResetAllCooldowns() 不看 IsRunning 逐个调用, OnSpawn 显式调用它。原 InterruptAll 保留为兜底中断,但注释改为陈述它 实际做的事——那句失真的注释正是这个缺陷藏了这么久的原因。 测试的冷却全程由生产代码写入:Execute() 启真实协程(编辑模式下跑到首个 yield 即挂起,_isRunning 留 true),再由 Interrupt() 经真实路径写冷却, 不反射直写字段,否则测到的是伪造状态而非真实时序的产物。 验证:编译 0 错 0 警;EditMode 264/264。 修复前该用例是唯一变红的一条(263/264),失败值 5.0f 恰为 cooldown(10) × 0.5,精确指向上述第 3 点。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91 lines
3.3 KiB
C#
91 lines
3.3 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace BaseGames.Enemies.Abilities
|
|
{
|
|
/// <summary>
|
|
/// 能力注册表(架构 07_EnemyModule §8.3)。
|
|
/// EnemyBase.Awake 时调用 <see cref="CollectFrom"/> 自动扫描子物体上所有
|
|
/// <see cref="EnemyAbilityBase"/> 组件,按 abilityId 建立 O(1) 查询字典。
|
|
/// </summary>
|
|
public sealed class EnemyAbilityRegistry
|
|
{
|
|
private readonly Dictionary<string, EnemyAbilityBase> _byId
|
|
= new Dictionary<string, EnemyAbilityBase>(8);
|
|
private readonly List<EnemyAbilityBase> _all = new List<EnemyAbilityBase>(8);
|
|
|
|
/// <summary>所有已注册能力(只读)。</summary>
|
|
public IReadOnlyList<EnemyAbilityBase> All => _all;
|
|
|
|
public void CollectFrom(GameObject root)
|
|
{
|
|
_byId.Clear();
|
|
_all.Clear();
|
|
root.GetComponentsInChildren(true, _all);
|
|
for (int i = 0; i < _all.Count; i++)
|
|
{
|
|
var ab = _all[i];
|
|
if (ab == null || ab.Config == null || string.IsNullOrEmpty(ab.Config.abilityId))
|
|
continue;
|
|
if (_byId.ContainsKey(ab.Config.abilityId))
|
|
{
|
|
Debug.LogError($"[EnemyAbilityRegistry] 重复 abilityId='{ab.Config.abilityId}' 于 {ab.gameObject.name},后注册者被忽略。请检查配置。", ab);
|
|
continue;
|
|
}
|
|
_byId.Add(ab.Config.abilityId, ab);
|
|
}
|
|
}
|
|
|
|
public EnemyAbilityBase Get(string abilityId)
|
|
{
|
|
if (string.IsNullOrEmpty(abilityId)) return null;
|
|
_byId.TryGetValue(abilityId, out var ab);
|
|
return ab;
|
|
}
|
|
|
|
public T Get<T>() where T : EnemyAbilityBase
|
|
{
|
|
for (int i = 0; i < _all.Count; i++)
|
|
if (_all[i] is T t) return t;
|
|
return null;
|
|
}
|
|
|
|
public bool Has(string abilityId) => _byId.ContainsKey(abilityId);
|
|
|
|
public void InterruptAll(InterruptReason reason)
|
|
{
|
|
for (int i = 0; i < _all.Count; i++)
|
|
{
|
|
var ab = _all[i];
|
|
if (ab != null && ab.IsRunning) ab.Interrupt(reason);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空所有能力的冷却。对象池复活时调用。
|
|
/// 与 <see cref="InterruptAll"/> 的区别是**不看 IsRunning**——出生时没有能力在跑,
|
|
/// 那道门会让中断路径一次都不执行,正是冷却跨命残留的成因。
|
|
/// </summary>
|
|
public void ResetAllCooldowns()
|
|
{
|
|
for (int i = 0; i < _all.Count; i++)
|
|
if (_all[i] != null) _all[i].ResetCooldown();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 中断指定互斥组内所有正在执行的能力。
|
|
/// 由 <see cref="EnemyAbilityBase"/> 在 Execute 开始时调用,确保同组互斥。
|
|
/// </summary>
|
|
public void InterruptGroup(string group, InterruptReason reason = InterruptReason.ExternalRequest)
|
|
{
|
|
if (string.IsNullOrEmpty(group)) return;
|
|
for (int i = 0; i < _all.Count; i++)
|
|
{
|
|
var ab = _all[i];
|
|
if (ab != null && ab.IsRunning && ab.Config?.exclusionGroup == group)
|
|
ab.Interrupt(reason);
|
|
}
|
|
}
|
|
}
|
|
}
|