using System.Collections; using UnityEngine; using BaseGames.Core; using BaseGames.Core.Pool; using BaseGames.World; namespace BaseGames.Enemies { /// /// 场景内的敌人刷新点。实现 ,与房间流式系统联动: /// /// 房间激活()时生成敌人实例。 /// 房间休眠()时清理仍存活的实例,确保下次进入时刷出新实例。 /// 可选 :敌人在房间内死亡后等待若干秒自动复活。 /// 可选的 :击杀后写入 , /// 使敌人在存档重载后不再复活(适用于 Boss 或仅出现一次的守卫)。 /// /// /// 挂载方式:在房间场景中放置空 GameObject 作为刷新点,挂上本组件, /// 并通过 指定要生成的敌人。 /// /// public class EnemyRespawner : MonoBehaviour, IRoomLifecycle { [Header("生成配置")] [Tooltip("要生成的敌人预制体(直接引用)。当 _poolKey 非空时,预制体作为兜底备选。")] [SerializeField] private GameObject _enemyPrefab; [Tooltip("对象池键(对应 GlobalObjectPool 中已预热的 AddressKey 常量)。\n" + "非空时优先通过 IObjectPoolService 取用对象;池服务不可用时回退至 _enemyPrefab。")] [SerializeField] private string _poolKey; [Header("房间内复活")] [Tooltip("0(默认)= 敌人死亡后不在房间内复活,仅下次进入房间时重新生成。\n" + "大于 0 = 死亡后等待 N 秒在原位复活,适用于需要持续巡逻的普通敌人。")] [Min(0f)] [SerializeField] private float _respawnDelay = 0f; [Tooltip("每次进入房间时,敌人最多在房间内复活的次数。\n" + "0 = 不限次数(仅在 _respawnDelay > 0 时有意义)。\n" + "示例:设为 2 表示同一次房间访问中敌人最多死而复生 2 次,第 3 次死后不再刷新。")] [Min(0)] [SerializeField] private int _maxRespawnCount = 0; [Header("永久击杀")] [Tooltip("空 = 每次进入房间均复活。\n" + "非空 = 击杀后将此 ID 写入 WorldStateRegistry.DestroyedObjectIds;存档后不再复活。\n" + "适用于 Boss 或仅出现一次的关键敌人。建议格式:ENM_{EnemyId}_{RoomId}")] [SerializeField] private string _persistentKillId; [Tooltip("永久击杀时写入的世界状态注册表。仅在 _persistentKillId 非空时使用。")] [SerializeField] private WorldStateRegistry _worldState; // ── 运行时 ──────────────────────────────────────────────────────────────── private EnemyBase _activeEnemy; /// 本次房间访问中敌人已在房间内复活的次数(不含初始生成)。 private int _respawnCountThisVisit; private Coroutine _respawnCoroutine; // ── IRoomLifecycle ──────────────────────────────────────────────────────── /// /// 房间休眠时清理当前存活的敌人实例,并取消待执行的复活协程。 /// 已进入死亡流程( = false)的实例由 /// 自行处理,此处不干预。 /// public void OnRoomDormant() { // 取消待执行的复活计时 if (_respawnCoroutine != null) { StopCoroutine(_respawnCoroutine); _respawnCoroutine = null; } if (_activeEnemy != null) { _activeEnemy.OnDied -= OnEnemyDied; if (_activeEnemy.IsAlive) { // 优先归还对象池;池服务不可用或无 PooledObject 时直接销毁 var po = _activeEnemy.GetComponent(); var pool = !string.IsNullOrEmpty(_poolKey) ? ServiceLocator.GetOrDefault() : null; if (pool != null && po != null) pool.Despawn(_poolKey, po); else Destroy(_activeEnemy.gameObject); } // IsAlive=false:EnemyBase.Die() 已通过动画回调调度销毁,自然完成即可 _activeEnemy = null; } _respawnCountThisVisit = 0; } /// /// 房间激活时生成敌人。永久死亡的敌人不会再次生成。 /// /// 出生上下文(含是否为复活流程等信息)。 public void OnRoomActivate(SpawnContext context) { // 永久击杀检查:存档中已标记为 Destroyed,不再生成 if (!string.IsNullOrEmpty(_persistentKillId) && _worldState != null && _worldState.IsDestroyed(_persistentKillId)) return; // 重置本轮复活计数 _respawnCountThisVisit = 0; // 快速折返时防止重复生成(上一轮实例尚存活) if (_activeEnemy != null && _activeEnemy.IsAlive) return; _activeEnemy = null; SpawnEnemy(); } // ── 内部 ────────────────────────────────────────────────────────────────── private void SpawnEnemy() { GameObject go = null; // 优先:对象池 if (!string.IsNullOrEmpty(_poolKey)) { var pool = ServiceLocator.GetOrDefault(); go = pool?.Spawn(_poolKey, transform.position, transform.rotation); } // 兜底:直接实例化 if (go == null && _enemyPrefab != null) go = Instantiate(_enemyPrefab, transform.position, transform.rotation); if (go == null) { Debug.LogError($"[EnemyRespawner] {name}:无法生成敌人,请检查 _enemyPrefab 或 _poolKey 配置。", this); return; } if (!go.TryGetComponent(out var enemy)) { Debug.LogWarning($"[EnemyRespawner] {name}:生成的 GameObject 上未找到 EnemyBase 组件。", this); return; } _activeEnemy = enemy; // 确保对象池复用路径也能正确重置运行时状态 _activeEnemy.OnSpawn(); _activeEnemy.OnDied += OnEnemyDied; } private void OnEnemyDied() { // 永久击杀:写入 WorldStateRegistry(存档管道持久化至 SaveData.World.DestroyedObjectIds) if (!string.IsNullOrEmpty(_persistentKillId) && _worldState != null) _worldState.MarkDestroyed(_persistentKillId); if (_activeEnemy != null) { _activeEnemy.OnDied -= OnEnemyDied; _activeEnemy = null; } // 房间内延迟复活:_respawnDelay > 0 且未超过复活上限 bool canRespawn = _respawnDelay > 0f && (_maxRespawnCount == 0 || _respawnCountThisVisit < _maxRespawnCount); if (canRespawn) _respawnCoroutine = StartCoroutine(RespawnAfterDelay()); } private IEnumerator RespawnAfterDelay() { yield return new WaitForSeconds(_respawnDelay); // 房间可能在等待期间进入休眠(OnRoomDormant 会 StopCoroutine,此处为双重保险) if (!gameObject.activeInHierarchy) yield break; // 再次检查永久击杀(等待期间存档状态可能已变化) if (!string.IsNullOrEmpty(_persistentKillId) && _worldState != null && _worldState.IsDestroyed(_persistentKillId)) yield break; _respawnCountThisVisit++; _respawnCoroutine = null; SpawnEnemy(); } private void OnDestroy() { // 场景卸载时确保取消订阅,防止野指针回调 if (_activeEnemy != null) _activeEnemy.OnDied -= OnEnemyDied; } // ── OnValidate ──────────────────────────────────────────────────────────── #if UNITY_EDITOR private void OnValidate() { // _persistentKillId 非空但 _worldState 未关联时,尝试自动查找场景内唯一实例 if (!string.IsNullOrEmpty(_persistentKillId) && _worldState == null) _worldState = FindFirstObjectByType(); } #endif // ── Editor Gizmos ───────────────────────────────────────────────────────── #if UNITY_EDITOR private static readonly Color s_GizmoColorNormal = new(1f, 0.35f, 0.2f, 0.85f); private static readonly Color s_GizmoColorPersistent = new(1f, 0.85f, 0.1f, 0.85f); private void OnDrawGizmos() { // 未选中时:小球 + 低透明度,便于关卡编辑时总览所有刷新点 Color c = !string.IsNullOrEmpty(_persistentKillId) ? s_GizmoColorPersistent : s_GizmoColorNormal; c.a = 0.3f; Gizmos.color = c; Gizmos.DrawWireSphere(transform.position, 0.25f); } private void OnDrawGizmosSelected() { bool isPersistent = !string.IsNullOrEmpty(_persistentKillId); Gizmos.color = isPersistent ? s_GizmoColorPersistent : s_GizmoColorNormal; Gizmos.DrawWireSphere(transform.position, 0.4f); var labelStyle = new GUIStyle(UnityEditor.EditorStyles.miniLabel) { normal = { textColor = Gizmos.color } }; string label = isPersistent ? $"{name} [永久]" : name; UnityEditor.Handles.Label(transform.position + Vector3.up * 0.65f, label, labelStyle); } #endif } }