【Unity】对话与任务系统基础架构 (二) - 对话系统实现详解

对话与任务系统基础架构 (二) - 对话系统实现详解

前言

上一篇文章介绍了整体架构设计,这次我们深入对话系统的具体实现。经过一段时间的开发和优化,我们的对话系统已经能够支持复杂的多分支对话、角色切换、事件触发等功能。

本篇将从以下几个方面详细介绍:

  • 配置表设计与数据结构
  • UI状态机的实现
  • 对话流程控制逻辑
  • 选择分支处理机制
  • 事件触发与系统集成

配置表设计详解

数据结构层次

我们采用了三层数据结构来组织对话内容:

DialogueMain (对话章节)
├── DialogueGroup (对话组1)
│   ├── DialogueData (对话1)
│   ├── DialogueData (对话2)
│   └── DialogueOption[] (选择选项)
├── DialogueGroup (对话组2)
└── ...

核心配置表结构

1. DialogueMain - 对话章节表
[Serializable]
public class DialogueMain
{
    public int id;                    // 章节ID
    public string name;               // 章节名称
    public int firstGroupId;          // 首个对话组ID
    public List<DialogueGroup> dialogueGroups; // 对话组列表
}
2. DialogueGroup - 对话组表
[Serializable]
public class DialogueGroup
{
    public int id;                    // 对话组ID
    public int chapterId;             // 所属章节ID
    public List<DialogueData> dialogueDatas;   // 对话数据列表
    public List<DialogueOption> dialogueOptions; // 选择选项列表
    public int nextGroupId;           // 下一个对话组ID
    public string triggerEvent;       // 触发事件配置
}
3. DialogueData - 单条对话表
[Serializable]
public class DialogueData
{
    public int id;                    // 对话ID
    public int groupId;               // 所属对话组ID
    public string characterName;      // 角色名称
    public string characterIcon;      // 角色头像路径
    public string content;            // 对话内容
    public float showTime;            // 显示时长
    public DialoguePosition position; // 对话位置(左/右/中)
}
4. DialogueOption - 选择选项表
[Serializable]
public class DialogueOption
{
    public int id;                    // 选项ID
    public int groupId;               // 所属对话组ID
    public string content;            // 选项文本
    public int targetGroupId;         // 目标对话组ID
    public string triggerEvent;       // 触发事件
    public bool isVisible;            // 是否可见
    public string condition;          // 显示条件
}

配置表示例

来看一个实际的配置例子:

DialogueMain表:

idnamefirstGroupId
1001新手村对话2001

DialogueGroup表:

idchapterIdnextGroupIdtriggerEvent
200110012002“1,StartTask_101”
20021001-1“2,GiveItem_sword”

DialogueData表:

idgroupIdcharacterNamecontentposition
30012001村长“欢迎来到新手村!”Left
30022001玩家“您好,我是新来的冒险者。”Right

DialogueOption表:

idgroupIdcontenttargetGroupIdtriggerEvent
40012001“我想接受任务”2002“1,StartTask_101”
40022001“我只是路过”-1“”

UI状态机实现

对话UI采用状态机模式管理不同的显示状态,这样可以清晰地控制UI的各种交互行为。

UI状态定义

public enum DialogueUIState
{
    None,           // 无状态
    Showing,        // 正在显示对话
    WaitingInput,   // 等待用户输入
    ShowingOptions, // 显示选择选项
    Playing,        // 自动播放中
    Finished        // 对话结束
}

状态机核心实现

public class DialogueUIStateMachine
{
    private DialogueUIState _currentState;
    private DialogueUIState _previousState;
    private Dictionary<DialogueUIState, IDialogueUIState> _states;
    
    public void Init()
    {
        _states = new Dictionary<DialogueUIState, IDialogueUIState>
        {
            { DialogueUIState.Showing, new DialogueShowingState() },
            { DialogueUIState.WaitingInput, new DialogueWaitingState() },
            { DialogueUIState.ShowingOptions, new DialogueOptionsState() },
            { DialogueUIState.Playing, new DialoguePlayingState() },
            { DialogueUIState.Finished, new DialogueFinishedState() }
        };
    }
    
    public void ChangeState(DialogueUIState newState)
    {
        if (_currentState == newState) return;
        
        // 退出当前状态
        _states[_currentState]?.OnExit();
        
        _previousState = _currentState;
        _currentState = newState;
        
        // 进入新状态
        _states[_currentState]?.OnEnter();
        
        Debug.Log($"对话状态切换: {_previousState} -> {_currentState}");
    }
    
    public void Update()
    {
        _states[_currentState]?.OnUpdate();
    }
}

具体状态实现

显示对话状态
public class DialogueShowingState : IDialogueUIState
{
    private float _typewriterSpeed = 0.05f;
    private Coroutine _typewriterCoroutine;
    
    public void OnEnter()
    {
        // 开始打字机效果
        _typewriterCoroutine = DialogueUI.Instance.StartCoroutine(TypewriterEffect());
    }
    
    public void OnUpdate()
    {
        // 检测用户点击跳过打字机效果
        if (Input.GetMouseButtonDown(0))
        {
            SkipTypewriter();
        }
    }
    
    public void OnExit()
    {
        if (_typewriterCoroutine != null)
        {
            DialogueUI.Instance.StopCoroutine(_typewriterCoroutine);
        }
    }
    
    private IEnumerator TypewriterEffect()
    {
        var dialogueText = DialogueUI.Instance.DialogueText;
        var fullText = DialogueUI.Instance.CurrentDialogue.content;
        
        dialogueText.text = "";
        
        for (int i = 0; i <= fullText.Length; i++)
        {
            dialogueText.text = fullText.Substring(0, i);
            yield return new WaitForSeconds(_typewriterSpeed);
        }
        
        // 打字机效果完成,切换到等待输入状态
        DialogueUI.Instance.StateMachine.ChangeState(DialogueUIState.WaitingInput);
    }
}
等待输入状态
public class DialogueWaitingState : IDialogueUIState
{
    public void OnEnter()
    {
        // 显示继续提示
        DialogueUI.Instance.ShowContinueHint(true);
    }
    
    public void OnUpdate()
    {
        if (Input.GetMouseButtonDown(0) || Input.GetKeyDown(KeyCode.Space))
        {
            DialogueManager.Instance.NextDialogue();
        }
    }
    
    public void OnExit()
    {
        DialogueUI.Instance.ShowContinueHint(false);
    }
}
选择选项状态
public class DialogueOptionsState : IDialogueUIState
{
    public void OnEnter()
    {
        DialogueUI.Instance.ShowOptions(DialogueManager.Instance.CurrentOptions);
    }
    
    public void OnUpdate()
    {
        // 处理选项选择
        HandleOptionSelection();
    }
    
    private void HandleOptionSelection()
    {
        for (int i = 0; i < DialogueManager.Instance.CurrentOptions.Count; i++)
        {
            if (Input.GetKeyDown(KeyCode.Alpha1 + i))
            {
                DialogueManager.Instance.SelectOption(i);
                break;
            }
        }
    }
}

对话管理器核心逻辑

DialogueManager主要职责

public class DialogueManager : MonoSingleton<DialogueManager>
{
    private DialogueDataManager _dataManager;
    private IDialogueUI _dialogueUI;
    
    // 当前对话状态
    public DialogueMain CurrentChapter { get; private set; }
    public DialogueGroup CurrentGroup { get; private set; }
    public DialogueData CurrentDialogue { get; private set; }
    public List<DialogueOption> CurrentOptions { get; private set; }
    
    private int _currentDialogueIndex = 0;
    
    public async ETTask Init()
    {
        _dataManager = new DialogueDataManager();
        await _dataManager.Init();
        
        _dialogueUI = FindObjectOfType<TestDialogueView>();
        
        RegisterEvents();
    }
}

对话流程控制

开始对话章节
public void StartDialogueChapter(int chapterId, int groupId = -1)
{
    CurrentChapter = _dataManager.GetDialogueMain(chapterId);
    if (CurrentChapter == null)
    {
        Debug.LogError($"找不到对话章节: {chapterId}");
        return;
    }
    
    // 确定起始对话组
    int startGroupId = groupId != -1 ? groupId : CurrentChapter.firstGroupId;
    StartDialogueGroup(startGroupId);
}

private void StartDialogueGroup(int groupId)
{
    CurrentGroup = _dataManager.GetDialogueGroup(groupId);
    if (CurrentGroup == null)
    {
        Debug.LogError($"找不到对话组: {groupId}");
        return;
    }
    
    _currentDialogueIndex = 0;
    CurrentOptions = CurrentGroup.dialogueOptions;
    
    // 显示UI并开始第一条对话
    _dialogueUI.Show();
    ShowCurrentDialogue();
}
对话推进逻辑
public void NextDialogue()
{
    _currentDialogueIndex++;
    
    // 检查是否还有更多对话
    if (_currentDialogueIndex < CurrentGroup.dialogueDatas.Count)
    {
        ShowCurrentDialogue();
    }
    else
    {
        // 当前组对话结束,检查是否有选择选项
        if (CurrentOptions != null && CurrentOptions.Count > 0)
        {
            ShowOptions();
        }
        else
        {
            // 没有选项,检查是否有下一个对话组
            ProcessGroupEnd();
        }
    }
}

private void ShowCurrentDialogue()
{
    CurrentDialogue = CurrentGroup.dialogueDatas[_currentDialogueIndex];
    _dialogueUI.ShowDialogue(CurrentDialogue);
    _dialogueUI.StateMachine.ChangeState(DialogueUIState.Showing);
}
对话组结束处理
private void ProcessGroupEnd()
{
    // 触发对话组完成事件
    TriggerGroupEvent(CurrentGroup.triggerEvent);
    
    // 检查是否有下一个对话组
    if (CurrentGroup.nextGroupId > 0)
    {
        StartDialogueGroup(CurrentGroup.nextGroupId);
    }
    else
    {
        // 整个对话章节结束
        EndDialogueChapter();
    }
}

private void EndDialogueChapter()
{
    _dialogueUI.Hide();
    _dialogueUI.StateMachine.ChangeState(DialogueUIState.Finished);
    
    // 发送对话章节完成事件
    UniEvent.SendMessage(new DialogueChapterCompleted 
    { 
        ChapterId = CurrentChapter.id 
    });
}

选择分支处理机制

选项显示与过滤

private void ShowOptions()
{
    // 过滤可见选项
    var visibleOptions = CurrentOptions.Where(IsOptionVisible).ToList();
    
    if (visibleOptions.Count == 0)
    {
        // 没有可见选项,直接结束
        ProcessGroupEnd();
        return;
    }
    
    _dialogueUI.ShowOptions(visibleOptions);
    _dialogueUI.StateMachine.ChangeState(DialogueUIState.ShowingOptions);
}

private bool IsOptionVisible(DialogueOption option)
{
    if (!option.isVisible) return false;
    
    // 检查显示条件
    if (!string.IsNullOrEmpty(option.condition))
    {
        return EvaluateCondition(option.condition);
    }
    
    return true;
}

条件评估系统

private bool EvaluateCondition(string condition)
{
    // 简单的条件评估系统
    // 格式: "TaskCompleted:101" 或 "ItemCount:sword>=5"
    
    var parts = condition.Split(':');
    if (parts.Length != 2) return true;
    
    string conditionType = parts[0];
    string conditionValue = parts[1];
    
    return conditionType switch
    {
        "TaskCompleted" => IsTaskCompleted(int.Parse(conditionValue)),
        "ItemCount" => EvaluateItemCount(conditionValue),
        "PlayerLevel" => EvaluatePlayerLevel(conditionValue),
        _ => true
    };
}

private bool IsTaskCompleted(int taskId)
{
    // 检查任务是否完成
    return TaskManager.Instance.IsTaskCompleted(taskId);
}

选项选择处理

public void SelectOption(int optionIndex)
{
    if (optionIndex < 0 || optionIndex >= CurrentOptions.Count)
    {
        Debug.LogError($"无效的选项索引: {optionIndex}");
        return;
    }
    
    var selectedOption = CurrentOptions[optionIndex];
    
    // 触发选项事件
    if (!string.IsNullOrEmpty(selectedOption.triggerEvent))
    {
        TriggerOptionEvent(selectedOption.triggerEvent);
    }
    
    // 跳转到目标对话组
    if (selectedOption.targetGroupId > 0)
    {
        StartDialogueGroup(selectedOption.targetGroupId);
    }
    else
    {
        // 没有目标组,结束对话
        EndDialogueChapter();
    }
}

事件触发与系统集成

事件配置格式

我们设计了一套灵活的事件配置格式:

"事件类型,事件数据"

常见的事件类型:

  • 1,StartTask_101 - 启动任务101
  • 2,GiveItem_sword_001 - 给予物品
  • 3,ChangeScene_town - 切换场景
  • 4,PlaySound_victory - 播放音效

事件解析与触发

private void TriggerGroupEvent(string eventConfig)
{
    if (string.IsNullOrEmpty(eventConfig)) return;
    
    var events = eventConfig.Split('|'); // 支持多个事件用|分隔
    
    foreach (string eventStr in events)
    {
        ProcessSingleEvent(eventStr);
    }
}

private void ProcessSingleEvent(string eventStr)
{
    var parts = eventStr.Split(',');
    if (parts.Length != 2)
    {
        Debug.LogError($"事件配置格式错误: {eventStr}");
        return;
    }
    
    int eventType = int.Parse(parts[0]);
    string eventData = parts[1];
    
    switch (eventType)
    {
        case 1: // 启动任务
            TriggerStartTask(eventData);
            break;
        case 2: // 给予物品
            TriggerGiveItem(eventData);
            break;
        case 3: // 切换场景
            TriggerChangeScene(eventData);
            break;
        default:
            Debug.LogWarning($"未知的事件类型: {eventType}");
            break;
    }
}

具体事件处理

private void TriggerStartTask(string taskData)
{
    // 解析任务数据: "StartTask_101" -> taskId = 101
    var taskId = int.Parse(taskData.Split('_')[1]);
    
    // 发送启动任务事件
    UniEvent.SendMessage(new TaskStartEvent { TaskId = taskId });
}

private void TriggerGiveItem(string itemData)
{
    // 解析物品数据: "GiveItem_sword_001_5" -> 物品ID, 数量
    var parts = itemData.Split('_');
    string itemId = parts[1];
    int count = parts.Length > 2 ? int.Parse(parts[2]) : 1;
    
    // 发送给予物品事件
    UniEvent.SendMessage(new GiveItemEvent 
    { 
        ItemId = itemId, 
        Count = count 
    });
}

UI实现细节

对话UI组件结构

public class TestDialogueView : MonoBehaviour, IDialogueUI
{
    [Header("UI组件")]
    public GameObject dialoguePanel;
    public Text characterNameText;
    public Image characterIcon;
    public Text dialogueText;
    public GameObject continueHint;
    public Transform optionsParent;
    public GameObject optionButtonPrefab;
    
    [Header("动画组件")]
    public Animator panelAnimator;
    public CanvasGroup canvasGroup;
    
    public DialogueUIStateMachine StateMachine { get; private set; }
    
    private void Awake()
    {
        StateMachine = new DialogueUIStateMachine();
        StateMachine.Init();
    }
}

对话显示实现

public void ShowDialogue(DialogueData dialogueData)
{
    CurrentDialogue = dialogueData;
    
    // 设置角色信息
    characterNameText.text = dialogueData.characterName;
    LoadCharacterIcon(dialogueData.characterIcon);
    
    // 设置对话位置
    SetDialoguePosition(dialogueData.position);
    
    // 准备显示文本(状态机会处理打字机效果)
    dialogueText.text = "";
}

private void LoadCharacterIcon(string iconPath)
{
    if (string.IsNullOrEmpty(iconPath))
    {
        characterIcon.gameObject.SetActive(false);
        return;
    }
    
    // 异步加载头像
    var sprite = Resources.Load<Sprite>(iconPath);
    if (sprite != null)
    {
        characterIcon.sprite = sprite;
        characterIcon.gameObject.SetActive(true);
    }
}

private void SetDialoguePosition(DialoguePosition position)
{
    var rectTransform = dialoguePanel.GetComponent<RectTransform>();
    
    switch (position)
    {
        case DialoguePosition.Left:
            rectTransform.anchorMin = new Vector2(0, 0);
            rectTransform.anchorMax = new Vector2(0.6f, 0.3f);
            break;
        case DialoguePosition.Right:
            rectTransform.anchorMin = new Vector2(0.4f, 0);
            rectTransform.anchorMax = new Vector2(1, 0.3f);
            break;
        case DialoguePosition.Center:
            rectTransform.anchorMin = new Vector2(0.1f, 0);
            rectTransform.anchorMax = new Vector2(0.9f, 0.3f);
            break;
    }
}

选项UI实现

public void ShowOptions(List<DialogueOption> options)
{
    // 清除现有选项
    ClearOptions();
    
    // 创建新选项按钮
    for (int i = 0; i < options.Count; i++)
    {
        CreateOptionButton(options[i], i);
    }
    
    // 显示选项面板
    optionsParent.gameObject.SetActive(true);
}

private void CreateOptionButton(DialogueOption option, int index)
{
    var buttonObj = Instantiate(optionButtonPrefab, optionsParent);
    var button = buttonObj.GetComponent<Button>();
    var text = buttonObj.GetComponentInChildren<Text>();
    
    text.text = $"{index + 1}. {option.content}";
    
    // 添加点击事件
    button.onClick.AddListener(() => {
        DialogueManager.Instance.SelectOption(index);
    });
    
    // 添加快捷键提示
    var shortcutText = buttonObj.transform.Find("ShortcutText")?.GetComponent<Text>();
    if (shortcutText != null)
    {
        shortcutText.text = $"[{index + 1}]";
    }
}

性能优化与最佳实践

1. 对象池优化

public class DialogueUIPool : MonoSingleton<DialogueUIPool>
{
    private Queue<GameObject> _optionButtonPool = new Queue<GameObject>();
    
    public GameObject GetOptionButton()
    {
        if (_optionButtonPool.Count > 0)
        {
            return _optionButtonPool.Dequeue();
        }
        
        return Instantiate(optionButtonPrefab);
    }
    
    public void ReturnOptionButton(GameObject button)
    {
        button.SetActive(false);
        _optionButtonPool.Enqueue(button);
    }
}

2. 资源异步加载

private async ETTask LoadCharacterIconAsync(string iconPath)
{
    var request = Resources.LoadAsync<Sprite>(iconPath);
    await request;
    
    if (request.asset != null)
    {
        characterIcon.sprite = request.asset as Sprite;
        characterIcon.gameObject.SetActive(true);
    }
}

3. 内存管理

public void OnDialogueEnd()
{
    // 清理引用
    CurrentDialogue = null;
    CurrentOptions = null;
    
    // 停止所有协程
    StopAllCoroutines();
    
    // 清理UI对象池
    DialogueUIPool.Instance.ClearPool();
}

调试与测试工具

对话编辑器工具

#if UNITY_EDITOR
[CustomEditor(typeof(DialogueManager))]
public class DialogueManagerEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
        
        var manager = target as DialogueManager;
        
        GUILayout.Space(10);
        GUILayout.Label("测试工具", EditorStyles.boldLabel);
        
        if (GUILayout.Button("测试对话章节1001"))
        {
            manager.StartDialogueChapter(1001);
        }
        
        if (GUILayout.Button("跳过当前对话"))
        {
            manager.NextDialogue();
        }
    }
}
#endif

运行时调试信息

private void OnGUI()
{
    if (!Application.isPlaying || !showDebugInfo) return;
    
    GUILayout.BeginArea(new Rect(10, 10, 300, 200));
    GUILayout.Label($"当前章节: {CurrentChapter?.id}");
    GUILayout.Label($"当前对话组: {CurrentGroup?.id}");
    GUILayout.Label($"对话索引: {_currentDialogueIndex}");
    GUILayout.Label($"UI状态: {_dialogueUI.StateMachine.CurrentState}");
    GUILayout.EndArea();
}

小结

本篇详细介绍了对话系统的核心实现,主要包括:

  1. 配置表设计:三层数据结构,支持复杂对话流程
  2. UI状态机:清晰的状态管理,支持各种交互
  3. 流程控制:完整的对话推进和分支处理逻辑
  4. 事件系统:灵活的事件配置和触发机制
  5. 性能优化:对象池、异步加载等优化手段

下一篇将介绍任务系统的具体实现,包括任务收集器、进度追踪、数据持久化等核心功能。


本篇涉及的关键技术点:

  • 状态机模式在UI中的应用
  • 协程实现打字机效果
  • 条件表达式解析
  • 事件驱动的系统集成
  • 配置表驱动的内容管理

下期预告:
《对话与任务系统基础架构 (三) - 任务系统实现详解》将深入介绍任务系统的收集器机制、进度追踪、数据持久化等核心实现。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值