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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user