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:
266
Assets/_Game/Scripts/Editor/Enemies/EnemyRespawnerEditor.cs
Normal file
266
Assets/_Game/Scripts/Editor/Enemies/EnemyRespawnerEditor.cs
Normal 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 = 0,Max 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, "确定");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 25ae5943339e21845a4f350aac520224
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user