Unity3D RPG实现 3 —— 对话、任务系统

2023-11-15

目录

成果展示

对话系统

对话的存储数据结构

对话的UI面板设置

创建对话&任务的 NPC

实现对话控制器显示主对话窗口的内容

创建对话的选项内容

任务系统

创建任务 UI 面板

 任务的存储数据结构

任务管理器与接受任务

任务控制相关脚本

实现点按任务显示信息

接受任务时检查任务物品是否契合

根据完成情况控制任务对话分支

拿到任务奖励

保存任务数据

代码汇总

背包部分

Dialogue部分

任务部分


成果展示

对话系统

对话的存储数据结构

一个对话框需要什么呢?我们先来想想:

首先需要一个字符串,表示内容,

为了方便对话间的跳转,我们还需要给每个对话唯一标识,因此需要ID

对话是由人说的,我们还需要人物头像

具体如下:

首先需要一个对话信息:

数据如下:含有当前对话的id、头像、文本以及选项

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

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


}

另一方面,每个对话可能会有选项,每个选项本身也有内容,并且一些选项可能意味着是否要接取任务,以及选择选项后,我们需要跳转到不同的对话,于是需要一个targetID

于是选项的数据结构如下:

[System.Serializable]
public class DialogueOption 
{   
    public string text;
    public string targetID;
    public bool takeQuest;
}

我们将所有的对话信息存储在SO中,因此:

[CreateAssetMenu(fileName ="New Dialogue",menuName ="Dialogue/Dialogue Data")]
public class DialougeData_SO : ScriptableObject
{

    public List<DialoguePiece> dialoguePieces = new List<DialoguePiece>();
}

此时完整的一个对话SO如下:

完整的数据结构如下

逻辑:在Dialogue Data中有若干个piece,每个piece中有若干个IDTextImageQuest,还包含有List<Option>

如果有Option,我们就通过获取其Target ID

对话的UI面板设置

创建一个面板:

设定Panel 

添加vertical layout Group

添加对话框还有按钮并设定其位置:

再添加一个头像:

在这种情况下, 如果有buttontext的话,布局就会比较乱,因此此处将头像图片放在Main Text下,并且锚点放在左边,这样锚点就是在对话框的左边了。

接下来还需要给Dialogue Panel添加verticalLayoutGroup,这样子物体才可以使用content size filter

此时头像的图片是以文字为基准点,所以就在对话框的外面了:

这样可以使得按钮在比较靠右的位置。

为了使其能在竖直方向上拉伸:

然后在DialoguePanelMain Text中都添加content Size Fitter

这样就可以根据文本数量的多少改变框的大小。

问题在于字少的时候,头像会超出位置。

设定一个最小高度即可:

记得给文字设定居中:

Button设定如下选项:

接下来为选择面板添加Button

将按钮拉长,并选择居于右部,就会发现此时Button位于右边。

选择text,改变其对话框长度:

多复制几个按钮,并将面板透明度设置为0

创建对话&任务的 NPC

导入素材后,要将场景素材升级到URP

别忘记添加基本的必要组件

  • 自己单独创建 Animator Controller 只添加一个 Idle 动画即可
  • 修改 Tag 及 Layer
  • 将这个人物保存成 Original Prefab 在你的文件夹中

实现对话控制器显示主对话窗口的内容

接下来实现在对话框中显示显示对话内容的效果。

首先要先将之前创建的UI的那些组件用脚本控制:

(UpdateDialogueData是用于根据传入的对话信息进行更新的函数,后续会补充完整)

public class DialogueUI : Singleton<DialogueUI>
{
    [Header("Basic Elements")]
    public Image icon;
    public Text mainText;
    public Button nextButton;

    public GameObject dialoguePanel;

    [Header("Data")]
    public DialougeData_SO currentData;
    int currentIndex = 0;
    
    public void UpdateDialogueData(DialougeData_SO data)
    {
        currentData = data;
        currentIndex = 0;
    }

}

 把那些栏拖拽到里面

NPC 添加 DialogueController 脚本,用于控制对话

当触发和npc的对话,打开对话面板时,我们就要传给对话UI面板给对话数据:

以一个简单的例子:

进行赋值

具体根据对话数据存储的头像,对话语句进行更新

如果对话数据不止一条对话内容,则我们可以继续往下更新对话

这样即可实现点击进入下一行对话,没有对话时面板消失。

创建对话的选项内容

将Button作为预制体:

创建对话的选项OptionUI的脚本,并用该脚本控制选项的组件:

为了实现在对话里生成选项,首先得为选项单独创建一个面板,并在脚本中获取选项所在的Panel及脚本:

因为对话会切换,所以选项也需要生成与销毁

接下来实现选项的销毁与创建:

在选项的UI脚本中,还需要根据传入的参数设定UI

这样即可实现生成选项:

接下来实现点击选项的事件:

我们希望根据选项所存储的下一个对话的ID实现跳转:

所以需要在optionUI里添加下一个对话ID的变量

然后在选项中更新:

为了降低耦合度,使用委托来实现接下来添加点击时需要执行的事件:

给Button添加Onclick事件

                     

获取了ID之后,然后就通过ID去对话的List中去获取该对话信息:

实现打字的效果:下载DOTween插件。

这样即可实现打字效果

但是此处还有一个问题,在于当next按钮消失时布局会发生改变。

还有currentIndex的顺序++,这个和选项无关

任务系统

创建任务 UI 面板

创建canvas:

创建一个panel附上图片,然后为实现任务左侧的滚动栏:

然后将滚动的栏删掉变成这样,(删除滑动条后记得调整viewport大小)

设定vertical group:

 

添加item slot:

以及item tooltip

 任务的存储数据结构

  • 创建任务基本变量
  • 创建 QuestRequire 用来追踪我们要完成的任务目标
  • 在 DialoguePiece 中加入 Quest 并设置一个新对话可以接受任务

思考一下,一个任务需要什么?需要有任务的名字、描述、要求、完成的状态。

任务的需求,我们以物品或者杀的怪物来举例,我们需要名字,需要的数量,有的数量,于是不难得出数据结构如下:

(任务可能有多个需求,所以需要用List来存储)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Quest", menuName = "Quest/Quest Data")]
public class QuestData_SO : ScriptableObject
{
	[System.Serializable]
	public class QuestRequire
	{
		public string name;
		public int requireAmount;
		public int currentAmount;
	}


	public string questName;
	[TextArea]
	public string description;

	//需要三种任务完成的状态,npc才会有不同的反应
	public bool isStarted;
	public bool isComplete;
	public bool isFinised;

	public List<QuestRequire> questRequires = new List<QuestRequire>();

}

此处在这里将任务所需的怪物变量改的名字相同:

在对话一栏中加入任务作为成员变量,因为,我们有时候需要根据对话来接受任务

然后设置对话:

别忘了,任务还需要有奖励:

 添加奖励:

(bug记录:DondestroyOnLoad的物体不能是子物体,因此:不能放在别人的子集

任务管理器与接受任务

接下来书写管理任务的管理器:用于管理全部的任务信息,使用List来存储全部的任务以及一些控制任务的函数。

public class QuestManager : Singleton<QuestManager>
{
    public List<QuestTask> tasks = new List<QuestTask>();
}

具体的任务的书写如下:包含任务数据,以及任务的三种状态。


    public class QuestTask
    {
        public QuestData_SO questData;
        public bool IsStarted {
            get { return questData.isStarted; }
            set { questData.isStarted = value; }
        }

        public bool IsComplete
        {
            get { return questData.isComplete; }
            set { questData.isComplete = value; }
        }

        public bool IsFinished
        {
            get { return questData.isFinished; }
            set { questData.isFinished  = value; }
        }

    }


有了存储任务的数据结构后,作为一个管理器,还需要基础的增、查、获取的功能:

    public bool HaveQuest(QuestData_SO data)//判断是否有这个任务
    {
        //在头文件中引入Ling,可以用于查找链表中的内容
        if (data != null)
            return tasks.Any(q => q.questData.questName == data.questName);
        else return false;
    }

    //根据任务数据的名字查找链表中的某一个任务
    public QuestTask GetTask(QuestData_SO data)
    {
        return tasks.Find(q => q.questData.questName == data.questName);
    }

上面有了查和获取,

增加任务的功能只需要直接往List中调用Add函数即可。

而什么时候需要添加任务呢?就是当玩家和NPC进行对话的时候来处理。

具体如下:

思路很简单,当玩家在对话中做出选择,判断本次的选项中是否含有任务,然后判断玩家是否选择了接任务的选项,如果含有,则判断该任务是否在列表中了,如果不在列表中,则将该任务实例化,并且加入到QuestManager的单例的List中。

这样即可实现对话后加任务的思路。

除此之外,我们接取任务后还希望设置任务的状态为开始状态:

但是直接修改并没有用,这只是修改的临时变量的值。

因此需要在QuestManager中根据任务的数据来查找管理器的链表中对应的任务

随后在此处设定开始:

任务控制相关脚本

接下来继续创建任务按钮以及任务需求的面板,并用脚本进行控制。

  • 创建 QuestNameButton 添加在 任务名字按钮 上
  • 创建 QuestRequirement 添加在 Requirement 上
  • 设置好以上两个类的变量并且拿到赋值

这个Button是最终面板中的这里:

 任务按钮需要哪些东西呢?由于我们希望点击任务的名字可以跳转到该任务并展示信息,所以需要按钮,以及任务的内容text。

所以button的控制脚本如下(任务按钮会存储该任务的信息,所以需要QuestData——SO变量)

为每个按钮做一个预制体,并且用脚本去控制

完善上面的脚本,并且书写设定按钮的函数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class QuestNameButton : MonoBehaviour
{
    public Text questNameText;
    public QuestData_SO currentData;
    //public Text questContentText;

    public void SetupNameButton(QuestData_SO quesData)
    {
        currentData = quesData;

        if (quesData.isComplete)
            questNameText.text = quesData.questName + "(完成)";
        else
            questNameText.text = quesData.questName;
    }

    private void Awake()
    {
        GetComponent<Button>().onClick.AddListener(UpdateQuestContent);
    }
    void UpdateQuestContent()
    {
        //questContentText.text = currentData.description;
        QuestUI.Instance.SetupRequireList(currentData);

        foreach(Transform item in QuestUI.Instance.rewardTransform)
        {
            Destroy(item.gameObject);
        }


        foreach(var item in currentData.rewards)//奖励可能不止一个所以需要循环列表
        {
            QuestUI.Instance.SetupRewardItem(item.itemData, item.amount);
        }
    }
}

此处设定任务内容有两种方式,一种是,我们使用单例的QuestUI(也就是实际上的Quest Manager)来统一显示这些数据

由于很多数据的显示我们是通过一个统一的管理器QuestUI来实现的,所以需要将相关的一些UI组件交由脚本来控制

继续完善任务控制脚本所需的组件:

接下来书写任务需求的脚本,它与UI面板中,它储存的是每个任务的需求和名字。

可以看到每个任务都带有这个脚本

接下来将单独的每个需求做成预制体,并将任务需求所在的Panel交由QuestUI来统一控制:

任务的奖励

奖励就是实际的物品Item

并且还需要获取奖励所在的面板

展示物品信息的代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class ShowTooltip : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    private ItemUI currentItemUI;

    void Awake()
    {
        currentItemUI = GetComponent<ItemUI>();
    }
    public void OnPointerEnter(PointerEventData eventData)
    {
        Debug.Log("mouse in slot");
        QuestUI.Instance.tooltip.gameObject.SetActive(true);
        QuestUI.Instance.tooltip.SetupTooltip(currentItemUI.currentItemData);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        QuestUI.Instance.tooltip.gameObject.SetActive(false);
    }
}

QuestUI完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class QuestUI : Singleton<QuestUI>
{
    [Header("Elements")]
    public GameObject quesPanel;
    public ItemToolTip tooltip;
    bool isOpen;

    [Header("Quest Name")]
    public RectTransform questListTransform;
    public QuestNameButton questNameButton;

    [Header("Text Content")]
    public Text quesContentText;

    [Header("Requirement")]
    public RectTransform requireTransform;
    public QuestRequirement requirement;

    [Header("Reward Panel")]
    public RectTransform rewardTransform;
    public ItemUI rewardUI;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            isOpen = !isOpen;
            quesPanel.SetActive(isOpen);
            quesContentText.text = string.Empty;
            SetupQuestList();
        }


        if (!isOpen)
            tooltip.gameObject.SetActive(false);
    }

    public void SetupQuestList()
    {
        //清除原来已有的任务
        foreach(Transform item in questListTransform)
        {
            Destroy(item.gameObject);
        }
        foreach(Transform item in rewardTransform)
        {
            Destroy(item.gameObject);
        }
        foreach (Transform item in requireTransform)
        {
            Destroy(item.gameObject);
        }

        //遍历列表中的list,接取任务
        foreach(var task in QuestManager.Instance.tasks)
        {
            var newTask = Instantiate(questNameButton, questListTransform);
            newTask.SetupNameButton(task.questData);
            //newTask.questContentText = quesContentText;
        }


    }

    public void SetupRequireList(QuestData_SO questData)
    {
        quesContentText.text = questData.description;
        //将涉及到QuestNameButton中的三处questContentText关闭,不使用在里面传东西然后赋值的形式了,改为在此处直接修改

        foreach (Transform item in requireTransform)
        {
            Destroy(item.gameObject);
        }
        foreach(var require in questData.questRequires)
        {
            var q = Instantiate(requirement, requireTransform);
            q.SetupRequirement(require.name, require.requireAmount, require.currentAmount);
        }
    }

    public void SetupRewardItem(ItemData_SO itemData,int amount)
    {
        var item = Instantiate(rewardUI, rewardTransform);
        item.SetupItemUI(itemData, amount);
    }

}
 

实现点按任务显示信息

  • 按照逻辑去写每一个需要的函数方法
  • 设置任务按钮显示对应任务名字
  • 实现点击名字按钮能显示任务详情以及任务需求
  • 调整 UI 布局

添加按钮的监听事件:显示任务的描述(实现当按下按钮时会更新信息的操作)

实现显示任务的需求

首先在需求的预制体里写一个供外部调用的接口

然后在管理器中调用该接口

在按钮点击事件中调用该函数:

这样即可显示:

但是任务信息和需求重叠在了一起。

在这里可以使得它强制布局:

这样显示就正常了

  • SetupRewardList的 方法实现
  • 创建 ShowTooltip 实现鼠标滑入显示信息
  • 创建多个任务并修复重复显示任务奖励的问题

对于任务信息的显示,除了之前的那种方法:

回顾一下之前的方法:

在QuestNameButton这个预制体的脚本里面给每个任务对应的text里声明该变量:

然后在设定任务时,将任务的需求的那个text赋值给它。

然后根据按钮更新数据的时候,再从任务的SO中获取任务内容再赋值:

这样的过程比较冗余。

现在删去其中在另外一个脚本QuestNameButton中声明变量并且传值然后修改的过程,直接在QuestUI中统一修改:

接下来实现展现需求的代码:

在questUI中,调用之前写的slotItem的方法:

这样即可实现

注意布局

接下来实现鼠标放到上面时显示信息的功能:

脚本并非挂在那个tooltip上,否则因为初始它是不可见的所以没法调用,应该放在reward item slot上。

那么和之前一样,需要实现接口即可:

然后放到上面时就显示设置active为true然后显示信息。

然后设置setupTooltip:

但是有个问题在于:

这个东西返回的是背包里的数据:

而不是实际存在的值。

所以此处需要添加新的变量:

将原来那个函数修改成这样:

这样即可实现,但是发现会出现遮挡现象:

将其放在最下方,就不会被遮挡了:

然后还有个问题在于背包关闭再次打开时,这个还是存在:这是因为即使关闭面板栏,tooltip依然是Active状态。

这样即可解决。

接下来实现多个任务时,出现了这样的问题:

任务来回切换会导致item Slot变多:

更新时清除奖励:

 

检测和更新任务进度

  • 创建函数在 敌人死亡 和 拾取物品时 更新任务进度
  • 使用 Linq 语句中的 Where 找到匹配的任务需求并检查是否满足
  • 修改特殊情况使用任务物品消耗后更新进度

任务进度:检查背包中的物品数量和任务所需的物品数量是否相同,

敌人死亡时进行调用

代码中如果出现两个任务,则两个任务中的都要对应减少。

书写检查任务的函数

提问:为什么有些函数放在QuestUI中有些函数放在QuestManager中,有些函数放在QuestData_SO中呢?

QuestUI是用来处理和UI界面的显示相关的函数。是MVC模式中的模型 Model

QuestData_SO中需要的函数很少,只需要涉及到修改状态数据的函数。是MVC中的视图View

而QuestManager用来处理读取数据以及和UI界面交互的功能。是MVC中的控制器Controller

拾取物品时也进行调用:

这样即可实现完成任务。

但是我们发现使用物品后,任务不会更新。

在此处进行修改:

接受任务时检查任务物品是否契合

  • 考虑可能存在的情况在接受任务的时候检查背包是否有任务物品
  • 在 QuestData_SO 中创建 RequireTargetName 拿到需求的名字列表
  • 循环列表中每一项在 Inventory 中检查是否存在并更新数据

有个问题在于如果接任务前背包就有两个蘑菇了,此时并不会任务面板并不会更新;
所以需要在InventoryManager中设定一个刚开始的函数中就去检查背包中的物品

检查任务物品:

由于一个questRequire中有多个物品:

因此需要一个包含其名字的list,循环时要判断每一个是否有

创建一个包含名字的链表:

这样就可以实现接受任务时就检查是否完成。

在questGiver里获取当前任务的任务状态:


 

根据完成情况控制任务对话分支

根据任务完成的状态,需要给予npc不同的对话,所以给npc创建一个脚本:

然后有QuestGiver去修改Dialogue Controller里面的数据

QuestGiver包含多种对话

[RequireComponent(typeof(DialogueController))]
public class QuestGiver : MonoBehaviour
{
    DialogueController controller;
    QuestData_SO currentQuest;

    public DialougeData_SO startDialogue;
    public DialougeData_SO progressDialogue;
    public DialougeData_SO completeDialogue;
    public DialougeData_SO finishDialogue;

    private void Awake()
    {
        controller = GetComponent<DialogueController>(); 
    }
}

对于发布任务的人,我们需要让它拿到任务的完成状态,才能根据不同状态执行不同对话:

然后赋值给当前任务:

在questGiver的update函数中则根据不同的状态进行切换。

然后自行设定四个对话的内容并传入:

有个bug会产生,问题在于不要出现这种情况

这样就实现了任务不同状态时的不同对话。

添加一个功能:

当玩家远离时则自动关闭对话:

拿到任务奖励及扣除报酬

拿到任务奖励需扣除报酬,那么可以这样,将奖励设置为-2:

接受完任务后,背包和栏里面的物品会更新,因此需要写一个根据任务里的物品判断背包是否有该物品的函数 

在任务数据里书写给予奖励的函数:

public void GiveRewards()
    {
        foreach(var reward in rewards)
        {
            if (reward.amount < 0)
            {
                int requireCount = Mathf.Abs(reward.amount);

                //优先在背包里找是否有该物品,
                if (InventoryManager.Instance.QuestItemInBag(reward.itemData) != null)
                {
                    //这种情况是背包里的东西不够,那就先在背包里扣除一部分,
                    if (InventoryManager.Instance.QuestItemInBag(reward.itemData).amount <= requireCount)
                    {
                        requireCount -= InventoryManager.Instance.QuestItemInBag(reward.itemData).amount;//所需的数量减少
                        InventoryManager.Instance.QuestItemInBag(reward.itemData).amount = 0;//背包里的商品扣除为0

                        //背包里东西不够,剩下的部分从行动栏里扣除
                        if (InventoryManager.Instance.QuestItemInAction(reward.itemData) != null)
                        {
                            InventoryManager.Instance.QuestItemInAction(reward.itemData).amount -= requireCount;
                        }

                    }
                    //这种情况就是背包里的东西直接够,那直接扣除就好
                    else
                    {
                        InventoryManager.Instance.QuestItemInBag(reward.itemData).amount -= requireCount;
                    }

                }
                //这种情况是背包里一点东西都没有,那就直接扣除行动栏里的物品
                else
                {
                    InventoryManager.Instance.QuestItemInAction(reward.itemData).amount -= requireCount;
                }
            }
            else
            {
                InventoryManager.Instance.inventoryData.AddItem(reward.itemData, reward.amount);
            }

            InventoryManager.Instance.inventoryUI.RefreshUI();
            InventoryManager.Instance.actionUI.RefreshUI();
        }
    }

在OptionUI中执行给与奖励:

这里做的时候出现了一个bug,问题在于之前的对话中没添加quest:

这样即可实现交付任务和扣除东西。

但是有个问题在于:

蘑菇会显示负数。

改动如下:

接下来还有一个小小问题,在于任务完成了,继续拾取蘑菇,任务进度还是会更新,解决方法:

然后在执行更新需求的代码里:

如果完成了则执行完成的设定 

在对话时,我们希望保持鼠标是固定的样式:

为了避免已完成的任务再次受到数据更新的影响: 

保存任务数据

QuestManager里面保存的数据以List类型保存,而里面的数据不是SO类型的,所以不能用之前那样的方法保存 

以前的保存都是通过Object来保存的:

因此此处实现一个非SO类型的保存方法:

虽然也可以通过将其改成SO的方式来实现,但此处换种方式:

注意到tasks虽然不是SO,但是task里面的QuestData是SO类型的,我们可以保存它

书写保存和Load的方法:

public void SaveQuestManager()
    {
        PlayerPrefs.SetInt("QuestCount", tasks.Count);
        for(int i = 0; i < tasks.Count; i++)
        {
            SaveManager.Instance.Save(tasks[i].questData, "task" + i);
        }
    }


    //加载数据的方式是通过重新新创建一个SO,然后让SO读取数据,然后再加入到tasks链表当中
    public void LoadQuestManager()
    {
        var quesCount = PlayerPrefs.GetInt("QuestCount");
        for(int i = 0; i < quesCount; i++)
        {
            var newQuest = ScriptableObject.CreateInstance<QuestData_SO>();//
            SaveManager.Instance.Load(newQuest, "task" + i);
            tasks.Add(new QuestTask { questData = newQuest });
        }
    }

读取数据的方法可以放在初始时:

在QuestManager中添加

 这样即可实现切换场景保存任务:

代码汇总

背包部分

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum ItemType { Useable,Weapon,Armor}
[CreateAssetMenu(fileName ="New Item",menuName ="Inventory/Item Data")]
public class ItemData_SO : ScriptableObject
{
    public ItemType itemType;
    public string itemName;
    public Sprite itemIcon;
    public int itemAmount;//这个物品有多少数量

    [TextArea]
    public string description = "";

    public bool stackable;//是否可堆叠


    [Header("Weapon")]
    public GameObject WeaponPrefab;

    public AttackData_SO weaponData;
    public AnimatorOverrideController weaponAnimator;

    [Header("Useable Item")]
    public UseableItemData_SO useableData;
}

  using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemPickUp : MonoBehaviour
{
    public ItemData_SO itemData;

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            Debug.Log("OnTrigger");
            //GameManager.Instance.playerStats.EquipWeapon(itemData);
            InventoryManager.Instance.inventoryData.AddItem(itemData, itemData.itemAmount);
            InventoryManager.Instance.inventoryUI.RefreshUI();

            QuestManager.Instance.UpdateQuestProgress(itemData.itemName, itemData.itemAmount);
            //装备武器
            Destroy(gameObject);
        }
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class InventoryManager : Singleton<InventoryManager>
{

    public class DragData
    {
        public SlotHolder orginalHolder;
        public RectTransform originalParent;
    }
    [Header("Inventory Data")]
    //使用类似之前那样的模板,游戏新开始时复制一份的操作
    //TODO:最后添加模板用于保存数据
    public InventoryData_SO inventoryTemplate;
    public InventoryData_SO inventoryData;

    public InventoryData_SO actionTemplate;
    public InventoryData_SO actionData;

    public InventoryData_SO equipmentTemplate;
    public InventoryData_SO equipmentData;

    [Header("Containers")]
    public ContainerUI inventoryUI;
    public ContainerUI actionUI;
    public ContainerUI equipmentUI;

    [Header("Drag Canvas")]
    public Canvas dragCanvas;

    public DragData currentDrag;
    protected override void Awake()
    {
        base.Awake();
        if (inventoryTemplate != null)
            inventoryData = Instantiate(inventoryTemplate);
        if (actionTemplate != null)
            actionData = Instantiate(actionTemplate);
        if (equipmentTemplate != null)
            equipmentData = Instantiate(equipmentTemplate);
    }
    private void Start()
    {
        LoadData();
        inventoryUI.RefreshUI();
        actionUI.RefreshUI();
        equipmentUI.RefreshUI();
    }

    [Header("UI Panel")]
    public GameObject bagPanel;
    public GameObject statsPanel;
    bool isOpen;

    [Header("Stats Text")]
    public Text healthText;
    public Text attackText;

    [Header("Tooltip")]
    public ItemToolTip tooltip;
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            isOpen = !isOpen;
            bagPanel.SetActive(isOpen);
            statsPanel.SetActive(isOpen);
        }

        UpdateStatsText(GameManager.Instance.playerStats.MaxHealth, GameManager.Instance.playerStats.attackData.minDamage,
            GameManager.Instance.playerStats.attackData.maxDamage);
    }

    public void UpdateStatsText(int health,int min,int max)
    {
        healthText.text = health.ToString();
        attackText.text = min + " - " + max;
    }

    #region 检查拖拽物品是否在每一个slot范围内
    public bool CheckInInventoryUI(Vector3 position)//此处这个位置是要传输进来的位置
    {
        for(int i = 0; i < inventoryUI.slotHolders.Length; i++)
        {
            RectTransform t = inventoryUI.slotHolders[i].transform as RectTransform;
            if (RectTransformUtility.RectangleContainsScreenPoint(t, position))//判断当前位置是否物品栏里
            {
                return true;
            }
            
        }
        return false;
    }

    public bool CheckInActionUI(Vector3 position)//此处这个位置是要传输进来的位置
    {
        for (int i = 0; i < actionUI.slotHolders.Length; i++)
        {
            RectTransform t = actionUI.slotHolders[i].transform as RectTransform;
            if (RectTransformUtility.RectangleContainsScreenPoint(t, position))//判断当前位置是否物品栏里
            {
                return true;
            }

        }
        return false;
    }

    public bool CheckInEquipmentUI(Vector3 position)//此处这个位置是要传输进来的位置
    {
        for (int i = 0; i < equipmentUI.slotHolders.Length; i++)
        {
            RectTransform t = equipmentUI.slotHolders[i].transform as RectTransform;
            if (RectTransformUtility.RectangleContainsScreenPoint(t, position))//判断当前位置是否物品栏里
            {
                return true;
            }

        }
        return false;
    }
    #endregion


    public void SaveData()  
    {
        SaveManager.Instance.Save(inventoryData, inventoryData.name);
        SaveManager.Instance.Save(actionData, actionData.name);
        SaveManager.Instance.Save(equipmentData, equipmentData.name);

    }

    public void LoadData()
    {
        SaveManager.Instance.Load(inventoryData, inventoryData.name);
        SaveManager.Instance.Load(actionData, actionData.name);
        SaveManager.Instance.Load(equipmentData, equipmentData.name);
    }

    #region 检测任务物品
    public void CheckQuestItemInBag(string questItemName)
    {
        foreach(var item in inventoryData.items)
        {
            if (item.itemData != null)
            {
                if (item.itemData.itemName == questItemName)
                    QuestManager.Instance.UpdateQuestProgress(item.itemData.itemName, item.amount);
            }
        }

        foreach (var item in actionData .items)
        {
            if (item.itemData != null)
            {
                if (item.itemData.itemName == questItemName)
                    QuestManager.Instance.UpdateQuestProgress(item.itemData.itemName, item.amount);
            }
        }

    }
    #endregion


    //检测背包和快捷栏里的物体是否有和任务相同的
    public InventoryItem QuestItemInBag(ItemData_SO questItem)
    {
        return inventoryData.items.Find(i => i.itemData == questItem);
    }

    public InventoryItem QuestItemInAction(ItemData_SO questItem)
    {
        return actionData.items.Find(i => i.itemData == questItem);
     }

}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public enum SlotType { BAG,WEAPON,ARMOR,ACTION}
public class SlotHolder : MonoBehaviour,IPointerClickHandler,IPointerEnterHandler,IPointerExitHandler
{
    // Start is called before the first frame update
    public SlotType slotType;//这个需要用来告诉属于哪一个UI面板,因为后面会有背包、行动
    public ItemUI itemUI;    //这是SlotHolder的子物体,Image和Text的父物体。



    //SlotHolder可以理解为格子里的物品的UI信息。
    //获取是为了给子物体中的image和text进行修改


    public void UpdateItem()
    {
        switch (slotType)
        { 
            case SlotType.BAG:
                itemUI.Bag = InventoryManager.Instance.inventoryData;
                break;
            case SlotType.WEAPON:
                itemUI.Bag = InventoryManager.Instance.equipmentData;
                //装备武器 切换武器
                if (itemUI.Bag.items[itemUI.Index].itemData != null)
                {
                    GameManager.Instance.playerStats.ChangeWeapon(itemUI.Bag.items[itemUI.Index].itemData);
                }
                else
                {
                    GameManager.Instance.playerStats.UnEquipWeapon();
                }
                break;
            case SlotType.ARMOR:
                itemUI.Bag = InventoryManager.Instance.equipmentData;
                break;
            case SlotType.ACTION:
                itemUI.Bag = InventoryManager.Instance.actionData;

                break;
        }

        var item = itemUI.Bag.items[itemUI.Index];//找到数据库中对应序号的对应物品
        itemUI.SetupItemUI(item.itemData, item.amount);
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        if (eventData.clickCount % 2 == 0)//代表是双击的话
        {
            UseItem();
        }
    }
    public void UseItem()
    {
        if (itemUI.GetItem().itemType == ItemType.Useable&&itemUI.Bag.items[itemUI.Index].amount>0)
        {
            GameManager.Instance.playerStats.ApplyHealth(itemUI.GetItem().useableData.healthPoint);
            itemUI.Bag.items[itemUI.Index].amount -= 1;

            QuestManager.Instance.UpdateQuestProgress(itemUI.GetItem().itemName, -1);
        }
        UpdateItem();
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        if (itemUI.GetItem())
        {
            InventoryManager.Instance.tooltip.SetupTooltip(itemUI.GetItem());
            InventoryManager.Instance.tooltip.gameObject.SetActive(true);
        }
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        InventoryManager.Instance.tooltip.gameObject.SetActive(false);
    }

    void OnDisable()
    {
        InventoryManager.Instance.tooltip.gameObject.SetActive(false);
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

[RequireComponent(typeof(ItemUI))]
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    ItemUI currentItemUI;
    //
    SlotHolder currentHolder;
    SlotHolder targetHolder;

    void Awake()
    {
        currentItemUI = GetComponent<ItemUI>();
        currentHolder = GetComponentInParent<SlotHolder>();//原先的格子的信息
    }
    public void OnBeginDrag(PointerEventData eventData)
    {
        InventoryManager.Instance.currentDrag = new InventoryManager.DragData();
        InventoryManager.Instance.currentDrag.orginalHolder = GetComponentInParent<SlotHolder>();
        InventoryManager.Instance.currentDrag.originalParent = (RectTransform)transform.parent;
        //记录原始数据
        transform.SetParent(InventoryManager.Instance.dragCanvas.transform,true);
    }

    public void OnDrag(PointerEventData eventData)
    {
        //跟随鼠标位置移动
        transform.position = eventData.position;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        //放下物品 交换数据
        if (EventSystem.current.IsPointerOverGameObject())//是否指向UI组件
        {
            if(InventoryManager.Instance.CheckInActionUI(eventData.position)
                || InventoryManager.Instance.CheckInInventoryUI(eventData.position)
                || InventoryManager.Instance.CheckInEquipmentUI(eventData.position))
                //判断是否在三个栏里面的格子里
            {
                if (eventData.pointerEnter.gameObject.GetComponent<SlotHolder>())
                    targetHolder = eventData.pointerEnter.gameObject.GetComponent<SlotHolder>();
                else
                    targetHolder = eventData.pointerEnter.gameObject.GetComponentInParent<SlotHolder>();
                //如果没找到,此时是因为被图片所挡住了,那么就获取其父类的component

                Debug.Log(eventData.pointerEnter.gameObject);
                //判断鼠标选中的物体是否有slot holder,

                //判断是否目标holder是我的原holder
                if(targetHolder!=InventoryManager.Instance.currentDrag.orginalHolder)
                //由于例如食物不能放在武器栏里,所以需要对其做区分
                    switch (targetHolder.slotType)
                    {
                        case SlotType.BAG:
                            SwapItem();
                            break;
                         
                        //下面这些if的判断就确保了只有相同的物品才能实现交换
                        case SlotType.WEAPON:
                            if (currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Weapon)
                                SwapItem();
                            break;

                        case SlotType.ARMOR:
                            if (currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Armor)
                                SwapItem();
                            break;

                        case SlotType.ACTION:
                            if(currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType==ItemType.Useable)
                                SwapItem();
                            break;
                   
                    }
                
                currentHolder.UpdateItem();//交换完毕后需要更新数据
                targetHolder.UpdateItem();


            }
        }

        transform.SetParent(InventoryManager.Instance.currentDrag.originalParent);

        RectTransform t = transform as RectTransform;
        t.offsetMax = -Vector3.one * 5;
        t.offsetMin = Vector2.one * 5;


    }

    public void SwapItem()
    {
        //targetHolder是鼠标指向的位置的格子。
        //获取目标格子上面显示的UI,UI图片对应它身上属于哪个背包的哪一个序号的物品
        var targetItem = targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index];//实际上就是获取鼠标指向的物品item

        var tempItem = currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index];//点击前的鼠标的格子的信息

        //如果是相同的物品就进行合并
        bool isSameItem = tempItem.itemData == targetItem.itemData;
        if (isSameItem && targetItem.itemData.stackable)//并且还要可堆叠才行
        {
            targetItem.amount += tempItem.amount;
            tempItem.itemData = null;
            tempItem.amount = 0;
        }
        else
        {
            currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index] = targetItem;
            //targetItem=tempItem;这样的写法不行因为targetItem只是我们获取的一个变量,应该直接用其本身去更换,即下一行的写法
            targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index] = tempItem;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class DragPanel : MonoBehaviour,IDragHandler,IPointerDownHandler
{
    RectTransform rectTransform;

    Canvas canvas;
    void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
        canvas = InventoryManager.Instance.GetComponent<Canvas>();
    }
    public void OnDrag(PointerEventData eventData)
    {
        rectTransform.anchoredPosition += eventData.delta/canvas.scaleFactor;//让其锚点的位置的改变量和鼠标的改变量相同即可
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        rectTransform.SetSiblingIndex(2);
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ContainerUI : MonoBehaviour
{
    public SlotHolder[] slotHolders;
    
    public void RefreshUI()
    {
        for(int i = 0; i < slotHolders.Length; i++)
        {
            slotHolders[i].itemUI.Index = i;
            slotHolders[i].UpdateItem();
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ItemToolTip : MonoBehaviour
{
    public Text itemNameText;
    public Text itemInfoText;
    RectTransform rectTransform;

    private void Awake()
    {
        rectTransform = GetComponent<RectTransform>();
    }
    public void SetupTooltip(ItemData_SO item)
    {
        itemNameText.text = item.itemName;
        itemInfoText.text = item.description;
    }

    void OnEnable()
    {
        //开始时会闪烁是因为没有定位好坐标,开始时先更新一下即可避免这种效果
        UpdatePosition();

    }
    private void Update()
    {
        UpdatePosition();
    }
    public void UpdatePosition()
    {
        Vector3 mousePos = Input.mousePosition;
        rectTransform.position = mousePos;

        Vector3[] corners = new Vector3[4];
        rectTransform.GetWorldCorners(corners);

        float width = corners[3].x - corners[0].x;
        float height = corners[1].y - corners[0].y;

        if (mousePos.y < height)
            rectTransform.position = mousePos + Vector3.up * height * 0.6f;
        else if (Screen.width - mousePos.x > width)
            rectTransform.position = mousePos + Vector3.right * width * 0.6f;
        else
            rectTransform.position = mousePos + Vector3.left * width * 0.6f;

    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ItemUI : MonoBehaviour
{
    public Image icon = null;
    public Text amount = null;

    public InventoryData_SO Bag { get; set; }
    public InventoryData_SO Action { get; set; }
    public InventoryData_SO Equipment { get; set; }

    public ItemData_SO currentItemData;

    public int Index { get; set; } = -1;//初始值设为-1是因为一开始序号是从0开始的,避免一开始去setup每个格子的时候出现数据的错位排序

    public void SetupItemUI(ItemData_SO item,int itemAmount)
    {
        if (itemAmount == 0)
        {
            Bag.items[Index].itemData = null;
            icon.gameObject.SetActive(false);
            return;
        }

        //想要实现如果数量小于0则不显示,不能在上面的if条件改成<=0,因为此时并没有实际的背包

        if (itemAmount < 0) item = null;//只需要跳过下面的null部分就行了,并且需要设置active为false
        if (item != null)
        {
            icon.sprite = item.itemIcon;
            amount.text = itemAmount.ToString();
            icon.gameObject.SetActive(true);//默认是可见的,此处设为不可见

            currentItemData = item;

        }
        else
            icon.gameObject.SetActive(false);
    }

    public ItemData_SO GetItem()
    {
        return Bag.items[Index].itemData;
    }

}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LootSpawner : MonoBehaviour
{
    [System.Serializable]
    public class LootItem
    {
        public GameObject item;
        [Range(0, 1)]
        public float weight;
    }

    public LootItem[] lootItems; 

    public void Spawnloot()
    {
        float currentValue = Random.value;

        for(int i = 0; i < lootItems.Length; i++)
        {
            if (currentValue <= lootItems[i].weight)
            {
                GameObject obj = Instantiate(lootItems[i].item);
                obj.transform.position = transform.position + Vector3.up * 2;
                break;//确保一次只掉落一个物品
            }
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ActionButton : MonoBehaviour
{
    public KeyCode actionKey;
    private SlotHolder currentSlotHolder;

    void Awake()
    {
        currentSlotHolder = GetComponent<SlotHolder>();

    }

    private void Update()
    {
        if (Input.GetKeyDown(actionKey) && currentSlotHolder.itemUI.GetItem())
            currentSlotHolder.UseItem();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="Useable Item",menuName ="Inventory/Useable Item Data")]
public class UseableItemData_SO : ScriptableObject
{
    //所有你想改变的数据
    public int healthPoint;
}

Dialogue部分

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DialogueController : MonoBehaviour
{
    public DialougeData_SO currentData;
    bool canTalk = false;

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player") && currentData != null)
        {
            canTalk = true;
            Debug.Log("can talk is true");

        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            DialogueUI.Instance.dialoguePanel.SetActive(false);
        }
    }

    private void Update()
    {
        if (canTalk && Input.GetKeyDown(KeyCode.F))//TODO:此处是否可以改编成使用事件的形式?
        {
            OpenDialogue();
        }
    }

    void OpenDialogue()
    {
        //打开UI面板
        //传输对话内容信息
        DialogueUI.Instance.UpdateDialogueData(currentData);
        DialogueUI.Instance.UpdateMainDialogue(currentData.dialoguePieces[0]);
    }


}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class DialogueOption 
{   
    public string text;
    public string targetID;
    public bool takeQuest;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

    [TextArea]
    public string text;

    public QuestData_SO quest;

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


}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="New Dialogue",menuName ="Dialogue/Dialogue Data")]
public class DialougeData_SO : ScriptableObject
{

    public List<DialoguePiece> dialoguePieces = new List<DialoguePiece>();
    public Dictionary<string, DialoguePiece> dialogueIndex = new Dictionary<string, DialoguePiece>();



    public QuestData_SO GetQuest()
    {
        QuestData_SO currentQuest = null;
        //循环对话中的任务,找到该任务并返回
        foreach (var piece in dialoguePieces)
        {
            if (piece.quest != null)
                currentQuest = piece.quest;
        }
        return currentQuest;
    }


    //如果是在Unity编辑器中,则字典随时改变时则进行修改,如果是打包则字典信息不会更改
#if UNITY_EDITOR
    void OnValidate()//一旦这个脚本中的数据被更改时会自动调用
    {
        dialogueIndex.Clear();
        //一旦信息有所更新,就会将信息存储在字典中
        foreach(var piece in dialoguePieces)
        {
            if (!dialogueIndex.ContainsKey(piece.ID))
                dialogueIndex.Add(piece.ID, piece);
        }
    }
#else
    void Awake()//保证在打包执行的游戏里第一时间获得对话的所有字典匹配 
    {
        dialogueIndex.Clear();
        foreach (var piece in dialoguePieces)
        {
            if (!dialogueIndex.ContainsKey(piece.ID))
                dialogueIndex.Add(piece.ID, piece);
        }
    }
#endif




}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class DialogueUI : Singleton<DialogueUI>
{
    [Header("Basic Elements")]
    public Image icon;
    public Text mainText;
    public Button nextButton;

    public GameObject dialoguePanel;

    [Header("Data")]
    public DialougeData_SO currentData;

    int currentIndex = 0;

    [Header("Options")]
    public RectTransform optionPanel;
    public OptionUI optionPrefab;


    protected override void Awake()
    {
        base.Awake();
        nextButton.onClick.AddListener(ContinueDialogue);//点击时如果后续还有对话则继续进行
    }

    void ContinueDialogue()
    {
        if (currentIndex < currentData.dialoguePieces.Count)
        {
            UpdateMainDialogue(currentData.dialoguePieces[currentIndex]);
        }
        else dialoguePanel.SetActive(false);
    }

    public void UpdateDialogueData(DialougeData_SO data)
    {
        currentData = data;
        currentIndex = 0;//保证每次都是从头开始对话
    }


    public void UpdateMainDialogue(DialoguePiece piece)
    {
        dialoguePanel.SetActive(true);
        currentIndex++;

        if (piece.image != null)
        {
            icon.enabled = true;
            icon.sprite = piece.image;
        }
        else icon.enabled = false;

        mainText.text = "";
        //mainText.text = piece.text;
        mainText.DOText(piece.text, 1f);

        //如果后续还有对话就按next,没有就不按next
        if (piece.options.Count == 0 && currentData.dialoguePieces.Count > 0)
        {
            nextButton.interactable = true;
            nextButton.gameObject.SetActive(true);
            //currentIndex++;//不应该放在这里++,应该每运行一次都让index++,因此应该放在上面
        }
        else
        {
            //nextButton.gameObject.SetActive(false);
            nextButton.transform.GetChild(0).gameObject.SetActive(false);//让字看不见
            nextButton.interactable = false;//并且删除交互功能 
        }
        CreateOptions(piece);//根据对话来创建选项
    }

    void CreateOptions(DialoguePiece piece)
    {
        if (optionPanel.childCount > 0)//销毁旧的选项
        {
            for(int i = 0; i < optionPanel.childCount; i++)
            {
                Destroy(optionPanel.GetChild(i).gameObject);
            }
        }

        //生成新的选项,并且调用选项,传入对话的选项信息,来更新option所显示的信息
        for (int i = 0; i < piece.options.Count; i++)
        {
            var option = Instantiate(optionPrefab, optionPanel);
            option.UpdateOption(piece,piece.options[i]);
        }

    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class OptionUI : MonoBehaviour
{
    public Text optionText;
    private Button thisButton;
    private DialoguePiece currentPiece;


    private bool takeQuest;
    private string nextPieceID;
    void Awake() 
    {
        thisButton = GetComponent<Button>();
        thisButton.onClick.AddListener(OnOptionClicked);
    }

    public void UpdateOption(DialoguePiece piece,DialogueOption option)
    {
        currentPiece = piece;
        optionText.text = option.text;
        nextPieceID = option.targetID;
        takeQuest = option.takeQuest;
    }


    public void OnOptionClicked()
    {
        if (currentPiece.quest != null)//如果当前选项含有任务
        {
            //则获取该任务
            var newTask = new QuestManager.QuestTask
            {
                questData = Instantiate(currentPiece.quest)//将quest信息实例化然后赋值给QuestTask类里面的questData
            };
            if (takeQuest)
            {
                Debug.Log("takeQuest is true");

                //判断是否在列表中
                if (QuestManager.Instance.HaveQuest(newTask.questData))
                {
                    Debug.Log("Quest in list");

                    //判断是否完成,若完成则给予奖励
                    if (QuestManager.Instance.GetTask(newTask.questData).IsComplete)
                    {
                        Debug.Log("任务完成,给予奖励");
                        newTask.questData.GiveRewards();
                        QuestManager.Instance.GetTask(newTask.questData).IsFinished = true;
                    }
                }
                else
                {
                    QuestManager.Instance.tasks.Add(newTask);
                    //newTask.IsStarted = true;这样的做法并没有用,这样只是修改的临时变量
                    QuestManager.Instance.GetTask(newTask.questData).IsStarted = true;
                    //添加到任务列表

                    foreach (var requireItem in newTask.questData.RequireTargetName())
                    {
                        InventoryManager.Instance.CheckQuestItemInBag(requireItem);
                    }
                }
            }
        }

        if (nextPieceID == "")
        {
            DialogueUI.Instance.dialoguePanel.SetActive(false);
            return;
        }
        else
        {
            //用ID去获取下一个对话
            DialogueUI.Instance.UpdateMainDialogue(DialogueUI.Instance.currentData.dialogueIndex[nextPieceID]);
        }
    }

}

任务部分

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Linq;

[CreateAssetMenu(fileName ="New Quest",menuName ="Quest/Quest Data")]
public class QuestData_SO : ScriptableObject
{
    [System.Serializable]
    public class QuestRequire
    {
        public string name;
        public int requireAmount;
        public int currentAmount;
    }


    public string questName;
    [TextArea]
    public string description;

    //需要三种任务完成的状态,npc才会有不同的反应
    public bool isStarted;
    public bool isComplete;
    public bool isFinished;

    public List<QuestRequire> questRequires = new List<QuestRequire>();
    public List<InventoryItem> rewards = new List<InventoryItem>();

    public void CheckQuestProgress()
    {
        var finishRequires = questRequires.Where(r => r.requireAmount <= r.currentAmount);
        isComplete = finishRequires.Count() == questRequires.Count;

        if (isComplete)
        {
            Debug.Log("任务完成");
        }
    }


    //当前任务需要收集/消灭的目标名字列表
    public List<string> RequireTargetName()
    {
        List<string> targetNameList = new List<string>();
        foreach(var require in questRequires)
        {
            targetNameList.Add(require.name);
        }
        return targetNameList;
    }


    public void GiveRewards()
    {
        foreach(var reward in rewards)
        {
            if (reward.amount < 0)
            {
                int requireCount = Mathf.Abs(reward.amount);

                //优先在背包里找是否有该物品,
                if (InventoryManager.Instance.QuestItemInBag(reward.itemData) != null)
                {
                    //这种情况是背包里的东西不够,那就先在背包里扣除一部分,
                    if (InventoryManager.Instance.QuestItemInBag(reward.itemData).amount <= requireCount)
                    {
                        requireCount -= InventoryManager.Instance.QuestItemInBag(reward.itemData).amount;//所需的数量减少
                        InventoryManager.Instance.QuestItemInBag(reward.itemData).amount = 0;//背包里的商品扣除为0

                        //背包里东西不够,剩下的部分从行动栏里扣除
                        if (InventoryManager.Instance.QuestItemInAction(reward.itemData) != null)
                            InventoryManager.Instance.QuestItemInAction(reward.itemData).amount -= requireCount;

                    }
                    //这种情况就是背包里的东西直接够,那直接扣除就好
                    else
                    {
                        InventoryManager.Instance.QuestItemInBag(reward.itemData).amount -= requireCount;
                    }

                }
                //这种情况是背包里一点东西都没有,那就直接扣除行动栏里的物品
                else
                {
                    InventoryManager.Instance.QuestItemInAction(reward.itemData).amount -= requireCount;
                }
            }
            else
            {
                InventoryManager.Instance.inventoryData.AddItem(reward.itemData, reward.amount);
            }

            InventoryManager.Instance.inventoryUI.RefreshUI();
            InventoryManager.Instance.actionUI.RefreshUI();
        }
    }



}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(DialogueController))]
public class QuestGiver : MonoBehaviour
{
    DialogueController controller;
    QuestData_SO currentQuest;

    public DialougeData_SO startDialogue;
    public DialougeData_SO progressDialogue;
    public DialougeData_SO completeDialogue;
    public DialougeData_SO finishDialogue;

    private void Awake()
    {
        controller = GetComponent<DialogueController>(); 
    }

    #region 获得任务状态
    public bool IsStarted
    {
        get
        {
            if (QuestManager.Instance.HaveQuest(currentQuest))
            {
                return QuestManager.Instance.GetTask(currentQuest).IsStarted;
            }
            else return false;
        }
    }

    public bool IsComplete
    {
        get
        {
            if (QuestManager.Instance.HaveQuest(currentQuest))
            {
                return QuestManager.Instance.GetTask(currentQuest).IsComplete;
            }
            else return false;
        }
    }

    public bool IsFinished
    {
        get
        {
            if (QuestManager.Instance.HaveQuest(currentQuest))
            {
                return QuestManager.Instance.GetTask(currentQuest).IsFinished;
            }
            else return false;
        }
    }
    #endregion

    private void Start()
    {
        controller.currentData = startDialogue;
        currentQuest = controller.currentData.GetQuest();
    }

    //根据状态切换对话
    void Update()
    {
        if (IsStarted)
        {
            if (IsComplete)
            {
                controller.currentData = completeDialogue;
            }
            else
            {
                controller.currentData = progressDialogue;
            }
        }

        if (IsFinished)
        {
            controller.currentData = finishDialogue;
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class QuestManager : Singleton<QuestManager>
{
    [System.Serializable]
    public class QuestTask
    {
        public QuestData_SO questData;
        public bool IsStarted {
            get { return questData.isStarted; }
            set { questData.isStarted = value; }
        }

        public bool IsComplete
        {
            get { return questData.isComplete; }
            set { questData.isComplete = value; }
        }

        public bool IsFinished
        {
            get { return questData.isFinished; }
            set { questData.isFinished  = value; }
        }

    }

    public List<QuestTask> tasks = new List<QuestTask>();

    //敌人死亡,拾取物品时调用
    public void UpdateQuestProgress(string requireName,int amount)
    {
        foreach(var task in tasks)
        {

            if (task.IsFinished)
                continue;//为了避免已完成的任务受到影响

            var matchTask = task.questData.questRequires.Find(r => r.name == requireName);
            if (matchTask != null)
                matchTask.currentAmount += amount;

            task.questData.CheckQuestProgress();
        }
    }

    public bool HaveQuest(QuestData_SO data)//判断是否有这个任务
    {
        //在头文件中引入Ling,可以用于查找链表中的内容
        if (data != null)
            return tasks.Any(q => q.questData.questName == data.questName);
        else return false;
    }

    //根据任务数据的名字查找链表中的某一个任务
    public QuestTask GetTask(QuestData_SO data)
    {
        return tasks.Find(q => q.questData.questName == data.questName);
    }

    private void Start()
    {
        LoadQuestManager();
    }


    public void SaveQuestManager()
    {
        PlayerPrefs.SetInt("QuestCount", tasks.Count);
        for(int i = 0; i < tasks.Count; i++)
        {
            SaveManager.Instance.Save(tasks[i].questData, "task" + i);
        }
    }


    //加载数据的方式是通过重新新创建一个SO,然后让SO读取数据,然后再加入到tasks链表当中
    public void LoadQuestManager()
    {
        var quesCount = PlayerPrefs.GetInt("QuestCount");
        for(int i = 0; i < quesCount; i++)
        {
            var newQuest = ScriptableObject.CreateInstance<QuestData_SO>();//
            SaveManager.Instance.Load(newQuest, "task" + i);
            tasks.Add(new QuestTask { questData = newQuest });
        }
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class QuestNameButton : MonoBehaviour
{
    public Text questNameText;
    public QuestData_SO currentData;
    //public Text questContentText;

    public void SetupNameButton(QuestData_SO quesData)
    {
        currentData = quesData;

        if (quesData.isComplete)
            questNameText.text = quesData.questName + "(完成)";
        else
            questNameText.text = quesData.questName;
    }

    private void Awake()
    {
        GetComponent<Button>().onClick.AddListener(UpdateQuestContent);
    }
    void UpdateQuestContent()
    {
        //questContentText.text = currentData.description;
        QuestUI.Instance.SetupRequireList(currentData);

        foreach(Transform item in QuestUI.Instance.rewardTransform)
        {
            Destroy(item.gameObject);
        }


        foreach(var item in currentData.rewards)//奖励可能不止一个所以需要循环列表
        {
            QuestUI.Instance.SetupRewardItem(item.itemData, item.amount);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class QuestRequirement : MonoBehaviour
{
    private Text requireName;
    private Text progressNumber;


    private void Awake()
    {
        requireName = GetComponent<Text>();
        progressNumber = transform.GetChild(0).GetComponent<Text>();
    }

    public void SetupRequirement(string name,int amount,int currentAmount)
    {
        requireName.text = name;
        progressNumber.text = currentAmount.ToString() + "/" + amount.ToString();
    }

    public void SetupRequirement(string name, bool isFinished)
    {
        if (isFinished)
        {
            requireName.text = name;
            progressNumber.text = "完成";
            requireName.color = Color.gray;
            progressNumber.color = Color.gray;
        }
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class QuestUI : Singleton<QuestUI>
{
    [Header("Elements")]
    public GameObject quesPanel;
    public ItemToolTip tooltip;
    bool isOpen;

    [Header("Quest Name")]
    public RectTransform questListTransform;
    public QuestNameButton questNameButton;

    [Header("Text Content")]
    public Text quesContentText;

    [Header("Requirement")]
    public RectTransform requireTransform;
    public QuestRequirement requirement;

    [Header("Reward Panel")]
    public RectTransform rewardTransform;
    public ItemUI rewardUI;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            isOpen = !isOpen;
            quesPanel.SetActive(isOpen);
            quesContentText.text = string.Empty;
            SetupQuestList();
        }


        if (!isOpen)
            tooltip.gameObject.SetActive(false);
    }

    public void SetupQuestList()
    {
        //清除原来已有的任务
        foreach(Transform item in questListTransform)
        {
            Destroy(item.gameObject);
        }
        foreach(Transform item in rewardTransform)
        {
            Destroy(item.gameObject);
        }
        foreach (Transform item in requireTransform)
        {
            Destroy(item.gameObject);
        }

        //遍历列表中的list,接取任务
        foreach(var task in QuestManager.Instance.tasks)
        {
            var newTask = Instantiate(questNameButton, questListTransform);
            newTask.SetupNameButton(task.questData);
            //newTask.questContentText = quesContentText;
        }


    }

    public void SetupRequireList(QuestData_SO questData)
    {
        quesContentText.text = questData.description;
        //将涉及到QuestNameButton中的三处questContentText关闭,不使用在里面传东西然后赋值的形式了,改为在此处直接修改

        foreach (Transform item in requireTransform)
        {
            Destroy(item.gameObject);
        }
        foreach(var require in questData.questRequires)
        {
            var q = Instantiate(requirement, requireTransform);
            if (questData.isFinished)
                q.SetupRequirement(require.name, true);
            else
                q.SetupRequirement(require.name, require.requireAmount, require.currentAmount);
        }
    }

    public void SetupRewardItem(ItemData_SO itemData,int amount)
    {
        var item = Instantiate(rewardUI, rewardTransform);
        item.SetupItemUI(itemData, amount);
    }

}
 
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class ShowTooltip : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
    private ItemUI currentItemUI;

    void Awake()
    {
        currentItemUI = GetComponent<ItemUI>();
    }
    public void OnPointerEnter(PointerEventData eventData)
    {
        Debug.Log("mouse in slot");
        QuestUI.Instance.tooltip.gameObject.SetActive(true);
        QuestUI.Instance.tooltip.SetupTooltip(currentItemUI.currentItemData);
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        QuestUI.Instance.tooltip.gameObject.SetActive(false);
    }
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Unity3D RPG实现 3 —— 对话、任务系统 的相关文章

  • 如何在plotly 3D曲面图中标记区域?

    我使用plotly从xyz数据创建3D高程剖面图 它与以下代码配合得很好 import plotly graph objects as go import pandas as pd import numpy as np Read data
  • 点列表的 3D 轮廓(凹壳)

    我有一个 C 中的 Vector3 点列表 我需要计算这些点的凹轮廓 确实有很多参考资料 特别是对于 凸 分辨率 由于格雷厄姆算法 我已经成功实现了 然而 由于我现在需要有效地计算凹轮廓 所以我迷失了 维基百科确实列出了很多用于凸计算的资源
  • 从 NumPy 数组中的数据绘制 3D 图像

    我有一个 NumPy 数组中的数据文件 我想查看 3D 图像 我正在分享一个示例 我可以在其中查看大小为 100 100 的 2D 图像 这是 xy 平面上 z 0 处的切片 import numpy as np from matplotl
  • OpenGL z轴指向哪里?

    我正在尝试了解 OpenGL 坐标系 我到处都看到它被描述为右撇子 但这与我的经验不符 我尝试绘制一些形状和 3 d 对象 我发现 z 轴显然指向 屏幕 而 x 指向右侧 y 指向上方 这是左手坐标系的描述 我缺少什么 编辑 例如 http
  • Unity3D:在 AA 解析后绘制粒子以提高性能

    我正在尝试评估 MSAA 对 Unity 中含有大量粒子的场景的影响 为此 我需要 使用 8x MSAA 绘制场景中的所有非粒子对象 使用上一个通道中解析的深度缓冲区来渲染所有 将非遮挡粒子系统转移到较小的渲染目标上 将 2 的颜色缓冲区与
  • C++ Irrlicht 程序未链接:“未定义对‘__imp_createDevice’的引用”

    我的 Irrlicht 程序无法链接 我使用的编译器是g Code include
  • 3D 数学:根据“向上”和“向上”正交向量计算倾斜(滚动)角度

    我希望这是提出这个问题的正确位置和这个一样 https stackoverflow com questions 3035590 bank angle from up vector and look at vector 但表示为纯数学而不是图
  • 如何创建热图来说明控制发散调色板中心颜色位置的网格差异?

    我有两个人脸 3D 网格 我希望使用热图来说明差异 我想使用红蓝发散色阶 我的数据可以查到here https github com Patricklv How to create heatmap illustraing 3D mesh d
  • 使用 Scipy/Numpy 在浊点的二维插值中仅获取“有效”点

    我有一个通过人的背部摄影测量获得的浊点 我正在尝试对其进行插值以获得规则网格 为此我正在使用scipy interpolate到目前为止取得了良好的成果 问题是 我正在使用的函数 scipy interpolate griddata 使用平
  • GL_CULL_FACE使所有对象消失

    我正在尝试在 openGL3 3 中创建一些简单的多边形 我有两种类型的对象 具有以下属性 对象 1 10 个顶点 按顺序在下面列出 存储在GL ARRAY BUFFER并使用GL TRIANGLE FAN v x y z w v 0 0
  • 在 RGL 中将立方体绘制到 3D 散点图中

    我正在尝试向 3D 散点图添加较小的立方体 网格 具有指定边长 我希望立方体位于原点 我该怎么做呢 我已经玩过cube3d 但我似乎无法将立方体正确定位 也无法使其成为网格 因此我可以看到它包含的数据点 这是我所拥有的 library rg
  • 如何从横滚、俯仰和偏航获取相机向上矢量?

    我需要从滚动角 俯仰角和偏航角 以度为单位 获取相机的向上矢量 以获得正确的外观 我已经尝试了几个小时不同的事情 但没有运气 这里的任何帮助将不胜感激 横滚 俯仰和偏航定义 3 轴旋转 从这些角度 您可以构建一个 3x3 变换矩阵来表达该旋
  • WPF 3D - 在复杂几何体上映射渐变画笔

    我想问是否有人知道如何在 WPF 3D 中的复杂对象上映射渐变画笔 结果应该类似于 matlab 中的 3D 图像 例如 3D 函数 假设您有一些想要可视化的 3 维数据 并且想要通过颜色区分某些级别的值 给定一个 GradientBrus
  • 无法使用 vcglib 简化 3D 模型,断言“0”失败

    I used vcglib https github com cnr isti vclab vcglib简化 3D 模型文件 我使用了 master a8e87662 git 存储库 我运行示例 trimesh clustering htt
  • 在 iOS 上的 SceneKit 中导入 3d 模型

    从 URL 导入 obj 文件并将其转换为 SCNNode 时遇到问题 这是代码 swift3 let url URL init string https cloud box com shared static ock9d81kakj91d
  • 如何计算正切和副法线?

    谈谈OpenGL着色语言 GLSL 中的凹凸贴图 镜面高光之类的东西 I have 顶点数组 例如 0 2 0 5 0 1 0 2 0 4 0 5 法线数组 例如 0 0 0 0 1 0 0 0 1 0 0 0 世界空间中点光源的位置 例如
  • 在 3d 空间中的两个平面之间进行插值

    我正在开发一种工具 可以让您在 3D 体积 上圈出 包围事物 我想通过标记 切片 1 和 3 并从该信息 填充 切片 2 来节省时间 两个简单的解决方案是 1 slice2 slice1 AND slice3 gets the overla
  • 在 R 中绘制 3D 数据

    我有一个 3D 数据集 data data frame x rep c 0 1 0 2 0 3 0 4 0 5 each 5 y rep c 1 2 3 4 5 5 data z runif 25 min data x data y 0 1
  • 向量数学,在两个向量之间的平面上查找坐标

    我正在尝试沿着样条线生成 3d 管 我有样条线的坐标 x1 y1 z1 x2 y2 z2 等 您可以在黄色插图中看到 在这些点上 我需要生成圆圈 其顶点将在稍后的体育场连接 这些圆需要垂直于样条线两条线段的 角 才能形成正确的管 请注意 出
  • C++ 从一组点进行平面插值

    我正在使用 PCL 点云 库用 C 进行编程 我的问题是 计算某些点的方差 但仅相对于平面的垂直轴 我会解释一下自己 所以我正在做的是将点云按表面平滑度划分为多个片段 区域生长分割 http pointclouds org document

随机推荐

  • DOTA目标检测数据集

    Dota开源目标检测数据集 DOTA v1 5包含16个类别中的40万个带注释的对象实例 这是DOTA v1 0的更新版本 它们都使用相同的航拍图像 但是DOTA v1 5修改并更新了对象的注释 其中许多在DOTA v1 0中丢失的10像素
  • 拷贝构造函数的调用方式以及相关问题【最清晰易懂】

    这几天一直有一个问题在我大脑里挥之不去 之前面试实习的时候也被问过 但是回答的不好 面试官问 你知道的构造函数有哪些 我说 无参构造函数 有参构造函数 拷贝构造函数 移动构造函数 关于一些函数的说明 面试官说 其实拷贝构造函数 移动构造函数
  • java给字符串数组追加字符串_java往字符串数组追加新数据

    public class Test public static void main String args 原字符串数组 String arr 原字符串数据1 原字符串数据2 执行数据添加 arr insert arr 需要追加的字符串数据
  • win11安装mysql5.7带安装包与常见问题如重装,初次登录不上,跳不了密码等

    目录 1下载 2安装 注意1 你的新建只有文件夹而且需要权限 注意2报错The service already existsThe current server installed 3初次登录与密码 注意3密码是只能输入进去的 4设置密码与
  • OpenCV总结3——图像拼接Stitching

    之前折腾过一段时间配准发现自己写的一点都不准 最近需要进行图像的拼接 偶然的机会查到了opencv原来有拼接的库 发现opencv处理配准之外还做了许多的操作 就这个机会查找了相关的资料 同时也研究了以下他的源代码 做一个简单的总结 Sti
  • css实现表单验证

    在我们的日常业务中 表单验证是个很常见设计需求 像一些登录注册框 问卷调查也都需要用到表单验证 一般我们的实现思路都是JS监听input框的输入内容 判断用户输入内容 从而以此来决定下一步的操作
  • 13.6 Production State Awareness (PSA)

    1 Introduction UFS设备可以利用有关其生产状态的知识 相应地调整内部操作 例如 在设备焊接之前加载到存储设备中的内容可能被破坏 其概率高于regular模式 UFS设备可以在设备焊接前使用 Special 内部操作加载内容
  • qt界面论坛

    http www cnblogs com appsucc archive 2012 03 14 2395657 html
  • JAVA核心知识点--Maven引入org.apache.tools.zip

    可以看出 org apache tools zip 是 ant jar里面的 所以要引入org apache tools zip 直接maven引入ant即可
  • 基于SSM框架的家教中介平台系统的设计与实现(源码免费获取)

    技术架构 Java语言 MySQL数据库 SSM框架 功能简介 1 系统登录 系统登录成为了管理员访问系统的路口 设计了系统登录界面 包括管理员名 密码和验证码 然后对登录进来的管理员判断身份信息 判断其为管理员 还是普通用户 2 管理员管
  • logback 对特殊日志进行过滤

    工作中需要对 logback 的日志进行定制化过滤 此时一般有两种方法 logger 在 logback spring xml 中添加如下配置 可以关闭对应类的日志
  • Intellij安装scala插件详解

    参考博客 1 http wwwlouxuemingcom blog 163 com blog static 20974782201321953144457 2 http blog csdn net stark summer article
  • cvm云服务器的,cvm云服务器如何登录

    cvm 登录到 本地为 Windows 计算机 1 在本地 Windows 机器上 单击开始菜单 运行 输入 mstsc 命令 即可打开远程桌面连接对话框 2 在输入框输入 Windows 服务器的公网 IP 登录 云服务器控制台 可查看云
  • 牛客剑指offer之【JZ12 矩阵中的路径】

    哈喽 这次真的是好久不见呀 我回来啦 接下来的日子我会不断更新牛客上的剑指offer题解 为什么这么做呢 是因为博主刷题总是刷了忘忘了刷 一样的题目换种形式就要做好久 说到底还是对知识点的理解不够透彻 加之算法对一个即将找工作的大学生来说更
  • 【Pytorch with fastai】第 15 章 :深入探讨应用程序架构

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • 计算机网络参考模型(OSI讲解)

    计算机网络参考模型 文章目录 计算机网络参考模型 一 什么是七层网络模型 二 每一层的功能与特点 三 osi模型和TCP IP模型的区别 四 数据封装和解封装的过程 1 封装与解封装过程 2 设备与层之间的关系 一 什么是七层网络模型 七层
  • 保护模式的分段

    一 分段的背景 在8086处理器诞生之前 内存寻址方式就是直接访问物理地址 8086处理器为了寻址1M的内存空间 把地址总线扩展到了20位 但是 一个尴尬的问题出现了 ALU的宽度只有16位 也就是说 ALU不能计算20位的地址 为了解决这
  • Less-11-12 【‘、“)】

    目录 第十一 十二关 1 在账号密码输入admin 查看成功效果 2 输入错误的账号密码 查看错误效果 3 找注入点 我们尝试在账户的输入框中加单引号 发现出现报错 4 加注释 查看页面是否恢复正常 可以看到页面成功执行了 证明当前网址存在
  • vscode搭建linux内核开发环境

    vscode在linux下搭建内核驱动开发环境 一 前言 Souce insight是一个阅读 开发linux内核驱动模块的好工具 但是Source insight是收费的软件 而且没有原生linux版本 要是想在纯linux环境下进行li
  • Unity3D RPG实现 3 —— 对话、任务系统

    目录 成果展示 对话系统 对话的存储数据结构 对话的UI面板设置 创建对话 任务的 NPC 实现对话控制器显示主对话窗口的内容 创建对话的选项内容 任务系统 创建任务 UI 面板 任务的存储数据结构 任务管理器与接受任务 任务控制相关脚本