using System.Collections;
using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Input;
using BaseGames.World;
using UnityEngine;
namespace BaseGames.Dialogue
{
///
/// 对话管理器(架构 14_NarrativeModule §4)。
/// 驱动 DialogueUI 打字机效果;管理 Action Map 切换;向 QuestManager 广播对话完成事件。
/// 在 Awake 中注册到 ServiceLocator。
///
public class DialogueManager : MonoBehaviour, IDialogueService
{
[Header("依赖")]
[Tooltip("对话 UI 组件。负责打字机效果、头像/说话人渲染、显示隐藏。")]
[SerializeField] private DialogueUI _dialogueBox;
[Tooltip("输入读取器 SO。监听 SubmitEvent(确认/跳过)推进对话行。")]
[SerializeField] private InputReaderSO _inputReader;
[Tooltip("世界状态注册表 SO。对话序列 variants 条件分支据此读取标志位。")]
[SerializeField] private WorldStateRegistry _worldState;
[Header("事件频道")]
[Tooltip("EVT_DialogueStarted:对话序列开始时广播(无 payload)。供输入系统切换 Action Map 至 UI 模式等监听。")]
[SerializeField] private VoidEventChannelSO _onDialogueStarted;
[Tooltip("EVT_DialogueEnded:对话序列全部行播完后广播(无 payload)。\n" +
"【重要】输入系统应监听此事件切回 Gameplay Action Map;\n" +
"若未连接此频道,DialogueManager 会直接调用 InputReader.EnableGameplayInput() 作为兜底。")]
[SerializeField] private VoidEventChannelSO _onDialogueEnded;
[Tooltip("EVT_NpcDialogueCompleted:payload = npcId(string)。每段对话结束时广播,驱动 QuestManager 中对话类目标进度。")]
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted;
[Tooltip("EVT_LineStarted:每行对话开始打字时广播(无 payload)。供音效/震动/打字音效系统监听。")]
[SerializeField] private VoidEventChannelSO _onLineStarted;
[Tooltip("EVT_LineEnded:玩家按确认键推进到下一行时广播(无 payload)。供音效/震动系统监听。")]
[SerializeField] private VoidEventChannelSO _onLineEnded;
[Tooltip("EVT_DialogueForceEnded:对话序列因超时被强制终止时广播(payload = npcId)。\n" +
"供埋点/异常追踪系统监听,以区分正常结束和超时强制中断。\n" +
"可选字段,留空则不广播此专用事件(ForceEnd 仍正常执行)。")]
[SerializeField] private StringEventChannelSO _onDialogueForceEnded;
[Tooltip("EVT_DialogueChoiceSelected:玩家选择对话选项时广播(payload = \"sequenceId/choiceIndex\")。\n" +
"供 QA 埋点、成就系统、或数据分析监听,以还原玩家的对话选择路径。\n" +
"可选字段,留空则不广播。")]
[SerializeField] private StringEventChannelSO _onDialogueChoiceSelected;
[Header("运行时限制")]
[Tooltip("分支选项最大嵌套深度。超过此深度触发循环引用保护,跳过当前分支继续播放。\n" +
"普通对话通常不超过 6 层;极端场景可调高,但推荐保持默认值 16。")]
[Min(1)] [SerializeField] private int _maxChoiceDepth = 16;
[Tooltip("待播对话序列队列容量上限。超过后新请求将被丢弃并记录警告。\n" +
"用于防止事件链或脚本误触导致无限排队。")]
[Min(1)] [SerializeField] private int _pendingQueueCapacity = 8;
[Tooltip("单段对话序列的最长播放时间(秒)。超时后强制结束当前序列,防止异常卡死。\n" +
"0 = 不限时(不推荐用于正式发布)。推荐 300s(5 分钟)覆盖最长剧情段落。")]
[Min(0)] [SerializeField] private float _sequenceTimeoutSeconds = 300f;
private bool _skipRequested;
private int _selectedChoiceIndex = -1;
private int _choiceDepth;
///
/// 每次 PlayImmediate 递增。HandleChoices 的选项回调在写入 _selectedChoiceIndex 前
/// 比对此值,确保打断后遗留的回调不会污染新序列的状态。
///
private int _playbackId;
// ── 一次性对话完成回调 ────────────────────────────────────────────────
// 通过 StartDialogue(..., onComplete) 注册;OnDialogueEnded 触发后调用一次后清空。
private System.Action _onCompleteCallback;
// ── 子协程通信字段(避免协程间 ref/out 参数)─────────────────────────
/// HandleChoices 子协程写入结果:玩家选中选项后的后续序列(null = 无后续)。
private DialogueSequenceSO _choiceBranchResult;
/// HandleChoices 子协程写入结果:true = 分支深度超限,优雅降级(继续播放后续行)。
private bool _branchDepthExceeded;
/// 当前正在播放对话的 NPC ID(无对话时为 null)。供外部系统主动查询"谁在说话"。
private string _currentNpcId;
// ── 复用 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;
}
// 等待玩家从分支选项中做出选择(_selectedChoiceIndex >= 0 时解除阻塞)
private sealed class WaitForChoice : CustomYieldInstruction
{
private readonly DialogueManager _m;
public WaitForChoice(DialogueManager m) => _m = m;
public override bool keepWaiting => _m._selectedChoiceIndex < 0;
}
private WaitTypingOrSkip _waitTypingOrSkip;
private WaitSkip _waitSkip;
private WaitForChoice _waitForChoice;
// 延迟 0.15s 防止玩家快速连击穿透:跳过打字机后立即触发选项0
private WaitForSeconds _waitChoiceInputGuard;
// 超时守卫等待指令(与 _sequenceTimeoutSeconds 同步,在 Awake 初始化,避免每次 PlayImmediate 分配)
private WaitForSeconds _waitSequenceTimeout;
///
/// 当 IsDialogueActive 时排队等待的对话请求。
/// 支持脚本触发的连续对话序列(如剧情链、事件链触发的对话),
/// 但容量上限为 8,避免因误触导致无限排队。
/// 使用 List 而非 Queue,以支持基于优先级的抢占式淘汰(队满时丢弃最低优先级项目)。
///
private readonly List<(DialogueSequenceSO seq, string npcId, int priority)> _pending = new();
/// 当前是否有对话正在播放。
public bool IsDialogueActive { get; private set; }
///
/// 当前正在播放对话的 NPC ID。无对话活跃时为 。
/// 供地图标记、HUD、分析埋点等外部系统主动查询"当前谁在说话",无需订阅事件。
///
public string CurrentNpcId => _currentNpcId;
/// 当前正在播放的对话优先级(0 = 默认)。高优先级请求可打断低优先级。
private int _currentPriority;
///
public event System.Action OnDialogueEnded;
// ── 生命周期 ──────────────────────────────────────────────────────
private void Awake()
{
if (ServiceLocator.GetOrDefault() != null) { Destroy(gameObject); return; }
ServiceLocator.Register(this);
_waitTypingOrSkip = new WaitTypingOrSkip(this);
_waitSkip = new WaitSkip(this);
_waitForChoice = new WaitForChoice(this);
_waitChoiceInputGuard = new WaitForSeconds(0.15f);
if (_sequenceTimeoutSeconds > 0f)
_waitSequenceTimeout = new WaitForSeconds(_sequenceTimeoutSeconds);
}
private void OnDestroy()
{
ServiceLocator.Unregister(this);
}
private void OnEnable()
{
if (_inputReader != null) _inputReader.SubmitEvent += OnSubmit;
}
private void OnDisable()
{
if (_inputReader != null) _inputReader.SubmitEvent -= OnSubmit;
// 若对话协程在组件禁用或场景切换时仍在运行,Unity 会强制杀死协程但不调用
// EndDialogue(),导致 Action Map 永久停留在 UI 模式。复用 ForceEnd() 统一处理。
ForceEnd();
}
// ── 公开 API ──────────────────────────────────────────────────────
///
/// 启动对话序列。
/// 若已有对话在播放:
/// - 当 高于当前对话优先级时,立即打断并播放新序列;
/// - 否则进入等待队列(上限 ),超出上限的请求被丢弃。
/// 由 InteractableNPC.Interact() 调用。
///
/// 要播放的对话序列 SO。
/// NPC 标识符,对话结束时随 EVT_NpcDialogueCompleted 广播。
/// 优先级。数值越大越优先;相同或更低优先级不会打断当前对话。
public void StartDialogue(DialogueSequenceSO sequence, string npcId = "", int priority = 0)
{
if (sequence == null) return;
if (IsDialogueActive)
{
// 高优先级:打断当前对话,立即播放
if (priority > _currentPriority)
{
StopAllCoroutines();
_dialogueBox?.HideChoices();
IsDialogueActive = false;
_skipRequested = false;
_selectedChoiceIndex = -1;
_choiceDepth = 0;
// 不清空队列,被打断的对话之后仍可继续播放
PlayImmediate(sequence, npcId, priority);
return;
}
if (_pending.Count < _pendingQueueCapacity)
{
_pending.Add((sequence, npcId, priority));
}
else
{
// 队满时:查找优先级最低的项目,若新请求优先级更高则淘汰之,否则丢弃新请求
int minIdx = 0;
for (int i = 1; i < _pending.Count; i++)
{
if (_pending[i].priority < _pending[minIdx].priority) minIdx = i;
}
if (priority > _pending[minIdx].priority)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning(
$"[DialogueManager] 待播队列已满(容量 {_pendingQueueCapacity})," +
$"序列 '{_pending[minIdx].seq.sequenceId}'(优先级 {_pending[minIdx].priority})" +
$"被优先级更高的 '{sequence.sequenceId}'(优先级 {priority})淘汰。");
#endif
_pending.RemoveAt(minIdx);
_pending.Add((sequence, npcId, priority));
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
else
Debug.LogWarning(
$"[DialogueManager] 待播队列已满(容量 {_pendingQueueCapacity})," +
$"序列 '{sequence.sequenceId}'(优先级 {priority})被丢弃,队列中最低优先级为 {_pending[minIdx].priority}。");
#endif
}
return;
}
PlayImmediate(sequence, npcId, priority);
}
///
public void StartDialogue(DialogueSequenceSO sequence, string npcId, int priority, System.Action onComplete)
{
if (onComplete != null)
{
// 若已有待回调,链式追加(不覆盖),保证先来先到
_onCompleteCallback += onComplete;
}
StartDialogue(sequence, npcId, priority);
}
private void PlayImmediate(DialogueSequenceSO sequence, string npcId, int priority = 0)
{
IsDialogueActive = true;
_currentNpcId = npcId;
_currentPriority = priority;
_skipRequested = false;
_selectedChoiceIndex = -1;
_choiceDepth = 0;
_playbackId++;
if (_inputReader != null) _inputReader.EnableUIInput();
_onDialogueStarted?.Raise();
StartCoroutine(PlaySequence(sequence, npcId));
// 启动超时守卫(0 = 不限时)
if (_sequenceTimeoutSeconds > 0f)
StartCoroutine(SequenceTimeoutGuard(npcId));
}
///
/// 超时守卫协程:若对话在 内未正常结束,
/// 强制终止并记录错误,防止游戏卡死在对话状态。
///
private IEnumerator SequenceTimeoutGuard(string npcId)
{
yield return _waitSequenceTimeout ?? new WaitForSeconds(_sequenceTimeoutSeconds);
if (!IsDialogueActive) yield break;
Debug.LogError(
$"[DialogueManager] 对话序列 (npcId='{npcId}') 超时 {_sequenceTimeoutSeconds}s 未结束," +
"强制终止。请检查是否存在无法退出的等待分支。");
_onDialogueForceEnded?.Raise(npcId);
ForceEnd();
}
///
/// 强制立即终止当前对话,清空等待队列,恢复游戏输入。
/// 场景切换/演出打断时调用;若无对话活跃则无操作。
///
public void ForceEnd()
{
if (!IsDialogueActive) return;
StopAllCoroutines();
_playbackId++; // 使所有残余的选项回调失效,防止下一帧写入新序列状态
_pending.Clear();
_dialogueBox?.HideChoices();
_dialogueBox?.Hide();
IsDialogueActive = false;
_currentNpcId = null;
_currentPriority = 0;
_skipRequested = false;
_selectedChoiceIndex = -1;
_choiceDepth = 0;
// 优先通过 _onDialogueEnded 事件让 InputManager 决定如何恢复输入;
// 若未连接事件频道(旧场景配置),直接恢复 Gameplay 输入作为兜底。
_onDialogueEnded?.Raise();
if (_onDialogueEnded == null) _inputReader?.EnableGameplayInput();
OnDialogueEnded?.Invoke();
// 触发一次性完成回调(正常结束和强制中断均触发)
var cb = _onCompleteCallback;
_onCompleteCallback = null;
cb?.Invoke();
}
// ── 输入回调 ──────────────────────────────────────────────────────
private void OnSubmit() => _skipRequested = true;
// ── 内部协程 ──────────────────────────────────────────────────────
private IEnumerator PlaySequence(DialogueSequenceSO startSequence, string npcId)
{
if (_dialogueBox == null)
{
Debug.LogError("[DialogueManager] _dialogueBox 未配置,对话无法显示。请在 Inspector 中指定 DialogueUI 组件。", this);
EndDialogue(npcId);
yield break;
}
// 使用显式序列栈替代递归,防止深链(100+ 序列)时 C# 调用栈溢出
var sequenceStack = new System.Collections.Generic.Stack();
sequenceStack.Push(startSequence);
while (sequenceStack.Count > 0)
{
var sequence = sequenceStack.Pop();
var resolved = ResolveVariant(sequence);
if (resolved.lines == null || resolved.lines.Length == 0)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning(
$"[DialogueManager] 序列 '{resolved.sequenceId}' 没有对话行(lines 为空)。" +
"对话将静默跳过此序列,可能是未完成配置。");
#endif
continue;
}
bool branchChosen = false;
foreach (var line in resolved.lines)
{
yield return StartCoroutine(PlayOneLine(line));
if (line.choices != null && line.choices.Length > 0)
{
yield return StartCoroutine(HandleChoices(line, resolved.sequenceId));
if (!_branchDepthExceeded)
{
if (_choiceBranchResult != null)
sequenceStack.Push(_choiceBranchResult);
branchChosen = true;
break;
}
// 深度超限:优雅降级,继续播放当前序列后续行
continue;
}
// 普通行:等待玩家按 Submit 推进
_skipRequested = false;
yield return _waitSkip;
_onLineEnded?.Raise();
}
_ = branchChosen;
}
EndDialogue(npcId);
}
///
/// 显示一行对话并等待打字机效果完成(期间允许跳过)。
/// 广播 EVT_LineStarted。不广播 EVT_LineEnded(由调用方在推进后广播)。
///
private IEnumerator PlayOneLine(DialogueLine line)
{
_skipRequested = false;
_dialogueBox.ShowLine(line);
_onLineStarted?.Raise();
yield return _waitTypingOrSkip;
if (_dialogueBox.IsTyping) _dialogueBox.SkipTyping();
}
///
/// 显示分支选项,等待玩家选择,并将结果写入 。
/// 若选项嵌套深度超过 ,将
/// 置为 true 并立即返回,调用方应优雅降级继续播放后续行而不是终止对话。
///
private IEnumerator HandleChoices(DialogueLine line, string sequenceId)
{
_choiceBranchResult = null;
_branchDepthExceeded = false;
_choiceDepth++;
if (_choiceDepth >= _maxChoiceDepth)
{
Debug.LogError(
$"[DialogueManager] 分支对话深度超过 {_maxChoiceDepth}," +
$"序列 \"{sequenceId}\" 可能存在循环引用。" +
"已跳过当前选项分支,继续播放后续内容。");
_dialogueBox?.HideChoices();
_skipRequested = false;
_selectedChoiceIndex = -1;
_branchDepthExceeded = true;
yield break;
}
// 清除打字机阶段积压的输入,防止选项显示后被立即误触发
_skipRequested = false;
_selectedChoiceIndex = -1;
// 延迟 0.15s:确保此前积压的"确认键"输入已被彻底消耗,
// 防止快速连击(跳过打字机→立即误选选项0)的穿透问题。
// 使用预创建的缓存实例,避免每次分配 WaitForSeconds 对象。
yield return _waitChoiceInputGuard;
// 捕获当前播放标记,防止被打断后遗留回调写入新序列的选择索引
int capturedId = _playbackId;
_dialogueBox.ShowChoices(line.choices, idx =>
{
if (_playbackId == capturedId) _selectedChoiceIndex = idx;
});
yield return _waitForChoice;
_dialogueBox.HideChoices();
_skipRequested = false;
_onLineEnded?.Raise();
var chosen = line.choices[_selectedChoiceIndex];
// 广播选择事件(供 QA 埋点、成就系统、数据分析使用)
_onDialogueChoiceSelected?.Raise($"{sequenceId}/{_selectedChoiceIndex}");
// 可选:将世界状态标志写入 WorldStateRegistry
if (!string.IsNullOrEmpty(chosen.setWorldFlag) && _worldState != null)
_worldState.SetFlag(chosen.setWorldFlag);
_choiceBranchResult = chosen.nextSequence;
}
private void EndDialogue(string npcId)
{
_dialogueBox?.Hide();
IsDialogueActive = false;
_currentNpcId = null;
_currentPriority = 0;
// 优先通过 _onDialogueEnded 事件让 InputManager 决定如何恢复输入;
// 若未连接事件频道(旧场景配置),直接恢复 Gameplay 输入作为兜底。
_onDialogueEnded?.Raise();
if (_onDialogueEnded == null) _inputReader?.EnableGameplayInput();
OnDialogueEnded?.Invoke();
if (!string.IsNullOrEmpty(npcId))
_onNpcDialogueCompleted?.Raise(npcId);
// 触发一次性完成回调(正常结束和强制中断均触发)
var cb = _onCompleteCallback;
_onCompleteCallback = null;
cb?.Invoke();
// 自动播放优先级最高的等待中对话(保证高优先级对话不被低优先级插队)
if (_pending.Count > 0)
{
int best = 0;
for (int i = 1; i < _pending.Count; i++)
if (_pending[i].priority > _pending[best].priority) best = i;
var item = _pending[best];
_pending.RemoveAt(best);
PlayImmediate(item.seq, item.npcId, item.priority);
}
}
///
/// 根据 WorldState 标志选择正确的序列版本。
/// 委托给 统一处理,消除重复逻辑。
///
private DialogueSequenceSO ResolveVariant(DialogueSequenceSO sequence)
{
var resolved = sequence.TryGetActiveVariant(_worldState);
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (resolved == sequence && sequence.variants != null && sequence.variants.Length > 0 && _worldState == null)
Debug.LogWarning(
$"[DialogueManager] 序列 '{sequence.sequenceId}' 有 {sequence.variants.Length} 个条件变体," +
"但 WorldStateRegistry 未注入,将使用默认序列。请检查 Inspector 中的 _worldState 字段。",
this);
#endif
return resolved;
}
}
}