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,266 @@
using UnityEditor;
using UnityEngine;
using BaseGames.Enemies;
using BaseGames.World;
namespace BaseGames.Editor
{
/// <summary>
/// <see cref="EnemyRespawner"/> 自定义 Inspector。
/// <list type="bullet">
/// <item>配置校验:生成来源为空、永久击杀 ID 已填但 WorldStateRegistry 未关联时显示警告。</item>
/// <item>运行时状态PlayMode 中显示当前激活实例信息。</item>
/// <item>批量验证菜单BaseGames / Enemies / Validate Respawners。</item>
/// </list>
/// </summary>
[CustomEditor(typeof(EnemyRespawner))]
public class EnemyRespawnerEditor : UnityEditor.Editor
{
// ── 序列化属性引用 ─────────────────────────────────────────────────────────
private SerializedProperty _prefabProp;
private SerializedProperty _poolKeyProp;
private SerializedProperty _respawnDelayProp;
private SerializedProperty _maxRespawnCountProp;
private SerializedProperty _persistentKillIdProp;
private SerializedProperty _worldStateProp;
// 运行时只读字段(反射获取私有字段)
private static readonly System.Reflection.FieldInfo s_ActiveEnemyField =
typeof(EnemyRespawner).GetField("_activeEnemy",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
private static readonly System.Reflection.FieldInfo s_RespawnCountField =
typeof(EnemyRespawner).GetField("_respawnCountThisVisit",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
private static readonly System.Reflection.FieldInfo s_RespawnCoroutineField =
typeof(EnemyRespawner).GetField("_respawnCoroutine",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
private void OnEnable()
{
_prefabProp = serializedObject.FindProperty("_enemyPrefab");
_poolKeyProp = serializedObject.FindProperty("_poolKey");
_respawnDelayProp = serializedObject.FindProperty("_respawnDelay");
_maxRespawnCountProp = serializedObject.FindProperty("_maxRespawnCount");
_persistentKillIdProp = serializedObject.FindProperty("_persistentKillId");
_worldStateProp = serializedObject.FindProperty("_worldState");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// 手动绘制字段,以便对 _maxRespawnCount 做条件性灰显
DrawPropertiesExcluding(serializedObject, "_maxRespawnCount");
bool hasDelay = _respawnDelayProp != null && _respawnDelayProp.floatValue > 0f;
using (new EditorGUI.DisabledScope(!hasDelay))
{
EditorGUILayout.PropertyField(_maxRespawnCountProp,
new GUIContent("复活上限 (Max Respawn Count)",
_maxRespawnCountProp?.tooltip ?? ""));
}
if (!hasDelay)
EditorGUILayout.HelpBox("Respawn Delay = 0Max Respawn Count 无效(敌人不会在房间内复活)。",
MessageType.None);
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space(6f);
DrawValidationSection();
if (Application.isPlaying)
{
EditorGUILayout.Space(6f);
DrawRuntimeSection();
}
}
// ── 配置校验区块 ──────────────────────────────────────────────────────────
private void DrawValidationSection()
{
EditorGUILayout.LabelField("配置检查", EditorStyles.boldLabel);
bool hasPrefab = _prefabProp.objectReferenceValue != null;
bool hasPoolKey = !string.IsNullOrWhiteSpace(_poolKeyProp.stringValue);
bool hasPersistentId = !string.IsNullOrWhiteSpace(_persistentKillIdProp.stringValue);
bool hasWorldState = _worldStateProp.objectReferenceValue != null;
// ── 生成来源 ───────────────────────────────────────────────────────
if (!hasPrefab && !hasPoolKey)
{
EditorGUILayout.HelpBox(
"_enemyPrefab 与 _poolKey 均为空,运行时无法生成敌人。\n" +
"请至少填写其中一项。",
MessageType.Error);
}
else if (hasPrefab && !hasPoolKey)
{
EditorGUILayout.HelpBox(
"当前使用直接实例化_enemyPrefab。\n" +
"频繁进出房间时建议改用对象池(填写 _poolKey以减少 GC 压力。",
MessageType.Info);
}
else
{
DrawOk("生成来源配置正常。");
}
// ── 永久击杀 ───────────────────────────────────────────────────────
if (hasPersistentId && !hasWorldState)
{
EditorGUILayout.HelpBox(
"_persistentKillId 已填写,但 _worldState 未关联。\n" +
"击杀记录将无法持久化,存档重载后敌人仍会复活。",
MessageType.Warning);
if (GUILayout.Button("自动查找场景内 WorldStateRegistry"))
{
var found = FindFirstObjectByType<WorldStateRegistry>();
if (found != null)
{
serializedObject.Update();
_worldStateProp.objectReferenceValue = found;
serializedObject.ApplyModifiedProperties();
}
else
{
EditorUtility.DisplayDialog(
"未找到",
"当前场景中没有 WorldStateRegistry 组件。\n" +
"请先在场景内放置 WorldStateRegistry。",
"确定");
}
}
}
else if (!hasPersistentId)
{
DrawOk("普通刷新点(每次进房复活)。");
}
else
{
DrawOk($"永久击杀已关联 WorldStateRegistry。\nID{_persistentKillIdProp.stringValue}");
}
}
// ── 运行时状态区块PlayMode 专用)────────────────────────────────────────
private void DrawRuntimeSection()
{
EditorGUILayout.LabelField("运行时状态", EditorStyles.boldLabel);
var respawner = (EnemyRespawner)target;
var activeEnemy = s_ActiveEnemyField?.GetValue(respawner) as EnemyBase;
int respawnCount = s_RespawnCountField != null
? (int)s_RespawnCountField.GetValue(respawner)
: 0;
bool waitingRespawn = s_RespawnCoroutineField?.GetValue(respawner) != null;
if (activeEnemy == null && !waitingRespawn)
{
EditorGUILayout.LabelField("当前实例", "无(房间休眠或尚未生成)");
}
else if (waitingRespawn)
{
var prevColor = GUI.color;
GUI.color = new Color(0.6f, 0.8f, 1f);
int maxCount = _maxRespawnCountProp?.intValue ?? 0;
string countStr = maxCount == 0
? $"{respawnCount} 次(无上限)"
: $"{respawnCount} / {maxCount} 次";
EditorGUILayout.LabelField("状态", $"⏳ 等待复活中…(已复活 {countStr}");
GUI.color = prevColor;
}
else if (activeEnemy != null)
{
string statusStr = activeEnemy.IsAlive ? "存活" : "死亡动画中";
var prevColor = GUI.color;
GUI.color = activeEnemy.IsAlive
? new Color(0.4f, 0.9f, 0.4f)
: new Color(1f, 0.55f, 0.3f);
EditorGUILayout.LabelField("当前实例", $"{activeEnemy.name} [{statusStr}]");
GUI.color = prevColor;
int maxCount = _maxRespawnCountProp?.intValue ?? 0;
if (_respawnDelayProp?.floatValue > 0f)
{
string countStr = maxCount == 0
? $"{respawnCount} 次(无上限)"
: $"{respawnCount} / {maxCount} 次";
EditorGUILayout.LabelField("本次访问已复活", countStr);
}
if (GUILayout.Button("在 Hierarchy 中定位", EditorStyles.miniButton))
Selection.activeGameObject = activeEnemy.gameObject;
}
// 强制每帧刷新,确保状态即时更新
Repaint();
}
// ── 通用辅助 ──────────────────────────────────────────────────────────────
private static void DrawOk(string message)
{
var prevColor = GUI.color;
GUI.color = new Color(0.4f, 0.9f, 0.4f);
EditorGUILayout.LabelField($"✓ {message}", EditorStyles.wordWrappedMiniLabel);
GUI.color = prevColor;
}
// ── 批量验证菜单 ──────────────────────────────────────────────────────────
[MenuItem("BaseGames/Enemies/Validate Respawners")]
private static void ValidateAllRespawners()
{
var all = FindObjectsByType<EnemyRespawner>(
FindObjectsInactive.Include, FindObjectsSortMode.None);
if (all.Length == 0)
{
EditorUtility.DisplayDialog("验证结果", "当前场景中未找到任何 EnemyRespawner。", "确定");
return;
}
int errors = 0;
int warnings = 0;
foreach (var r in all)
{
var so = new SerializedObject(r);
var prefab = so.FindProperty("_enemyPrefab");
var poolKey = so.FindProperty("_poolKey");
var killId = so.FindProperty("_persistentKillId");
var worldState = so.FindProperty("_worldState");
bool hasPrefab = prefab?.objectReferenceValue != null;
bool hasPoolKey = !string.IsNullOrWhiteSpace(poolKey?.stringValue);
bool hasKillId = !string.IsNullOrWhiteSpace(killId?.stringValue);
bool hasWS = worldState?.objectReferenceValue != null;
string scene = r.gameObject.scene.name;
string path = $"{scene}/{r.gameObject.name}";
if (!hasPrefab && !hasPoolKey)
{
errors++;
Debug.LogError($"[EnemyRespawner] {path}_enemyPrefab 与 _poolKey 均为空,无法生成敌人。", r.gameObject);
}
if (hasKillId && !hasWS)
{
warnings++;
Debug.LogWarning($"[EnemyRespawner] {path}_persistentKillId=\"{killId.stringValue}\" 但 _worldState 未关联,击杀记录无法持久化。", r.gameObject);
}
}
string msg = errors == 0 && warnings == 0
? $"全部 {all.Length} 个 EnemyRespawner 配置正常。"
: $"检查完成:{errors} 个错误,{warnings} 个警告(共 {all.Length} 个)。\n详细信息见 Console 日志。";
EditorUtility.DisplayDialog("验证结果", msg, "确定");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25ae5943339e21845a4f350aac520224
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: