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.
This commit is contained in:
2026-05-23 21:23:09 +08:00
parent a1b4e629aa
commit 520f84999b
34 changed files with 1710 additions and 63 deletions

View File

@@ -0,0 +1,246 @@
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
}
}