赞
踩
一年多没写博了,因为迫不得已转行,这破游戏也搁置了好久,过完年也有一个月了,回来找找感觉。那就记录一下任务系统的开发吧,方便以后回忆。(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(写这篇文章时的版本)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。