fix(core): 对象池真正通知 IPoolable,复用的敌人不再以死亡状态出场
IPoolable 的接口文档写着「由 PooledObject 子类或同 GameObject 上的其他 MonoBehaviour 实现,并在 PooledObject.OnSpawn/OnDespawn 中手动驱动」, 但那两个方法是空的 public virtual,全库无任何 PooledObject 子类, GlobalObjectPool 也从不查找 IPoolable 组件——这份契约从未生效过。 全库 .OnSpawn(/.OnDespawn( 的调用点只有 4 处,真正驱动 EnemyBase (唯一的 IPoolable 实现者)的只有 EnemyRespawner 一处手动补调。后果: - EnemyBase.OnDespawn 在生产中从未被调用,其中的 _nav?.Stop() 是死代码; - 更严重的是取出侧:PerformDeath 会 ForceState(Dead) 并禁用全部碰撞体后归池, 而 EnemySpawnerOnEvent / ChaoFengBoss 召唤 / RangedEnemy 这些取用方 没有补调 OnSpawn,拿到的复用敌人仍停在 Dead 态、碰撞体全关、HP 为 0—— 一个不可交互也不会动的幽灵。EnemyRespawner 路径侥幸没暴露,只因为它手动补了。 修法是让池履行它自己声明的契约,而不是让各调用方继续手动补调: PooledObject.OnSpawn/OnDespawn 转发给本物体上的 IPoolable。只扫本物体、 不向下扫子物体——接口文档限定的就是「同 GameObject」,向下扫会让嵌套的 可池化对象被两个 PooledObject 各通知一次。组件集惰性解析一次并缓存 (池化本就是为省开销,不能每次进出都遍历组件)。 同时统一两条归池路径的顺序:GlobalObjectPool.Despawn 原本先 SetActive(false) 再 OnDespawn,而 PooledObject.ForceReturnToPool 是反过来的。转发接通后这个 顺序就成了可观察行为,两条路径必须给出同一份契约——统一为「先通知再停用」, 清理才跑在还活跃的对象上。 EnemyRespawner 那次手动 OnSpawn 随之改为只在兜底实例化路径调用:池化路径 现在由转发完成,重复调用会让 OnSpawn 末尾的 Spawned?.Invoke() 触发两次, 即重复执行出生能力。(今天重复触发会被 Execute() 的 !_isRunning 门挡住, 但那是运气不是设计。) 验证:编译 0 错;EditMode 273/273(269 + 新增 4)。 分阶段观察过失败面:只加转发时,2 条转发测试转绿、顺序测试仍红在 WasActiveOnDespawn 上;补顺序后全绿。 变异验证——把扫描范围改成 GetComponentsInChildren 后,边界测试恰好变红 (该测试在修复前是平凡通过的,需确认它真有约束力),还原后复验 273/273。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -154,8 +154,10 @@ namespace BaseGames.Core.Pool
|
||||
aliveRef.Remove(po.AliveNode);
|
||||
po.AliveNode = null;
|
||||
}
|
||||
po.gameObject.SetActive(false);
|
||||
// 先通知再停用:清理要跑在还活跃的对象上(可用协程、可读组件状态)。
|
||||
// 与 PooledObject.ForceReturnToPool 的顺序一致——两条归池路径必须给出同一份契约。
|
||||
po.OnDespawn();
|
||||
po.gameObject.SetActive(false);
|
||||
|
||||
int maxCount = _maxCounts.GetValueOrDefault(key, 0);
|
||||
int queueSize = _pools.TryGetValue(key, out var queue) ? queue.Count : 0;
|
||||
|
||||
@@ -24,14 +24,37 @@ namespace BaseGames.Core.Pool
|
||||
// 组件缓存(避免反复 GetComponent)
|
||||
private readonly Dictionary<Type, Component> _componentCache = new();
|
||||
|
||||
// 本物体上的 IPoolable 实现者。首次使用时解析一次并缓存——池化本就是为省开销,
|
||||
// 不能每次 Spawn/Despawn 都遍历一遍组件。惰性解析而非依赖 Setup:
|
||||
// 结果与解析时机无关,也就不存在"忘了调 Setup 就静默不通知"的失败模式。
|
||||
private IPoolable[] _poolables;
|
||||
private IPoolable[] Poolables => _poolables ??= GetComponents<IPoolable>();
|
||||
|
||||
public void Setup(string key, GlobalObjectPool pool)
|
||||
{
|
||||
AddressKey = key;
|
||||
_pool = pool;
|
||||
}
|
||||
|
||||
public virtual void OnSpawn() { }
|
||||
public virtual void OnDespawn(){ }
|
||||
/// <summary>
|
||||
/// 由池在取出对象后调用,转发给本物体上的所有 <see cref="IPoolable"/>。
|
||||
/// 子类覆盖时必须调 base,否则 IPoolable 实现者收不到通知。
|
||||
/// </summary>
|
||||
public virtual void OnSpawn()
|
||||
{
|
||||
var ps = Poolables;
|
||||
for (int i = 0; i < ps.Length; i++) ps[i].OnSpawn();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由池在归还对象时调用(对象仍处于活跃状态),转发给本物体上的所有 <see cref="IPoolable"/>。
|
||||
/// 子类覆盖时必须调 base。
|
||||
/// </summary>
|
||||
public virtual void OnDespawn()
|
||||
{
|
||||
var ps = Poolables;
|
||||
for (int i = 0; i < ps.Length; i++) ps[i].OnDespawn();
|
||||
}
|
||||
|
||||
// ── 归还 API ──────────────────────────────────────────────────────
|
||||
/// <summary>立即归还到对象池。</summary>
|
||||
@@ -67,8 +90,10 @@ namespace BaseGames.Core.Pool
|
||||
|
||||
/// <summary>
|
||||
/// 可选接口:若池化对象需要在 Spawn/Despawn 时执行额外逻辑,
|
||||
/// 由 PooledObject 子类或同 GameObject 上的其他 MonoBehaviour 实现,
|
||||
/// 并在 PooledObject.OnSpawn/OnDespawn 中手动驱动。
|
||||
/// 由 PooledObject 子类或**同 GameObject 上**的其他 MonoBehaviour 实现。
|
||||
/// <see cref="PooledObject.OnSpawn"/> / <see cref="PooledObject.OnDespawn"/> 会自动转发,
|
||||
/// 调用方无需手动驱动。
|
||||
/// 只扫本物体、不向下扫子物体:嵌套的可池化对象会因此被两个 PooledObject 各通知一次。
|
||||
/// </summary>
|
||||
public interface IPoolable
|
||||
{
|
||||
|
||||
@@ -127,12 +127,14 @@ namespace BaseGames.Enemies
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
GameObject go = null;
|
||||
bool fromPool = false;
|
||||
|
||||
// 优先:对象池
|
||||
if (!string.IsNullOrEmpty(_poolKey))
|
||||
{
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
go = pool?.Spawn(_poolKey, transform.position, transform.rotation);
|
||||
fromPool = go != null;
|
||||
}
|
||||
|
||||
// 兜底:直接实例化
|
||||
@@ -152,8 +154,10 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
|
||||
_activeEnemy = enemy;
|
||||
// 确保对象池复用路径也能正确重置运行时状态
|
||||
_activeEnemy.OnSpawn();
|
||||
// 池化路径的重置由 PooledObject.OnSpawn 转发 IPoolable 完成,这里不能再调一次:
|
||||
// OnSpawn 末尾会 Spawned?.Invoke(),重复触发即重复执行出生能力。
|
||||
// 兜底实例化路径没有池,需要在这里补一次。
|
||||
if (!fromPool) _activeEnemy.OnSpawn();
|
||||
_activeEnemy.OnDied += OnEnemyDied;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user