Files
zeling_v2/Docs/Design/15_DialogueSystem.md
2026-05-08 11:04:00 +08:00

25 KiB
Raw Permalink Blame History

15 · 对话系统与交互接口

命名空间 BaseGames.Dialogue
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.UIDialogueBox· BaseGames.InputAction Map 切换)


目录

  1. 系统总览
  2. IInteractable — 通用交互接口
  3. 交互提示 UIInteractionPrompt
  4. 数据结构DialogueLine / DialogueSequenceSO
  5. DialogueManager
  6. DialogueUI对话框组件
  7. InteractableNPC
  8. 对话触发条件
  9. 输入 Action Map 切换
  10. 与 ShopNPC 集成
  11. 事件频道
  12. 编辑器友好设计
  13. NPC 好感度系统

1. 系统总览

对话系统职责:
  ├─ IInteractable 接口    → 统一所有可交互物件NPC/告示牌/机关)的交互入口
  ├─ InteractionPrompt UI  → 玩家靠近可交互物件时显示"按 [F] 交互"提示
  ├─ DialogueSequenceSO    → 对话内容数据化,策划直接在 Inspector 中编辑台词
  ├─ DialogueManager       → 管理对话播放状态,驱动 DialogueUI
  ├─ DialogueUI            → 打字机效果文字框、说话人名称、继续提示
  └─ 对话条件系统          → 根据游戏进程显示不同对话内容P1 分支)

2. IInteractable — 通用交互接口

统一所有可交互物件的接口,PlayerInteractState 只需知道此接口:

namespace BaseGames.World
{
    public interface IInteractable
    {
        // 是否当前可以被交互(如 ShopNPC 战斗中不可交互)
        bool CanInteract { get; }

        // 玩家按下交互键时调用
        void Interact(Transform player);

        // 玩家进入/离开交互范围时调用(用于驱动 InteractionPrompt 显隐)
        void OnPlayerEnterRange(Transform player);
        void OnPlayerExitRange();
    }
}

已实现 IInteractable 的组件

组件 说明
SavePoint 存档(已存在,补充实现 IInteractable
InteractableNPC 触发对话序列
ShopNPC 先触发对话,再打开商店 UI
AbilityUnlock 拾取能力道具(已存在,补充实现 IInteractable
DirectionalInteractable 机关/开关(见 08_WorldSystem.md §9.6
Sign 告示牌显示单行静态文字IInteractable 最简实现)

3. 交互提示 UIInteractionPrompt

当玩家进入 IInteractable 的交互范围Trigger Collider在物件上方显示交互提示图标

InteractionPrompt PrefabWorld Space Canvas
├── Icon         (Sprite键盘 [F] / 手柄 [A] 图标,随当前输入设备切换)
└── Label        (TextMeshPro"交互" 或留空)

挂载方式:每个 IInteractable GameObject 下都挂载一个 InteractionPrompt 子对象Prefab 实例),默认隐藏:

public class InteractionPromptController : MonoBehaviour
{
    [SerializeField] GameObject _promptRoot;
    [SerializeField] Image      _icon;
    [SerializeField] Sprite     _keyboardIcon;
    [SerializeField] Sprite     _gamepadIcon;

    public void Show()
    {
        _promptRoot.SetActive(true);
        // 根据当前活跃输入设备切换图标
        bool isGamepad = /* InputSystem.devices 检查 */;
        _icon.sprite = isGamepad ? _gamepadIcon : _keyboardIcon;
    }

    public void Hide() => _promptRoot.SetActive(false);
}

检测范围:独立的 CircleCollider2DTrigger半径约 1.5 单位,检测 Player 层,比战斗 HurtBox 更大,保证靠近就显示提示。


4. 数据结构DialogueLine / DialogueSequenceSO

DialogueLine单行对话数据

[Serializable]
public class DialogueLine
{
    public string      speakerName;      // 说话人名字(空字符串 = 旁白,不显示名称框)
    public Sprite      speakerPortrait;  // P1说话人头像
    [TextArea(3, 6)]
    public string      text;            // 对话文本(支持 <color>、<b> 等 TextMeshPro 富文本标签)
    public float       displaySpeed = 0.03f;  // 打字机速度(秒/字)
    public AudioEventSO voiceSFX;       // P1语音音效如每次出字播放的 "咚咚" 音)
}

DialogueSequenceSO

[CreateAssetMenu(menuName = "Dialogue/DialogueSequence")]
public class DialogueSequenceSO : ScriptableObject
{
    public DialogueLine[] lines;

    [Header("触发条件P1 分支对话)")]
    public DialogueCondition[] conditions;   // 满足条件时才使用此序列(否则跳到下一个)
}

资产路径:Assets/ScriptableObjects/Dialogue/DS_{NpcName}_{Context}.asset


5. DialogueManager

DialogueManager 常驻 Persistent 场景,管理对话播放状态:

namespace BaseGames.Dialogue
{
    public class DialogueManager : MonoBehaviour
    {
        public static DialogueManager Instance { get; private set; }

        [SerializeField] DialogueUI _dialogueUI;
        [SerializeField] VoidEventChannelSO _onDialogueStarted;
        [SerializeField] VoidEventChannelSO _onDialogueEnded;
        [SerializeField] InputReaderSO _inputReader;

        bool _isPlaying;
        DialogueSequenceSO _currentSequence;
        int _currentLineIndex;

        public bool IsPlaying => _isPlaying;

        public void StartDialogue(DialogueSequenceSO sequence)
        {
            if (_isPlaying) return;
            _isPlaying       = true;
            _currentSequence = sequence;
            _currentLineIndex = 0;

            _inputReader.EnableGameplayInput(false);
            _inputReader.EnableUIInput(true);          // 切换到 UI Action Map
            _inputReader.ConfirmEvent += OnConfirm;   // [F]/[A] 确认/跳过

            _onDialogueStarted.Raise();
            ShowCurrentLine();
        }

        void ShowCurrentLine()
        {
            if (_currentLineIndex >= _currentSequence.lines.Length)
            {
                EndDialogue();
                return;
            }
            _dialogueUI.ShowLine(_currentSequence.lines[_currentLineIndex]);
        }

        // 玩家按确认键:若打字机未完成则立即显示全文;若已完成则翻页
        void OnConfirm()
        {
            if (_dialogueUI.IsTyping)
                _dialogueUI.SkipTyping();
            else
            {
                _currentLineIndex++;
                ShowCurrentLine();
            }
        }

        void EndDialogue()
        {
            _isPlaying = false;
            _inputReader.ConfirmEvent -= OnConfirm;
            _inputReader.EnableUIInput(false);
            _inputReader.EnableGameplayInput(true);
            _dialogueUI.Hide();
            _onDialogueEnded.Raise();
        }
    }
}

6. DialogueUI对话框组件

挂载在 Canvas_Overlay 下的 DialogueBox 子对象(见 10_UISystem.md §2

public class DialogueUI : MonoBehaviour
{
    [SerializeField] GameObject  _rootPanel;
    [SerializeField] TMP_Text    _speakerNameText;
    [SerializeField] TMP_Text    _dialogueText;
    [SerializeField] GameObject  _speakerNamePanel;  // 无名称时隐藏整个名称框
    [SerializeField] GameObject  _continuePrompt;    // "▼" 图标,打字完成后显示
    [SerializeField] Image       _speakerPortrait;   // P1人物头像框

    Coroutine _typingCoroutine;
    public bool IsTyping { get; private set; }

    public void ShowLine(DialogueLine line)
    {
        _rootPanel.SetActive(true);
        _continuePrompt.SetActive(false);

        bool hasSpeaker = !string.IsNullOrEmpty(line.speakerName);
        _speakerNamePanel.SetActive(hasSpeaker);
        if (hasSpeaker) _speakerNameText.text = line.speakerName;

        if (_typingCoroutine != null) StopCoroutine(_typingCoroutine);
        _typingCoroutine = StartCoroutine(TypeLine(line));
    }

    IEnumerator TypeLine(DialogueLine line)
    {
        IsTyping = true;
        // 性能:使用 StringBuilder 避免每帧字符串分配O(n²) → O(n)
        var sb = new System.Text.StringBuilder(line.text.Length);
        _dialogueText.text = "";
        foreach (char c in line.text)
        {
            sb.Append(c);
            _dialogueText.SetText(sb);          // TMP SetText(StringBuilder) 零分配
            yield return new WaitForSecondsRealtime(line.displaySpeed);
        }
        IsTyping = false;
        _continuePrompt.SetActive(true);
    }

    public void SkipTyping()
    {
        if (_typingCoroutine != null) StopCoroutine(_typingCoroutine);
        _dialogueText.text = _currentLine?.text ?? "";
        IsTyping = false;
        _continuePrompt.SetActive(true);
    }

    public void Hide() => _rootPanel.SetActive(false);

    DialogueLine _currentLine;   // ShowLine 时缓存以供 SkipTyping 使用
}

对话框布局

DialogueBox固定在屏幕底部Canvas_Overlay
├── DialogueBG           (半透明黑色面板,圆角,内边距 20px)
├── SpeakerNamePanel     (左上角,有说话人时显示)
│   └── SpeakerNameText  (TextMeshPro像素字体白色)
├── DialogueText         (TextMeshPro像素字体白色最大 3 行)
├── PortraitFrame        (P1右侧人物头像框)
└── ContinuePrompt       (右下角 "▼" 动画图标,打字完成后出现)

7. InteractableNPC

NPC 交互的通用实现,继承 IInteractable

public class InteractableNPC : MonoBehaviour, IInteractable
{
    [SerializeField] DialogueSequenceSO[] _dialogueSequences;   // 多段对话按顺序播放
    [SerializeField] InteractionPromptController _prompt;

    int _currentSequenceIndex = 0;
    bool _canInteract = true;

    public bool CanInteract => _canInteract;

    public void OnPlayerEnterRange(Transform player) => _prompt.Show();
    public void OnPlayerExitRange()                   => _prompt.Hide();

    public void Interact(Transform player)
    {
        if (!_canInteract || DialogueManager.Instance.IsPlaying) return;

        // 选取当前有效的对话序列P1条件检查
        var sequence = GetCurrentSequence();
        if (sequence == null) return;

        DialogueManager.Instance.StartDialogue(sequence);
        // 订阅对话结束事件,推进对话索引
        _onDialogueEnded.OnEventRaised += OnDialogueEnded;
    }

    void OnDialogueEnded()
    {
        _onDialogueEnded.OnEventRaised -= OnDialogueEnded;
        // 推进到下一段对话(如已是最后一段则保持在最后一段,重复播放)
        if (_currentSequenceIndex < _dialogueSequences.Length - 1)
            _currentSequenceIndex++;
    }

    DialogueSequenceSO GetCurrentSequence()
        => _currentSequenceIndex < _dialogueSequences.Length
            ? _dialogueSequences[_currentSequenceIndex]
            : null;
}

NPC 面朝玩家

对话开始时NPC 自动翻转 Sprite 朝向玩家(类 Hollow Knight 行为):

// DialogueManager.StartDialogue() 中:
// 若 sequence 的 owner 是 InteractableNPC则 npc.FaceToward(player.position)

8. 对话触发条件

P1 扩展:根据游戏进程显示不同对话内容(如击败 Boss 后 NPC 台词变化):

[Serializable]
public class DialogueCondition
{
    public ConditionType type;         // BossDefeated / AbilityUnlocked / ItemOwned / FirstVisit
    public string        requiredId;   // Boss ID / Ability ID / Item ID
    public bool          negate;       // true = 条件取反(如"尚未击败某Boss"
}

// InteractableNPC.GetCurrentSequence() P1 实现:
DialogueSequenceSO GetCurrentSequence()
{
    foreach (var seq in _dialogueSequences)
    {
        if (AllConditionsMet(seq.conditions))
            return seq;
    }
    return _dialogueSequences[^1];   // 最后一段作为兜底
}

9. 输入 Action Map 切换

对话进行时禁用 Gameplay 输入,仅响应 UI 输入(确认/跳过):

状态 Gameplay Map UI Map 说明
正常游玩 启用 禁用 移动/攻击/弹反
对话中 禁用 启用 仅 [F]/[A] 翻页
暂停菜单 禁用 启用 菜单导航
// InputReaderSO 扩展方法:
public void EnableGameplayInput(bool enable)
    => _playerInput.SwitchCurrentActionMap(enable ? "Gameplay" : "UI");

// 对话期间绑定的 Action
// UI Map 的 "Submit"(对应键盘 Enter/F、手柄 A→ DialogueManager.OnConfirm()
// UI Map 的 "Cancel"(对应键盘 Esc、手柄 B→ P1跳过整段对话

10. 与 ShopNPC 集成

ShopNPC 继承 InteractableNPC,在对话结束后打开商店 UI

public class ShopNPC : InteractableNPC
{
    [SerializeField] ShopInventorySO _inventory;
    [SerializeField] VoidEventChannelSO _onDialogueEnded;
    bool _shopOpenedThisVisit = false;

    protected override void OnDialogueEnded()
    {
        base.OnDialogueEnded();
        // 对话结束(仅首次对话后才打开商店,之后再次交互直接开店)
        if (!_shopOpenedThisVisit || _currentSequenceIndex > 0)
        {
            _shopOpenedThisVisit = true;
            OpenShop();
        }
    }

    void OpenShop()
    {
        // 零耦合:通过事件频道请求打开商店 UI不直接调用 UIManager.Instance
        _onOpenShopRequested.Raise(_inventory);   // ShopInventoryEventChannelSO
    }
}

ShopInventoryEventChannelSOShopInventorySO 类型事件频道)发布后,UIManager 组件订阅该频道并调用其内部的 OpenShopPanel(ShopInventorySO)。这样 ShopNPC 不持有对 UIManager 的任何引用。


---

## 11. 事件频道

新增频道(`Assets/ScriptableObjects/Events/Dialogue/`

| 资产名 | 类型 | 用途 |
|--------|------|------|
| `OnDialogueStarted.asset` | `VoidEventChannelSO` | 对话开始GameManager 可锁定相关状态 |
| `OnDialogueEnded.asset` | `VoidEventChannelSO` | 对话结束恢复输入ShopNPC 监听 |

---

## 12. 编辑器友好设计

- `DialogueSequenceSO` Custom Inspector逐行预览对话内容带 NPC 名字颜色标识,支持换行显示完整台词
- `InteractableNPC` 交互范围 Gizmo在 Scene View 显示交互半径(蓝色虚线圆圈)
- `DialogueManager` Inspector实时显示"是否正在播放"状态 + 当前对话序列名 + 当前行索引
- `InteractionPrompt` 在编辑器中始终显示(`[ExecuteInEditMode]`),方便调整位置
- EditorWindow `DialogueValidator`:扫描全部 `DialogueSequenceSO`,检测空 text 字段、缺失 speakerName 等数据问题UI Toolkit `ListView` + 可点击 `Label``CreateGUI()` 实现)

---

## 13. NPC 好感度系统

并非所有 NPC 都需要好感度。本系统适用于对玩家行为有明显反应的重要 NPC商人、第一个遇到的 NPC、隐藏的 NPC等。

### 13.1 NPCRelationshipSO

```csharp
[CreateAssetMenu(menuName = "Dialogue/NPCRelationship")]
public class NPCRelationshipSO : ScriptableObject
{
    [Header("基础信息")]
    public string npcId;          // 必须与 InteractableNPC._npcId 一致
    public string displayName;

    [Header("好感度阈値")]
    [Tooltip("对话变化阈値,升序配置")]
    public AffinityThreshold[] thresholds;

    [Header("初始值")]
    [Range(0, 100)]
    public int initialAffinity = 50;
}

[Serializable]
public struct AffinityThreshold
{
    [Range(0, 100)] public int minAffinity;
    public string              stateLabel;    // 如 "Cold" / "Neutral" / "Friendly" / "Trusted"
    public DialogueSequenceSO  greetingDialogue;  // 该阶段打招对话
    public Sprite              npcPortrait;       // 对话框头像(可能随状态变化表情)
}

13.2 RelationshipManager

/// <summary>
/// 单例,负责读写全局 NPC 好感度数据。
/// </summary>
public class RelationshipManager : MonoBehaviour
{
    public static RelationshipManager Instance { get; private set; }

    // Key = npcId
    readonly Dictionary<string, int> _affinities = new();

    void Awake()
    {
        Instance = this;
        LoadFromSave();
    }

    public int GetAffinity(string npcId)
        => _affinities.TryGetValue(npcId, out int v) ? v : 50;

    public void ChangeAffinity(string npcId, int delta)
    {
        int current = GetAffinity(npcId);
        _affinities[npcId] = Mathf.Clamp(current + delta, 0, 100);
        SaveToCurrent();
    }

    public string GetStateLabel(NPCRelationshipSO rel)
    {
        int affinity = GetAffinity(rel.npcId);
        AffinityThreshold best = rel.thresholds[0];
        foreach (var t in rel.thresholds)
            if (affinity >= t.minAffinity) best = t;
        return best.stateLabel;
    }

    public DialogueSequenceSO GetGreetingDialogue(NPCRelationshipSO rel)
    {
        int affinity = GetAffinity(rel.npcId);
        DialogueSequenceSO best = rel.thresholds[0].greetingDialogue;
        foreach (var t in rel.thresholds)
            if (affinity >= t.minAffinity && t.greetingDialogue != null)
                best = t.greetingDialogue;
        return best;
    }

    void LoadFromSave()
    {
        var saved = SaveManager.Instance.GetRelationships();
        foreach (var kv in saved)
            _affinities[kv.Key] = kv.Value;
    }

    void SaveToCurrent()
    {
        SaveManager.Instance.SetRelationships(_affinities);
    }
}

13.3 InteractableNPC 与好感度集成

InteractableNPC 增加可选的 NPCRelationshipSO 字段:

public class InteractableNPC : MonoBehaviour, IInteractable
{
    // 原有字段保持不变 ...

    [Header("好感度(可选)")]
    [SerializeField] NPCRelationshipSO _relationship;    // 若为 null 则不使用好感度系统

    protected virtual DialogueSequenceSO GetCurrentDialogue()
    {
        // 如果配置了好感度 SO优先使用好感度驱动的问候对话
        if (_relationship != null)
        {
            var greeting = RelationshipManager.Instance.GetGreetingDialogue(_relationship);
            if (greeting != null) return greeting;
        }

        // 回退到原有进程驱动的对话选择逻辑
        foreach (var entry in _dialogueEntries)
        {
            if (entry.condition == null || entry.condition.IsMet())
                return entry.dialogue;
        }
        return _defaultDialogue;
    }
}

13.4 好感度变化方式

触发方式 示例 实现位置
对话选择 玩家选择友善回应 DialogueLine.affinityDelta 字段,DialogueManager 播放后调用 ChangeAffinity
被保护 NPC 击杀刺客后 NPC 生气阶段提升 EventChainSO Action: ChangeAffinityAction
交物品 将 NPC 要的物品交给他 ShopNPC.GiftItem() 调用 ChangeAffinity
时间衰减 长时间不与 NPC 互动 可选功能,通过定时器缓慢减少(默认不启用)

13.5 合法 DialogueLine 扩展

// DialogueLine 新增字段
[Serializable]
public class DialogueLine
{
    // 原有字段...
    public string speakerName;
    [TextArea(2, 6)] public string text;
    public AudioClip voiceClip;
    public Sprite    speakerPortrait;

    [Header("好感度影响(可选)")]
    [Tooltip("此行对话结束后对说话 NPC 的好感度变化(正=提升,负=降低)")]
    public int affinityDelta;   // 0 表示不变化
}

13.6 SaveData 扩展

"relationships": {
  "npc_merchant": 72,
  "npc_elder": 45,
  "npc_hidden_master": 10
}
// SaveManager 扩展
public Dictionary<string, int> GetRelationships()
    => _saveData.relationships ?? new Dictionary<string, int>();

public void SetRelationships(Dictionary<string, int> data)
{
    _saveData.relationships = data;
    WriteDirty();
}

13.7 好感度条件 — AffinityThresholdCondition

可用于 NavHintConditionEventChainConditionDialogueTriggerCondition

[CreateAssetMenu(menuName = "Dialogue/Condition/AffinityThreshold")]
public class AffinityThresholdCondition : DialogueTriggerCondition
{
    public string npcId;
    [Range(0, 100)] public int minAffinity;
    [Range(0, 100)] public int maxAffinity = 100;

    public override bool IsMet()
    {
        int v = RelationshipManager.Instance.GetAffinity(npcId);
        return v >= minAffinity && v <= maxAffinity;
    }
}

典型用例

  • minAffinity=80 → NPC 信任玩家后才告诉他隐藏路点
  • minAffinity=0, maxAffinity=20 → 好感极低时 NPC 拒绝交易

14. NarrativeNPC 世界状态响应WorldState Integration

本节为 50_NarrativeDesignSystem.md 的对话层集成说明。
NarrativeNPC 是对 InteractableNPC 的扩展,支持基于 WorldStateRegistry 全局标志的对话版本切换。

14.1 NarrativeNPC 与 InteractableNPC 的关系

InteractableNPC (基类)
  └── NarrativeNPC (扩展)
        ├── 继承好感度系统§13
        ├── 继承基础对话触发§8
        └── 新增WorldStateRegistry 标志驱动的对话版本选择

14.2 DialogueVersion世界状态对话版本

[Serializable]
public struct DialogueVersion
{
    [Tooltip("版本标签(调试用,如 'pre_cave_boss' / 'post_true_ending'")]
    public string label;

    [Tooltip("需要 WorldStateRegistry 中存在的所有标志")]
    public string[] requiredFlags;

    [Tooltip("若 WorldStateRegistry 中存在任一标志,此版本被排除")]
    public string[] blockedByFlags;

    [Tooltip("优先级,数值越高越优先")]
    public int priority;

    public DialogueSequenceSO dialogue;
}

14.3 NarrativeNPC 对话选择逻辑

public class NarrativeNPC : InteractableNPC
{
    [Header("世界状态对话版本")]
    [SerializeField] DialogueVersion[] _worldStateDialogues;

    protected override DialogueSequenceSO GetCurrentDialogue()
    {
        // 1. 优先通过世界状态选择对话版本
        var registry = WorldStateRegistry.Instance;
        DialogueVersion? best = null;

        foreach (var version in _worldStateDialogues)
        {
            // 检查是否所有 requiredFlags 存在
            bool allRequired = Array.TrueForAll(
                version.requiredFlags, f => registry.HasFlag(f));
            // 检查是否有 blockedByFlags 阻挡
            bool anyBlocked = Array.Exists(
                version.blockedByFlags, f => registry.HasFlag(f));

            if (allRequired && !anyBlocked)
            {
                if (!best.HasValue || version.priority > best.Value.priority)
                    best = version;
            }
        }

        if (best.HasValue) return best.Value.dialogue;

        // 2. 回退到好感度驱动对话§13
        // 3. 最终回退到基类默认对话§7
        return base.GetCurrentDialogue();
    }
}

14.4 世界状态标志命名规范

50_NarrativeDesignSystem §2.1 保持一致:

前缀 含义 示例
boss_ Boss 击败 boss_forest_defeated, boss_cave_defeated
quest_ 任务状态 quest_elder_request_complete
npc_ NPC 特定事件 npc_merchant_migrated_to_cave
item_ 关键道具获取 item_echo_fragment_5_collected
area_ 区域发现/解锁 area_ruins_secret_found
story_ 剧情节点达成 story_true_ending_conditions_met

14.5 典型配置示例ElderSage NPC

ElderSage NPC DialogueVersions:
  [0] label: "初次见面"
      requiredFlags: []
      blockedByFlags: ["boss_forest_defeated"]
      priority: 0
      → dialogue: elder_intro.asset

  [1] label: "森林Boss击败后"
      requiredFlags: ["boss_forest_defeated"]
      blockedByFlags: ["boss_cave_defeated"]
      priority: 10
      → dialogue: elder_after_forest.asset

  [2] label: "Cave Boss击败后"
      requiredFlags: ["boss_forest_defeated", "boss_cave_defeated"]
      blockedByFlags: ["story_true_ending_conditions_met"]
      priority: 20
      → dialogue: elder_after_cave.asset

  [3] label: "True Ending条件满足后"
      requiredFlags: ["story_true_ending_conditions_met"]
      blockedByFlags: []
      priority: 100
      → dialogue: elder_true_ending_hint.asset

14.6 事件链集成NarrativeNodeSO 效果触发)

当对话的最后一行触发 NarrativeNodeSO 效果时(如 NPC 迁移、世界标志设置),通过 DialogueLineonComplete UnityEvent 调用 WorldStateRegistry.SetFlag()

// 在 DialogueLine 中(或 EventChainSO Action 中)
WorldStateRegistry.Instance.SetFlag("npc_elder_migrated_to_ruins");
NPCMigrationService.Instance.TriggerMigration("npc_elder", "ruins_temple");

这确保对话与世界状态的变化原子性发生,不存在对话播完但状态未更新的窗口。