Files
zeling_v2/Assets/_Game/Scripts/Enemies/EnemyRespawner.cs
Joywayer 520f84999b Add enemy respawner and related components for room lifecycle management
- Implemented EnemyRespawner to manage enemy spawning and respawning within rooms.
- Added IRoomLifecycle interface for room activation and dormancy handling.
- Created supporting classes and metadata for enemy perception and threat assessment.
- Established streaming system components for room state management and transitions.
- Added necessary metadata files for new scripts to ensure proper integration with Unity.
2026-05-23 21:23:09 +08:00

247 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Pool;
using BaseGames.World;
namespace BaseGames.Enemies
{
/// <summary>
/// 场景内的敌人刷新点。实现 <see cref="IRoomLifecycle"/>,与房间流式系统联动:
/// <list type="bullet">
/// <item>房间激活(<see cref="OnRoomActivate"/>)时生成敌人实例。</item>
/// <item>房间休眠(<see cref="OnRoomDormant"/>)时清理仍存活的实例,确保下次进入时刷出新实例。</item>
/// <item>可选 <see cref="_respawnDelay"/>:敌人在房间内死亡后等待若干秒自动复活。</item>
/// <item>可选的 <see cref="_persistentKillId"/>:击杀后写入 <see cref="WorldStateRegistry"/>
/// 使敌人在存档重载后不再复活(适用于 Boss 或仅出现一次的守卫)。</item>
/// </list>
/// <para>
/// 挂载方式:在房间场景中放置空 GameObject 作为刷新点,挂上本组件,
/// 并通过 <see cref="_enemyPrefab"/> 或 <see cref="_poolKey"/> 指定要生成的敌人。
/// </para>
/// </summary>
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;
/// <summary>本次房间访问中敌人已在房间内复活的次数(不含初始生成)。</summary>
private int _respawnCountThisVisit;
private Coroutine _respawnCoroutine;
// ── IRoomLifecycle ────────────────────────────────────────────────────────
/// <summary>
/// 房间休眠时清理当前存活的敌人实例,并取消待执行的复活协程。
/// 已进入死亡流程(<see cref="EnemyBase.IsAlive"/> = false的实例由
/// <see cref="EnemyBase.Die"/> 自行处理,此处不干预。
/// </summary>
public void OnRoomDormant()
{
// 取消待执行的复活计时
if (_respawnCoroutine != null)
{
StopCoroutine(_respawnCoroutine);
_respawnCoroutine = null;
}
if (_activeEnemy != null)
{
_activeEnemy.OnDied -= OnEnemyDied;
if (_activeEnemy.IsAlive)
{
// 优先归还对象池;池服务不可用或无 PooledObject 时直接销毁
var po = _activeEnemy.GetComponent<PooledObject>();
var pool = !string.IsNullOrEmpty(_poolKey)
? ServiceLocator.GetOrDefault<IObjectPoolService>()
: null;
if (pool != null && po != null)
pool.Despawn(_poolKey, po);
else
Destroy(_activeEnemy.gameObject);
}
// IsAlive=falseEnemyBase.Die() 已通过动画回调调度销毁,自然完成即可
_activeEnemy = null;
}
_respawnCountThisVisit = 0;
}
/// <summary>
/// 房间激活时生成敌人。永久死亡的敌人不会再次生成。
/// </summary>
/// <param name="context">出生上下文(含是否为复活流程等信息)。</param>
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<IObjectPoolService>();
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<EnemyBase>(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<WorldStateRegistry>();
}
#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
}
}