using UnityEngine; namespace BaseGames.Dialogue { /// /// 对话行结构(架构 14_NarrativeModule §3)。 /// 每行包含说话人、文本(本地化 Key)和可选的语音片段。 /// /// 推荐通过 actor 引用 DialogueActorSO 统一管理头像/名称; /// speakerNameKey / portraitSprite 作为无 SO 时的直接覆盖(保持向后兼容)。 /// [System.Serializable] public struct DialogueLine { [Tooltip("说话人角色(推荐)。actor 优先;留空时回退到 speakerNameKey / portraitSprite")] public DialogueActorSO actor; [Tooltip("说话人本地化 key,留空时使用 actor.nameKey")] public string speakerNameKey; [TextArea(2, 5)] public string textKey; // 本地化文本 key(如 "DLG_Elder_001") [Tooltip("说话人头像,留空时使用 actor.portrait")] public Sprite portraitSprite; public AudioClip voiceClip; // 可选语音 [Min(0.01f)] public float typewriterDelay; // 每字符延迟(秒,0 = 使用默认 0.03f) /// /// 获取最终使用的说话人名称 Key:actor 优先,回退到直接字段。 /// public string ResolvedNameKey => actor != null && !string.IsNullOrEmpty(actor.nameKey) ? actor.nameKey : speakerNameKey; /// /// 获取最终使用的头像:actor 优先,回退到直接字段。 /// public Sprite ResolvedPortrait => actor != null && actor.portrait != null ? actor.portrait : portraitSprite; } /// /// 对话序列 SO(架构 14_NarrativeModule §3)。 /// 一个 NPC 对话场合对应一个序列,由若干 DialogueLine 组成。 /// 资产路径: Assets/ScriptableObjects/Dialogue/DLG_{NpcId}_{Context}.asset /// [CreateAssetMenu(menuName = "BaseGames/Dialogue/DialogueSequence")] public class DialogueSequenceSO : ScriptableObject { public string sequenceId; // 全局唯一,如 "DLG_Elder_Quest_Available" public DialogueLine[] lines; /// /// 条件变体:所有 requiredFlags 均满足时替换整个序列(AND 关系)。 /// 与 NarrativeNPC.DialogueVersion 的多条件语义保持一致。 /// [System.Serializable] public struct ConditionalVariant { [Tooltip("全部满足时激活此变体(AND 关系)。留空表示无条件。")] [WorldStateFlag] public string[] requiredFlags; public DialogueSequenceSO sequence; } public ConditionalVariant[] variants; #if UNITY_EDITOR // sequenceId → 资产路径,5 秒 TTL,跨所有 DialogueSequenceSO.OnValidate 共用, // 避免每次 Save 都重扫所有同类 SO(O(1) 路径比对代替 O(n) 全量扫描)。 private static System.Collections.Generic.Dictionary s_seqIdToPath; private static double s_seqIdsCacheTime = -10.0; private static System.Collections.Generic.Dictionary GetSequenceIdCache() { double now = UnityEditor.EditorApplication.timeSinceStartup; if (s_seqIdToPath != null && now - s_seqIdsCacheTime < 5.0) return s_seqIdToPath; s_seqIdToPath = new System.Collections.Generic.Dictionary(System.StringComparer.Ordinal); string[] guids = UnityEditor.AssetDatabase.FindAssets("t:DialogueSequenceSO"); foreach (var guid in guids) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); var seq = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (seq != null && !string.IsNullOrEmpty(seq.sequenceId) && !s_seqIdToPath.ContainsKey(seq.sequenceId)) s_seqIdToPath[seq.sequenceId] = path; } s_seqIdsCacheTime = now; return s_seqIdToPath; } private void OnValidate() { if (string.IsNullOrEmpty(sequenceId)) { sequenceId = name; UnityEditor.EditorUtility.SetDirty(this); } // 检测重复 sequenceId:缓存路径 vs 自身路径比对(O(1)),5 秒内无需重扫。 var cache = GetSequenceIdCache(); string myPath = UnityEditor.AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(myPath) && cache.TryGetValue(sequenceId, out var existingPath) && existingPath != myPath) { Debug.LogError( $"[DialogueSequenceSO] sequenceId '{sequenceId}' 与 " + $"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this); s_seqIdsCacheTime = -10.0; } } #endif } }