一:数据定义
逻辑: 一段对话由多条对话组成,每条对话有可以有多个选项,每个选项可以跳转到不同的对话条。
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);
}
}