Unity 简单RPG对话系统——龙之谷2的NPC对话系统

Unity 简单RPG对话系统——龙之谷2的NPC对话系统

龙之谷2手游正式上线后不久,试玩了十几分钟(包括捏脸的5分钟),之后就再也没有打开过了。
本文章将对龙之谷2的NPC对话系统进行高仿,同时考虑到 策划可能不断修改对话内容 工具的重复利用,将部分通过编辑器模式来进行制作。

写在前边:

1.参考文章

知乎: Yumir——用128行代码实现一个文字冒险游戏
站内: 虚拟喵——Unity 编辑器扩展总结 五:数组或list集合的显示方式

2.素材使用
unity AssetStore——Unity-Chan! Model

3.根据如下游戏内截图,拆分实现步骤


Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第1张图片

正式开始

一、新建Unity工程,并设置UIPanel

  1. 这里为了 偷懒 方便演示,使用Unity娘到场景中,并复制出两个作为NPC,每个人物模型的Perfab作为空物体**Handle的子物体

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第2张图片

  1. NPC1Handle和NPC2Handle分别创建子物体SphereCollider,调整至合适大小,勾选Is Trigger,并重命名为DialogTrigger,Tag选择为NPCDialog,作为主角经过时触发对话界面的触发器

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第3张图片

  1. 新建Canvas–>Panel,重命名为DialogPanel,根据个人感觉调整DialogPanel的显示区域
  2. Canvas目录下新建一个空物体,添加GridLayoutGroup组件,用作问答类对话加载答案选项,这里重命名为AnswerGrid
  3. 新建一个空物体,用来挂载接下来的对话系统控制组件,这里重命名为DialogSystem

整体的层级图如下:

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第4张图片

二、调整项目内Player的控制

根据自己的项目情况,让场景内放置好的人物能够实现简单的移动,这里以我下载好的Unity娘为例

  • MainCamera代码:ThirdPersonCamera.cs,找到FixedUpdate ()部分,取消鼠标控制摄像机(因为后边希望通过鼠标左键控制对话进度,当然有需要的同学可以分开写鼠标操作)

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第5张图片

  • Player组件中,去除用不到的组件,并新建一个DialogTriggerEvent.cs,用于控制Player和NPC相遇弹出对话框,具体代码内容稍后再写

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第6张图片

三、逻辑代码拆分

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第7张图片

四、对话系统控制组件和他的自定义Inspector显示

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自定义):

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第8张图片

五、对话系统显示

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;
    }
}

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第9张图片
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 commonDic 用于存储正常你一句我一句的对话内容,Dictionary switchDic 用于存储问答式的对话内容

  • NPC讲话时,playerTalkText的值填写0

  • Player讲话时,NPCTalkText的值什么也不填写

  • 对话结束后应跳转到一个两人都没有讲话内容的一条对话,保证在最后一次对话显示后,正常关闭对话UI

以下长图为我填写的对话内容,尤其注意ID 1006,ID 1108,ID 1201在这里是必须的

Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第10张图片
Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第11张图片
Unity 简单RPG对话系统——龙之谷2的NPC对话系统_第12张图片

效果展示:

首先和NPC1对话

和NPC1对话完再去找NPC2

写在最后:

1.总体缺点比较明显,NPC多了再去代码的列表里修改内容,就太麻烦了,不好查改
2.只适合于和一个NPC的对话内容一次讲完的设定,如果和黑魂一样可以对话到一半走开,就不行了
3.两个Dictionary之间交换数据时unity会报错,目前还没有找到解决方案,具体BUG位置写在了TalkWin.cs中,欢迎道友指正
4.项目地址:Simple dialog system 门钥匙:hhhh

你可能感兴趣的:(Unity实战,unity,unity3d,c#)