Unity 2D独立开发手记(六):基于任务系统的对话系统

大家见过哪个RPG没有对话剧情的?对吧,所以是很有必要写一个对话系统的,这里为什么不说对话功能呢?如果仅仅是功能,那我何必加上基于任务系统这段字呢,说明除了对话,还有其他功能。而基于的那个任务系统长什么样?得翻翻我前面的文章,或者我直接上链接吧:通用任务系统。此时,这个任务系统的一些内容已经融合到这个对话系统里面了,而之前是独立存在的,所以在这篇文章中还会重复一次任务系统,因为不少代码变了,大家可以对照一下。

但是,首先我并没有打算做成带分支的对话系统,一来没有明确思路,说白了也就是技术不足,只知道树或者图肯定是要用的了,二来即使有思路然后实现了分支,仅凭我一己之力可处理不来这带分支的剧情╮( ̄▽ ̄")╭;其次,这个系统是仿造《黑色沙漠》的对话系统开发的,因为有那么一段时间都在玩这个游戏,它对我这个破游戏影响还是蛮深的,估计到时好多东西我都要“山寨”过来,话说回来,所以这个对话系统的通用性并不强。

所以说,我这个对话系统长啥样?先展示一下:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第1张图片

还是这个熟悉的界面,有些地方稍微改了几个字,不过其实是深藏不露的。与玛丽亚对话变成了酱紫:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第2张图片

这是该NPC默认的第一句话,此时,有一个“继续”选项,这里为什么说是选项呢,且听下文分解。点继续后,NPC说出了第二句话:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第3张图片

此时,NPC的话已经说完了,继续按钮消失。而右上角有个“返回”,什么作用呢?就是初始化对话,变回这样:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第4张图片

OK,点右上角的“结束”按钮结束佟丽娅的采访,去找恩格斯对话:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第5张图片

没什么好想的,直接上来一句日常。可以看到,相比玛丽亚,右上角多了一个“任务”按钮,说明该NPC是有任务的,点击该任务按钮:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第6张图片

对话框里就展示了NPC的任务列表。emmm至于为什么是这些任务,其实我太懒了,Ctrl+D拷贝的……好吧,然后右下角有个翻页按钮,功能可想而知:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第7张图片

额,那要是有多行话呢?哼哼,除非不自讨苦吃,还是可以处理的:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第8张图片

变成了三页,翻到第二页看看:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第9张图片

可以看到,上翻页和下翻页同时出现了。那么,点击一个任务,触发任务开头对话:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第10张图片

可以看到,和普通对话没什么两样,而且右上角的任务按钮没了。——等等,谁是你哥们?

好吧,点击继续,进行接下来的对话:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第11张图片Unity 2D独立开发手记(六):基于任务系统的对话系统_第12张图片

Unity 2D独立开发手记(六):基于任务系统的对话系统_第13张图片Unity 2D独立开发手记(六):基于任务系统的对话系统_第14张图片

Unity 2D独立开发手记(六):基于任务系统的对话系统_第15张图片

好,对话到任务的最后一句,弹出了任务详情和任务奖励,并有“接受”和“拒绝”两个选项按钮。其实拒绝就是返回的间接按钮,在这里我接受任务吧,然后会自动返回初始,接着再重新点击任务按钮,刚才接取的任务显示已经在“进行中”了:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第16张图片

那么再次点击这个任务会怎样?看着:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第17张图片

可以看到,跟没接任务时不一样了,此时NPC给了一些提示。既然接了任务,那就看看我的任务有哪些:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第18张图片

上面点击了唯一的一个任务,此时任务详情窗口罗列了任务目标,这跟NPC处的有些许不一样。行,去找玛丽亚对话,第一句话跟上面的没什么区别,而第二句也就是最后一句说完后,弹出了一个新选项:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第19张图片

这就是任务的对话型目标的选项了,“对话型目标”这个词在下文将反复提及,意思是什么,看到这里大家应该懂了吧(;¬_¬)

点击选项以进行任务目标:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第20张图片

好吧,又和正常的对话一样了。继续到最后一句:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第21张图片

好像没什么特殊的,但是,此时回去看任务详情,该对话目标已经完成:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第22张图片

而且,再回去找玛丽亚对话,之前展示的对话型目标选项也已经没了:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第23张图片

嘿嘿,好玩吧?结束对话,用左下角的模拟按钮杀怪或者捡道具以完成后续目标,然后回去找恩格斯尝试交任务:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第24张图片

和上面的没太大区别吧。不急,点任务:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第25张图片

对话又不一样了呢。继续到最后一句:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第26张图片

新选项——“完成”,功能顾名思义,此时又弹出了任务详情,以便玩家确认任务奖励。点“完成”当然就是完成当前任务了。那么如果有多个任务对话在同一个目标身上,会怎么样?这当然也处理了。先在恩格斯那里接任务吧,然后看到列表里有一堆任务了:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第27张图片

再去去找玛丽亚,此时,又有不同咯:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第28张图片

有四个选项,而且没有翻页,说明就只有四个选项。那么是不是应该还有一个,五个才对?不是的,大家仔细看上面最后一个任务,是要杀5只“骷髅”怪物才能继续,所以这里玛丽亚还没有那个任务的对话目标。因此,如果完成了前置目标,选项理所当然就有了:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第29张图片

可以看到,选项出现在了第二个分页里了。好吧,就展示到这里吧,毕竟不是记录成果嘛,而是记录做法。

言归正传,大家大概知道了,这个对话系统有什么功能了:对话一般按句进行,每句话的下方都会有选项,正常的是一个“继续”选项。当NPC没任务时,去对话,没有任务按钮,只是进行普通对话。而有任务的NPC显示了任务按钮,点击后出现任务列表,点击列表的任务,会开始新对话,对应于该任务,在这个对话的最后,会询问玩家是否接取该任务,而玩家接取了该任务后,该任务在列表中则变成了进行中的任务,再次点击,对话和此前不同了。然后去和与任务对话目标相关联的NPC对话,该NPC多出了与任务相应的选项,点击后有新对话,完成对话后可完成相应的任务对话目标。而当玩家没有完成此对话目标的前置目标时,不会有相应的选项。

那么,这样一个基于任务系统的对话系统,要怎么实现呢?最先做的,当然是给对话写一个类,这样,整个对话系统,是以对话为单位进行的,这个类我还是继续用ScriptableObject吧(PS:好像有点依赖和滥用了呢_(:3J∠)_):

using UnityEngine;

[CreateAssetMenu(fileName = "dialogue", menuName = "Zetan/剧情/新对话")]
public class Dialogue : ScriptableObject
{
    [SerializeField]
    private Saying[] words;
    public Saying[] Words
    {
        get
        {
            return words;
        }
    }
}
[System.Serializable]
public class Saying
{
    [SerializeField]
    private string talkerName;
    public string TalkerName
    {
        get
        {
            return talkerName;
        }
    }

    [SerializeField, TextArea]
    private string words;
    public string Words
    {
        get
        {
            return words;
        }
    }
}

没什么好说的,我觉得我变量和方法的命名应该够言简意赅了。和以前创建道具和任务一样,创建了几个对话,并随便填了一下,就成这样了:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第30张图片

有了对话,就要有触发对话的NPC吧,这个我叫做Talker,原意好像是话痨,不过这样好记啊,啊哈哈。这个NPC类是这样的:

using System.Collections.Generic;
using UnityEngine;

public class Talker : NPC, ITalkAble
{
    [SerializeField]
    private Dialogue defaultDialogue;
    public Dialogue DefaultDialogue
    {
        get
        {
            return defaultDialogue;
        }
    }
    /// 
    /// 存储对象身上的对话型目标,这跟前期任务系统用委托和事件处理不一样了
    /// 
    public List talkToThisObjectives = new List();

    public event NPCTalkListener OnTalkBeginEvent;
    public event NPCTalkListener OnTalkFinishedEvent;

    public void OnTalkBegin()
    {
        if (OnTalkBeginEvent != null) OnTalkBeginEvent();
    }

    public void OnTalkFinished()
    {
        if (OnTalkFinishedEvent != null) OnTalkFinishedEvent();
        QuestManager.Instance.UpdateObjectivesText();
    }
}

public delegate void NPCTalkListener();

public interface ITalkAble
{
    Dialogue DefaultDialogue { get; }
    event NPCTalkListener OnTalkBeginEvent;
    event NPCTalkListener OnTalkFinishedEvent;
    void OnTalkBegin();
    void OnTalkFinished();
}

/// 
/// 记录对话目标信息,用于对话系统生成对话目标选项
/// 
public class TalkObjectiveInfo
{
    public string questTitle;

    public TalkObjective talkObjective;

    public TalkObjectiveInfo(string title, TalkObjective objective)
    {
        questTitle = title;
        talkObjective = objective;
    }
}

其实不用刻意写一个ITalkAble接口,如果看过我早期的任务系统,会发现,QuestGiver的一些内容迁移到Talker来了,所以,这个ITalkAble接口是以前QuestGiver遗留的,后面我应该会删掉。此时的QuestGiver不再继承NPC了,而是继承自Talker,最终重构成了这样:

using System.Collections.Generic;
using UnityEngine;

public class QuestGiver : Talker {

    [SerializeField]
    private Quest[] questsStored;
    public Quest[] QuestsStored
    {
        get
        {
            return questsStored;
        }
    }

    [SerializeField, ReadOnly]
    private List questInstances = new List();
    public List QuestInstances
    {
        get
        {
            return questInstances;
        }

        private set
        {
            questInstances = value;
        }
    }

    public void Init()
    {
        InitQuest(questsStored);
    }
    /// 
    /// 使用任务信息创建任务实例
    /// 
    /// 任务信息
    public void InitQuest(Quest[] questsStoraged)
    {
        if (questsStoraged == null) return;
        if (QuestInstances.Count > 0) QuestInstances.Clear();
        foreach (Quest quest in questsStoraged)
        {
            if (quest)
            {
                Quest tempq = Instantiate(quest);
                foreach (CollectObjective co in tempq.CollectObjectives)
                    tempq.Objectives.Add(co);
                foreach (KillObjective ko in tempq.KillObjectives)
                    tempq.Objectives.Add(ko);
                foreach (TalkObjective to in tempq.TalkObjectives)
                    tempq.Objectives.Add(to);
                foreach (MoveObjective mo in tempq.MoveObjectives)
                    tempq.Objectives.Add(mo);
                if (tempq.CmpltObjectiveInOrder)
                {
                    tempq.Objectives.Sort((x, y) =>
                    {
                        if (x.OrderIndex > y.OrderIndex) return 1;
                        else if (x.OrderIndex < y.OrderIndex) return -1;
                        else return 0;
                    });
                    for (int i = 1; i < tempq.Objectives.Count; i++)
                    {
                        if (tempq.Objectives[i].OrderIndex >= tempq.Objectives[i - 1].OrderIndex)
                        {
                            tempq.Objectives[i].PrevObjective = tempq.Objectives[i - 1];
                            tempq.Objectives[i - 1].NextObjective = tempq.Objectives[i];
                        }
                    }
                }
                int i1, i2, i3, i4;
                i1 = i2 = i3 = i4 = 0;
                foreach(Objective o in tempq.Objectives)
                {
                    if (o is CollectObjective)
                    {
                        o.runtimeID = tempq._ID + "_CO" + i1;
                        i1++;
                    }
                    if (o is KillObjective)
                    {
                        o.runtimeID = tempq._ID + "_KO" + i2;
                        i2++;
                    }
                    if (o is TalkObjective)
                    {
                        o.runtimeID = tempq._ID + "_TO" + i3;
                        i3++;
                    }
                    if (o is MoveObjective)
                    {
                        o.runtimeID = tempq._ID + "_MO" + i4;
                        i4++;
                    }
                }
                tempq.MOriginQuestGiver = this;
                tempq.MCurrentQuestGiver = this;
                QuestInstances.Add(tempq);
            }
        }
    }


    /// 
    /// 向此对象交接任务。因为往往会有些任务不在同一个NPC接取并完成,所以就要在两个NPC之间交接该任务
    /// 
    /// 需要进行交接的任务
    public void TransferQuestToThis(Quest quest)
    {
        if (!quest) return;
        QuestInstances.Add(quest);
        quest.MCurrentQuestGiver.QuestInstances.Remove(quest);
        quest.MCurrentQuestGiver = this;
    }
}

然后后面很多任务系统早期的类都被重构了,大家可以仔细对比,我就不多说了。然后任务NPC界面变成了这样:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第31张图片

多了个默认对话,可以直接把想要的对话直接拖拽到默认对话框里了。前面的Talker我有个注释提到,不使用委托和事件更新对话型目标了,转而用一个对话型目标表来处理,怎么做的?那就看看也被重构了的早期任务系统中的PlayerQuestManager吧:

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

public class QuestManager : MonoBehaviour
{
    private static QuestManager instance;
    public static QuestManager Instance
    {
        get
        {
            if (instance == null || !instance.gameObject)
                instance = FindObjectOfType();
            return instance;
        }
    }

    [SerializeField]
    private GameObject questPrefab;

    [SerializeField]
    private Transform questListParent;

    [SerializeField]
    private CanvasGroup questsWindow;

    [SerializeField, Space, Header("任务详情UI相关")]
    private CanvasGroup descriptionWindow;

    [SerializeField]
    private Text descriptionText;

    [SerializeField]
    private GameObject abandonButton;

    [SerializeField]
    private Text money_EXPText;

    [SerializeField]
    private ItemAgent[] rewardCells;

    private List QuestAgents = new List();

    [SerializeField, Space, ReadOnly, Header("任务列表")]
    private List questsOngoing = new List();
    public List QuestsOngoing
    {
        get
        {
            return questsOngoing;
        }
    }

    [SerializeField, ReadOnly]
    private List questsComplete = new List();
    public List QuestsComplete
    {
        get
        {
            return questsComplete;
        }
    }

    private Quest SelectedQuest;

    #region 任务处理相关
    /// 
    /// 接取任务
    /// 
    /// 要接取的任务
    public bool AcceptQuest(Quest quest)
    {
        if (!quest) return false;
        if (HasQuest(quest)) return false;
        QuestAgent qa = Instantiate(questPrefab, questListParent).GetComponent();
        qa.MQuest = quest;
        qa.TitleText.text = quest.Title;
        QuestAgents.Add(qa);
        foreach (Objective o in quest.Objectives)
        {
            if (o is CollectObjective)
            {
                CollectObjective co = o as CollectObjective;
                BagManager.Instance.OnGetItemEvent += co.UpdateCollectAmountUp;
                BagManager.Instance.OnLoseItemEvent += co.UpdateCollectAmountDown;
                if (co.CheckBagAtAcpt) co.UpdateCollectAmountUp(co.ItemID, BagManager.Instance.GetItemAmountByID(co.ItemID));
            }
            else if (o is KillObjective)
            {
                KillObjective ko = o as KillObjective;
                try
                {
                    foreach (Enermy enermy in GameManager.Instance.AllEnermy[ko.EnermyID])
                        enermy.OnDeathEvent += ko.UpdateKillAmount;
                }
                catch
                {
                    Debug.LogWarningFormat("[找不到敌人] ID: {0}", ko.EnermyID);
                    continue;
                }
            }
            else if (o is TalkObjective)
            {
                TalkObjective to = o as TalkObjective;
                try
                {
                    GameManager.Instance.AllTalker[to.TalkerID].talkToThisObjectives.Add(new TalkObjectiveInfo(quest.Title, to));
                }
                catch
                {
                    Debug.LogWarningFormat("[找不到NPC] ID: {0}", to.TalkerID);
                    continue;
                }
            }
            else if (o is MoveObjective)
            {
                MoveObjective mo = o as MoveObjective;
                try
                {
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent += mo.UpdateMoveIntoStatus;
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent += mo.UpdateMoveAwayStatus;
                }
                catch
                {
                    Debug.LogWarningFormat("[找不到任务点] ID: {0}", mo.PointID);
                    continue;
                }
            }
            o.OnCompleteThisEvent += UpdateCollectObjectives;
        }
        quest.IsOngoing = true;
        QuestsOngoing.Add(quest);
        if (!quest.CmpltOnOriginalNPC)
        {
            try
            {
                (GameManager.Instance.AllTalker[quest._IDOfNPCToComplete] as QuestGiver).TransferQuestToThis(quest);
            }
            catch
            {
                Debug.LogWarningFormat("[找不到NPC] ID: {0}", quest._IDOfNPCToComplete);
            }
        }
        return true;
    }
    /// 
    /// 放弃任务
    /// 
    /// 要放弃的任务
    public bool AbandonQuest(Quest quest)
    {
        if (HasQuest(quest) && quest && quest.Abandonable)
        {
            quest.IsOngoing = false;
            QuestsOngoing.Remove(quest);
            foreach (Objective o in quest.Objectives)
            {
                if (o is CollectObjective)
                {
                    CollectObjective co = o as CollectObjective;
                    co.CurrentAmount = 0;
                    BagManager.Instance.OnGetItemEvent -= co.UpdateCollectAmountUp;
                    BagManager.Instance.OnLoseItemEvent -= co.UpdateCollectAmountDown;
                }
                if (o is KillObjective)
                {
                    KillObjective ko = o as KillObjective;
                    ko.CurrentAmount = 0;
                    foreach (Enermy enermy in GameManager.Instance.AllEnermy[ko.EnermyID])
                    {
                        enermy.OnDeathEvent -= ko.UpdateKillAmount;
                    }
                }
                if (o is TalkObjective)
                {
                    TalkObjective to = o as TalkObjective;
                    to.CurrentAmount = 0;
                    GameManager.Instance.AllTalker[to.TalkerID].talkToThisObjectives.RemoveAll(x => x.talkObjective == to);
                }
                if (o is MoveObjective)
                {
                    MoveObjective mo = o as MoveObjective;
                    mo.CurrentAmount = 0;
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent -= mo.UpdateMoveIntoStatus;
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent -= mo.UpdateMoveAwayStatus;
                }
                o.OnCompleteThisEvent -= UpdateCollectObjectives;
            }
            if (!quest.CmpltOnOriginalNPC)
            {
                quest.MOriginQuestGiver.TransferQuestToThis(quest);
            }
            return true;
        }
        return false;
    }
    /// 
    /// 放弃当前展示的任务
    /// 
    public void AbandonSelectedQuest()
    {
        if (!SelectedQuest) return;
        if (AbandonQuest(SelectedQuest))
        {
            QuestAgent qa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
            if (qa)
            {
                QuestAgents.Remove(qa);
                Destroy(qa.gameObject);
            }
            CloseDescriptionWindow();
        }
    }
    /// 
    /// 更新某个收集类任务目标,用于在其他前置目标完成时,更新后置收集类目标
    /// 
    /// 下一个目标
    void UpdateCollectObjectives(Objective nextObj)
    {
        Objective tempObj = nextObj;
        CollectObjective co;
        while (tempObj != null)
        {
            if (tempObj is CollectObjective)
            {
                co = tempObj as CollectObjective;
                co.CurrentAmount = BagManager.Instance.GetItemAmountByID(co.ItemID);
            }
            tempObj = tempObj.NextObjective;
            co = null;
        }
    }
    /// 
    /// 完成任务
    /// 
    /// 要放弃的任务
    /// 是否读档模式
    /// 是否成功完成任务
    public bool CompleteQuest(Quest quest, bool loadMode = false)
    {
        if (!quest) return false;
        if (HasQuest(quest) && quest.IsComplete)
        {
            quest.IsOngoing = false;
            QuestsOngoing.Remove(quest);
            QuestAgent qa = QuestAgents.Find(x => x.MQuest == quest);
            if (qa)
            {
                QuestAgents.Remove(qa);
                Destroy(qa.gameObject);
            }
            quest.MCurrentQuestGiver.QuestInstances.Remove(quest);
            QuestsComplete.Add(quest);
            foreach (Objective o in quest.Objectives)
            {
                o.OnCompleteThisEvent -= UpdateCollectObjectives;
                if (o is CollectObjective)
                {
                    CollectObjective co = o as CollectObjective;
                    BagManager.Instance.OnGetItemEvent -= co.UpdateCollectAmountUp;
                    BagManager.Instance.OnLoseItemEvent -= co.UpdateCollectAmountDown;
                    if (!loadMode && co.LoseItemAtSubmit) BagManager.Instance.LoseItemByID(co.ItemID, o.Amount);
                }
                if (o is KillObjective)
                {
                    foreach (Enermy enermy in GameManager.Instance.AllEnermy[(o as KillObjective).EnermyID])
                    {
                        enermy.OnDeathEvent -= (o as KillObjective).UpdateKillAmount;
                    }
                }
                if (o is TalkObjective)
                {
                    GameManager.Instance.AllTalker[(o as TalkObjective).TalkerID].talkToThisObjectives.RemoveAll(x => x.talkObjective == (o as TalkObjective));
                }
                if (o is MoveObjective)
                {
                    MoveObjective mo = o as MoveObjective;
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent -= mo.UpdateMoveIntoStatus;
                    GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent -= mo.UpdateMoveAwayStatus;
                }
            }
            if (!loadMode)
                foreach (ItemBase item in quest.MQuestReward.Items)
                {
                    BagManager.Instance.GetItem(item);
                }
            //TODO 经验和金钱的处理
            return true;
        }
        return false;
    }

    public bool HasQuest(Quest quest)
    {
        return QuestsOngoing.Contains(quest);
    }
    public bool HasCompleteQuest(Quest quest)
    {
        return QuestsComplete.Contains(quest);
    }
    public bool HasCompleteQuestWithID(string questID)
    {
        return QuestsComplete.Exists(x => x._ID == questID);
    }
    #endregion

    #region UI相关
    public void ShowDescription(Quest quest)
    {
        if (!quest) return;
        QuestAgent qa = QuestAgents.Find(x => x.MQuest == quest);
        if (qa)
        {
            if (SelectedQuest && SelectedQuest != quest)
            {
                QuestAgent tqa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
                tqa.TitleText.color = Color.black;
            }
            qa.TitleText.color = Color.blue;
        }
        SelectedQuest = quest;
        UpdateObjectivesText();
        money_EXPText.text = string.Format("[奖励]\n经验:\n{0}\n金币:\n{1}", quest.MQuestReward._EXP, quest.MQuestReward.Money);
        foreach (ItemAgent rwc in rewardCells)
        {
            rwc.Item = null;
            rwc.Icon.overrideSprite = null;
        }
        foreach (ItemBase item in quest.MQuestReward.Items)
            foreach (ItemAgent rwc in rewardCells)
            {
                if (rwc.Item == null)
                {
                    rwc.Item = item;
                    rwc.Icon.overrideSprite = item.Icon;
                    break;
                }
            }
        abandonButton.SetActive(quest.Abandonable);
    }

    public void UpdateObjectivesText()
    {
        foreach (QuestAgent qa in QuestAgents)
            qa.UpdateQuestStatus();
        if (SelectedQuest == null) return;
        string objectives = string.Empty;
        for (int i = 0; i < SelectedQuest.Objectives.Count; i++)
            objectives += SelectedQuest.Objectives[i].DisplayName +
                "[" + SelectedQuest.Objectives[i].CurrentAmount + "/" + SelectedQuest.Objectives[i].Amount + "]" +
                (SelectedQuest.Objectives[i].IsComplete ? "(达成)\n" : "\n");
        descriptionText.text = string.Format("{0}\n[委托人: {1}]\n{2}\n\n任务目标{3}\n{4}",
            SelectedQuest.Title,
            SelectedQuest.MOriginQuestGiver.Name,
            SelectedQuest.Description,
            SelectedQuest.IsComplete ? "(完成)" : SelectedQuest.IsOngoing ? "(进行中)" : string.Empty,
            objectives);
    }

    public void CloseDescriptionWindow()
    {
        QuestAgent qa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
        if (qa) qa.TitleText.color = Color.black;
        SelectedQuest = null;
        descriptionWindow.alpha = 0;
        descriptionWindow.blocksRaycasts = false;
    }
    public void OpenDescriptionWindow(QuestAgent questAgent)
    {
        DialogueManager.Instance.CloseQuestDescriptionWindow();
        ShowDescription(questAgent.MQuest);
        descriptionWindow.alpha = 1;
        descriptionWindow.blocksRaycasts = true;
    }

    public void CloseQuestWindow()
    {
        questsWindow.alpha = 0;
        questsWindow.blocksRaycasts = false;
        CloseDescriptionWindow();
    }
    public void OpenQuestWindow()
    {
        questsWindow.alpha = 1;
        questsWindow.blocksRaycasts = true;
        DialogueManager.Instance.CloseQuestDescriptionWindow();
    }
    #endregion
}

直接连名字也变了,以前是为了和QuestGiverQuestManager区分,现在没必要了,至于为什么,看下文。其中,在接取任务时,就用到了Talker的TalkToThisObjectives,是往里加相应对话目标以进行目标更新,而不是像以前那样把对话型目标的更新方法订阅给Talker的OnTalkFinishedEvent。所以,完成或者放弃任务时,只需从中Remove相应对话型目标。

然后,为了实现演示中出现的功能,任务类当然也被重构了:

using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
[CreateAssetMenu(fileName = "quest", menuName = "Zetan/任务/新任务")]
public class Quest : ScriptableObject
{
    [Header("任务基本信息")]
    [SerializeField]
    private string ID;
    public string _ID
    {
        get
        {
            return ID;
        }
    }

    [SerializeField, TextArea(1, 1)]
    private string tittle;
    public string Title
    {
        get
        {
            return tittle;
        }
    }

    [SerializeField]
    [TextArea]
    private string description;
    public string Description
    {
        get
        {
            return description;
        }
    }

    [SerializeField]
    private bool abandonable = true;
    public bool Abandonable
    {
        get
        {
            return abandonable;
        }
    }

    [SerializeField, Space, Header("任务接取条件")]
    private QuestAcceptCondition[] acceptConditions;
    public QuestAcceptCondition[] AcceptConditions
    {
        get
        {
            return acceptConditions;
        }
    }

    [Space, Header("任务对话")]
    [SerializeField]
    private Dialogue beginDialogue;
    public Dialogue BeginDialogue
    {
        get
        {
            return beginDialogue;
        }
    }
    [SerializeField]
    private Dialogue ongoingDialogue;
    public Dialogue OngoingDialogue
    {
        get
        {
            return ongoingDialogue;
        }
    }
    [SerializeField]
    private Dialogue completeDialogue;
    public Dialogue CompleteDialogue
    {
        get
        {
            return completeDialogue;
        }
    }

    [Space, Header("任务报酬")]
    [SerializeField]
    private QuestReward questReward;
    public QuestReward MQuestReward
    {
        get
        {
            return questReward;
        }
    }

    [Space, Header("任务完成方式")]
    [SerializeField]
    private bool cmpltOnOriginalNPC = true;
    public bool CmpltOnOriginalNPC
    {
        get
        {
            return cmpltOnOriginalNPC;
        }
    }
    [SerializeField]
    [ConditionalHide("cmpltOnOriginalNPC", true, true)]
    private string IDOfNPCToComplete;
    public string _IDOfNPCToComplete
    {
        get
        {
            return IDOfNPCToComplete;
        }
    }

    [Space, Header("任务目标")]
    [SerializeField]
    [Tooltip("勾选此项,则勾选InOrder的目标按OrderIndex从小到大的顺序执行,若相同,则表示可以同时进行;若目标没有勾选InOrder,则表示该目标不受顺序影响。")]
    private bool cmpltObjectiveInOrder = false;
    public bool CmpltObjectiveInOrder
    {
        get
        {
            return cmpltObjectiveInOrder;
        }
    }

    [System.NonSerialized]
    private List objectives = new List();//存储所有目标,在运行时用到,初始化时自动填,不用人为干预,详见QuestGiver类
    public List Objectives
    {
        get
        {
            return objectives;
        }
    }

    [SerializeField]
    private CollectObjective[] collectObjectives;
    public CollectObjective[] CollectObjectives
    {
        get
        {
            return collectObjectives;
        }
    }

    [SerializeField]
    private KillObjective[] killObjectives;
    public KillObjective[] KillObjectives
    {
        get
        {
            return killObjectives;
        }
    }

    [SerializeField]
    private TalkObjective[] talkObjectives;
    public TalkObjective[] TalkObjectives
    {
        get
        {
            return talkObjectives;
        }
    }

    [SerializeField]
    private MoveObjective[] moveObjectives;
    public MoveObjective[] MoveObjectives
    {
        get
        {
            return moveObjectives;
        }
    }

    [HideInInspector]
    public QuestGiver MOriginQuestGiver;

    [HideInInspector]
    public QuestGiver MCurrentQuestGiver;

    [HideInInspector]
    public bool IsOngoing;//任务是否正在执行,在运行时用到

    public bool IsComplete
    {
        get
        {
            foreach (CollectObjective co in collectObjectives)
                if (!co.IsComplete) return false;
            foreach (KillObjective ko in killObjectives)
                if (!ko.IsComplete) return false;
            foreach (TalkObjective to in talkObjectives)
                if (!to.IsComplete) return false;
            foreach (MoveObjective mo in moveObjectives)
                if (!mo.IsComplete) return false;
            return true;
        }
    }

    public bool AcceptAble
    {
        get
        {
            foreach (QuestAcceptCondition qac in AcceptConditions)
            {
                if (!qac.IsEligible) return false;
            }
            return true;
        }
    }

    /// 
    /// 判断该任务是否需要某个道具,用于丢弃某个道具时,判断能不能丢
    /// 
    /// 所需判定的道具
    /// 所需判定的数量
    /// 
    public bool RequiredItem(string itemID, int leftAmount)
    {
        if (CmpltObjectiveInOrder)
        {
            foreach (Objective o in Objectives)
            {
                //当目标是收集类目标时才进行判断
                if (o is CollectObjective && itemID == (o as CollectObjective).ItemID)
                {
                    if (o.IsComplete && o.InOrder)
                    {
                        //如果剩余的道具数量不足以维持该目标完成状态
                        if (o.Amount > leftAmount)
                        {
                            Objective tempObj = o.NextObjective;
                            while (tempObj != null)
                            {
                                //则判断是否有后置目标在进行,以保证在打破该目标的完成状态时,后置目标不受影响
                                if (tempObj.CurrentAmount > 0 && tempObj.OrderIndex > o.OrderIndex)
                                {
                                    //Debug.Log("Required");
                                    return true;
                                }
                                tempObj = tempObj.NextObjective;
                            }
                        }
                        //Debug.Log("NotRequired3");
                        return false;
                    }
                    //Debug.Log("NotRequired2");
                    return false;
                }
            }
        }
        //Debug.Log("NotRequired1");
        return false;
    }
}
#region 任务报酬
[System.Serializable]
public class QuestReward
{
    [SerializeField]
    private int money;
    public int Money
    {
        get
        {
            return money;
        }
    }

    [SerializeField]
    private int EXP;
    public int _EXP
    {
        get
        {
            return EXP;
        }
    }

    [SerializeField]
    private ItemBase[] items;
    public ItemBase[] Items
    {
        get
        {
            return items;
        }
    }
}
#endregion

#region 任务条件
/// 
/// 任务接收条件
/// 
[System.Serializable]
public class QuestAcceptCondition
{
    [SerializeField]
    private QuestCondition acceptCondition = QuestCondition.None;
    public QuestCondition AcceptCondition
    {
        get
        {
            return acceptCondition;
        }
    }

    [SerializeField]
    [ConditionalHide("acceptCondition", (int)~(QuestCondition.None | QuestCondition.ComplexQuest | QuestCondition.HasItem), true)]
    private int level;
    public int Level
    {
        get
        {
            return level;
        }
    }

    [SerializeField]
    [ConditionalHide("acceptCondition", (int)QuestCondition.ComplexQuest, true)]
    private string IDOfCompleteQuest;
    public string _IDOfCompleteQuest
    {
        get
        {
            return IDOfCompleteQuest;
        }
    }

    [SerializeField]
    [ConditionalHide("acceptCondition", (int)QuestCondition.ComplexQuest, true)]
    private Quest completeQuest;
    public Quest CompleteQuest
    {
        get
        {
            return completeQuest;
        }
    }

    [SerializeField]
    [ConditionalHide("acceptCondition", (int)QuestCondition.HasItem, true)]
    private string IDOfOwnedItem;
    public string _IDOfOwnedItem
    {
        get
        {
            return IDOfOwnedItem;
        }
    }

    [SerializeField]
    [ConditionalHide("acceptCondition", (int)QuestCondition.HasItem, true)]
    private ItemBase ownedItem;
    public ItemBase OwnedItem
    {
        get
        {
            return ownedItem;
        }
    }

    public bool IsEligible
    {
        get
        {
            switch (AcceptCondition)
            {
                case QuestCondition.ComplexQuest:
                    if (_IDOfCompleteQuest != string.Empty)
                        return QuestManager.Instance.HasCompleteQuestWithID(_IDOfCompleteQuest);
                    else return QuestManager.Instance.HasCompleteQuestWithID(CompleteQuest._ID);
                case QuestCondition.HasItem:
                    if (_IDOfOwnedItem != string.Empty)
                        return BagManager.Instance.HasItemWithID(_IDOfOwnedItem);
                    else return BagManager.Instance.HasItemWithID(OwnedItem._ID);
                default: return true;
            }
        }
    }
}

//使用2的幂数方便进行位运算
public enum QuestCondition
{
    None = 1,
    LevelLargeThen = 2,
    LevelLessThen = 4,
    LevelLargeOrEqualsThen = 8,
    LevelLessOrEqualsThen = 16,
    ComplexQuest = 32,
    HasItem = 64
}
#endregion

#region 任务目标
public delegate void UpdateNextObjListener(Objective objToDeal);
[System.Serializable]
/// 
/// 任务目标
/// 
public abstract class Objective
{
    [HideInInspector]
    public string runtimeID;

    [SerializeField]
    private string displayName;
    public string DisplayName
    {
        get
        {
            return displayName;
        }
    }

    [SerializeField]
    private int amount;
    public int Amount
    {
        get
        {
            return amount;
        }
    }

    private int currentAmount;
    public int CurrentAmount
    {
        get
        {
            return currentAmount;
        }

        set
        {
            bool befCmplt = IsComplete;
            if (value < amount && value >= 0)
                currentAmount = value;
            else if (value < 0)
            {
                currentAmount = 0;
            }
            else currentAmount = amount;
            if (!befCmplt && IsComplete && OnCompleteThisEvent != null)
                OnCompleteThisEvent(NextObjective);
        }
    }

    public bool IsComplete
    {
        get
        {
            if (currentAmount >= amount)
                return true;
            return false;
        }
    }

    [SerializeField]
    private bool inOrder;
    public bool InOrder
    {
        get
        {
            return inOrder;
        }
    }

    [SerializeField]
    [ConditionalHide("inOrder", true)]
    private int orderIndex;
    public int OrderIndex
    {
        get
        {
            return orderIndex;
        }
    }

    [System.NonSerialized]
    public Objective PrevObjective;
    [System.NonSerialized]
    public Objective NextObjective;

    public event UpdateNextObjListener OnCompleteThisEvent;

    protected virtual void UpdateStatus()
    {
        if (IsComplete) return;
        if (!InOrder) CurrentAmount++;
        else if (InOrder && AllPrevObjCmplt) CurrentAmount++;
    }

    public bool AllPrevObjCmplt//判定所有前置目标是否都完成
    {
        get
        {
            Objective tempObj = PrevObjective;
            while (tempObj != null)
            {
                if (!tempObj.IsComplete && tempObj.OrderIndex < OrderIndex)
                {
                    return false;
                }
                tempObj = tempObj.PrevObjective;
            }
            return true;
        }
    }
    public bool HasNextObjOngoing//判定是否有后置目标正在进行
    {
        get
        {
            Objective tempObj = NextObjective;
            while (tempObj != null)
            {
                if (tempObj.CurrentAmount > 0 && tempObj.OrderIndex > OrderIndex)
                {
                    return true;
                }
                tempObj = tempObj.NextObjective;
            }
            return false;
        }
    }
}
/// 
/// 收集类目标
/// 
[System.Serializable]
public class CollectObjective : Objective
{
    [SerializeField]
    private string itemID;
    public string ItemID
    {
        get
        {
            return itemID;
        }
    }

    [SerializeField]
    private bool checkBagAtAcpt = true;//用于标识是否在接取任务时检查背包道具看是否满足目标,否则目标重头开始计数
    public bool CheckBagAtAcpt
    {
        get
        {
            return checkBagAtAcpt;
        }
    }
    [SerializeField]
    private bool loseItemAtSubmit = true;//用于标识是否在提交任务时失去相应道具
    public bool LoseItemAtSubmit
    {
        get
        {
            return loseItemAtSubmit;
        }
    }

    public void UpdateCollectAmountUp(string itemID, int leftAmount)//得道具时用到
    {
        if (itemID == ItemID)
        {
            for (int i = 0; i < leftAmount; i++)
            {
                UpdateStatus();
            }
        }
    }

    public void UpdateCollectAmountDown(string itemID, int leftAmount)//丢道具时用到
    {
        if (itemID == ItemID)
        {
            //前置目标都完成且没有后置目标在进行时,才允许更新
            if (AllPrevObjCmplt && !HasNextObjOngoing) CurrentAmount = leftAmount;
        }
    }
}
/// 
/// 打怪类目标
/// 
[System.Serializable]
public class KillObjective : Objective
{
    [SerializeField]
    private string enermyID;
    public string EnermyID
    {
        get
        {
            return enermyID;
        }
    }

    public void UpdateKillAmount()
    {
        UpdateStatus();
    }
}
/// 
/// 谈话类目标
/// 
[System.Serializable]
public class TalkObjective : Objective
{
    [SerializeField]
    private string talkerID;
    public string TalkerID
    {
        get
        {
            return talkerID;
        }
    }

    [SerializeField]
    private Dialogue dialogue;
    public Dialogue Dialogue
    {
        get
        {
            return dialogue;
        }
    }

    public void UpdateTalkStatus()
    {
        UpdateStatus();
    }
}
/// 
/// 移动到点类目标
/// 
[System.Serializable]
public class MoveObjective : Objective
{
    [SerializeField]
    private string pointID;
    public string PointID
    {
        get
        {
            return pointID;
        }
    }

    public void UpdateMoveIntoStatus(QuestPoint point)
    {
        if (point._ID == PointID)
            UpdateStatus();
    }

    public void UpdateMoveAwayStatus(QuestPoint point)
    {
        if (point._ID == PointID && !HasNextObjOngoing)
            CurrentAmount--;
    }
}
#endregion

Title不知道什么时候多打了一个t,字段名是tittle啊,懒得改了,知道是这么一回事就行。回到任务编辑界面,多了这些东西:

Unity 2D独立开发手记(六):基于任务系统的对话系统_第32张图片

任务对话一栏的数据当然是用于实现前面演示的那种根据任务状态而进行不同对话的功能了。

NPC有了,对话有了,理所当然,一个处理对话的单例也要有,它将用于处理对话的成句进行、处理对话选项、处理对话型目标等。也不遮遮掩掩了,它是这样的:

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

public class DialogueManager : MonoBehaviour
{
    private static DialogueManager instance;
    public static DialogueManager Instance
    {
        get
        {
            if (instance == null || !instance.gameObject)
                instance = FindObjectOfType();
            return instance;
        }
    }

    [SerializeField]
    private CanvasGroup dialogueWindow;
    [SerializeField]
    private Text nameText;
    [SerializeField]
    private Text wordsText;
    [SerializeField]
    private Transform optionsParent;
    [SerializeField]
    private GameObject optionPrefab;

    [SerializeField, ReadOnly]
    private DialogueType dialogueType = DialogueType.Normal;

    private Queue Words = new Queue();

    private QuestGiver questGiver;
    private Talker talker;

    [Space, Header("选项相关")]
    [SerializeField]
    private Button pageUpButton;
    [SerializeField]
    private Button pageDownButton;
    [SerializeField]
    private Text pageText;

    [SerializeField]
    private float textLineHeight = 22.35832f;
    [SerializeField]
    private int lineAmount = 5;

    private int page = 1;
    public int Page
    {
        get
        {
            return page;
        }
        set
        {
            if (value > 1) page = value;
            else page = 1;
        }
    }

    private int MaxPage = 1;
    [HideInInspector]
    public List OptionAgents;

    private TalkObjective talkObjective;
    private Quest MQuest;

    [Space, Header("任务详情UI相关")]
    [SerializeField]
    private GameObject questButton;
    [SerializeField]
    private CanvasGroup descriptionWindow;
    [SerializeField]
    private Text descriptionText;
    [SerializeField]
    private Text money_EXPText;
    [SerializeField]
    private ItemAgent[] rewardCells;

    #region 开始新对话
    public void StartDialogue(Dialogue dialogue)
    {
        if (dialogue.Words.Length < 1 || !dialogue) return;
        Words.Clear();
        foreach (Saying saying in dialogue.Words)
        {
            Words.Enqueue(saying);
        }
        SayNextWords();
        if (OptionAgents.Count < 1) optionsParent.gameObject.SetActive(false);
        wordsText.gameObject.SetActive(true);
        OpenDialogueWindow();
        pageUpButton.gameObject.SetActive(false);
        pageDownButton.gameObject.SetActive(false);
        pageText.gameObject.SetActive(false);
    }

    public void StartQuestGiverDialogue(QuestGiver questGiver)
    {
        this.questGiver = questGiver;
        questGiver.OnTalkBegin();
        dialogueType = DialogueType.Giver;
        if (questGiver.QuestInstances.Count > 0) questButton.SetActive(true);
        else questButton.SetActive(false);
        StartDialogue(questGiver.DefaultDialogue);
    }

    public void StartNormalTalkerDialogue(Talker talker)
    {
        this.talker = talker;
        talker.OnTalkBegin();
        dialogueType = DialogueType.Normal;
        questButton.SetActive(false);
        StartDialogue(talker.DefaultDialogue);
    }

    public void StartQuestDialogue(Quest quest)
    {
        MQuest = quest;
        dialogueType = DialogueType.Quest;
        ClearOptions();
        if (!MQuest.IsComplete && !MQuest.IsOngoing) StartDialogue(quest.BeginDialogue);
        else if (!MQuest.IsComplete && MQuest.IsOngoing) StartDialogue(quest.OngoingDialogue);
        else StartDialogue(quest.CompleteDialogue);
    }

    public void StartObjectiveDialogue(TalkObjective talkObjective)
    {
        this.talkObjective = talkObjective;
        dialogueType = DialogueType.Objective;
        ClearOptions();
        StartDialogue(talkObjective.Dialogue);
    }
    #endregion

    #region 处理对话选项
    /// 
    /// 生成继续按钮选项
    /// 
    void MakeContinueOption()
    {
        OptionAgent oa = OptionAgents.Find(x => x.optionType == OptionType.Continue);
        if (oa) OpenOptionArea();
        if (Words.Count > 1)
        {
            //如果还有话没说完,弹出一个“继续”按钮
            if (!oa)
            {
                oa = Instantiate(optionPrefab, optionsParent).GetComponent();
                oa.optionType = OptionType.Continue;
                oa.TitleText.text = "继续";
                OptionAgents.Add(oa);
                OpenOptionArea();
            }
        }
        else if (oa)
        {
            //如果话说完了,把“继续”按钮去掉
            OptionAgents.Remove(oa);
            Destroy(oa.gameObject);
        }
        //当继续选项出现时,总没有其他选项出现,因此不必像下面一样还要处理一下,除非自己作死把行数写满让继续按钮没地方放
    }
    /// 
    /// 生成任务列表的选项
    /// 
    void MakeTalkerQuestOption()
    {
        int leftLineCount = lineAmount - (int)(wordsText.preferredHeight / textLineHeight);
        int index = 1;
        ClearOptions();
        foreach (Quest quest in questGiver.QuestInstances)
        {
            if (!QuestManager.Instance.HasCompleteQuest(quest) && quest.AcceptAble)
            {
                OptionAgent oa = Instantiate(optionPrefab, optionsParent).GetComponent();
                oa.optionType = OptionType.Quest;
                oa.MQuest = quest;
                oa.TitleText.text = quest.Title + (quest.IsComplete ? "(完成)" : quest.IsOngoing ? "(进行中)" : string.Empty);
                OptionAgents.Add(oa);
                if (index > leftLineCount) oa.gameObject.SetActive(false);
                index++;
            }
        }
        MaxPage = Mathf.CeilToInt(OptionAgents.Count * 1.0f / (leftLineCount * 1.0f));//总页数
        if (MaxPage > 1)
        {
            pageUpButton.gameObject.SetActive(false);
            pageDownButton.gameObject.SetActive(true);
            pageText.gameObject.SetActive(true);
            pageText.text = Page.ToString() + "/" + MaxPage.ToString();
        }
        else
        {
            pageUpButton.gameObject.SetActive(false);
            pageDownButton.gameObject.SetActive(false);
            pageText.gameObject.SetActive(false);
        }
    }
    /// 
    /// 生成对话目标列表的选项
    /// 
    void MakeTalkerObjectiveOption()
    {
        int leftLineCount = lineAmount - (int)(wordsText.preferredHeight / textLineHeight);
        int index = 1;
        if (questGiver.talkToThisObjectives != null && questGiver.talkToThisObjectives.Count > 0)
        {
            ClearOptions();
            foreach (TalkObjectiveInfo toi in questGiver.talkToThisObjectives)
            {
                if (toi.talkObjective.AllPrevObjCmplt && !toi.talkObjective.HasNextObjOngoing)
                {
                    OptionAgent oa = Instantiate(optionPrefab, optionsParent).GetComponent();
                    oa.optionType = OptionType.TalkObjective;
                    oa.TitleText.text = toi.questTitle;
                    oa.talkObjective = toi.talkObjective;
                    OptionAgents.Add(oa);
                    if (index > leftLineCount) oa.gameObject.SetActive(false);
                    index++;
                }
            }
        }
        MaxPage = Mathf.CeilToInt(OptionAgents.Count * 1.0f / (leftLineCount * 1.0f));
        if (MaxPage > 1)
        {
            pageUpButton.gameObject.SetActive(false);
            pageDownButton.gameObject.SetActive(true);
            pageText.gameObject.SetActive(true);
            pageText.text = Page.ToString() + "/" + MaxPage.ToString();
        }
        else
        {
            pageUpButton.gameObject.SetActive(false);
            pageDownButton.gameObject.SetActive(false);
            pageText.gameObject.SetActive(false);
        }
    }

    public void OptionPageUp()
    {
        int leftLineCount = lineAmount - (int)(wordsText.preferredHeight / textLineHeight);
        if (page > 0)
        {
            Page--;
            for (int i = 0; i < leftLineCount; i++)
            {
                if ((page - 1) * leftLineCount + i < OptionAgents.Count && (page - 1) * leftLineCount + i >= 0)
                    OptionAgents[(page - 1) * leftLineCount + i].gameObject.SetActive(true);
                if (page * leftLineCount + i >= 0 && page * leftLineCount + i < OptionAgents.Count)
                    OptionAgents[page * leftLineCount + i].gameObject.SetActive(false);
            }
        }
        if (Page == 1 && MaxPage > 1)
        {
            pageUpButton.gameObject.SetActive(false);
            pageDownButton.gameObject.SetActive(true);
        }
        else
        {
            pageUpButton.gameObject.SetActive(true);
            pageDownButton.gameObject.SetActive(true);
        }
        pageText.text = Page.ToString() + "/" + MaxPage.ToString();
    }

    public void OptionPageDown()
    {
        int leftLineCount = lineAmount - (int)(wordsText.preferredHeight / textLineHeight);
        if (page < Mathf.CeilToInt(OptionAgents.Count * 1.0f / (leftLineCount * 1.0f)))
        {
            for (int i = 0; i < leftLineCount; i++)
            {
                if ((page - 1) * leftLineCount + i < OptionAgents.Count && (page - 1) * leftLineCount + i >= 0)
                    OptionAgents[(page - 1) * leftLineCount + i].gameObject.SetActive(false);
                if (page * leftLineCount + i >= 0 && page * leftLineCount + i < OptionAgents.Count)
                    OptionAgents[page * leftLineCount + i].gameObject.SetActive(true);
            }
            Page++;
        }
        if(Page == MaxPage && MaxPage > 1)
        {
            pageUpButton.gameObject.SetActive(true);
            pageDownButton.gameObject.SetActive(false);
        }
        else
        {
            pageUpButton.gameObject.SetActive(true);
            pageDownButton.gameObject.SetActive(true);
        }
        pageText.text = Page.ToString() + "/" + MaxPage.ToString();
    }
    #endregion

    #region 处理每句话
    /// 
    /// 转到下一句话
    /// 
    public void SayNextWords()
    {
        MakeContinueOption();
        if (Words.Count == 1)
        {
            HandlingLastWords();
            //因为Dequeue之后,话就没了,Words.Count就不是1了,而是0,所以要在此之前做这一步
        }
        if (Words.Count > 0)
        {
            nameText.text = Words.Peek().TalkerName;
            wordsText.text = Words.Dequeue().Words;
        }
        if (dialogueType == DialogueType.Normal)
            CloseDialogueWindow();
    }

    /// 
    /// 处理最后一句对话
    /// 
    private void HandlingLastWords()
    {
        if (dialogueType == DialogueType.Giver && questGiver)
        {
            questGiver.OnTalkFinished();
            MakeTalkerObjectiveOption();
        }
        else if (dialogueType == DialogueType.Normal && talker != null)
        {
            talker.OnTalkFinished();
        }
        else if (dialogueType == DialogueType.Objective && talkObjective != null)
        {
            HandlingLastObjectiveWords();
        }
        else if (dialogueType == DialogueType.Quest && MQuest)
        {
            HandlingLastQuestWords();
        }
        if (OptionAgents.FindAll(x => x.optionType == OptionType.TalkObjective).Count > 0 && !questGiver.talkToThisObjectives.Exists(x => x.talkObjective.Dialogue == null))
        {
            //如果该目标身上有对话类目标,而且每个目标都有正确对话数据,则展示对话选项
            optionsParent.gameObject.SetActive(true);
        }
    }
    /// 
    /// 处理最后一句对话型目标的对话
    /// 
    private void HandlingLastObjectiveWords()
    {
        talkObjective.UpdateTalkStatus();
        if (talkObjective.IsComplete)
        {
            OptionAgent oa = OptionAgents.Find(x => x.talkObjective == talkObjective);
            if (oa && oa.gameObject)
            {
                //去掉该对话目标自身的对话型目标选项
                OptionAgents.Remove(oa);
                Destroy(oa.gameObject);
            }
            //目标已经完成,不再需要保留在对话人的目标列表里,从对话人的对话型目标里删掉相应信息
            questGiver.talkToThisObjectives.RemoveAll(x => x.talkObjective == talkObjective);
        }
        talkObjective = null;//重置管理器的对话目标以防出错
        QuestManager.Instance.UpdateObjectivesText();
    }
    /// 
    /// 处理最后一句任务的对话
    /// 
    private void HandlingLastQuestWords()
    {
        if (!MQuest.IsOngoing || MQuest.IsComplete)
        {
            ClearOptions();
            //若是任务对话的最后一句,则根据任务情况弹出确认按钮
            OptionAgent yes = Instantiate(optionPrefab, optionsParent).GetComponent();
            OptionAgents.Add(yes);
            yes.optionType = OptionType.Comfirm;
            yes.MQuest = MQuest;
            yes.YesOrNo = true;
            yes.TitleText.text = MQuest.IsComplete ? "完成" : "接受";
            if (!MQuest.IsComplete)
            {
                OptionAgent no = Instantiate(optionPrefab, optionsParent).GetComponent();
                OptionAgents.Add(no);
                no.optionType = OptionType.Comfirm;
                no.YesOrNo = false;
                no.TitleText.text = "拒绝";
            }
            OpenQuestDescriptionWindow(MQuest);
        }
        MQuest = null;
    }
    #endregion

    #region UI相关
    public void OpenDialogueWindow()
    {
        dialogueWindow.alpha = 1;
        dialogueWindow.blocksRaycasts = true;
    }
    public void CloseDialogueWindow()
    {
        dialogueWindow.alpha = 0;
        dialogueWindow.blocksRaycasts = false;
        questGiver = null;
        MQuest = null;
        ClearOptions();
        CloseQuestDescriptionWindow();
    }

    public void OpenOptionArea()
    {
        if (OptionAgents.Count < 1) return;
        optionsParent.gameObject.SetActive(true);
        QuestManager.Instance.CloseDescriptionWindow();
    }
    public void CloseOptionArea()
    {
        optionsParent.gameObject.SetActive(false);
        CloseQuestDescriptionWindow();
    }

    public void OpenQuestDescriptionWindow(Quest quest)
    {
        QuestManager.Instance.CloseDescriptionWindow();
        ShowDescription(quest);
        descriptionWindow.alpha = 1;
        descriptionWindow.blocksRaycasts = true;
    }
    public void CloseQuestDescriptionWindow()
    {
        MQuest = null;
        descriptionWindow.alpha = 0;
        descriptionWindow.blocksRaycasts = false;
    }
    private void ShowDescription(Quest quest)
    {
        if (quest == null) return;
        MQuest = quest;
        descriptionText.text = string.Format("{0}\n[委托人: {1}]\n{2}", MQuest.Title, MQuest.MOriginQuestGiver.Name, MQuest.Description);
        money_EXPText.text = string.Format("[奖励]\n经验:\n{0}\n金币:\n{1}", quest.MQuestReward._EXP, quest.MQuestReward.Money);
        foreach (ItemAgent rwc in rewardCells)
        {
            rwc.Item = null;
            rwc.Icon.overrideSprite = null;
        }
        foreach (ItemBase item in quest.MQuestReward.Items)
            foreach (ItemAgent rw in rewardCells)
            {
                if (rw.Item == null)
                {
                    rw.Item = item;
                    rw.Icon.overrideSprite = item.Icon;
                    break;
                }
            }
    }
    #endregion

    #region 其它
    public void LoadTalkerQuest()
    {
        if (questGiver == null) return;
        questButton.SetActive(false);
        Skip();
        MakeTalkerQuestOption();
        OpenOptionArea();
    }

    public void Skip()
    {
        while (Words.Count > 0)
            SayNextWords();
    }

    public void GotoDefault()
    {
        ClearOptions();
        CloseQuestDescriptionWindow();
        StartQuestGiverDialogue(questGiver);
    }

    private void ClearOptions()
    {
        for (int i = 0; i < OptionAgents.Count; i++)
        {
            if (OptionAgents[i]) Destroy(OptionAgents[i].gameObject);
        }
        OptionAgents.Clear();
        Page = 1;
        pageUpButton.gameObject.SetActive(false);
        pageDownButton.gameObject.SetActive(false);
        pageText.gameObject.SetActive(false);
    }

    private void ClearOptionExceptContinue()
    {
        for (int i = 0; i < OptionAgents.Count; i++)
        {
            if (OptionAgents[i] && OptionAgents[i].optionType != OptionType.Continue) Destroy(OptionAgents[i].gameObject);
        }
        OptionAgents.RemoveAll(x => !x.gameObject);
        Page = 1;
        pageUpButton.gameObject.SetActive(false);
        pageDownButton.gameObject.SetActive(false);
        pageText.gameObject.SetActive(false);
    }
    #endregion

    private enum DialogueType
    {
        Normal,
        Quest,
        Giver,
        Objective
    }
}

其实,那个StartQuestGiverDialogue()方法的功能完全可以并在StartNormalTalkerDialogue()里面的,因为QuestGiver喊Talker作爹,所以往后我会做的,现在就先酱。然后仔细看可以发现,早期任务系统中,管理NPC任务的QuestGiverQuestManager类中的一些方法好像这个对话管理器也有哦?没错,实际上,更夸张的是,QuestGiverQuestManager已经没了,它被整合到了该对话管理器中了。其中,有个textLineHeight和lineAmount是什么鬼?呃,是对话文本的行高和对话框的总行数,我的文字区域和选项区域是分开的,在它们的父对象使用Vertical Layout Group组件进行拼接,所以要计算剩余的行数看还能装下多少行选项按钮。最后怎么实现翻页?思路就是,先把所有的选项按钮都生成吧,再把可见行之外的隐藏掉,然后在上下翻页时,成组显示或隐藏选项,就行了。看到前面的,大家想想,若按我的思路,不用委托和事件,也应该知道其他类型比如打怪型目标的更新方法了吧?无非就是给目标对象加一个目标列表,例如List,然后在怪死掉时遍历每一个Objective调用其更新方法,OK?

OptionAgent,是处理选项按钮功能的类,实现如下:

using UnityEngine;
using UnityEngine.UI;

public class OptionAgent : MonoBehaviour
{
    public Text TitleText;

    [ReadOnly]
    public OptionType optionType;

    [HideInInspector]
    public Quest MQuest;

    [HideInInspector]
    public TalkObjective talkObjective;

    [HideInInspector]
    public bool YesOrNo;

    public void Click()
    {
        switch(optionType)
        {
            case OptionType.Quest:
                DialogueManager.Instance.StartQuestDialogue(MQuest);
                //DialogueManager.Instance.OptionAgents.Remove(this);
                //Destroy(gameObject);
                break;
            case OptionType.TalkObjective:
                DialogueManager.Instance.StartObjectiveDialogue(talkObjective);
                //Destroy(gameObject);
                break;
            case OptionType.Comfirm:
                if (YesOrNo)
                {
                    if (!MQuest.IsComplete) QuestManager.Instance.AcceptQuest(MQuest);
                    else QuestManager.Instance.CompleteQuest(MQuest);
                }
                DialogueManager.Instance.CloseQuestDescriptionWindow();
                DialogueManager.Instance.GotoDefault();
                //Destroy(gameObject);
                break;
            case OptionType.Continue:
            default:
                DialogueManager.Instance.SayNextWords();
                break;
        }
    }
}
public enum OptionType
{
    Quest,
    TalkObjective,
    Comfirm,
    Continue
}

很容易看得懂吧,我就不多说了。此时,OptionAgent已经替代了早期任务系统里NPC任务列表里的QuestAgent的功能,所以QuestAgent现在只服务于玩家,被重构成了这样:

using UnityEngine;
using UnityEngine.UI;

public class QuestAgent : MonoBehaviour {

    [HideInInspector]
    public Quest MQuest;

    public Text TitleText;

    public void UpdateQuestStatus()
    {
        if (MQuest) TitleText.text = MQuest.Title + (MQuest.IsComplete ? "(完成)" : string.Empty);
    }

    public void Click()
    {
        if (!MQuest) return;
        QuestManager.Instance.ShowDescription(MQuest);
        QuestManager.Instance.OpenDescriptionWindow(this);
    }
}

我也不再像以前那样在Update里更新任务的标题了,太浪费资源,现在和更新目标状态一样,每触发一次目标的更新才同时更新任务标题,和更新任务描述一样。

SaveManager也有两个方法有改动:

    void LoadQuest(SaveData data)
    {
        foreach (QuestData questData in data.ongoingQuestDatas)
        {
            HandlingQuestData(questData);
            QuestManager.Instance.UpdateObjectivesText();
        }

        foreach (QuestData questData in data.completeQuestDatas)
        {
            Quest quest = HandlingQuestData(questData);
            QuestManager.Instance.CompleteQuest(quest, true);
        }
    }
    Quest HandlingQuestData(QuestData questData)
    {
        QuestGiver questGiver = GameManager.Instance.AllTalker[questData.originalGiverID] as QuestGiver;
        Quest quest = questGiver.QuestInstances.Find(x => x._ID == questData.questID);
        foreach (ObjectiveData od in questData.objectiveDatas)
        {
            foreach (Objective o in quest.Objectives)
            {
                if (o.runtimeID == od.runtimeID)
                {
                    o.CurrentAmount = od.currentAmount;
                    break;
                }
            }
        }
        QuestManager.Instance.AcceptQuest(quest);
        return quest;
    }

最后,一个基于任务系统的对话系统就完成了。还是那句老话,很多地方我想得不太周到,有些代码段不知道有没有必要留着,然后呢,还是老样子,没用对象池,然后不加节制地Destroy和Instantiate,无论activeSelf与否不加节制的SetActive(),这是一大忌。代码中很多被复用的代码段,我也没有做成方法,总之,就是没有优化代码吧。而且,我写文章的对象是我自己和Unity初学者,对大佬们来说,我的代码可谓是又臭又长啊_(:3J∠)_没错,这就是我,一个编程逻辑超烂,烂到只能自己搞独立开发,但还是一心想死磕做游戏,并在他人面前自诩技术宅的死肥宅(╯-╰)/

我会在最新的代码中优化和增强各篇文章的系统,不过不会再更新文章了,想了解最新系统功能更复杂、优化过的源码,还请移步我的GitHub。下一篇文章“开发手记(七)”增强了该篇对话系统的功能,实现了分支、选项等,感兴趣的不要错过哦。

你可能感兴趣的:(Unity2D游戏开发)