第三部分:对话系统

        一:数据定义

       逻辑: 一段对话由多条对话组成,每条对话有可以有多个选项,每个选项可以跳转到不同的对话条。

        1:DialoguePiece(一条对话)

[System.Serializable]
public class DialoguePiece 
{
    public string ID;

    public Sprite image;

    [TextArea]
    public string text;

    public List<DialogueOption> options = new List<DialogueOption>();
}

        2:DialogueOption(一条选项)

[System.Serializable]
public class DialogueOption
{
    
    public string text;

    public string targetID;

    public bool takeQuest;
}

        3:DialogueData_SO(一段对话内容)        

        List<DialoguePiece>用于在编辑器窗口编写对话内容。

        字典<string,int> 代表了一条对话的ID在List中的下标(用于实现对话的跳转)。通过使用OnValidate函数自动生成配置字典。

[CreateAssetMenu(fileName = "New Dialogue",menuName= "Dialogue/Dialogue Data")]
public class DialogueData_SO : ScriptableObject
{
    public List<DialoguePiece> dialoguePieces;

    public Dictionary<string, int> stringToDialoguePiece = new();

    #if UNITY_EDITOR
    private void OnValidate()
    {
        stringToDialoguePiece.Clear();
        for (int i = 0; i < dialoguePieces.Count; i++)
        {
            stringToDialoguePiece.Add(dialoguePieces[i].ID, i);
        }

    }
    #endif
}

        二:对话实现

        1:DialogueController(给对话的单位添加,负责开始对话)

        包含了单位的对话内容(dialogueData),在进入触发器范围时可以按E键触发对话。

public class DialogueController : MonoBehaviour
{
    public DialogueData_SO dialogueData;

    private bool canTalk = false;

    private void Update()
    {
        if (canTalk && Input.GetKeyDown(KeyCode.E) && !DialogueUIManager.Instance.IsTalking) 
        {
            //进入对话
            OpenDialogue();
        }
    }

    private void OpenDialogue()
    {
        //打开UI面板 传入当前的dialogueData
        DialogueUIManager.Instance.OpenDialogue(dialogueData);
    }
    
    

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            canTalk = true;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            canTalk = false;
            DialogueUIManager.Instance.CloseDialogue();
        }
    }
}

        2:OptionUI (处理选项功能)

        设置所属的对话Data以及根据nextPieceID在Data的字典中查找下一条对话。

public class OptionUI : MonoBehaviour
{
    [SerializeField]private TextMeshProUGUI optionText;

    [SerializeField]private Button thisButton;

    private DialogueData_SO currentDialogueData;

    private string nextPieceID;

    private void Awake()
    {
        thisButton.onClick.AddListener(OnOptionClicked);
    }

    public void SettingOption(DialogueData_SO dialogueData, DialogueOption option)
    {
        currentDialogueData = dialogueData;
        optionText.text = option.text;
        nextPieceID = option.targetID;
    }

    private void OnOptionClicked()
    {
        //跳转到选项的下一句对话
        if (nextPieceID == string.Empty)
        {
            DialogueUIManager.Instance.CloseDialogue();
        }
        else
        {
            var targetPlace = DialogueUIManager.Instance.GetDialoguePiecesByName(nextPieceID);
            DialogueUIManager.Instance.UpdateMainDialogue(currentDialogueData.dialoguePieces[targetPlace]);
        }
    }
}

        3:DialogueUIManager(对话管理类)

        包含了打开对话面板,关闭对话面板,继续对话等内容。代码逻辑不复杂,要点见注释。(其中在进入对话和离开对话时设置了全局事件:设置鼠标,停止人物等等)

public class DialogueUIManager : Singleton<DialogueUIManager>
{
    [Header("引用")] [SerializeField] private Image dialogueIcon;

    [SerializeField] private TextMeshProUGUI dialogueText;
    [SerializeField] private Button nextButton;

    [SerializeField] private GameObject dialoguePanel;

    [SerializeField] private Transform optionPanel;
    
    [SerializeField] private OptionUI optionPrefab;

    public bool IsTalking { get; set; }  //当前是否正在对话

    private DialogueData_SO currentDialogueData;

    private int currentIndex = 0;        //当前对话进行到的pieces中的位置

    protected override void Awake()
    {
        base.Awake();
        //第一种继续对话的方式:点击Next按钮
        nextButton.onClick.AddListener(ContinueDialogue);
    }

    private void Update()
    {
        //第二种继续对话的方式:按下E键 (要在没有选项的前提下)
        if (IsTalking && Input.GetKeyDown(KeyCode.E) && nextButton.gameObject.activeInHierarchy)
        {
            ContinueDialogue();
        }
    }

    public void OpenDialogue(DialogueData_SO dialogueData)
    {
        currentDialogueData = dialogueData;
        currentIndex = 0;
        IsTalking = true;
        dialoguePanel.SetActive(true);
        UpdateMainDialogue(currentDialogueData.dialoguePieces[0]);
        GlobalEvent.CallOnEnterDialogue();
    }

    public void CloseDialogue()
    {
        currentDialogueData = null;
        currentIndex = 0;
        dialoguePanel.SetActive(false);
        //由于开启对话和结束对话有可能是一个按键 有可能会在结束对话的瞬间后开启对话 因此设置一个协程的延迟调用(延迟一帧即可)
        StartCoroutine(SetIsTalking(false));
        GlobalEvent.CallOnExitDialogue();
    }

    //返回指定名字对话所在DialogueData中的下标
    public int GetDialoguePiecesByName(string name)
    {
        return currentDialogueData.stringToDialoguePiece[name];
    }

    public void UpdateMainDialogue(DialoguePiece dialoguePiece)
    {
        //更新图片内容
        if (dialoguePiece.image != null)
        {
            dialogueIcon.sprite = dialoguePiece.image;
            dialogueIcon.enabled = true;
        }
        else dialogueIcon.enabled = false;

        //更新文本内容
        dialogueText.text = dialoguePiece.text;
        //var t = DOTween.To(() => string.Empty, value => dialogueText.text = value, dialoguePiece.text, 1f).SetEase(Ease.Linear);

        //处理NextButton(只有当没有选项时才选择有NextButton)
        if (dialoguePiece.options.Count == 0)
        {
            nextButton.gameObject.SetActive(true);
        }
        else nextButton.gameObject.SetActive(false);

        currentIndex = currentDialogueData.stringToDialoguePiece[dialoguePiece.ID] + 1;

        //创建Options
        CreateOptions(dialoguePiece);
    }

    private void CreateOptions(DialoguePiece dialogPieces)
    {
        //先销毁原有的Option 后创建新的Option
        foreach (Transform child in optionPanel) Destroy(child.gameObject);

        foreach (var option in dialogPieces.options)
        {
            var optionUI = Instantiate(optionPrefab, optionPanel);
            optionUI.SettingOption(currentDialogueData, option);
        }
        
    }

    private void ContinueDialogue()
    {
        if (currentIndex <= currentDialogueData.dialoguePieces.Count - 1)
            UpdateMainDialogue(currentDialogueData.dialoguePieces[currentIndex]);
        else
            CloseDialogue();
    }

    private IEnumerator SetIsTalking(bool value)
    {
        yield return null;
        IsTalking = value;
    }
}

        三:对话编辑插件

效果图:

[CustomEditor(typeof(DialogueData_SO))]
public class DialogueCustomEditor : Editor
{
    public override void OnInspectorGUI()
    {
        if(GUILayout.Button("Open in Editor"))
        {
            DialogueEditor.InitWindow(target as DialogueData_SO);
        }

        base.OnInspectorGUI();

    }
}

public class DialogueEditor : EditorWindow
{
    DialogueData_SO currentData;

    ReorderableList piecesList = null;

    Vector2 scrollPos = Vector2.zero;

    Dictionary<string, ReorderableList> optionListDict = new Dictionary<string, ReorderableList>();

    [MenuItem("Dbvv/Dialogue Editor")]

    public static void Init()
    {
        DialogueEditor editorWindow = GetWindow<DialogueEditor>("Dialogue Editor");
        editorWindow.autoRepaintOnSceneChange = true;
    }

    public static void InitWindow(DialogueData_SO data)
    {
        DialogueEditor editorWindow = GetWindow<DialogueEditor>("Dialogue Editor");
        editorWindow.currentData = data;

    }

    [OnOpenAsset]
    public static bool OpenAsset(int instanceID,int line)
    {
        DialogueData_SO data = EditorUtility.InstanceIDToObject(instanceID) as DialogueData_SO;

        if(data!=null)
        {
            DialogueEditor.InitWindow(data);
            return true;
        }
        return false;
    }

    private void OnSelectionChange()
    {
        //选择改变时调用
        var newData=Selection.activeObject as DialogueData_SO;
        if(newData)
        {
            currentData = newData;
            SetupReorderableList();
        }
        else
        {
            currentData = null;
            piecesList = null;
        }
        Repaint();
    }

    private void OnGUI()
    {
        if(currentData!=null)
        {
            EditorGUILayout.LabelField(currentData.name, EditorStyles.boldLabel);
            GUILayout.Space(10);

            scrollPos = GUILayout.BeginScrollView(scrollPos, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));

            if (piecesList == null)
                SetupReorderableList();

            piecesList.DoLayoutList();
            GUILayout.EndScrollView();
        }
        else
        {
            if(GUILayout.Button("Create New Dialogue"))
            {
                string dataPath = "Assets/Game Data/Dialogue Data/";
                if (!Directory.Exists(dataPath))
                    Directory.CreateDirectory(dataPath);

                DialogueData_SO newData = ScriptableObject.CreateInstance<DialogueData_SO>();
                newData.dialoguePieces = new List<DialoguePiece>();
                AssetDatabase.CreateAsset(newData, dataPath+"/"+"New Dialogue.asset");
                currentData = newData;

            }

            GUILayout.Label("NO DATA SELECTED!", EditorStyles.boldLabel);
        }
    }

    private void OnDisable()
    {
        optionListDict.Clear();
    }

    private void SetupReorderableList()
    {
        piecesList = new ReorderableList(currentData.dialoguePieces, typeof(DialoguePiece), true, true, true, true);
       
        piecesList.drawHeaderCallback += OnDrawPieceHeader;

        piecesList.drawElementCallback += OnDrawPieceListElement;

        piecesList.elementHeightCallback += OnHeightChanged;
    }

    private float OnHeightChanged(int index)
    {
        return GetPieceHeight(currentData.dialoguePieces[index]);
    }

    float GetPieceHeight(DialoguePiece piece)
    {
        var height = EditorGUIUtility.singleLineHeight;

        var isExpand = piece.canExpand;

        if(isExpand)
        {
            height += EditorGUIUtility.singleLineHeight * 9;

            var options = piece.options;

            if (options.Count > 1)
            {
                height += EditorGUIUtility.singleLineHeight * options.Count;
            }
        }


        return height;
    }

    private void OnDrawPieceHeader(Rect rect)
    {
        GUI.Label(rect, "Dialogue Pieces");
    }

    private void OnDrawPieceListElement(Rect rect, int index, bool isActive, bool isFocused)
    {
        EditorUtility.SetDirty(currentData);

        GUIStyle textStyle = new GUIStyle("TextField");

        if(index<currentData.dialoguePieces.Count)
        {
            DialoguePiece currentPiece = currentData.dialoguePieces[index];

            Rect tempRect = rect;

            tempRect.height = EditorGUIUtility.singleLineHeight;

            currentPiece.canExpand = EditorGUI.Foldout(tempRect, currentPiece.canExpand, currentPiece.ID);

            if(currentPiece.canExpand)
            {
                tempRect.width = 30;
                tempRect.y += tempRect.height;
                EditorGUI.LabelField(tempRect, "ID");

                tempRect.x += tempRect.width;
                tempRect.width = 100;
                currentPiece.ID = EditorGUI.TextField(tempRect, currentPiece.ID);

                tempRect.x += tempRect.width + 10;
                EditorGUI.LabelField(tempRect, "Quest");

                tempRect.x += 40;
                currentPiece.taskData = (TaskData_SO)EditorGUI.ObjectField(tempRect, currentPiece.taskData, typeof(TaskData_SO), false);

                tempRect.y += EditorGUIUtility.singleLineHeight + 5;
                tempRect.x = rect.x;
                tempRect.height = 60;
                tempRect.width = tempRect.height;
                currentPiece.image = (Sprite)EditorGUI.ObjectField(tempRect, currentPiece.image, typeof(Sprite), false);

                //文本框
                tempRect.x += tempRect.width + 5;
                tempRect.width = rect.width - tempRect.x;
                textStyle.wordWrap = true;
                currentPiece.text = (string)EditorGUI.TextField(tempRect, currentPiece.text, textStyle);

                //画选项
                tempRect.y += tempRect.height + 5;
                tempRect.x = rect.x;
                tempRect.width = rect.width;

                string optionListKey = currentPiece.ID + currentPiece.text;

                if (optionListKey != string.Empty) 
                {
                    if (!optionListDict.ContainsKey(optionListKey))
                    {
                        var optionList = new ReorderableList(currentPiece.options, typeof(DialogueOption), true, true, true, true);

                        optionList.drawHeaderCallback += OnDrawOptionHeader;

                        optionList.drawElementCallback += (optionRect, optionIndex, optionActive, optionFocused) =>
                        {
                            OnDrawOptionElement(currentPiece, optionRect, optionIndex, optionActive, optionFocused);
                        };

                        optionListDict[optionListKey] = optionList;
                    }

                    optionListDict[optionListKey].DoList(tempRect);

                }
            }

        }
    }

    private void OnDrawOptionHeader(Rect rect)
    {
        GUI.Label(rect, "Option Text");
        rect.x += rect.width * 0.5f + 10;
        GUI.Label(rect, "Target ID");
        rect.x += rect.width * 0.3f;
        GUI.Label(rect, "Apply");
    }

    private void OnDrawOptionElement(DialoguePiece currentPiece, Rect optionRect, int optionIndex, bool optionActive, bool optionFocused)
    {
        var currentOption = currentPiece.options[optionIndex];
        var tempRect = optionRect;

        tempRect.width = optionRect.width * 0.5f;
        currentOption.text = EditorGUI.TextField(tempRect, currentOption.text);

        tempRect.x += tempRect.width + 5;
        tempRect.width = optionRect.width * 0.3f;
        currentOption.targetID = EditorGUI.TextField(tempRect, currentOption.targetID);

        tempRect.x += tempRect.width + 5;
        tempRect.width = optionRect.width * 0.2f;
        currentOption.takeTask = EditorGUI.Toggle(tempRect, currentOption.takeTask);

    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值