feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management
- Implemented WorldStateFlagAttribute to mark string fields as world state flags. - Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states. - Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector. - Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors. - Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars. - Established QuestModule for managing QuestSO assets, including objectives and branches. - Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
This commit is contained in:
209
Assets/_Game/Scripts/Editor/Dialogue/NarrativeNPCEditor.cs
Normal file
209
Assets/_Game/Scripts/Editor/Dialogue/NarrativeNPCEditor.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.World;
|
||||
|
||||
namespace BaseGames.Editor.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// NarrativeNPC 自定义 Inspector。
|
||||
/// 在默认字段之下显示"对话版本激活状态"面板,
|
||||
/// 以色彩提示每个 DialogueVersion 在当前 WorldStateRegistry 中是否激活,
|
||||
/// 方便策划人员在编辑模式下即时核查对话版本切换逻辑。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(NarrativeNPC))]
|
||||
public class NarrativeNPCEditor : UnityEditor.Editor
|
||||
{
|
||||
private static bool s_foldout = true;
|
||||
|
||||
private static readonly Color ColorActive = new(0.25f, 0.75f, 0.40f, 1f);
|
||||
private static readonly Color ColorInactive = new(0.55f, 0.55f, 0.55f, 1f);
|
||||
private static readonly Color ColorBlocked = new(0.85f, 0.40f, 0.35f, 1f);
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
DrawDefaultInspector();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"PlayMode 中显示的是 WorldStateRegistry SO 的初始序列化值,非运行时动态 flags。\n" +
|
||||
"如需查看实际激活版本,请在运行时检查 NPC 日志或在 SO 上断点调试。",
|
||||
MessageType.Info);
|
||||
}
|
||||
|
||||
s_foldout = EditorGUILayout.BeginFoldoutHeaderGroup(s_foldout, "对话版本激活状态");
|
||||
|
||||
if (s_foldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
DrawVersionStatus();
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
// ── 版本状态绘制 ──────────────────────────────────────────────────────
|
||||
|
||||
private void DrawVersionStatus()
|
||||
{
|
||||
// 获取 WorldStateRegistry(运行时或编辑器 SO 引用均可)
|
||||
var worldStateProp = serializedObject.FindProperty("_worldState");
|
||||
var registry = worldStateProp?.objectReferenceValue as WorldStateRegistry;
|
||||
|
||||
var versionsProp = serializedObject.FindProperty("_dialogueVersions");
|
||||
if (versionsProp == null || !versionsProp.isArray || versionsProp.arraySize == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("未设置任何对话版本。", MessageType.None);
|
||||
return;
|
||||
}
|
||||
|
||||
if (registry == null)
|
||||
{
|
||||
EditorGUILayout.HelpBox("未指定 WorldStateRegistry,无法预览激活状态。\n请在 Inspector 中设置 World State 字段。", MessageType.Warning);
|
||||
DrawVersionLabelsOnly(versionsProp);
|
||||
return;
|
||||
}
|
||||
|
||||
bool higherActive = false;
|
||||
|
||||
for (int i = 0; i < versionsProp.arraySize; i++)
|
||||
{
|
||||
var element = versionsProp.GetArrayElementAtIndex(i);
|
||||
|
||||
string label = GetStringProp(element, "versionLabel");
|
||||
string displayName = string.IsNullOrEmpty(label) ? $"版本 {i}" : label;
|
||||
string dialogueName = GetObjectPropName(element, "dialogue");
|
||||
|
||||
// 计算激活状态(直接迭代 SerializedProperty,不分配中间 string[])
|
||||
bool missingRequired = false;
|
||||
string missingFlag = null;
|
||||
var reqProp = element.FindPropertyRelative("requiredFlags");
|
||||
if (reqProp != null && reqProp.isArray)
|
||||
{
|
||||
for (int j = 0; j < reqProp.arraySize; j++)
|
||||
{
|
||||
var f = reqProp.GetArrayElementAtIndex(j).stringValue;
|
||||
if (!string.IsNullOrEmpty(f) && !registry.HasFlag(f))
|
||||
{
|
||||
missingRequired = true;
|
||||
missingFlag = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasBlocker = false;
|
||||
string blockerKey = null;
|
||||
if (!missingRequired)
|
||||
{
|
||||
var blkProp = element.FindPropertyRelative("blockedByFlags");
|
||||
if (blkProp != null && blkProp.isArray)
|
||||
{
|
||||
for (int j = 0; j < blkProp.arraySize; j++)
|
||||
{
|
||||
var f = blkProp.GetArrayElementAtIndex(j).stringValue;
|
||||
if (!string.IsNullOrEmpty(f) && registry.HasFlag(f))
|
||||
{
|
||||
hasBlocker = true;
|
||||
blockerKey = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool conditionsMet = !missingRequired && !hasBlocker;
|
||||
bool isActive = conditionsMet && !higherActive;
|
||||
|
||||
Color statusColor;
|
||||
string statusText;
|
||||
|
||||
if (isActive)
|
||||
{
|
||||
statusColor = ColorActive;
|
||||
statusText = "✓ 激活中";
|
||||
}
|
||||
else if (conditionsMet && higherActive)
|
||||
{
|
||||
statusColor = ColorInactive;
|
||||
statusText = "⏩ 被更高优先级覆盖";
|
||||
}
|
||||
else if (hasBlocker)
|
||||
{
|
||||
statusColor = ColorBlocked;
|
||||
statusText = $"✗ blockedByFlag 阻断 [{blockerKey}]";
|
||||
}
|
||||
else
|
||||
{
|
||||
statusColor = ColorInactive;
|
||||
statusText = $"✗ 缺少 requiredFlag [{missingFlag}]";
|
||||
}
|
||||
|
||||
DrawVersionRow(i, displayName, dialogueName, statusText, statusColor);
|
||||
|
||||
if (conditionsMet) higherActive = true;
|
||||
}
|
||||
|
||||
// 兜底说明
|
||||
EditorGUILayout.Space(2);
|
||||
var fallbackProp = serializedObject.FindProperty("_fallbackDialogue");
|
||||
if (fallbackProp?.objectReferenceValue != null)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
EditorGUILayout.LabelField("兜底台词", fallbackProp.objectReferenceValue.name);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawVersionLabelsOnly(SerializedProperty versionsProp)
|
||||
{
|
||||
for (int i = 0; i < versionsProp.arraySize; i++)
|
||||
{
|
||||
var element = versionsProp.GetArrayElementAtIndex(i);
|
||||
string label = GetStringProp(element, "versionLabel");
|
||||
string display = string.IsNullOrEmpty(label) ? $"版本 {i}" : label;
|
||||
EditorGUILayout.LabelField($" {i}. {display}", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawVersionRow(int index, string versionLabel, string dialogueName, string statusText, Color statusColor)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
// 序号+名称
|
||||
EditorGUILayout.LabelField($"{index}. {versionLabel}", GUILayout.Width(160));
|
||||
|
||||
// 对话 SO 名称
|
||||
if (!string.IsNullOrEmpty(dialogueName))
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(true))
|
||||
EditorGUILayout.LabelField(dialogueName, EditorStyles.miniLabel, GUILayout.Width(140));
|
||||
}
|
||||
|
||||
// 状态徽章
|
||||
var prevColor = GUI.contentColor;
|
||||
GUI.contentColor = statusColor;
|
||||
EditorGUILayout.LabelField(statusText, EditorStyles.boldLabel);
|
||||
GUI.contentColor = prevColor;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 辅助:从 SerializedProperty 读取字段 ─────────────────────────────
|
||||
|
||||
private static string GetStringProp(SerializedProperty parent, string name)
|
||||
{
|
||||
var p = parent.FindPropertyRelative(name);
|
||||
return p != null ? p.stringValue : string.Empty;
|
||||
}
|
||||
|
||||
private static string GetObjectPropName(SerializedProperty parent, string name)
|
||||
{
|
||||
var p = parent.FindPropertyRelative(name);
|
||||
if (p == null || p.objectReferenceValue == null) return string.Empty;
|
||||
return p.objectReferenceValue.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Assets/_Game/Scripts/Editor/Dialogue/WorldStateFlagDrawer.cs
Normal file
195
Assets/_Game/Scripts/Editor/Dialogue/WorldStateFlagDrawer.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.EventChain;
|
||||
using BaseGames.Editor;
|
||||
|
||||
namespace BaseGames.Editor.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 Inspector 中为 [WorldStateFlag] 标记的 string 字段提供已知标志下拉菜单。
|
||||
/// 扫描项目内所有 SetFlagAction / FlagSetCondition / ConditionalVariant.requiredFlags 收集已使用的标志名,
|
||||
/// 供策划选取;同时保留直接输入新标志的能力。
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(WorldStateFlagAttribute))]
|
||||
internal sealed class WorldStateFlagDrawer : PropertyDrawer
|
||||
{
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
if (property.propertyType != SerializedPropertyType.String)
|
||||
{
|
||||
EditorGUI.PropertyField(position, property, label);
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
// 文本输入区 + 下拉箭头按钮
|
||||
const float btnW = 22f;
|
||||
var textRect = new Rect(position.x, position.y, position.width - btnW - 2f, position.height);
|
||||
var btnRect = new Rect(position.xMax - btnW, position.y, btnW, position.height);
|
||||
|
||||
property.stringValue = EditorGUI.TextField(textRect, label, property.stringValue);
|
||||
|
||||
if (GUI.Button(btnRect, EditorGUIUtility.IconContent("d_icon dropdown"), EditorStyles.iconButton))
|
||||
{
|
||||
var flags = WorldStateFlagCollector.CollectKnownFlags();
|
||||
var menu = new GenericMenu();
|
||||
var current = property.stringValue;
|
||||
var propPath = property.propertyPath;
|
||||
var so = property.serializedObject;
|
||||
|
||||
foreach (var flag in flags)
|
||||
{
|
||||
var captured = flag;
|
||||
menu.AddItem(
|
||||
new GUIContent(captured),
|
||||
current == captured,
|
||||
() =>
|
||||
{
|
||||
var prop = so.FindProperty(propPath);
|
||||
if (prop != null)
|
||||
{
|
||||
prop.stringValue = captured;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (flags.Count == 0)
|
||||
menu.AddDisabledItem(new GUIContent("(项目中尚未定义任何标志)"));
|
||||
|
||||
menu.AddSeparator(string.Empty);
|
||||
menu.AddItem(new GUIContent("刷新列表"), false, WorldStateFlagCollector.Invalidate);
|
||||
|
||||
menu.ShowAsContext();
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 标志扫描器 ────────────────────────────────────────────────────────────
|
||||
// SO 来源(SetFlagAction / FlagSetCondition / DialogueSequenceSO):5 秒 TTL 自动刷新。
|
||||
// NarrativeNPC prefab 来源:仅在用户点击"刷新列表"时执行(开销高,手动触发)。
|
||||
|
||||
internal static class WorldStateFlagCollector
|
||||
{
|
||||
private static double _lastSoCollect = -10.0;
|
||||
private static double _lastPrefabCollect = double.MinValue; // 默认不自动触发
|
||||
private static List<string> _cache = new();
|
||||
private static SortedSet<string> _prefabFlags = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 强制下次 CollectKnownFlags() 时重新扫描 SO 和 prefab(含 NarrativeNPC)。
|
||||
/// 由下拉菜单的"刷新列表"触发。
|
||||
/// </summary>
|
||||
public static void Invalidate()
|
||||
{
|
||||
_lastSoCollect = -10.0;
|
||||
_lastPrefabCollect = -10.0; // 重置 prefab 缓存使其在下次调用时立即扫描
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 收集项目内所有已使用的世界状态标志名。
|
||||
/// - SO 来源(SetFlagAction / FlagSetCondition / ConditionalVariant):5 秒缓存。
|
||||
/// - NarrativeNPC prefab 来源:仅在调用 Invalidate() 后才重新扫描。
|
||||
/// </summary>
|
||||
public static List<string> CollectKnownFlags()
|
||||
{
|
||||
double now = EditorApplication.timeSinceStartup;
|
||||
|
||||
bool soStale = now - _lastSoCollect >= 5.0;
|
||||
bool prefabStale = now - _lastPrefabCollect >= 5.0 && _lastPrefabCollect < 0; // 仅在 Invalidate 后为负
|
||||
|
||||
if (!soStale && !prefabStale)
|
||||
return _cache;
|
||||
|
||||
var found = new SortedSet<string>(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (soStale)
|
||||
{
|
||||
CollectSoFlags(found);
|
||||
_lastSoCollect = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
// SO 缓存仍有效,直接把已有 SO 标志加入合并集
|
||||
foreach (var f in _cache)
|
||||
{
|
||||
// _cache 里混有 prefab 标志,先全加;下面再合并 prefab
|
||||
found.Add(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (prefabStale)
|
||||
{
|
||||
_prefabFlags.Clear();
|
||||
CollectNarrativeNpcFlags(_prefabFlags);
|
||||
_lastPrefabCollect = now;
|
||||
}
|
||||
|
||||
foreach (var f in _prefabFlags)
|
||||
found.Add(f);
|
||||
|
||||
_cache = new List<string>(found);
|
||||
return _cache;
|
||||
}
|
||||
|
||||
private static void CollectSoFlags(SortedSet<string> found)
|
||||
{
|
||||
foreach (var a in AssetOperations.FindAll<SetFlagAction>())
|
||||
if (!string.IsNullOrWhiteSpace(a.flagId)) found.Add(a.flagId.Trim());
|
||||
|
||||
foreach (var c in AssetOperations.FindAll<FlagSetCondition>())
|
||||
if (!string.IsNullOrWhiteSpace(c.flagId)) found.Add(c.flagId.Trim());
|
||||
|
||||
foreach (var seq in AssetOperations.FindAll<DialogueSequenceSO>())
|
||||
{
|
||||
if (seq.variants == null) continue;
|
||||
foreach (var v in seq.variants)
|
||||
{
|
||||
if (v.requiredFlags == null) continue;
|
||||
foreach (var f in v.requiredFlags)
|
||||
if (!string.IsNullOrWhiteSpace(f)) found.Add(f.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CollectNarrativeNpcFlags(SortedSet<string> found)
|
||||
{
|
||||
var prefabGuids = AssetDatabase.FindAssets("t:Prefab");
|
||||
foreach (var guid in prefabGuids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (prefab == null) continue;
|
||||
|
||||
var npc = prefab.GetComponent<NarrativeNPC>();
|
||||
if (npc == null) continue;
|
||||
|
||||
var so = new SerializedObject(npc);
|
||||
var versProp = so.FindProperty("_dialogueVersions");
|
||||
if (versProp == null || !versProp.isArray) continue;
|
||||
|
||||
for (int i = 0; i < versProp.arraySize; i++)
|
||||
{
|
||||
var elem = versProp.GetArrayElementAtIndex(i);
|
||||
ExtractStringArrayProp(elem.FindPropertyRelative("requiredFlags"), found);
|
||||
ExtractStringArrayProp(elem.FindPropertyRelative("blockedByFlags"), found);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractStringArrayProp(SerializedProperty arr, SortedSet<string> found)
|
||||
{
|
||||
if (arr == null || !arr.isArray) return;
|
||||
for (int i = 0; i < arr.arraySize; i++)
|
||||
{
|
||||
var val = arr.GetArrayElementAtIndex(i).stringValue;
|
||||
if (!string.IsNullOrWhiteSpace(val)) found.Add(val.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user