龙之谷2手游正式上线后不久,试玩了十几分钟(包括捏脸的5分钟),之后就再也没有打开过了。
本文章将对龙之谷2的NPC对话系统进行高仿,同时考虑到 策划可能不断修改对话内容 工具的重复利用,将部分通过编辑器模式来进行制作。
1.参考文章
知乎: Yumir——用128行代码实现一个文字冒险游戏
站内: 虚拟喵——Unity 编辑器扩展总结 五:数组或list集合的显示方式
2.素材使用
unity AssetStore——Unity-Chan! Model
3.根据如下游戏内截图,拆分实现步骤
整体的层级图如下:
根据自己的项目情况,让场景内放置好的人物能够实现简单的移动,这里以我下载好的Unity娘为例
1.为场景中的对话系统控制器挂载代码(DialogSystemController.cs),控制NPC图片和姓名的显示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogSystemController : MonoBehaviour
{
[SerializeField]
public List<NPCItem> npcItemArray = new List<NPCItem>();
public static DialogSystemController Instance;
void Awake()
{
Instance = this;
}
}
[System.Serializable]
public class NPCItem
{
[SerializeField]
public string ID;
[SerializeField]
public Sprite icon;
[SerializeField]
public string name;
}
2.Assets目录下,新建Editor文件夹,新建DialogSystemEditor,依赖于DialogSystemController.cs,自定义Inspector面板显示内容
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditorInternal;
[CustomEditor(typeof(DialogSystemController))]
public class DialogSystemEditor : Editor
{
private ReorderableList _npcItemArray;
private void OnEnable()
{
_npcItemArray = new ReorderableList(serializedObject, serializedObject.FindProperty("npcItemArray")
, true, true, true, true);
//自定义列表名称
_npcItemArray.drawHeaderCallback = (Rect rect) =>
{
GUI.Label(rect, "NPC Array");
};
//定义元素的高度
_npcItemArray.elementHeight = 88;
//自定义绘制列表元素
_npcItemArray.drawElementCallback = (Rect rect, int index, bool selected, bool focused) =>
{
//根据index获取对应元素
SerializedProperty item = _npcItemArray.serializedProperty.GetArrayElementAtIndex(index);
rect.height -= 4;
rect.y += 2;
EditorGUI.PropertyField(rect, item, new GUIContent("Index " + index));
};
//当删除元素时候的回调函数,实现删除元素时,有提示框跳出
_npcItemArray.onRemoveCallback = (ReorderableList list) =>
{
if (EditorUtility.DisplayDialog("Warnning", "Do you want to remove this element?", "Remove", "Cancel"))
{
ReorderableList.defaultBehaviours.DoRemoveButton(list);
}
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
//自动布局绘制列表
_npcItemArray.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
}
3.通过PropertyDrawer来绘制PlayerItem的样式,注意这是对NPCItem类的绘制,不是DialogSystemController类。同样是编辑器类,需要放在Editor文件夹下
using UnityEngine;
using UnityEditor;
using UnityEngine.UI;
[CustomPropertyDrawer(typeof(NPCItem))]
public class DialogSystemDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
using (new EditorGUI.PropertyScope(position, label, property))
{
//设置属性名宽度
EditorGUIUtility.labelWidth = 60;
position.height = EditorGUIUtility.singleLineHeight;
var iconRect = new Rect(position)
{
width = 64,
height = 64
};
var IDRect = new Rect(position)
{
width = position.width - 80,
x = position.x + 80
};
var nameRect = new Rect(IDRect)
{
y = IDRect.y + EditorGUIUtility.singleLineHeight + 5
};
var iconProperty = property.FindPropertyRelative("icon");
var IDProperty = property.FindPropertyRelative("ID");
var nameProperty = property.FindPropertyRelative("name");
iconProperty.objectReferenceValue = EditorGUI.ObjectField(iconRect, iconProperty.objectReferenceValue, typeof(Sprite), false);
IDProperty.stringValue = EditorGUI.TextField(IDRect, IDProperty.displayName, IDProperty.stringValue);
nameProperty.stringValue = EditorGUI.TextField(nameRect, nameProperty.displayName, nameProperty.stringValue);
}
}
}
效果如下(ID为场景中NPC的具体名字,Icon和Name自定义):
1.为DialogPanel新增View代码,DialogPanelView.cs,主要任务是接收Conrtoller的代码,显示出对话系统的UI层
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class DialogPanelView : MonoBehaviour
{
[Header("Player")]
public Image PlayerImage;
public Text PlayerName;
public Text PlayerText;
[Header("NPC")]
public Image NPCImage;
public Text NPCName;
public Text NPCText;
[Header("Panels")]
public GameObject playerDialogPanel;
public GameObject NPCDialogPanel;
public static DialogPanelView Instance;
void Awake()
{
Instance = this;
}
void Start()
{
PlayerImage.GetComponent<Image>();
PlayerName.GetComponent<Text>();
PlayerText.GetComponent<Text>();
NPCImage.GetComponent<Image>();
NPCName.GetComponent<Text>();
NPCText.GetComponent<Text>();
}
}
2.再次为DialogPanel新增代码,TalkWin.cs,主要任务是通过DOTween插件实现主角和NPC的对话内容显示
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
using System;
public class TalkWin : MonoBehaviour
{
public int textID;
public Text NPCTalkText;
public Text playerTalkText;
public GameObject answerGrid;
public GameObject answer;
private List<GameObject> answers = new List<GameObject>();
public CommonTalkNode[] commonTalkNodes;
public SwitchTalkNode[] switchTalkNodes;
private Dictionary<int, CommonTalkNode> commonDic = new Dictionary<int, CommonTalkNode>();
private Dictionary<int, SwitchTalkNode> switchDic = new Dictionary<int, SwitchTalkNode>();
public static TalkWin instance;
private void Awake()
{
instance = this;
foreach (CommonTalkNode item in commonTalkNodes)
{
commonDic.Add(item.ID, item);
}
foreach (SwitchTalkNode item in switchTalkNodes)
{
switchDic.Add(item.ID, item);
}
}
private void Start()
{
WhenMouseClick();
GetComponent<Button>().onClick.AddListener(WhenMouseClick);
}
private void WhenMouseClick()
{
//NPC为对话模式
if (textID > 1000 && textID < 2000)
{
UpdateTalkWinShow(commonDic[textID].NPCTalkText, commonDic[textID].playerTalkText, (float)commonDic[textID].charSpeed);
textID = commonDic[textID].nextID;
//Debug.Log(textID);
}
//NPC为问答模式
else if (textID > 2000 && textID < 3000)
{
UpdateTalkWinShow(switchDic[textID].NPCTalkText, switchDic[textID].playerTalkText, (float)switchDic[textID].charSpeed);
CreateAnswerUI(switchDic[textID].switchText);
GetComponent<Button>().interactable = false;
}
//结束对话,关闭对话Panel
//BUG:KeyNotFoundException: The given key was not present in the dictionary
if (commonDic.ContainsKey(commonDic[textID].ID) && commonDic[textID].NPCID != CheckNPCID())
{
DialogTriggerEvent.Instance.DialogPanel.SetActive(false);
}
}
public void WhenSwitchNodeGetAnswer(int number)
{
textID = switchDic[textID].switchNextID[number];
foreach (GameObject item in answers)
{
Destroy(item);
}
GetComponent<Button>().interactable = true;
WhenMouseClick();
}
public void UpdateTalkWinShow(string NPCTalkText, string playerTalkText, float charSpeed)
{
if(playerTalkText != "0") //主角说话时,开启playerPanel,关闭NPCPanel
{
DialogPanelView.Instance.playerDialogPanel.SetActive(true);
DialogPanelView.Instance.NPCDialogPanel.SetActive(false);
this.playerTalkText.text = "";
this.playerTalkText.DOText(playerTalkText, charSpeed * playerTalkText.Length);
}
else //主角不说话时,关闭playerPanel,开启NPCPanel
{
DialogPanelView.Instance.playerDialogPanel.SetActive(false);
DialogPanelView.Instance.NPCDialogPanel.SetActive(true);
this.NPCTalkText.text = "";
this.NPCTalkText.DOText(NPCTalkText, charSpeed * NPCTalkText.Length);
}
}
public void CreateAnswerUI(string[] switchText)
{
for (int i = 0; i < switchText.Length; i++)
{
GameObject go = Instantiate(answer, answerGrid.transform);
go.GetComponent<QuestionUI>().SetAnswerUI(switchText[i],i);
answers.Add(go);
}
}
public string CheckNPCID()
{
return DialogTriggerEvent.Instance.NPCSingle;
}
}
///
/// 所有句子的父类
///
public abstract class TalkNode
{
// 通过NPCID进行定位
public string NPCID;
// 每个句子独有的ID
public int ID;
//NPC文本
public string NPCTalkText;
//player文本
public string playerTalkText;
// 字符速度
public double charSpeed;
public TalkNode(string NPCID, int ID, string NPCTalkText,string playerTalkText, double charSpeed)
{
this.NPCID = NPCID;
this.ID = ID;
this.NPCTalkText = NPCTalkText;
this.playerTalkText = playerTalkText;
this.charSpeed = charSpeed;
}
}
[Serializable]
public class CommonTalkNode : TalkNode
{
public int nextID;
public CommonTalkNode(string NPCID , int ID, string NPCTalkText, string playerTalkText, double charSpeed, int nextID) : base(NPCID , ID, NPCTalkText, playerTalkText, charSpeed)
{
this.nextID = nextID;
}
}
[Serializable]
public class SwitchTalkNode : TalkNode
{
public string[] switchText;
public int[] switchNextID;
public SwitchTalkNode(string NPCID, int ID, string NPCTalkText, string playerTalkText, double charSpeed, string[] switchText, int[] switchNextID) : base(NPCID, ID, NPCTalkText, playerTalkText, charSpeed)
{
this.switchText = switchText;
this.switchNextID = switchNextID;
}
}
3.在场景中AnswerGrid下新建Image组件,重命名为Answer,调整至合适大小,并且加入Button组件,并为他挂载新的代码QuestionUI.cs,为其添加一个Text子物体AnswerText,添加为预制体,用于显示问答对话中的答案选项
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class QuestionUI : MonoBehaviour
{
public Text text;
public int number;
public void Start()
{
GetComponent<Button>().onClick.AddListener(()=> {
TalkWin.instance.WhenSwitchNodeGetAnswer(number); });
}
public void SetAnswerUI(string s,int i)
{
text.text = s;
number = i;
}
}
4.Player触发对话系统代码DialogTriggerEvent.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DialogTriggerEvent : MonoBehaviour
{
public GameObject DialogPanel;
List<NPCItem> npcList;
GameObject[] NPCgameObject;
public string NPCSingle = null; //传递到TalkWin
public static DialogTriggerEvent Instance;
void Awake()
{
Instance = this;
}
void Start()
{
NPCgameObject = GameObject.FindGameObjectsWithTag("NPC");
if(DialogSystemController.Instance.npcItemArray.Count!=0)
{
npcList = DialogSystemController.Instance.npcItemArray;
}
}
//进入对话区域开启Panle,结束对话关闭Panel
private void OnTriggerEnter(Collider collider)
{
if(collider.tag == "NPCDialog")
{
DialogPanel.SetActive(true);
GetNPCByID(collider.transform);
}
}
//通过当前Player触发的Trigger判断是在和哪个NPC对话
public void GetNPCByID(Transform transform)
{
for (int i = 0; i < NPCgameObject.Length; i++)
{
Transform targetNPC = transform.parent.Find(NPCgameObject[i].name);
if(targetNPC)
for (int j = 0; j < npcList.Count; j++)
{
if (npcList[j].ID == targetNPC.name)
{
NPCSingle = targetNPC.name; //传递到TalkWin
//设置当前NPC的名字和图片
DialogPanelView.Instance.NPCName.text = npcList[j].name;
DialogPanelView.Instance.NPCImage.sprite = npcList[j].icon;
DialogPanelView.Instance.NPCImage.rectTransform.position = new Vector3(289.85f, 292, 0);
}
}
}
}
}
1.各个代码拖入其需要的GameObject
2.DialogPanel的TalkWin代码中填写对话内容
这里需要注意几点:
TalkWin.cs类中包括两个Dictionary,其中 Dictionary
NPC讲话时,playerTalkText的值填写0
Player讲话时,NPCTalkText的值什么也不填写
对话结束后应跳转到一个两人都没有讲话内容的一条对话,保证在最后一次对话显示后,正常关闭对话UI
以下长图为我填写的对话内容,尤其注意ID 1006,ID 1108,ID 1201在这里是必须的
首先和NPC1对话
和NPC1对话完再去找NPC2
1.总体缺点比较明显,NPC多了再去代码的列表里修改内容,就太麻烦了,不好查改
2.只适合于和一个NPC的对话内容一次讲完的设定,如果和黑魂一样可以对话到一半走开,就不行了
3.两个Dictionary之间交换数据时unity会报错,目前还没有找到解决方案,具体BUG位置写在了TalkWin.cs中,欢迎道友指正
4.项目地址:Simple dialog system 门钥匙:hhhh