Unity 2D独立开发手记(五):通用任务系统

一年多没写博了,因为迫不得已转行,这破游戏也搁置了好久,过完年也有一个月了,回来找找感觉。那就记录一下任务系统的开发吧,方便以后回忆。(2022年3月注:文章中的任务系统太旧了,仅供思路参考,获取新版请访问我的GitHub)

任务系统是每个RPG游戏的核心,很大程度上它支撑着RPG的剧情以及角色的互动等方面的内容。

作为一个单机,任务系统用的当然是C#的委托和事件来写了,如果大家有更好的实现方法,那就当看个思路吧(虽然我的思路也不咋地)。

因为任务系统往往伴随着任务报酬,例如给一些道具什么的;以及收集某个道具,这类的目标。所以,在此之前给道具写一个类是有必要的,这个类本人继承ScpritableObject,做成.asset使用起来很方便,当然大家想写一个可以存到数据库里的,那最好不过了。不过我暂时就是想用ScpritableObject,砸地啦┻━┻ ヘ╰( •̀ε•́ ╰)

首先说说基本思路(PS:这个思路是过了几个月后面补充的,新版的任务系统改了很多东西,我仅凭记忆复原旧版的思路,所以可能和当时的代码有些出入):

1、首先得有最基本的类——任务类,将继承自ScpritableObject,包含任务的ID、标题、描述、目标、奖励、接取条件、与NPC交互时的对话等等。在下文它是Quest类;

2、上面也说了,为了完成任务目标、获得任务奖励,得有一个道具基类,往后的道具例如武器类、防具类,将继承自该基类,这里不讲背包系统,所以道具类只简单的包含ID、名称、描述等基本信息。在下文它是ItemBase类;

3、得有一个简单的背包类,用于侦听道具的获取和失去事件,以更新任务目标。在下文它是BagManager类;

4、同上,得有一个侦听对话事件以更新任务目标进度的东西,暂时把它做成接口,在下文叫ITalkAble,包含对话侦听器、对话时触发的事件等;

5、也同上,敌人也得是一个有击杀侦听器的类,然后这个类当然还得包含ID、名称、击杀函数等。在下文它是Enemy类;

6、需要一个任务管理器类,用于向订阅了任务更新事件的侦听器发布消息,以触发调用相应的函数,并更新UI、处理UI行为。在下文它是PlayerQuestManager;

7、需要一个NPC类,用于供玩家互动以接取、提交任务;在下文它是QuestGiver;

8、需要一个NPC任务管理类,用于调用任务管理器类的相关方法以完成以上的接取、提交等互动,并更新UI、处理UI行为;在下文它是QuestGiverQuestManager;

9、存档用数据类。一般,不需要把整个任务类的各个字段的数据都保存,只需要记住任务的ID,任务的接取情况,以及每个目标的进度就可以读取以还原一个任务的进度了,所以另起一个存档用数据类来记这些东西,在下文它是SaveData;

10、存档管理器,用于向文件中写入数据以存档、读入数据以读档。在下文它是SaveManager;

吐槽一下,网上很多所谓的视频教程,敷衍得很,打着“任务系统”的名号招摇撞骗,点进去看是怎么样的?没有接取、放弃功能,没有多种任务目标,而是直接把任务定死在一些UI上,然后在打死怪的时候更新一下UI的文本。……,他们管这玩意儿,叫“系统”?我这个虽然不咋地,不过大家放心,它是个真正意义上的任务系统。吐槽到这里。

下面是道具类的基类,我用ID来辨别道具的不同。其实比较好的思路是用种类来辨别,因为ID往往是用作唯一标识的,而有时候有些道具不可叠加,它们在背包里也是独立存在的,这时候如果需要移除特定的道具,借助不同的ID来删除就很方便,可能一些极端情况下,不能保证传进方法去的道具实例是想要的实例。综上,使用string ItemBase.Type之类的字段属性来区别道具比较好。好吧,扯远了,那么道具基类是这样实现的(注:这篇文章同一个代码框里的都表示在同一个.cs里,因为我懒得排版~):

using UnityEngine;

[System.Serializable]
public abstract class ItemBase: ScriptableObject
{
    [SerializeField]
    private string ID;
    public string _ID
    {
        get
        {
            return ID;
        }
    }

    [SerializeField]
    new private string name;
    public string Name
    {
        get
        {
            return name;
        }
    }

    [SerializeField, ReadOnly]
    private ItemType itemType;
    public ItemType ItemType
    {
        get
        {
            return itemType;
        }
        protected set
        {
            itemType = value;
        }
    }

    [SerializeField]
    private ItemLevel level;
    public ItemLevel Level
    {
        get
        {
            return level;
        }
    }

    [SerializeField]
    private Sprite icon;
    public Sprite Icon
    {
        get
        {
            return icon;
        }
    }

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

}
public interface IUsable
{
    void OnUse();
}

public enum ItemType
{
    其他,
    药物,
    武器,
    防具,
}

public enum ItemLevel
{
    凡品,
    精品,
    珍品,
    极品,
    绝品,
}

其中,枚举我用了中文,只是为了方便识别和定义,打心里还是非常建议用英文的。接口IUsable,这里没有用到,但是作用显而易见,有的道具是不能使用的,例如任务的某个关键道具,编程化来讲就是,当某个道具派生类实现了这个接口,说明该类代表的道具类型是可用的,当然还可以加入什么判定使用条件之类的,不过这里是记录任务系统,而不是道具系统,所以略过了。其中还有个自定义的ReadOnly标签(Unity不自带的),与该任务系统无关,所以我也不打算把它的具体实现放上来了,总之作用就是让某个字段在Inspector可远观而不可亵玩焉。PS:大家如果有问题的可以留言私我,最好邮件,因为不经常上来,所以留言不一定看得见(●´∀`●),而且,由于度娘搜索资源的更新机制,这篇文章可能一个多月之后才会被大家用百度搜到,所以等大家开始读到我的文章时,可能就是今天(9201年3月9日)一个多月之后的事,我都不知道干啥去了……

那么派生一个简单的武器类吧:

using UnityEngine;

[CreateAssetMenu(fileName = "Weapon", menuName = "Zetan/道具/新武器")]
[System.Serializable]
public class WeaponItem : ItemBase, IUsable
{
    public WeaponItem()
    {
        ItemType = ItemType.武器;
    }
    public void OnUse()
    {
        Debug.Log("UseWeapon");
    }
}

还是那句话,这里不是记录道具系统,所以我也不再多说了_(:3J∠)_。此时,在Project右键,应该可以看到一个新按钮“Zetan->道具->新武器”,点击,则生成了一个道具,随便填了点信息,如下所示:

可以看到,Unity的ScriptableableObject真的好用。接下来该分析一下我们Unity任务系统的主角——“任务”了。在很多RPG中,任务往往伴随多个目标,这些目标是按顺序执行还是可以同时进行?任务可能还有接受条件之类的,比如说玩家等级大于多少,或者完成了什么任务之类的;同时上面也说了,还有个任务报酬。而任务目标的种类,往往就是收集道具,击杀敌人,与某个NPC谈话,或者移动到某处等等,我这个任务系统,实现并简单测试了例举的这四个中的前三个,至于后面那个,因为懒得搭场景,所以不想测试了,就留给大家自理吧(‵▽′)ψ

那么怎么做呢?说来话长:

using System.Collections.Generic;
using UnityEngine;

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

    [SerializeField]
    private string tittle;
    public string Tittle
    {
        get
        {
            return tittle;
        }
    }

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

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

    [SerializeField]
    private QuestAcceptCondition[] acceptConditions;
    public QuestAcceptCondition[] AcceptConditions
    {
        get
        {
            return acceptConditions;
        }
    }

    [SerializeField]
    private QuestGroup questGroup;
    public QuestGroup MQuestGroup
    {
        get
        {
            return questGroup;
        }

        set
        {
            questGroup = value;
        }
    }

    [SerializeField]
    private QuestReward questReward;
    public QuestReward MQuestReward
    {
        get
        {
            return questReward;
        }
    }

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

    [System.NonSerialized]
    private List<Objective> objectives = new List<Objective>();//存储所有目标,在运行时用到,初始化时自动填,不用人为干预,详见QuestGiver类
    public List<Objective> 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;
        }
    }

    [System.NonSerialized]
    private QuestGiver originQuestGiver;
    public QuestGiver MOriginQuestGiver
    {
        get
        {
            return originQuestGiver;
        }

        set
        {
            originQuestGiver = value;
        }
    }

    [System.NonSerialized]
    private QuestGiver currentQuestGiver;
    public QuestGiver MCurrentQuestGiver
    {
        get
        {
            return currentQuestGiver;
        }

        set
        {
            currentQuestGiver = value;
        }
    }

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

    /// <summary>
    /// 判断该任务是否需要某个道具,用于丢弃某个道具时,判断能不能丢
    /// </summary>
    /// <param name="itemID">所需判定的道具</param>
    /// <param name="leftAmount">所需判定的数量</param>
    /// <returns></returns>
    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 任务条件
/// <summary>
/// 任务接收条件
/// </summary>
[System.Serializable]
public class QuestAcceptCondition
{
    [SerializeField]
    private QuestCondition acceptCondition;
    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 PlayerQuestManager.Instance.HasCompleteQuestWithID(_IDOfCompleteQuest);
                    else return PlayerQuestManager.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 false;
            }
        }
    }
}

//使用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 nextObj);
[System.Serializable]
/// <summary>
/// 任务目标
/// </summary>
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(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;

    [field: System.NonSerialized]
    public event UpdateNextObjListener OnCompleteThisEvent;

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

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

    [SerializeField]
    private bool checkBagAtAccept = true;//用于标识是否在接取任务时检查背包道具看是否满足目标,否则目标重头开始计数
    public bool CheckBagAtAccept
    {
        get
        {
            return checkBagAtAccept;
        }

        set
        {
            checkBagAtAccept = value;
        }
    }

    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;
        }
    }
}
/// <summary>
/// 打怪类目标
/// </summary>
[System.Serializable]
public class KillObjective : Objective
{
    [SerializeField]
    private string enermyID;
    public string EnermyID
    {
        get
        {
            return enermyID;
        }
    }

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

    public void UpdateTalkStatus()
    {
        UpdateStatus();
    }
}
/// <summary>
/// 移动到点类目标
/// </summary>
[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

上面的任务类,基本包含了所需内容。其中,一些我自认为大家可能会觉得晦涩难懂的地方,用注释简单解释了一下,实在不懂欢迎骚扰(〃 ̄︶ ̄)人( ̄︶ ̄〃)。有一个ConditionHide自定义标签,同上,不给出,用于勾选某个布尔字段或者选择某些枚举字段时,显示或者隐藏一些的字段。任务类里面涉及的其他一些类在后文会说到,请翻阅,而最后面的任务组QuestGroup暂时没用到,先不贴上来了,初衷是让某些任务在列表里成组。好吧,任务类写完了,这时,和创建道具一样,Project右键Zetan->任务->新任务可以在Project创建任务了,随便填了一些信息,如下所示:

内容好像很丰富,不过看起来很乱,因为懒得写Editor,当然这样也不是不能用,来打我啊o( ̄ヘ ̄o#)

 O几把K,有了任务,接下来就需要有处理它们的大佬们了,首先来个任务NPC吧:

using System.Collections.Generic;
using UnityEngine;

public class QuestGiver : NPC, ITalkAble {

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

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

        set
        {
            questInstances = value;
        }
    }

    public event NPCTalkListener OnTalkBeginEvent;
    public event NPCTalkListener OnTalkFinishedEvent;

    private void Start()
    {
        InitQuest(questsStored);
    }

    public void InitQuest(Quest[] questsStored)
    {
        if (questsStored == null) return;
        foreach (Quest quest in questsStored)
        {
            if (quest)
            {
                Quest temp = Instantiate(quest);
                foreach (CollectObjective co in temp.CollectObjectives)
                    temp.Objectives.Add(co);
                foreach (KillObjective ko in temp.KillObjectives)
                    temp.Objectives.Add(ko);
                foreach (TalkObjective to in temp.TalkObjectives)
                    temp.Objectives.Add(to);
                foreach (MoveObjective mo in temp.MoveObjectives)
                    temp.Objectives.Add(mo);
                if (temp.CmpltObjectiveInOrder)
                {
                    temp.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 < temp.Objectives.Count; i++)
                    {
                        if (temp.Objectives[i].OrderIndex >= temp.Objectives[i - 1].OrderIndex)
                        {
                            temp.Objectives[i].PrevObjective = temp.Objectives[i - 1];
                            temp.Objectives[i - 1].NextObjective = temp.Objectives[i];
                        }
                    }
                }
                for (int i = 0; i < temp.Objectives.Count; i++)
                {
                    temp.Objectives[i].runtimeID = temp._ID + "_O" + i;
                }
                temp.MOriginQuestGiver = this;
                temp.MCurrentQuestGiver = this;
                QuestInstances.Add(temp);
            }
        }
    }


    /// <summary>
    /// 向此对象交接任务。因为往往会有些任务不在同一个NPC接取并完成,所以就要在两个NPC之间交接该任务
    /// </summary>
    /// <param name="quest">要交接的任务</param>
    public void TransferQuestToThis(Quest quest)
    {
        if (!quest) return;
        QuestInstances.Add(quest);
        quest.MCurrentQuestGiver.QuestInstances.Remove(quest);
        quest.MCurrentQuestGiver = this;
        if (QuestGiverQuestManager.Instance.SelectedQuest && QuestGiverQuestManager.Instance.SelectedQuest == quest)
        {
            QuestAgent qa = QuestGiverQuestManager.Instance.QuestAgents.Find(x => x.MQuest == quest);
            if (qa)
            {
                QuestGiverQuestManager.Instance.QuestAgents.Remove(qa);
                Destroy(qa.gameObject);
            }
            QuestGiverQuestManager.Instance.CloseDescriptionWindow();
        }
    }

    public void OnTalkBegin()
    {
        if (OnTalkBeginEvent != null) OnTalkBeginEvent();
        QuestGiverQuestManager.Instance.OpenQuestWindow();
        QuestGiverQuestManager.Instance.LoadGiverQuest(this);
    }

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

public delegate void NPCTalkListener();

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

该类继承自NPC,But这个NPC类我好像只有一个ID和一个Name字段,就不放上来浪费版面了。其中,ITalkAble接口与IUsable接口同理,在游戏世界里,并不是所有NPC都能对话吧,所以就……同时,该类里面有个任务实例的存储,因为,如果在运行时直接修改ScriptableObject的内容的话,相应的资源文件中的内容也会永久性改变,这会怎么样?当玩家完成某个任务时,根据目标进行情况会改变任务的信息(进行中、完成等),而当玩家不想玩这个存档了,删掉,重新开档时,玩家接取该任务,会导致该任务直接完成。所以为了避免这种情况,必须创建新实例来处理,而不是处理原任务本身。

该类里面提到的Manager当然是单例了,因为需要在NPC那里接任务的吧,那么得有一个管理NPC任务的窗口,比如一个显示可接取的任务的表,点击表上面的任务可以接取任务等。不过UI搭建懒得写上来,有不会的到时认真摸索我上传的工程就行了。搭建UI时大家可能会用到Content Size Fitter组件,很多时候会出现增加子对象或扩大子对象时,带该组件的对象其大小不是向下扩张,而是向上扩张的情况,比如说一个“曰”,加一个子对象“丨”,想让它扩张成“甲”,但是却变成了“由”或者“申”。那么怎么解决呢?此时该UI对象Pivot的不是(0.5,0.5)嘛,改成(0.5,1)就行了。该方法同样适用于加了Content Size Fitter的Text对象。

好吧,那么上面提到的管理NPC任务的巨佬是这样的:

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

public class QuestGiverQuestManager : MonoBehaviour {

    private static QuestGiverQuestManager instance;
    public static QuestGiverQuestManager Instance
    {
        get
        {
            if (instance == null)
                instance = FindObjectOfType<QuestGiverQuestManager>();
            return instance;
        }
    }

    [SerializeField]
    private GameObject questPrefab;

    [SerializeField]
    private Transform questListParent;

    [SerializeField]
    private CanvasGroup questsWindow;

    [SerializeField]
    private CanvasGroup descriptionWindow;

    [SerializeField]
    private Text giverName;

    [SerializeField]
    private Text description;

    [SerializeField]
    private Text money_EXP;

    [SerializeField]
    private ItemAgent[] rewardCells;

    [SerializeField]
    private Button acceptBtn;

    [SerializeField]
    private Button completeBtn;

    [SerializeField, Space]
    private List<QuestAgent> questAgents = new List<QuestAgent>();
    public List<QuestAgent> QuestAgents
    {
        get
        {
            return questAgents;
        }

        set
        {
            questAgents = value;
        }
    }

    private Quest selectedQuest;
    public Quest SelectedQuest
    {
        get
        {
            return selectedQuest;
        }

        private set
        {
            selectedQuest = value;
        }
    }

    [SerializeField, ReadOnly]
    private QuestGiver questGiver;

    #region 任务处理相关
    public void LoadGiverQuest(QuestGiver giver)
    {
        if (giver == null) return;
        CloseDescriptionWindow();
        questGiver = giver;
        if (QuestAgents.Count > 0)
        {
            int count = QuestAgents.Count;
            for (int i = 0; i < count; i++)
            {
                Destroy(QuestAgents[i].gameObject);
            }
            QuestAgents.Clear();
        }
        foreach (Quest quest in giver.QuestInstances)
        {
            if (!PlayerQuestManager.Instance.HasCompleteQuest(quest) && quest.AcceptAble)
            {
                QuestAgent qa = Instantiate(questPrefab, questListParent).GetComponent<QuestAgent>();
                qa.IsPlayerQuest = false;
                qa.MQuest = quest;
                qa.Title.text = quest.Tittle;
                QuestAgents.Add(qa);
            }
        }
        giverName.text = giver.Name;
    }

    public void AcceptSeletedQuest()
    {
        if (!SelectedQuest) return;
        PlayerQuestManager.Instance.AcceptQuest(SelectedQuest);
        UpdateObjectivesText();
    }

    public void CompleteSeletedQuest()
    {
        if (!SelectedQuest) return;
        if (PlayerQuestManager.Instance.CompleteQuest(SelectedQuest))
        {
            LoadGiverQuest(questGiver);
            CloseDescriptionWindow();
        }
    }
    #endregion

    #region UI相关
    public void ShowDescription(Quest quest)
    {
        if (quest == null) return;
        SelectedQuest = quest;
        UpdateObjectivesText();
        money_EXP.text = string.Format("[奖励]\n<size=14>经验:\n{0}\n金币:\n{1}</size>", quest.MQuestReward._EXP, quest.MQuestReward.Money);
        foreach (ItemAgent rwc in rewardCells)
            rwc.Item = null;
        foreach (ItemBase item in quest.MQuestReward.Items)
            foreach (ItemAgent rw in rewardCells)
            {
                if (rw.Item == null)
                {
                    rw.Item = item;
                    rw.Icon.sprite = item.Icon;
                    break;
                }
            }
    }

    public void UpdateObjectivesText()
    {
        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");
        description.text = string.Format("<size=16><b>{0}</b></size>\n[委托人: {1}]\n{2}\n\n<size=16><b>任务目标{3}</b></size>\n{4}",
            SelectedQuest.Tittle,
            SelectedQuest.MOriginQuestGiver.Name,
            SelectedQuest.Description, 
            SelectedQuest.IsComplete ? "(完成)" : SelectedQuest.IsOngoing ? "(进行中)" : "",
            objectives);
        acceptBtn.gameObject.SetActive(!SelectedQuest.IsOngoing);
        completeBtn.gameObject.SetActive(SelectedQuest.IsComplete);
    }

    public void CloseDescriptionWindow()
    {
        descriptionWindow.alpha = 0;
        descriptionWindow.blocksRaycasts = false;
    }
    public void OpenDescriptionWindow(QuestAgent questAgent)
    {
        PlayerQuestManager.Instance.CloseDescriptionWindow();
        ShowDescription(questAgent.MQuest);
        descriptionWindow.alpha = 1;
        descriptionWindow.blocksRaycasts = true;
    }

    public void CloseQuestWindow()
    {
        questsWindow.GetComponent<CanvasGroup>().alpha = 0;
        questsWindow.GetComponent<CanvasGroup>().blocksRaycasts = false;
        CloseDescriptionWindow();
    }
    public void OpenQuestWindow()
    {
        questsWindow.GetComponent<CanvasGroup>().alpha = 1;
        questsWindow.GetComponent<CanvasGroup>().blocksRaycasts = true;
        PlayerQuestManager.Instance.CloseDescriptionWindow();
    }
    #endregion
}

其中,QuestAgent类用来单个处理任务,以在列表中用任务名称显示任务,并在点击时弹出任务详情,实现如下:

using UnityEngine;
using UnityEngine.UI;

public class QuestAgent : MonoBehaviour {

    private Quest quest;
    public Quest MQuest
    {
        get
        {
            return quest;
        }

        set
        {
            quest = value;
        }
    }

    [SerializeField]
    private Text title;
    public Text Title
    {
        get
        {
            return title;
        }

        set
        {
            title = value;
        }
    }

    [ReadOnly]
    public bool IsPlayerQuest;

    private void Update()
    {
        if(MQuest) Title.text = MQuest.Tittle + (MQuest.IsComplete ? "(完成)" : MQuest.IsOngoing && !IsPlayerQuest ? "(进行中)" : "");
    }

    public void Click()
    {
        if (!MQuest) return;
        if (IsPlayerQuest)
        {
            PlayerQuestManager.Instance.ShowDescription(quest);
            PlayerQuestManager.Instance.OpenDescriptionWindow(this);
        }
        else
        {
            QuestGiverQuestManager.Instance.ShowDescription(quest);
            QuestGiverQuestManager.Instance.OpenDescriptionWindow(this);
        }
    }
}

IsPlayerQuest,用于标识这是玩家任务窗口里的任务还是NPC任务列表里的任务(有点拗口_(:3J∠)_。

Manager里面的ItemAgent类与QuestAgent功能类似,用于显示道具图标以及数量,并提供点击打开道具详情窗口的方法,当然,这里没写。ItemAgent的实现是这样的:

using UnityEngine;
using UnityEngine.UI;

public class ItemAgent : MonoBehaviour {

    [SerializeField]
    private Image icon;
    public Image Icon
    {
        get
        {
            if (icon == null)
                icon = transform.Find("Icon").GetComponent<Image>();
            return icon;
        }
    }

    private ItemBase item;
    public ItemBase Item
    {
        get
        {
            return item;
        }

        set
        {
            item = value;
        }
    }

    public void OnClick()
    {
        //TODO 显示道具详情
    }
}

上面提到了另一个巨佬PlayerQuestManager,其实和管理NPC任务的巨佬差不多,无非就是对象变成了玩家而已,好吧,废话不多说:

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

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

    [SerializeField]
    private GameObject questPrefab;

    [SerializeField]
    private Transform questListParent;

    [SerializeField]
    private CanvasGroup questsWindow;

    [SerializeField]
    private CanvasGroup descriptionWindow;

    [SerializeField]
    private Text description;


    [SerializeField]
    private Text money_EXP;

    [SerializeField]
    private ItemAgent[] rewardCells;

    [SerializeField, Space]
    private List<QuestAgent> questAgents = new List<QuestAgent>();
    public List<QuestAgent> QuestAgents
    {
        get
        {
            return questAgents;
        }

        set
        {
            questAgents = value;
        }
    }

    [SerializeField]
    private List<Quest> questsOngoing = new List<Quest>();
    public List<Quest> QuestsOngoing
    {
        get
        {
            return questsOngoing;
        }
    }

    [SerializeField]
    private List<Quest> questsCompleted = new List<Quest>();
    public List<Quest> QuestsComplete
    {
        get
        {
            return questsCompleted;
        }
    }

    private Quest selectedQuest;
    public Quest SelectedQuest
    {
        get
        {
            return selectedQuest;
        }

        private set
        {
            selectedQuest = value;
        }
    }

    #region 任务处理相关
    /// <summary>
    /// 接取任务
    /// </summary>
    /// <param name="quest">要接取的任务</param>
    public bool AcceptQuest(Quest quest)
    {
        if (!quest) return false;
        if (HasQuest(quest)) return false;
        QuestAgent qa = Instantiate(questPrefab, questListParent).GetComponent<QuestAgent>();
        qa.IsPlayerQuest = true;
        qa.MQuest = quest;
        qa.Title.text = quest.Tittle;
        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.CheckBagAtAccept) 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.AllQuestGiver[to.TalkerID].OnTalkFinishedEvent += to.UpdateTalkStatus;
                }
                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.AllQuestGiver[quest._IDOfNPCToComplete].TransferQuestToThis(quest);
            }
            catch
            {
                Debug.LogWarningFormat("[找不到NPC] ID: {0}", quest._IDOfNPCToComplete);
            }
        }
        return true;
    }
    /// <summary>
    /// 放弃任务
    /// </summary>
    /// <param name="quest">要放弃的任务</param>
    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.AllQuestGiver[to.TalkerID].OnTalkFinishedEvent -= to.UpdateTalkStatus;
                }
                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;
    }
    /// <summary>
    /// 放弃当前展示的任务
    /// </summary>
    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();
        }
    }
    /// <summary>
    /// 更新某个任务目标,用于在其他前置目标完成时,更新后置目标
    /// </summary>
    /// <param name="nextObj">下一个目标</param>
    public 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;
        }
    }
    /// <summary>
    /// 完成任务
    /// </summary>
    /// <param name="quest">要放弃的任务</param>
    /// <param name="loadMode">是否读档模式</param>
    /// <returns>是否成功完成任务</returns>
    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);
            }
            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) 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.AllQuestGiver[(o as TalkObjective).TalkerID].OnTalkFinishedEvent -= (o as TalkObjective).UpdateTalkStatus;
                }
                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.Title.color = Color.black;
            }
            qa.Title.color = Color.blue;
        }
        SelectedQuest = quest;
        UpdateObjectivesText();
        money_EXP.text = string.Format("[奖励]\n<size=14>经验:\n{0}\n金币:\n{1}</size>", quest.MQuestReward._EXP, quest.MQuestReward.Money);
        foreach (ItemAgent rwc in rewardCells)
            rwc.Item = null;
        foreach (ItemBase item in quest.MQuestReward.Items)
            foreach (ItemAgent rw in rewardCells)
            {
                if (rw.Item == null)
                {
                    rw.Item = item;
                    rw.Icon.sprite = item.Icon;
                    break;
                }
            }
    }

    public void UpdateObjectivesText()
    {
        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");
        description.text = string.Format("<size=16><b>{0}</b></size>\n[委托人: {1}]\n{2}\n\n<size=16><b>任务目标{3}</b></size>\n{4}",
            SelectedQuest.Tittle,
            SelectedQuest.MOriginQuestGiver.Name,
            SelectedQuest.Description,
            SelectedQuest.IsComplete ? "(完成)" : SelectedQuest.IsOngoing ? "(进行中)" : "",
            objectives);
    }

    public void CloseDescriptionWindow()
    {
        QuestAgent qa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
        if (qa) qa.Title.color = Color.black;
        SelectedQuest = null;
        descriptionWindow.alpha = 0;
        descriptionWindow.blocksRaycasts = false;
    }
    public void OpenDescriptionWindow(QuestAgent questAgent)
    {
        QuestGiverQuestManager.Instance.CloseDescriptionWindow();
        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;
        QuestGiverQuestManager.Instance.CloseDescriptionWindow();
    }
    #endregion
}

怎么样,是不是和QuestGiverQuestManager很像?其中,任务完成方法里有个loadMode的布尔型参数,在读取存档处理任务系统时会用到。

前面那个巨佬都提到了BagManager这个巨♂佬,它当然是管理背包物品的单例了,但是还是那句话,这里是写任务系统,不是道具系统,所以也只是实现任务所需功能:

using System.Collections.Generic;
using UnityEngine;

public delegate void ItemInfoListener(string itemID, int amount);

public class BagManager : MonoBehaviour {

    private static BagManager instance;
    public static BagManager Instance
    {
        get
        {
            if (instance == null)
                instance = FindObjectOfType<BagManager>();
            return instance;
        }
    }

    public event ItemInfoListener OnGetItemEvent;
    public event ItemInfoListener OnLoseItemEvent;

    private Dictionary<string, List<ItemBase>> items = new Dictionary<string, List<ItemBase>>();
    public Dictionary<string, List<ItemBase>> Items
    {
        get
        {
            return items;
        }
    }

    public void GetItem(ItemBase item, int amount = 1)
    {
        if (!item) return;
        int originAmount = GetItemAmountByID(item._ID);
        for (int i = 0; i < amount; i++)
        {
            if (Items.ContainsKey(item._ID)) Items[item._ID].Add(item);
            else
            {
                Items.Add(item._ID, new List<ItemBase>());
                Items[item._ID].Add(item);
            }
        }
        if (OnGetItemEvent != null) OnGetItemEvent(item._ID, GetItemAmountByID(item._ID) - originAmount);
        PlayerQuestManager.Instance.UpdateObjectivesText();
        QuestGiverQuestManager.Instance.UpdateObjectivesText();
    }

    public int GetItemAmountByID(string id)
    {
        if (Items.ContainsKey(id))
        {
            return Items[id].Count;
        }
        return 0;
    }

    public bool HasItemWithID(string id)
    {
        return GetItemAmountByID(id) > 0;
    }

    public void LoseItem(ItemBase item)
    {
        if (!HasItemWithID(item._ID)) return;
        if (!item || ThereIsQuestRequiredItem(item._ID, GetItemAmountByID(item._ID) - 1) || GetItemAmountByID(item._ID) < 1) return;
        items[item._ID].Remove(item);
        if (Items[item._ID].Count <= 0) Items.Remove(item._ID);
        if (OnLoseItemEvent != null) OnLoseItemEvent(item._ID, GetItemAmountByID(item._ID));
        PlayerQuestManager.Instance.UpdateObjectivesText();
        QuestGiverQuestManager.Instance.UpdateObjectivesText();
    }
    public void LoseItemByID(string itemID, int amount = 1)
    {
        if (!HasItemWithID(itemID)) return;
        if (itemID == string.Empty || ThereIsQuestRequiredItem(itemID, GetItemAmountByID(itemID) - amount) || GetItemAmountByID(itemID) < amount) return;
        for (int i = 0; i < amount; i++)
        {
            Items[itemID].RemoveAt(Items[itemID].Count - 1);
            if (Items[itemID].Count <= 0)
            {
                Items.Remove(itemID);
                break;
            }
        }
        if (OnLoseItemEvent != null) OnLoseItemEvent(itemID, GetItemAmountByID(itemID));
        PlayerQuestManager.Instance.UpdateObjectivesText();
        QuestGiverQuestManager.Instance.UpdateObjectivesText();
    }

    /// <summary>
    /// 判定是否有某个任务需要某数量的某个道具
    /// </summary>
    /// <param name="itemID">要判定的道具</param>
    /// <param name="amount">要判定的数量</param>
    /// <returns>是否需要该道具</returns>
    bool ThereIsQuestRequiredItem(string itemID, int amount)
    {
        foreach (Quest quest in PlayerQuestManager.Instance.QuestsOngoing)
            if (quest.RequiredItem(itemID, amount))
                return true;
        return false;
    }
}

好像没什么好说的,一切尽在不言中。

上面还提到了Enermy和QuestPoint,它们是这样的,也只是简单实现:

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

public delegate void EnermyDeathListener();

public class Enermy : MonoBehaviour {

    [SerializeField]
    private string ID;
    public string _ID
    {
        get
        {
            return ID;
        }

        set
        {
            ID = value;
        }
    }

    [SerializeField]
    private string _name;
    public string Name
    {
        get
        {
            return _name;
        }

        set
        {
            _name = value;
        }
    }

    public event EnermyDeathListener OnDeathEvent;

    public void Death()
    {
        if (OnDeathEvent != null)
            OnDeathEvent();
        PlayerQuestManager.Instance.UpdateObjectivesText();
        QuestGiverQuestManager.Instance.UpdateObjectivesText();
        //TODO
    }
}
using UnityEngine;

public delegate void MoveToPointListener(QuestPoint point);

public class QuestPoint : MonoBehaviour {

    [SerializeField]
    private string ID;
    public string _ID
    {
        get
        {
            return ID;
        }
    }

    public event MoveToPointListener OnMoveIntoEvent;
    public event MoveToPointListener OnMoveAwayEvent;

    private void OnTriggerEnter(Collider other)
    {
        if (OnMoveIntoEvent != null) OnMoveIntoEvent(this);
    }

    private void OnTriggerStay(Collider other)
    {
        //TODO
    }

    private void OnTriggerExit(Collider other)
    {
        if (OnMoveAwayEvent != null) OnMoveAwayEvent(this);
    }


    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (OnMoveIntoEvent != null) OnMoveIntoEvent(this);
    }

    private void OnTriggerStay2D(Collider2D collision)
    {
        //TODO
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (OnMoveAwayEvent != null) OnMoveAwayEvent(this);
    }
}

最后,还有一个GameManager单例,其实这个名称可以不是这样的,不过我也不知道出于哪些原因,我居然把它命名成这个。它是用来在运行时,间接存储游戏世界所有互动对象的,比如NPC、敌人和任务点等,实现如下:

using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {

    private static GameManager instance;
    public static GameManager Instance
    {
        get
        {
            if (instance == null || !instance.gameObject)
                instance = FindObjectOfType<GameManager>();
            return instance;
        }
    }

    [SerializeField]
    private string itemInfosPath = "1";
    private Dictionary<string, ItemBase> itemDataBase;
    public Dictionary<string, ItemBase> ItemDataBase
    {
        get
        {
            return itemDataBase;
        }
    }

    private Dictionary<string, List<Enermy>> allEnermy = new Dictionary<string, List<Enermy>>();
    public Dictionary<string, List<Enermy>> AllEnermy
    {
        get
        {
            allEnermy.Clear();
            Enermy[] enermies = FindObjectsOfType<Enermy>();
            foreach (Enermy enermy in enermies)
            {
                if (!allEnermy.ContainsKey(enermy._ID))
                {
                    allEnermy.Add(enermy._ID, new List<Enermy>());
                }
                allEnermy[enermy._ID].Add(enermy);
            }
            return allEnermy;
        }
    }

    private Dictionary<string, QuestGiver> allQuestGiver = new Dictionary<string, QuestGiver>();
    public Dictionary<string, QuestGiver> AllQuestGiver
    {
        get
        {
            allQuestGiver.Clear();
            QuestGiver[] questGivers = FindObjectsOfType<QuestGiver>();
            foreach (QuestGiver giver in questGivers)
            {
                try
                {
                    allQuestGiver.Add(giver._ID, giver);
                }
                catch
                {
                    Debug.LogWarningFormat("[Add quest giver error] ID: {0}  Name: {1}", giver._ID, giver.Name);
                }
            }
            return allQuestGiver;
        }
    }

    private Dictionary<string, QuestPoint> allQuestPoint = new Dictionary<string, QuestPoint>();
    public Dictionary<string, QuestPoint> AllQuestPoint
    {
        get
        {
            allQuestPoint.Clear();
            QuestPoint[] questPoints = FindObjectsOfType<QuestPoint>();
            foreach (QuestPoint point in questPoints)
            {
                try
                {
                    allQuestPoint.Add(point._ID, point);
                }
                catch
                {
                    Debug.LogWarningFormat("[Add quest point error] ID: {0}", point._ID);
                }
            }
            return allQuestPoint;
        }
    }

    public void Init()
    {
        itemDataBase = new Dictionary<string, ItemBase>();
        ItemBase[] items = Resources.LoadAll<ItemBase>(itemInfosPath);
        foreach (ItemBase item in items)
        {
            try
            {
                itemDataBase.Add(item._ID, item);
            }
            catch
            {
                Debug.LogWarningFormat("[Add item error] ID: {0}  Name: {1}", item._ID, item.Name);
                continue;
            }
        }
        foreach (KeyValuePair<string, QuestGiver> kvp in AllQuestGiver)
            kvp.Value.Init();
    }

    private void Start()
    {
        Init();
    }

    public ItemBase GetItemInstanceByID(string id)
    {
        ItemBase item = Instantiate(itemDataBase[id]);
        if(item != null)
            switch (item.ItemType)
            {
                case ItemType.武器: return item as WeaponItem;
                default:return item;
            }
        return item;
    }
}

这个逻辑写得有点不完善,甚至乱来,不过就先不斤斤计较了,重点是任务系统,苍天饶过谁(•̀⌄•́)。

好了,代码部分到此结束。弄一些东西做测试了(文章里面当然这么说了,其实写代码和搭UI我是同时进行的(•̀ᴗ•́)و)。

在场景中,创建测试对象,并加上相应的组件……(此处省略N个字)……一个简单的任务系统就是谢样死了:

 (⊙_⊙;)咦,图咋么扁了,好吧无所谓了,只是稍微展示一下。╮(╯-╰)╭好吧…继续,接取任务后:

然后,先找玛丽亚问问在哪吧……

打开玩家任务窗查看任务,哟( ̄y▽ ̄)╭Ohoho…第一个目标完成咯。得,去怼几个怪,捡几个破烂看看先:

 

恶民猛膜命秒没,还不错,怼到完成试试:

 

 好吧,虽然完成了,但是手贱啊,点错放弃了,就成这样了:

(国骂)……从头开始吧……接取,与佟丽娅对话:

奶死,不用捡破烂了,直接打怪了,怼怼怼怼……于是完成了,好吧回去找恩格斯提交吧。emmm这个害我背书的SB还有新任务的咯:

好吧,我直接去AV玛丽亚不行吗,还捡什么破烂:

Excuse me?直接AV都不行,好吧好吧……捡几把,捡几把~

好吧,可以顺利任务了,那么该考虑存档了,毕竟单机RPG,不能存档的话确定不是玩FC时代没放纽扣电池的卡带?

怎么做呢?先弄个存档数据类:

using System.Collections.Generic;

[System.Serializable]
public class SaveData
{
    public List<ItemData> itemDatas = new List<ItemData>();

    public List<QuestData> ongoingQuestDatas = new List<QuestData>();
    public List<QuestData> completeQuestDatas = new List<QuestData>();
}

[System.Serializable]
public class ItemData
{
    public string itemID;

    public int itemAmount;

    public ItemData(string id, int amount)
    {
        itemID = id;
        itemAmount = amount;
    }
}

[System.Serializable]
public class QuestData
{
    public string questID;

    public string originGiverID;

    public List<ObjectiveData> objectiveDatas = new List<ObjectiveData>();

    public QuestData(Quest quest)
    {
        questID = quest._ID;
        originGiverID = quest.MOriginQuestGiver._ID;
        foreach(Objective o in quest.Objectives)
        {
            objectiveDatas.Add(new ObjectiveData(o));
        }
    }
}
[System.Serializable]
public class ObjectiveData
{
    public string runtimeID;

    public int currentAmount;

    public ObjectiveData(Objective objective)
    {
        runtimeID = objective.runtimeID;
        currentAmount = objective.CurrentAmount;
    }
}

然后,来一个存档管理器:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
using UnityEngine.SceneManagement;

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

    private static bool DontDestroyOnLoadOnce;

    public string dataName = "SaveData.zdat";

    private void Awake()
    {
        if (!DontDestroyOnLoadOnce)
        {
            DontDestroyOnLoad(this);
            DontDestroyOnLoadOnce = true;
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public bool Save()
    {
        FileStream fs = OpenFile(Application.persistentDataPath + "/" + dataName, FileMode.Create);
        try
        {
            BinaryFormatter bf = new BinaryFormatter();

            SaveData data = new SaveData();

            SaveBag(data);
            SavePlayerQuest(data);

            bf.Serialize(fs, data);
            fs.Close();

            return true;
        }
        catch (System.Exception ex)
        {
            if (fs != null) fs.Close();
            Debug.LogError(ex.Message);
            return false;
        }
    }

    void SaveBag(SaveData data)
    {
        foreach (KeyValuePair<string, List<ItemBase>> itemList in BagManager.Instance.Items)
        {
            data.itemDatas.Add(new ItemData(itemList.Key, itemList.Value.Count));
        }
    }

    void SavePlayerQuest(SaveData data)
    {
        foreach (Quest quest in PlayerQuestManager.Instance.QuestsOngoing)
        {
            data.ongoingQuestDatas.Add(new QuestData(quest));
        }
        foreach (Quest quest in PlayerQuestManager.Instance.QuestsComplete)
        {
            data.completeQuestDatas.Add(new QuestData(quest));
        }
    }

    public bool Load()
    {
        try
        {
            StartCoroutine(LoadAsync());
            return true;
        }
        catch (System.Exception ex)
        {
            Debug.LogError(ex.Message);
            return false;
        }
    }
    IEnumerator LoadAsync()
    {
        AsyncOperation ao = SceneManager.LoadSceneAsync("QuestTest");
        ao.allowSceneActivation = false;
        yield return new WaitUntil(() => { return ao.progress >= 0.9f; });
        ao.allowSceneActivation = true;
        yield return new WaitUntil(() => { return ao.isDone; });
        FileStream fs = OpenFile(Application.persistentDataPath + "/" + dataName, FileMode.Open);
        try
        {
            GameManager.Instance.Init();
            BinaryFormatter bf = new BinaryFormatter();

            SaveData data = new SaveData();

            data = bf.Deserialize(fs) as SaveData;
            fs.Close();

            LoadBag(data);
            LoadPlayerQuest(data);
        }
        catch
        {
            if (fs != null) fs.Close();
            StopCoroutine(LoadAsync());
            throw;
        }
    }


    void LoadBag(SaveData data)
    {
        foreach (ItemData itemData in data.itemDatas)
        {
            BagManager.Instance.GetItem(GameManager.Instance.GetItemInstanceByID(itemData.itemID), itemData.itemAmount);
        }
    }

    void LoadPlayerQuest(SaveData data)
    {
        foreach (QuestData questData in data.ongoingQuestDatas)
        {
            HandlingQuestData(questData);
        }

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

    FileStream OpenFile(string path, FileMode fileMode)
    {
        try
        {
            return new FileStream(path, fileMode);
        }
        catch
        {
            return null;
        }
    }
}

方法很笨,令人惭愧,不过还是能简单实现存档读档了。至于测试,我就不贴上来了。


简单的任务系统就这样完成了,没做指示器,就是比如在打死怪物时界面跳出“击杀骷髅[1/5]”这样的小提示。虽然功能对我来说较为完整,但是代码不完善、不健壮,比如随意SetActive(),没做对象池,随意Destroy()后又Instantiate(),很多地方也没有考虑try……catch……一些功能的实现方法简直是小学生水平,没办法,技术有限╮( ̄▽ ̄")╭。而且有些Bug我没测到的,欢迎大家反馈,而不足之处,也欢迎大家指出,希望能共同进步,为自己热爱的事业疯狂打Call !

想获取完整及最新源码还请光顾我的GitHub。想知道对话类目标NPC对话的实现,请移步下一篇文章“开发手记”系列(八)或(九)篇,详细跟进对话系统。

脾气不好,礼貌吐槽。项目不大,直接度盘:

链接: https://pan.baidu.com/s/1JPeS7m4AP9GHhmeRf6hOiw 提取码: 3p5d(写这篇文章时的版本)

  • 47
    点赞
  • 174
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫需要

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值