- 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.
247 lines
11 KiB
C#
247 lines
11 KiB
C#
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=false:EnemyBase.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
|
||
}
|
||
}
|