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