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:
81
Assets/_Game/Scripts/Dialogue/DialogueActorSO.cs
Normal file
81
Assets/_Game/Scripts/Dialogue/DialogueActorSO.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// 对话角色定义 SO(架构 14_NarrativeModule §3)。
|
||||
/// 将 NPC 的显示名、头像、对话气泡颜色集中在一处管理。
|
||||
/// DialogueLine.actor 引用此 SO,修改头像/名称只需改一个资产,
|
||||
/// 无需批量编辑所有对话行。
|
||||
///
|
||||
/// 资产路径:Assets/_Game/Data/Dialogue/Actors/Actor_{actorId}.asset
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Dialogue/DialogueActor")]
|
||||
public class DialogueActorSO : ScriptableObject
|
||||
{
|
||||
[Header("标识")]
|
||||
[Tooltip("唯一 ID,如 \"NPC_Elder\",供 DialogueLine 引用")]
|
||||
public string actorId;
|
||||
|
||||
[Header("显示")]
|
||||
[Tooltip("本地化 Key,格式如 \"NPC_Elder_Name\"")]
|
||||
public string nameKey;
|
||||
|
||||
[Tooltip("对话 UI 中显示的头像")]
|
||||
public Sprite portrait;
|
||||
|
||||
[Tooltip("对话气泡/说话人名称的主题颜色(可选)")]
|
||||
public Color accentColor = Color.white;
|
||||
|
||||
[Tooltip("是否为玩家角色(影响对话 UI 排版方向)")]
|
||||
public bool isPlayer;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// actorId → 资产路径,5 秒 TTL,跨所有 DialogueActorSO.OnValidate 共用。
|
||||
// 与 QuestSO / DialogueSequenceSO 保持一致的 O(1) 重复检测策略。
|
||||
private static System.Collections.Generic.Dictionary<string, string> s_actorIdToPath;
|
||||
private static double s_actorIdsCacheTime = -10.0;
|
||||
|
||||
private static System.Collections.Generic.Dictionary<string, string> GetActorIdCache()
|
||||
{
|
||||
double now = UnityEditor.EditorApplication.timeSinceStartup;
|
||||
if (s_actorIdToPath != null && now - s_actorIdsCacheTime < 5.0)
|
||||
return s_actorIdToPath;
|
||||
|
||||
s_actorIdToPath = new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:DialogueActorSO");
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
var actor = UnityEditor.AssetDatabase.LoadAssetAtPath<DialogueActorSO>(path);
|
||||
if (actor != null && !string.IsNullOrEmpty(actor.actorId) && !s_actorIdToPath.ContainsKey(actor.actorId))
|
||||
s_actorIdToPath[actor.actorId] = path;
|
||||
}
|
||||
s_actorIdsCacheTime = now;
|
||||
return s_actorIdToPath;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(actorId))
|
||||
{
|
||||
Debug.LogWarning($"[DialogueActorSO] '{name}' 缺少 actorId,保存前请填写。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测重复 actorId:缓存路径 vs 自身路径比对(O(1)),5 秒内无需重扫。
|
||||
var cache = GetActorIdCache();
|
||||
string myPath = UnityEditor.AssetDatabase.GetAssetPath(this);
|
||||
if (!string.IsNullOrEmpty(myPath) &&
|
||||
cache.TryGetValue(actorId, out var existingPath) &&
|
||||
existingPath != myPath)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[DialogueActorSO] actorId '{actorId}' 与 " +
|
||||
$"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this);
|
||||
s_actorIdsCacheTime = -10.0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
@@ -22,9 +23,36 @@ namespace BaseGames.Dialogue
|
||||
[SerializeField] private VoidEventChannelSO _onDialogueStarted;
|
||||
[SerializeField] private VoidEventChannelSO _onDialogueEnded;
|
||||
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // → EVT_NpcDialogueCompleted (npcId)
|
||||
[SerializeField] private VoidEventChannelSO _onLineStarted; // 每行开始打字时广播
|
||||
[SerializeField] private VoidEventChannelSO _onLineEnded; // 每行玩家确认后广播
|
||||
|
||||
private bool _skipRequested;
|
||||
|
||||
// ── 复用 Yield 指令,避免协程每行 new WaitUntil 闭包 ───────────────
|
||||
private sealed class WaitTypingOrSkip : CustomYieldInstruction
|
||||
{
|
||||
private readonly DialogueManager _m;
|
||||
public WaitTypingOrSkip(DialogueManager m) => _m = m;
|
||||
public override bool keepWaiting => _m._dialogueBox.IsTyping && !_m._skipRequested;
|
||||
}
|
||||
private sealed class WaitSkip : CustomYieldInstruction
|
||||
{
|
||||
private readonly DialogueManager _m;
|
||||
public WaitSkip(DialogueManager m) => _m = m;
|
||||
public override bool keepWaiting => !_m._skipRequested;
|
||||
}
|
||||
|
||||
private WaitTypingOrSkip _waitTypingOrSkip;
|
||||
private WaitSkip _waitSkip;
|
||||
|
||||
/// <summary>
|
||||
/// 当 IsDialogueActive 时排队等待的对话请求。
|
||||
/// 支持脚本触发的连续对话序列(如剧情链、事件链触发的对话),
|
||||
/// 但容量上限为 8,避免因误触导致无限排队。
|
||||
/// </summary>
|
||||
private readonly Queue<(DialogueSequenceSO seq, string npcId)> _pending = new();
|
||||
private const int PendingCapacity = 8;
|
||||
|
||||
/// <summary>当前是否有对话正在播放。</summary>
|
||||
public bool IsDialogueActive { get; private set; }
|
||||
|
||||
@@ -34,6 +62,8 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
if (ServiceLocator.GetOrDefault<IDialogueService>() != null) { Destroy(gameObject); return; }
|
||||
ServiceLocator.Register<IDialogueService>(this);
|
||||
_waitTypingOrSkip = new WaitTypingOrSkip(this);
|
||||
_waitSkip = new WaitSkip(this);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -49,25 +79,46 @@ namespace BaseGames.Dialogue
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_inputReader != null) _inputReader.SubmitEvent -= OnSubmit;
|
||||
_pending.Clear();
|
||||
|
||||
// 若对话协程在组件禁用或场景切换时仍在运行,Unity 会强制杀死协程但不调用
|
||||
// EndDialogue(),导致 Action Map 永久停留在 UI 模式。此处主动恢复。
|
||||
if (IsDialogueActive)
|
||||
{
|
||||
StopAllCoroutines();
|
||||
_dialogueBox?.Hide();
|
||||
IsDialogueActive = false;
|
||||
_inputReader?.EnableGameplayInput();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公开 API ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 启动对话序列。若已有对话在播放则忽略新请求。
|
||||
/// 启动对话序列。
|
||||
/// 若已有对话在播放,请求会进入等待队列(上限 <see cref="PendingCapacity"/>),
|
||||
/// 待当前对话结束后依序自动播放;超出上限的请求被丢弃。
|
||||
/// 由 InteractableNPC.Interact() 调用。
|
||||
/// </summary>
|
||||
/// <param name="sequence">要播放的对话序列 SO。</param>
|
||||
/// <param name="npcId">NPC 标识符,对话结束时随 EVT_NpcDialogueCompleted 广播。</param>
|
||||
public void StartDialogue(DialogueSequenceSO sequence, string npcId = "")
|
||||
{
|
||||
if (IsDialogueActive || sequence == null) return;
|
||||
if (sequence == null) return;
|
||||
if (IsDialogueActive)
|
||||
{
|
||||
if (_pending.Count < PendingCapacity)
|
||||
_pending.Enqueue((sequence, npcId));
|
||||
return;
|
||||
}
|
||||
PlayImmediate(sequence, npcId);
|
||||
}
|
||||
|
||||
private void PlayImmediate(DialogueSequenceSO sequence, string npcId)
|
||||
{
|
||||
IsDialogueActive = true;
|
||||
_skipRequested = false;
|
||||
|
||||
// 切换到 UI Action Map(禁用玩家移动输入)
|
||||
_inputReader.EnableUIInput();
|
||||
|
||||
_skipRequested = false;
|
||||
if (_inputReader != null) _inputReader.EnableUIInput();
|
||||
_onDialogueStarted?.Raise();
|
||||
StartCoroutine(PlaySequence(sequence, npcId));
|
||||
}
|
||||
@@ -80,21 +131,28 @@ namespace BaseGames.Dialogue
|
||||
|
||||
private IEnumerator PlaySequence(DialogueSequenceSO sequence, string npcId)
|
||||
{
|
||||
// 选择条件变体(查询 ConditionalVariant.conditionFlag — 未实现 WorldStateRegistry 查询时直接使用默认序列)
|
||||
var resolved = ResolveVariant(sequence);
|
||||
|
||||
if (resolved.lines == null || resolved.lines.Length == 0)
|
||||
{
|
||||
EndDialogue(npcId);
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var line in resolved.lines)
|
||||
{
|
||||
_skipRequested = false;
|
||||
_dialogueBox.ShowLine(line);
|
||||
_onLineStarted?.Raise();
|
||||
|
||||
// 等待打字完成,期间允许跳过
|
||||
yield return new WaitUntil(() => !_dialogueBox.IsTyping || _skipRequested);
|
||||
yield return _waitTypingOrSkip;
|
||||
if (_dialogueBox.IsTyping) _dialogueBox.SkipTyping();
|
||||
|
||||
// 等待玩家按 Submit 推进下一行
|
||||
_skipRequested = false;
|
||||
yield return new WaitUntil(() => _skipRequested);
|
||||
yield return _waitSkip;
|
||||
_onLineEnded?.Raise();
|
||||
}
|
||||
|
||||
EndDialogue(npcId);
|
||||
@@ -105,18 +163,24 @@ namespace BaseGames.Dialogue
|
||||
_dialogueBox.Hide();
|
||||
IsDialogueActive = false;
|
||||
|
||||
// 恢复 Gameplay Action Map
|
||||
_inputReader.EnableGameplayInput();
|
||||
if (_inputReader != null) _inputReader.EnableGameplayInput();
|
||||
|
||||
_onDialogueEnded?.Raise();
|
||||
|
||||
if (!string.IsNullOrEmpty(npcId))
|
||||
_onNpcDialogueCompleted?.Raise(npcId);
|
||||
|
||||
// 自动播放队首等待中的对话(脚本触发的连续序列)
|
||||
if (_pending.Count > 0)
|
||||
{
|
||||
var (seq, id) = _pending.Dequeue();
|
||||
PlayImmediate(seq, id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ConditionalVariant 选择正确的序列版本。
|
||||
/// 按顺序检查 variants:第一个满足 WorldStateRegistry 标志的变体胜出;
|
||||
/// 按顺序检查 variants:所有 requiredFlags 均满足的第一个变体胜出(AND 关系);
|
||||
/// 未注入 WorldStateRegistry 或无满足条件的变体时,返回原序列(默认)。
|
||||
/// </summary>
|
||||
private DialogueSequenceSO ResolveVariant(DialogueSequenceSO sequence)
|
||||
@@ -128,12 +192,31 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
foreach (var variant in sequence.variants)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(variant.conditionFlag)
|
||||
&& variant.sequence != null
|
||||
&& _worldState.HasFlag(variant.conditionFlag))
|
||||
return variant.sequence;
|
||||
if (variant.sequence == null) continue;
|
||||
if (variant.requiredFlags == null || variant.requiredFlags.Length == 0)
|
||||
return variant.sequence; // 无条件变体:直接采用
|
||||
|
||||
bool allMet = true;
|
||||
foreach (var flag in variant.requiredFlags)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(flag) && !_worldState.HasFlag(flag))
|
||||
{
|
||||
allMet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allMet) return variant.sequence;
|
||||
}
|
||||
}
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
else if (sequence.variants.Length > 0)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[DialogueManager] 序列 '{sequence.sequenceId}' 有 {sequence.variants.Length} 个条件变体," +
|
||||
"但 WorldStateRegistry 未注入,将使用默认序列。请检查 Inspector 中的 _worldState 字段。",
|
||||
this);
|
||||
}
|
||||
#endif
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
@@ -5,17 +5,38 @@ namespace BaseGames.Dialogue
|
||||
/// <summary>
|
||||
/// 对话行结构(架构 14_NarrativeModule §3)。
|
||||
/// 每行包含说话人、文本(本地化 Key)和可选的语音片段。
|
||||
///
|
||||
/// 推荐通过 actor 引用 DialogueActorSO 统一管理头像/名称;
|
||||
/// speakerNameKey / portraitSprite 作为无 SO 时的直接覆盖(保持向后兼容)。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct DialogueLine
|
||||
{
|
||||
public string speakerNameKey; // 本地化 key(如 "NPC_Elder_Name")
|
||||
[Tooltip("说话人角色(推荐)。actor 优先;留空时回退到 speakerNameKey / portraitSprite")]
|
||||
public DialogueActorSO actor;
|
||||
|
||||
[Tooltip("说话人本地化 key,留空时使用 actor.nameKey")]
|
||||
public string speakerNameKey;
|
||||
[TextArea(2, 5)]
|
||||
public string textKey; // 本地化文本 key(如 "DLG_Elder_001")
|
||||
public Sprite portraitSprite; // 可选说话人头像
|
||||
|
||||
[Tooltip("说话人头像,留空时使用 actor.portrait")]
|
||||
public Sprite portraitSprite;
|
||||
public AudioClip voiceClip; // 可选语音
|
||||
[Min(0.01f)]
|
||||
public float typewriterDelay; // 每字符延迟(秒,0 = 使用默认 0.03f)
|
||||
|
||||
/// <summary>
|
||||
/// 获取最终使用的说话人名称 Key:actor 优先,回退到直接字段。
|
||||
/// </summary>
|
||||
public string ResolvedNameKey => actor != null && !string.IsNullOrEmpty(actor.nameKey)
|
||||
? actor.nameKey : speakerNameKey;
|
||||
|
||||
/// <summary>
|
||||
/// 获取最终使用的头像:actor 优先,回退到直接字段。
|
||||
/// </summary>
|
||||
public Sprite ResolvedPortrait => actor != null && actor.portrait != null
|
||||
? actor.portrait : portraitSprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,13 +50,66 @@ namespace BaseGames.Dialogue
|
||||
public string sequenceId; // 全局唯一,如 "DLG_Elder_Quest_Available"
|
||||
public DialogueLine[] lines;
|
||||
|
||||
/// <summary>条件变体:满足特定世界标志时替换整个序列。</summary>
|
||||
/// <summary>
|
||||
/// 条件变体:所有 requiredFlags 均满足时替换整个序列(AND 关系)。
|
||||
/// 与 NarrativeNPC.DialogueVersion 的多条件语义保持一致。
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct ConditionalVariant
|
||||
{
|
||||
public string conditionFlag; // WorldState flag key
|
||||
[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<string, string> s_seqIdToPath;
|
||||
private static double s_seqIdsCacheTime = -10.0;
|
||||
|
||||
private static System.Collections.Generic.Dictionary<string, string> 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<string, string>(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<DialogueSequenceSO>(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ namespace BaseGames.Dialogue
|
||||
private DialogueLine _currentLine;
|
||||
private const float DefaultTypewriterDelay = 0.03f;
|
||||
|
||||
// 缓存单个 WaitForSecondsRealtime,避免 TypeLine 每字符 new 分配。
|
||||
// 当 delay 值改变时才重新创建(不同行可能有不同打字速度)。
|
||||
private WaitForSecondsRealtime _cachedTypeDelay;
|
||||
private float _cachedTypeDelayValue = -1f;
|
||||
|
||||
/// <summary>当前是否仍在执行打字机效果。</summary>
|
||||
public bool IsTyping { get; private set; }
|
||||
|
||||
@@ -35,20 +40,22 @@ namespace BaseGames.Dialogue
|
||||
public void ShowLine(DialogueLine line)
|
||||
{
|
||||
_currentLine = line;
|
||||
_rootPanel.SetActive(true);
|
||||
_continuePrompt.SetActive(false);
|
||||
if (_rootPanel != null) _rootPanel.SetActive(true);
|
||||
if (_continuePrompt != null) _continuePrompt.SetActive(false);
|
||||
|
||||
// 说话人名称
|
||||
bool hasSpeaker = !string.IsNullOrEmpty(line.speakerNameKey);
|
||||
// 说话人名称(actor 优先,回退到直接字段)
|
||||
string resolvedNameKey = line.ResolvedNameKey;
|
||||
bool hasSpeaker = !string.IsNullOrEmpty(resolvedNameKey);
|
||||
if (_speakerNamePanel != null) _speakerNamePanel.SetActive(hasSpeaker);
|
||||
if (hasSpeaker && _speakerNameText != null)
|
||||
_speakerNameText.text = LocalizationManager.Get(line.speakerNameKey, "Dialogue");
|
||||
_speakerNameText.text = LocalizationManager.Get(resolvedNameKey, "Dialogue");
|
||||
|
||||
// 头像
|
||||
// 头像(actor 优先,回退到直接字段)
|
||||
var resolvedPortrait = line.ResolvedPortrait;
|
||||
if (_speakerPortrait != null)
|
||||
{
|
||||
_speakerPortrait.gameObject.SetActive(line.portraitSprite != null);
|
||||
if (line.portraitSprite != null) _speakerPortrait.sprite = line.portraitSprite;
|
||||
_speakerPortrait.gameObject.SetActive(resolvedPortrait != null);
|
||||
if (resolvedPortrait != null) _speakerPortrait.sprite = resolvedPortrait;
|
||||
}
|
||||
|
||||
// 语音播放
|
||||
@@ -95,6 +102,14 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
IsTyping = true;
|
||||
float delay = line.typewriterDelay > 0f ? line.typewriterDelay : DefaultTypewriterDelay;
|
||||
|
||||
// 复用缓存的 WaitForSecondsRealtime;仅当 delay 值变化时才重新 new
|
||||
if (_cachedTypeDelay == null || !Mathf.Approximately(_cachedTypeDelayValue, delay))
|
||||
{
|
||||
_cachedTypeDelay = new WaitForSecondsRealtime(delay);
|
||||
_cachedTypeDelayValue = delay;
|
||||
}
|
||||
|
||||
string text = LocalizationManager.Get(line.textKey ?? "", "Dialogue");
|
||||
|
||||
// 性能:StringBuilder 避免每帧字符串分配(O(n²) → O(n))
|
||||
@@ -105,7 +120,7 @@ namespace BaseGames.Dialogue
|
||||
{
|
||||
sb.Append(c);
|
||||
if (_dialogueText != null) _dialogueText.SetText(sb); // TMP SetText(StringBuilder) 零分配
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
yield return _cachedTypeDelay;
|
||||
}
|
||||
|
||||
IsTyping = false;
|
||||
|
||||
@@ -26,6 +26,9 @@ namespace BaseGames.Dialogue
|
||||
return version.dialogue;
|
||||
}
|
||||
|
||||
if (_fallbackDialogue == null)
|
||||
Debug.LogWarning($"[NarrativeNPC] '{name}' 没有版本满足当前条件,且未配置兜底对话 (_fallbackDialogue)。", gameObject);
|
||||
|
||||
return _fallbackDialogue;
|
||||
}
|
||||
}
|
||||
@@ -43,9 +46,11 @@ namespace BaseGames.Dialogue
|
||||
public DialogueSequenceSO dialogue;
|
||||
|
||||
[Tooltip("全部满足才激活此版本(AND 关系)")]
|
||||
[WorldStateFlag]
|
||||
public string[] requiredFlags;
|
||||
|
||||
[Tooltip("有任意一个 = 此版本不激活(NOT 关系)")]
|
||||
[WorldStateFlag]
|
||||
public string[] blockedByFlags;
|
||||
|
||||
/// <summary>检查此版本的激活条件(AND requiredFlags / NOT blockedByFlags)。</summary>
|
||||
|
||||
10
Assets/_Game/Scripts/Dialogue/WorldStateFlagAttribute.cs
Normal file
10
Assets/_Game/Scripts/Dialogue/WorldStateFlagAttribute.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Dialogue
|
||||
{
|
||||
/// <summary>
|
||||
/// 标记一个 string 或 string[] 字段为世界状态标志 Key。
|
||||
/// 在 Inspector 中会显示已知标志下拉菜单,支持直接输入新标志。
|
||||
/// </summary>
|
||||
public sealed class WorldStateFlagAttribute : PropertyAttribute { }
|
||||
}
|
||||
Reference in New Issue
Block a user