当前位置:   article > 正文

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

unity2d游戏任务系统

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

  1. using UnityEngine;
  2. [System.Serializable]
  3. public abstract class ItemBase: ScriptableObject
  4. {
  5. [SerializeField]
  6. private string ID;
  7. public string _ID
  8. {
  9. get
  10. {
  11. return ID;
  12. }
  13. }
  14. [SerializeField]
  15. new private string name;
  16. public string Name
  17. {
  18. get
  19. {
  20. return name;
  21. }
  22. }
  23. [SerializeField, ReadOnly]
  24. private ItemType itemType;
  25. public ItemType ItemType
  26. {
  27. get
  28. {
  29. return itemType;
  30. }
  31. protected set
  32. {
  33. itemType = value;
  34. }
  35. }
  36. [SerializeField]
  37. private ItemLevel level;
  38. public ItemLevel Level
  39. {
  40. get
  41. {
  42. return level;
  43. }
  44. }
  45. [SerializeField]
  46. private Sprite icon;
  47. public Sprite Icon
  48. {
  49. get
  50. {
  51. return icon;
  52. }
  53. }
  54. [SerializeField, TextArea]
  55. private string description;
  56. public string Description
  57. {
  58. get
  59. {
  60. return description;
  61. }
  62. }
  63. }
  64. public interface IUsable
  65. {
  66. void OnUse();
  67. }
  68. public enum ItemType
  69. {
  70. 其他,
  71. 药物,
  72. 武器,
  73. 防具,
  74. }
  75. public enum ItemLevel
  76. {
  77. 凡品,
  78. 精品,
  79. 珍品,
  80. 极品,
  81. 绝品,
  82. }

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

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

  1. using UnityEngine;
  2. [CreateAssetMenu(fileName = "Weapon", menuName = "Zetan/道具/新武器")]
  3. [System.Serializable]
  4. public class WeaponItem : ItemBase, IUsable
  5. {
  6. public WeaponItem()
  7. {
  8. ItemType = ItemType.武器;
  9. }
  10. public void OnUse()
  11. {
  12. Debug.Log("UseWeapon");
  13. }
  14. }

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

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

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

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. [System.Serializable]
  4. [CreateAssetMenu(fileName = "quest", menuName = "Zetan/任务/新任务")]
  5. public class Quest : ScriptableObject
  6. {
  7. [SerializeField]
  8. private string ID;
  9. public string _ID
  10. {
  11. get
  12. {
  13. return ID;
  14. }
  15. }
  16. [SerializeField]
  17. private string tittle;
  18. public string Tittle
  19. {
  20. get
  21. {
  22. return tittle;
  23. }
  24. }
  25. [SerializeField]
  26. [TextArea]
  27. private string description;
  28. public string Description
  29. {
  30. get
  31. {
  32. return description;
  33. }
  34. }
  35. [SerializeField]
  36. private bool abandonable;
  37. public bool Abandonable
  38. {
  39. get
  40. {
  41. return abandonable;
  42. }
  43. }
  44. [SerializeField]
  45. private QuestAcceptCondition[] acceptConditions;
  46. public QuestAcceptCondition[] AcceptConditions
  47. {
  48. get
  49. {
  50. return acceptConditions;
  51. }
  52. }
  53. [SerializeField]
  54. private QuestGroup questGroup;
  55. public QuestGroup MQuestGroup
  56. {
  57. get
  58. {
  59. return questGroup;
  60. }
  61. set
  62. {
  63. questGroup = value;
  64. }
  65. }
  66. [SerializeField]
  67. private QuestReward questReward;
  68. public QuestReward MQuestReward
  69. {
  70. get
  71. {
  72. return questReward;
  73. }
  74. }
  75. [Space]
  76. [SerializeField]
  77. private bool cmpltOnOriginalNPC = true;
  78. public bool CmpltOnOriginalNPC
  79. {
  80. get
  81. {
  82. return cmpltOnOriginalNPC;
  83. }
  84. }
  85. [SerializeField]
  86. [ConditionalHide("cmpltOnOriginalNPC", true, true)]
  87. private string IDOfNPCToComplete;
  88. public string _IDOfNPCToComplete
  89. {
  90. get
  91. {
  92. return IDOfNPCToComplete;
  93. }
  94. }
  95. [Space]
  96. [SerializeField]
  97. [Tooltip("勾选此项,则勾选InOrder的目标按OrderIndex从小到大的顺序执行,若相同,则表示可以同时进行;若目标没有勾选InOrder,则表示该目标不受顺序影响。")]
  98. private bool cmpltObjectiveInOrder = false;
  99. public bool CmpltObjectiveInOrder
  100. {
  101. get
  102. {
  103. return cmpltObjectiveInOrder;
  104. }
  105. }
  106. [System.NonSerialized]
  107. private List<Objective> objectives = new List<Objective>();//存储所有目标,在运行时用到,初始化时自动填,不用人为干预,详见QuestGiver类
  108. public List<Objective> Objectives
  109. {
  110. get
  111. {
  112. return objectives;
  113. }
  114. }
  115. [SerializeField]
  116. private CollectObjective[] collectObjectives;
  117. public CollectObjective[] CollectObjectives
  118. {
  119. get
  120. {
  121. return collectObjectives;
  122. }
  123. }
  124. [SerializeField]
  125. private KillObjective[] killObjectives;
  126. public KillObjective[] KillObjectives
  127. {
  128. get
  129. {
  130. return killObjectives;
  131. }
  132. }
  133. [SerializeField]
  134. private TalkObjective[] talkObjectives;
  135. public TalkObjective[] TalkObjectives
  136. {
  137. get
  138. {
  139. return talkObjectives;
  140. }
  141. }
  142. [SerializeField]
  143. private MoveObjective[] moveObjectives;
  144. public MoveObjective[] MoveObjectives
  145. {
  146. get
  147. {
  148. return moveObjectives;
  149. }
  150. }
  151. [System.NonSerialized]
  152. private QuestGiver originQuestGiver;
  153. public QuestGiver MOriginQuestGiver
  154. {
  155. get
  156. {
  157. return originQuestGiver;
  158. }
  159. set
  160. {
  161. originQuestGiver = value;
  162. }
  163. }
  164. [System.NonSerialized]
  165. private QuestGiver currentQuestGiver;
  166. public QuestGiver MCurrentQuestGiver
  167. {
  168. get
  169. {
  170. return currentQuestGiver;
  171. }
  172. set
  173. {
  174. currentQuestGiver = value;
  175. }
  176. }
  177. [HideInInspector]
  178. public bool IsOngoing;//任务是否正在执行,在运行时用到
  179. public bool IsComplete
  180. {
  181. get
  182. {
  183. foreach (CollectObjective co in collectObjectives)
  184. if (!co.IsComplete) return false;
  185. foreach (KillObjective ko in killObjectives)
  186. if (!ko.IsComplete) return false;
  187. foreach (TalkObjective to in talkObjectives)
  188. if (!to.IsComplete) return false;
  189. foreach (MoveObjective mo in moveObjectives)
  190. if (!mo.IsComplete) return false;
  191. return true;
  192. }
  193. }
  194. public bool AcceptAble
  195. {
  196. get
  197. {
  198. foreach (QuestAcceptCondition qac in AcceptConditions)
  199. {
  200. if (!qac.IsEligible) return false;
  201. }
  202. return true;
  203. }
  204. }
  205. /// <summary>
  206. /// 判断该任务是否需要某个道具,用于丢弃某个道具时,判断能不能丢
  207. /// </summary>
  208. /// <param name="itemID">所需判定的道具</param>
  209. /// <param name="leftAmount">所需判定的数量</param>
  210. /// <returns></returns>
  211. public bool RequiredItem(string itemID, int leftAmount)
  212. {
  213. if (CmpltObjectiveInOrder)
  214. {
  215. foreach (Objective o in Objectives)
  216. {
  217. //当目标是收集类目标时才进行判断
  218. if (o is CollectObjective && itemID == (o as CollectObjective).ItemID)
  219. {
  220. if (o.IsComplete && o.InOrder)
  221. {
  222. //如果剩余的道具数量不足以维持该目标完成状态
  223. if (o.Amount > leftAmount)
  224. {
  225. Objective tempObj = o.NextObjective;
  226. while (tempObj != null)
  227. {
  228. //则判断是否有后置目标在进行,以保证在打破该目标的完成状态时,后置目标不受影响
  229. if (tempObj.CurrentAmount > 0 && tempObj.OrderIndex > o.OrderIndex)
  230. {
  231. //Debug.Log("Required");
  232. return true;
  233. }
  234. tempObj = tempObj.NextObjective;
  235. }
  236. }
  237. //Debug.Log("NotRequired3");
  238. return false;
  239. }
  240. //Debug.Log("NotRequired2");
  241. return false;
  242. }
  243. }
  244. }
  245. //Debug.Log("NotRequired1");
  246. return false;
  247. }
  248. }
  249. #region 任务报酬
  250. [System.Serializable]
  251. public class QuestReward
  252. {
  253. [SerializeField]
  254. private int money;
  255. public int Money
  256. {
  257. get
  258. {
  259. return money;
  260. }
  261. }
  262. [SerializeField]
  263. private int EXP;
  264. public int _EXP
  265. {
  266. get
  267. {
  268. return EXP;
  269. }
  270. }
  271. [SerializeField]
  272. private ItemBase[] items;
  273. public ItemBase[] Items
  274. {
  275. get
  276. {
  277. return items;
  278. }
  279. }
  280. }
  281. #endregion
  282. #region 任务条件
  283. /// <summary>
  284. /// 任务接收条件
  285. /// </summary>
  286. [System.Serializable]
  287. public class QuestAcceptCondition
  288. {
  289. [SerializeField]
  290. private QuestCondition acceptCondition;
  291. public QuestCondition AcceptCondition
  292. {
  293. get
  294. {
  295. return acceptCondition;
  296. }
  297. }
  298. [SerializeField]
  299. [ConditionalHide("acceptCondition", (int)~(QuestCondition.None | QuestCondition.ComplexQuest | QuestCondition.HasItem), true)]
  300. private int level;
  301. public int Level
  302. {
  303. get
  304. {
  305. return level;
  306. }
  307. }
  308. [SerializeField]
  309. [ConditionalHide("acceptCondition", (int)QuestCondition.ComplexQuest, true)]
  310. private string IDOfCompleteQuest;
  311. public string _IDOfCompleteQuest
  312. {
  313. get
  314. {
  315. return IDOfCompleteQuest;
  316. }
  317. }
  318. [SerializeField]
  319. [ConditionalHide("acceptCondition", (int)QuestCondition.ComplexQuest, true)]
  320. private Quest completeQuest;
  321. public Quest CompleteQuest
  322. {
  323. get
  324. {
  325. return completeQuest;
  326. }
  327. }
  328. [SerializeField]
  329. [ConditionalHide("acceptCondition", (int)QuestCondition.HasItem, true)]
  330. private string IDOfOwnedItem;
  331. public string _IDOfOwnedItem
  332. {
  333. get
  334. {
  335. return IDOfOwnedItem;
  336. }
  337. }
  338. [SerializeField]
  339. [ConditionalHide("acceptCondition", (int)QuestCondition.HasItem, true)]
  340. private ItemBase owneditem;
  341. public ItemBase Owneditem
  342. {
  343. get
  344. {
  345. return owneditem;
  346. }
  347. }
  348. public bool IsEligible
  349. {
  350. get
  351. {
  352. switch (AcceptCondition)
  353. {
  354. case QuestCondition.ComplexQuest:
  355. if (_IDOfCompleteQuest != string.Empty)
  356. return PlayerQuestManager.Instance.HasCompleteQuestWithID(_IDOfCompleteQuest);
  357. else return PlayerQuestManager.Instance.HasCompleteQuestWithID(CompleteQuest._ID);
  358. case QuestCondition.HasItem:
  359. if (_IDOfOwnedItem != string.Empty)
  360. return BagManager.Instance.HasItemWithID(_IDOfOwnedItem);
  361. else return BagManager.Instance.HasItemWithID(Owneditem._ID);
  362. default: return false;
  363. }
  364. }
  365. }
  366. }
  367. //使用2的幂数方便进行位运算
  368. public enum QuestCondition
  369. {
  370. None = 1,
  371. LevelLargeThen = 2,
  372. LevelLessThen = 4,
  373. LevelLargeOrEqualsThen = 8,
  374. LevelLessOrEqualsThen = 16,
  375. ComplexQuest = 32,
  376. HasItem = 64
  377. }
  378. #endregion
  379. #region 任务目标
  380. public delegate void UpdateNextObjListener(Objective nextObj);
  381. [System.Serializable]
  382. /// <summary>
  383. /// 任务目标
  384. /// </summary>
  385. public abstract class Objective
  386. {
  387. [HideInInspector]
  388. public string runtimeID;
  389. [SerializeField]
  390. private string displayName;
  391. public string DisplayName
  392. {
  393. get
  394. {
  395. return displayName;
  396. }
  397. }
  398. [SerializeField]
  399. private int amount;
  400. public int Amount
  401. {
  402. get
  403. {
  404. return amount;
  405. }
  406. }
  407. private int currentAmount;
  408. public int CurrentAmount
  409. {
  410. get
  411. {
  412. return currentAmount;
  413. }
  414. set
  415. {
  416. bool befCmplt = IsComplete;
  417. if (value < amount && value >= 0)
  418. currentAmount = value;
  419. else if (value < 0)
  420. {
  421. currentAmount = 0;
  422. }
  423. else currentAmount = amount;
  424. if (!befCmplt && IsComplete)
  425. OnCompleteThisEvent(NextObjective);
  426. }
  427. }
  428. public bool IsComplete
  429. {
  430. get
  431. {
  432. if (currentAmount >= amount)
  433. return true;
  434. return false;
  435. }
  436. }
  437. [SerializeField]
  438. private bool inOrder;
  439. public bool InOrder
  440. {
  441. get
  442. {
  443. return inOrder;
  444. }
  445. }
  446. [SerializeField]
  447. [ConditionalHide("inOrder", true)]
  448. private int orderIndex;
  449. public int OrderIndex
  450. {
  451. get
  452. {
  453. return orderIndex;
  454. }
  455. }
  456. [System.NonSerialized]
  457. public Objective PrevObjective;
  458. [System.NonSerialized]
  459. public Objective NextObjective;
  460. [field: System.NonSerialized]
  461. public event UpdateNextObjListener OnCompleteThisEvent;
  462. protected virtual void UpdateStatus()
  463. {
  464. if (IsComplete) return;
  465. if (!InOrder) CurrentAmount++;
  466. else if (InOrder && AllPrevObjCmplt) CurrentAmount++;
  467. }
  468. protected bool AllPrevObjCmplt//判定所有前置目标都是否完成
  469. {
  470. get
  471. {
  472. Objective tempObj = PrevObjective;
  473. while (tempObj != null)
  474. {
  475. if (!tempObj.IsComplete && tempObj.OrderIndex < OrderIndex)
  476. {
  477. return false;
  478. }
  479. tempObj = tempObj.PrevObjective;
  480. }
  481. return true;
  482. }
  483. }
  484. protected bool HasNextObjOngoing//判定是否有后置目标正在进行
  485. {
  486. get
  487. {
  488. Objective tempObj = NextObjective;
  489. while (tempObj != null)
  490. {
  491. if (tempObj.CurrentAmount > 0 && tempObj.OrderIndex > OrderIndex)
  492. {
  493. return true;
  494. }
  495. tempObj = tempObj.NextObjective;
  496. }
  497. return false;
  498. }
  499. }
  500. }
  501. /// <summary>
  502. /// 收集类目标
  503. /// </summary>
  504. [System.Serializable]
  505. public class CollectObjective : Objective
  506. {
  507. [SerializeField]
  508. private string itemID;
  509. public string ItemID
  510. {
  511. get
  512. {
  513. return itemID;
  514. }
  515. }
  516. [SerializeField]
  517. private bool checkBagAtAccept = true;//用于标识是否在接取任务时检查背包道具看是否满足目标,否则目标重头开始计数
  518. public bool CheckBagAtAccept
  519. {
  520. get
  521. {
  522. return checkBagAtAccept;
  523. }
  524. set
  525. {
  526. checkBagAtAccept = value;
  527. }
  528. }
  529. public void UpdateCollectAmountUp(string itemID, int leftAmount)//得道具时用到
  530. {
  531. if (itemID == ItemID)
  532. {
  533. for (int i = 0; i < leftAmount; i++)
  534. {
  535. UpdateStatus();
  536. }
  537. }
  538. }
  539. public void UpdateCollectAmountDown(string itemID, int leftAmount)//丢道具时用到
  540. {
  541. if (itemID == ItemID)
  542. {
  543. //前置目标都完成且没有后置目标在进行时,才允许更新
  544. if (AllPrevObjCmplt && !HasNextObjOngoing) CurrentAmount = leftAmount;
  545. }
  546. }
  547. }
  548. /// <summary>
  549. /// 打怪类目标
  550. /// </summary>
  551. [System.Serializable]
  552. public class KillObjective : Objective
  553. {
  554. [SerializeField]
  555. private string enermyID;
  556. public string EnermyID
  557. {
  558. get
  559. {
  560. return enermyID;
  561. }
  562. }
  563. public void UpdateKillAmount()
  564. {
  565. UpdateStatus();
  566. }
  567. }
  568. /// <summary>
  569. /// 谈话类目标
  570. /// </summary>
  571. [System.Serializable]
  572. public class TalkObjective : Objective
  573. {
  574. [SerializeField]
  575. private string talkerID;
  576. public string TalkerID
  577. {
  578. get
  579. {
  580. return talkerID;
  581. }
  582. }
  583. public void UpdateTalkStatus()
  584. {
  585. UpdateStatus();
  586. }
  587. }
  588. /// <summary>
  589. /// 移动到点类目标
  590. /// </summary>
  591. [System.Serializable]
  592. public class MoveObjective : Objective
  593. {
  594. [SerializeField]
  595. private string pointID;
  596. public string PointID
  597. {
  598. get
  599. {
  600. return pointID;
  601. }
  602. }
  603. public void UpdateMoveIntoStatus(QuestPoint point)
  604. {
  605. if(point._ID == PointID)
  606. UpdateStatus();
  607. }
  608. public void UpdateMoveAwayStatus(QuestPoint point)
  609. {
  610. if (point._ID == PointID && !HasNextObjOngoing) CurrentAmount--;
  611. }
  612. }
  613. #endregion

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

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

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

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. public class QuestGiver : NPC, ITalkAble {
  4. [SerializeField]
  5. private Quest[] questsStored;
  6. public Quest[] QuestsStored
  7. {
  8. get
  9. {
  10. return questsStored;
  11. }
  12. }
  13. [SerializeField, ReadOnly]
  14. private List<Quest> questInstances = new List<Quest>();
  15. public List<Quest> QuestInstances
  16. {
  17. get
  18. {
  19. return questInstances;
  20. }
  21. set
  22. {
  23. questInstances = value;
  24. }
  25. }
  26. public event NPCTalkListener OnTalkBeginEvent;
  27. public event NPCTalkListener OnTalkFinishedEvent;
  28. private void Start()
  29. {
  30. InitQuest(questsStored);
  31. }
  32. public void InitQuest(Quest[] questsStored)
  33. {
  34. if (questsStored == null) return;
  35. foreach (Quest quest in questsStored)
  36. {
  37. if (quest)
  38. {
  39. Quest temp = Instantiate(quest);
  40. foreach (CollectObjective co in temp.CollectObjectives)
  41. temp.Objectives.Add(co);
  42. foreach (KillObjective ko in temp.KillObjectives)
  43. temp.Objectives.Add(ko);
  44. foreach (TalkObjective to in temp.TalkObjectives)
  45. temp.Objectives.Add(to);
  46. foreach (MoveObjective mo in temp.MoveObjectives)
  47. temp.Objectives.Add(mo);
  48. if (temp.CmpltObjectiveInOrder)
  49. {
  50. temp.Objectives.Sort((x, y) =>
  51. {
  52. if (x.OrderIndex > y.OrderIndex) return 1;
  53. else if (x.OrderIndex < y.OrderIndex) return -1;
  54. else return 0;
  55. });
  56. for (int i = 1; i < temp.Objectives.Count; i++)
  57. {
  58. if (temp.Objectives[i].OrderIndex >= temp.Objectives[i - 1].OrderIndex)
  59. {
  60. temp.Objectives[i].PrevObjective = temp.Objectives[i - 1];
  61. temp.Objectives[i - 1].NextObjective = temp.Objectives[i];
  62. }
  63. }
  64. }
  65. for (int i = 0; i < temp.Objectives.Count; i++)
  66. {
  67. temp.Objectives[i].runtimeID = temp._ID + "_O" + i;
  68. }
  69. temp.MOriginQuestGiver = this;
  70. temp.MCurrentQuestGiver = this;
  71. QuestInstances.Add(temp);
  72. }
  73. }
  74. }
  75. /// <summary>
  76. /// 向此对象交接任务。因为往往会有些任务不在同一个NPC接取并完成,所以就要在两个NPC之间交接该任务
  77. /// </summary>
  78. /// <param name="quest">要交接的任务</param>
  79. public void TransferQuestToThis(Quest quest)
  80. {
  81. if (!quest) return;
  82. QuestInstances.Add(quest);
  83. quest.MCurrentQuestGiver.QuestInstances.Remove(quest);
  84. quest.MCurrentQuestGiver = this;
  85. if (QuestGiverQuestManager.Instance.SelectedQuest && QuestGiverQuestManager.Instance.SelectedQuest == quest)
  86. {
  87. QuestAgent qa = QuestGiverQuestManager.Instance.QuestAgents.Find(x => x.MQuest == quest);
  88. if (qa)
  89. {
  90. QuestGiverQuestManager.Instance.QuestAgents.Remove(qa);
  91. Destroy(qa.gameObject);
  92. }
  93. QuestGiverQuestManager.Instance.CloseDescriptionWindow();
  94. }
  95. }
  96. public void OnTalkBegin()
  97. {
  98. if (OnTalkBeginEvent != null) OnTalkBeginEvent();
  99. QuestGiverQuestManager.Instance.OpenQuestWindow();
  100. QuestGiverQuestManager.Instance.LoadGiverQuest(this);
  101. }
  102. public void OnTalkFinished()
  103. {
  104. if (OnTalkFinishedEvent != null) OnTalkFinishedEvent();
  105. PlayerQuestManager.Instance.UpdateObjectivesText();
  106. QuestGiverQuestManager.Instance.UpdateObjectivesText();
  107. }
  108. }
  109. public delegate void NPCTalkListener();
  110. public interface ITalkAble
  111. {
  112. event NPCTalkListener OnTalkBeginEvent;
  113. event NPCTalkListener OnTalkFinishedEvent;
  114. void OnTalkBegin();
  115. void OnTalkFinished();
  116. }

该类继承自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任务的巨佬是这样的:

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.UI;
  5. public class QuestGiverQuestManager : MonoBehaviour {
  6. private static QuestGiverQuestManager instance;
  7. public static QuestGiverQuestManager Instance
  8. {
  9. get
  10. {
  11. if (instance == null)
  12. instance = FindObjectOfType<QuestGiverQuestManager>();
  13. return instance;
  14. }
  15. }
  16. [SerializeField]
  17. private GameObject questPrefab;
  18. [SerializeField]
  19. private Transform questListParent;
  20. [SerializeField]
  21. private CanvasGroup questsWindow;
  22. [SerializeField]
  23. private CanvasGroup descriptionWindow;
  24. [SerializeField]
  25. private Text giverName;
  26. [SerializeField]
  27. private Text description;
  28. [SerializeField]
  29. private Text money_EXP;
  30. [SerializeField]
  31. private ItemAgent[] rewardCells;
  32. [SerializeField]
  33. private Button acceptBtn;
  34. [SerializeField]
  35. private Button completeBtn;
  36. [SerializeField, Space]
  37. private List<QuestAgent> questAgents = new List<QuestAgent>();
  38. public List<QuestAgent> QuestAgents
  39. {
  40. get
  41. {
  42. return questAgents;
  43. }
  44. set
  45. {
  46. questAgents = value;
  47. }
  48. }
  49. private Quest selectedQuest;
  50. public Quest SelectedQuest
  51. {
  52. get
  53. {
  54. return selectedQuest;
  55. }
  56. private set
  57. {
  58. selectedQuest = value;
  59. }
  60. }
  61. [SerializeField, ReadOnly]
  62. private QuestGiver questGiver;
  63. #region 任务处理相关
  64. public void LoadGiverQuest(QuestGiver giver)
  65. {
  66. if (giver == null) return;
  67. CloseDescriptionWindow();
  68. questGiver = giver;
  69. if (QuestAgents.Count > 0)
  70. {
  71. int count = QuestAgents.Count;
  72. for (int i = 0; i < count; i++)
  73. {
  74. Destroy(QuestAgents[i].gameObject);
  75. }
  76. QuestAgents.Clear();
  77. }
  78. foreach (Quest quest in giver.QuestInstances)
  79. {
  80. if (!PlayerQuestManager.Instance.HasCompleteQuest(quest) && quest.AcceptAble)
  81. {
  82. QuestAgent qa = Instantiate(questPrefab, questListParent).GetComponent<QuestAgent>();
  83. qa.IsPlayerQuest = false;
  84. qa.MQuest = quest;
  85. qa.Title.text = quest.Tittle;
  86. QuestAgents.Add(qa);
  87. }
  88. }
  89. giverName.text = giver.Name;
  90. }
  91. public void AcceptSeletedQuest()
  92. {
  93. if (!SelectedQuest) return;
  94. PlayerQuestManager.Instance.AcceptQuest(SelectedQuest);
  95. UpdateObjectivesText();
  96. }
  97. public void CompleteSeletedQuest()
  98. {
  99. if (!SelectedQuest) return;
  100. if (PlayerQuestManager.Instance.CompleteQuest(SelectedQuest))
  101. {
  102. LoadGiverQuest(questGiver);
  103. CloseDescriptionWindow();
  104. }
  105. }
  106. #endregion
  107. #region UI相关
  108. public void ShowDescription(Quest quest)
  109. {
  110. if (quest == null) return;
  111. SelectedQuest = quest;
  112. UpdateObjectivesText();
  113. money_EXP.text = string.Format("[奖励]\n<size=14>经验:\n{0}\n金币:\n{1}</size>", quest.MQuestReward._EXP, quest.MQuestReward.Money);
  114. foreach (ItemAgent rwc in rewardCells)
  115. rwc.Item = null;
  116. foreach (ItemBase item in quest.MQuestReward.Items)
  117. foreach (ItemAgent rw in rewardCells)
  118. {
  119. if (rw.Item == null)
  120. {
  121. rw.Item = item;
  122. rw.Icon.sprite = item.Icon;
  123. break;
  124. }
  125. }
  126. }
  127. public void UpdateObjectivesText()
  128. {
  129. if (SelectedQuest == null) return;
  130. string objectives = string.Empty;
  131. for (int i = 0; i < SelectedQuest.Objectives.Count; i++)
  132. objectives += SelectedQuest.Objectives[i].DisplayName +
  133. "[" + SelectedQuest.Objectives[i].CurrentAmount + "/" + SelectedQuest.Objectives[i].Amount + "]" +
  134. (SelectedQuest.Objectives[i].IsComplete ? "(达成)\n" : "\n");
  135. description.text = string.Format("<size=16><b>{0}</b></size>\n[委托人: {1}]\n{2}\n\n<size=16><b>任务目标{3}</b></size>\n{4}",
  136. SelectedQuest.Tittle,
  137. SelectedQuest.MOriginQuestGiver.Name,
  138. SelectedQuest.Description,
  139. SelectedQuest.IsComplete ? "(完成)" : SelectedQuest.IsOngoing ? "(进行中)" : "",
  140. objectives);
  141. acceptBtn.gameObject.SetActive(!SelectedQuest.IsOngoing);
  142. completeBtn.gameObject.SetActive(SelectedQuest.IsComplete);
  143. }
  144. public void CloseDescriptionWindow()
  145. {
  146. descriptionWindow.alpha = 0;
  147. descriptionWindow.blocksRaycasts = false;
  148. }
  149. public void OpenDescriptionWindow(QuestAgent questAgent)
  150. {
  151. PlayerQuestManager.Instance.CloseDescriptionWindow();
  152. ShowDescription(questAgent.MQuest);
  153. descriptionWindow.alpha = 1;
  154. descriptionWindow.blocksRaycasts = true;
  155. }
  156. public void CloseQuestWindow()
  157. {
  158. questsWindow.GetComponent<CanvasGroup>().alpha = 0;
  159. questsWindow.GetComponent<CanvasGroup>().blocksRaycasts = false;
  160. CloseDescriptionWindow();
  161. }
  162. public void OpenQuestWindow()
  163. {
  164. questsWindow.GetComponent<CanvasGroup>().alpha = 1;
  165. questsWindow.GetComponent<CanvasGroup>().blocksRaycasts = true;
  166. PlayerQuestManager.Instance.CloseDescriptionWindow();
  167. }
  168. #endregion
  169. }

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

  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. public class QuestAgent : MonoBehaviour {
  4. private Quest quest;
  5. public Quest MQuest
  6. {
  7. get
  8. {
  9. return quest;
  10. }
  11. set
  12. {
  13. quest = value;
  14. }
  15. }
  16. [SerializeField]
  17. private Text title;
  18. public Text Title
  19. {
  20. get
  21. {
  22. return title;
  23. }
  24. set
  25. {
  26. title = value;
  27. }
  28. }
  29. [ReadOnly]
  30. public bool IsPlayerQuest;
  31. private void Update()
  32. {
  33. if(MQuest) Title.text = MQuest.Tittle + (MQuest.IsComplete ? "(完成)" : MQuest.IsOngoing && !IsPlayerQuest ? "(进行中)" : "");
  34. }
  35. public void Click()
  36. {
  37. if (!MQuest) return;
  38. if (IsPlayerQuest)
  39. {
  40. PlayerQuestManager.Instance.ShowDescription(quest);
  41. PlayerQuestManager.Instance.OpenDescriptionWindow(this);
  42. }
  43. else
  44. {
  45. QuestGiverQuestManager.Instance.ShowDescription(quest);
  46. QuestGiverQuestManager.Instance.OpenDescriptionWindow(this);
  47. }
  48. }
  49. }

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

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

  1. using UnityEngine;
  2. using UnityEngine.UI;
  3. public class ItemAgent : MonoBehaviour {
  4. [SerializeField]
  5. private Image icon;
  6. public Image Icon
  7. {
  8. get
  9. {
  10. if (icon == null)
  11. icon = transform.Find("Icon").GetComponent<Image>();
  12. return icon;
  13. }
  14. }
  15. private ItemBase item;
  16. public ItemBase Item
  17. {
  18. get
  19. {
  20. return item;
  21. }
  22. set
  23. {
  24. item = value;
  25. }
  26. }
  27. public void OnClick()
  28. {
  29. //TODO 显示道具详情
  30. }
  31. }

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

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEngine.UI;
  4. public class PlayerQuestManager : MonoBehaviour
  5. {
  6. private static PlayerQuestManager instance;
  7. public static PlayerQuestManager Instance
  8. {
  9. get
  10. {
  11. if (instance == null || !instance.gameObject)
  12. instance = FindObjectOfType<PlayerQuestManager>();
  13. return instance;
  14. }
  15. }
  16. [SerializeField]
  17. private GameObject questPrefab;
  18. [SerializeField]
  19. private Transform questListParent;
  20. [SerializeField]
  21. private CanvasGroup questsWindow;
  22. [SerializeField]
  23. private CanvasGroup descriptionWindow;
  24. [SerializeField]
  25. private Text description;
  26. [SerializeField]
  27. private Text money_EXP;
  28. [SerializeField]
  29. private ItemAgent[] rewardCells;
  30. [SerializeField, Space]
  31. private List<QuestAgent> questAgents = new List<QuestAgent>();
  32. public List<QuestAgent> QuestAgents
  33. {
  34. get
  35. {
  36. return questAgents;
  37. }
  38. set
  39. {
  40. questAgents = value;
  41. }
  42. }
  43. [SerializeField]
  44. private List<Quest> questsOngoing = new List<Quest>();
  45. public List<Quest> QuestsOngoing
  46. {
  47. get
  48. {
  49. return questsOngoing;
  50. }
  51. }
  52. [SerializeField]
  53. private List<Quest> questsCompleted = new List<Quest>();
  54. public List<Quest> QuestsComplete
  55. {
  56. get
  57. {
  58. return questsCompleted;
  59. }
  60. }
  61. private Quest selectedQuest;
  62. public Quest SelectedQuest
  63. {
  64. get
  65. {
  66. return selectedQuest;
  67. }
  68. private set
  69. {
  70. selectedQuest = value;
  71. }
  72. }
  73. #region 任务处理相关
  74. /// <summary>
  75. /// 接取任务
  76. /// </summary>
  77. /// <param name="quest">要接取的任务</param>
  78. public bool AcceptQuest(Quest quest)
  79. {
  80. if (!quest) return false;
  81. if (HasQuest(quest)) return false;
  82. QuestAgent qa = Instantiate(questPrefab, questListParent).GetComponent<QuestAgent>();
  83. qa.IsPlayerQuest = true;
  84. qa.MQuest = quest;
  85. qa.Title.text = quest.Tittle;
  86. QuestAgents.Add(qa);
  87. foreach (Objective o in quest.Objectives)
  88. {
  89. if (o is CollectObjective)
  90. {
  91. CollectObjective co = o as CollectObjective;
  92. BagManager.Instance.OnGetItemEvent += co.UpdateCollectAmountUp;
  93. BagManager.Instance.OnLoseItemEvent += co.UpdateCollectAmountDown;
  94. if (co.CheckBagAtAccept) co.UpdateCollectAmountUp(co.ItemID, BagManager.Instance.GetItemAmountByID(co.ItemID));
  95. }
  96. else if (o is KillObjective)
  97. {
  98. KillObjective ko = o as KillObjective;
  99. try
  100. {
  101. foreach (Enermy enermy in GameManager.Instance.AllEnermy[ko.EnermyID])
  102. enermy.OnDeathEvent += ko.UpdateKillAmount;
  103. }
  104. catch
  105. {
  106. Debug.LogWarningFormat("[找不到敌人] ID: {0}", ko.EnermyID);
  107. continue;
  108. }
  109. }
  110. else if (o is TalkObjective)
  111. {
  112. TalkObjective to = o as TalkObjective;
  113. try
  114. {
  115. GameManager.Instance.AllQuestGiver[to.TalkerID].OnTalkFinishedEvent += to.UpdateTalkStatus;
  116. }
  117. catch
  118. {
  119. Debug.LogWarningFormat("[找不到NPC] ID: {0}", to.TalkerID);
  120. continue;
  121. }
  122. }
  123. else if (o is MoveObjective)
  124. {
  125. MoveObjective mo = o as MoveObjective;
  126. try
  127. {
  128. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent += mo.UpdateMoveIntoStatus;
  129. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent += mo.UpdateMoveAwayStatus;
  130. }
  131. catch
  132. {
  133. Debug.LogWarningFormat("[找不到任务点] ID: {0}", mo.PointID);
  134. continue;
  135. }
  136. }
  137. o.OnCompleteThisEvent += UpdateCollectObjectives;
  138. }
  139. quest.IsOngoing = true;
  140. QuestsOngoing.Add(quest);
  141. if (!quest.CmpltOnOriginalNPC)
  142. {
  143. try
  144. {
  145. GameManager.Instance.AllQuestGiver[quest._IDOfNPCToComplete].TransferQuestToThis(quest);
  146. }
  147. catch
  148. {
  149. Debug.LogWarningFormat("[找不到NPC] ID: {0}", quest._IDOfNPCToComplete);
  150. }
  151. }
  152. return true;
  153. }
  154. /// <summary>
  155. /// 放弃任务
  156. /// </summary>
  157. /// <param name="quest">要放弃的任务</param>
  158. public bool AbandonQuest(Quest quest)
  159. {
  160. if (HasQuest(quest) && quest && quest.Abandonable)
  161. {
  162. quest.IsOngoing = false;
  163. QuestsOngoing.Remove(quest);
  164. foreach (Objective o in quest.Objectives)
  165. {
  166. if (o is CollectObjective)
  167. {
  168. CollectObjective co = o as CollectObjective;
  169. co.CurrentAmount = 0;
  170. BagManager.Instance.OnGetItemEvent -= co.UpdateCollectAmountUp;
  171. BagManager.Instance.OnLoseItemEvent -= co.UpdateCollectAmountDown;
  172. }
  173. if (o is KillObjective)
  174. {
  175. KillObjective ko = o as KillObjective;
  176. ko.CurrentAmount = 0;
  177. foreach (Enermy enermy in GameManager.Instance.AllEnermy[ko.EnermyID])
  178. {
  179. enermy.OnDeathEvent -= ko.UpdateKillAmount;
  180. }
  181. }
  182. if (o is TalkObjective)
  183. {
  184. TalkObjective to = o as TalkObjective;
  185. to.CurrentAmount = 0;
  186. GameManager.Instance.AllQuestGiver[to.TalkerID].OnTalkFinishedEvent -= to.UpdateTalkStatus;
  187. }
  188. if (o is MoveObjective)
  189. {
  190. MoveObjective mo = o as MoveObjective;
  191. mo.CurrentAmount = 0;
  192. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent -= mo.UpdateMoveIntoStatus;
  193. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent -= mo.UpdateMoveAwayStatus;
  194. }
  195. o.OnCompleteThisEvent -= UpdateCollectObjectives;
  196. }
  197. if (!quest.CmpltOnOriginalNPC)
  198. {
  199. quest.MOriginQuestGiver.TransferQuestToThis(quest);
  200. }
  201. return true;
  202. }
  203. return false;
  204. }
  205. /// <summary>
  206. /// 放弃当前展示的任务
  207. /// </summary>
  208. public void AbandonSelectedQuest()
  209. {
  210. if (!SelectedQuest) return;
  211. if (AbandonQuest(SelectedQuest))
  212. {
  213. QuestAgent qa = questAgents.Find(x => x.MQuest == SelectedQuest);
  214. if (qa)
  215. {
  216. questAgents.Remove(qa);
  217. Destroy(qa.gameObject);
  218. }
  219. CloseDescriptionWindow();
  220. }
  221. }
  222. /// <summary>
  223. /// 更新某个任务目标,用于在其他前置目标完成时,更新后置目标
  224. /// </summary>
  225. /// <param name="nextObj">下一个目标</param>
  226. public void UpdateCollectObjectives(Objective nextObj)
  227. {
  228. Objective tempObj = nextObj;
  229. CollectObjective co;
  230. while (tempObj != null)
  231. {
  232. if (tempObj is CollectObjective)
  233. {
  234. co = tempObj as CollectObjective;
  235. co.CurrentAmount = BagManager.Instance.GetItemAmountByID(co.ItemID);
  236. }
  237. tempObj = tempObj.NextObjective;
  238. co = null;
  239. }
  240. }
  241. /// <summary>
  242. /// 完成任务
  243. /// </summary>
  244. /// <param name="quest">要放弃的任务</param>
  245. /// <param name="loadMode">是否读档模式</param>
  246. /// <returns>是否成功完成任务</returns>
  247. public bool CompleteQuest(Quest quest, bool loadMode = false)
  248. {
  249. if (!quest) return false;
  250. if (HasQuest(quest) && quest.IsComplete)
  251. {
  252. quest.IsOngoing = false;
  253. QuestsOngoing.Remove(quest);
  254. QuestAgent qa = questAgents.Find(x => x.MQuest == quest);
  255. if (qa)
  256. {
  257. questAgents.Remove(qa);
  258. Destroy(qa.gameObject);
  259. }
  260. QuestsComplete.Add(quest);
  261. foreach (Objective o in quest.Objectives)
  262. {
  263. o.OnCompleteThisEvent -= UpdateCollectObjectives;
  264. if (o is CollectObjective)
  265. {
  266. CollectObjective co = o as CollectObjective;
  267. BagManager.Instance.OnGetItemEvent -= co.UpdateCollectAmountUp;
  268. BagManager.Instance.OnLoseItemEvent -= co.UpdateCollectAmountDown;
  269. if (!loadMode) BagManager.Instance.LoseItemByID(co.ItemID, o.Amount);
  270. }
  271. if (o is KillObjective)
  272. {
  273. foreach (Enermy enermy in GameManager.Instance.AllEnermy[(o as KillObjective).EnermyID])
  274. {
  275. enermy.OnDeathEvent -= (o as KillObjective).UpdateKillAmount;
  276. }
  277. }
  278. if (o is TalkObjective)
  279. {
  280. GameManager.Instance.AllQuestGiver[(o as TalkObjective).TalkerID].OnTalkFinishedEvent -= (o as TalkObjective).UpdateTalkStatus;
  281. }
  282. if (o is MoveObjective)
  283. {
  284. MoveObjective mo = o as MoveObjective;
  285. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveIntoEvent -= mo.UpdateMoveIntoStatus;
  286. GameManager.Instance.AllQuestPoint[mo.PointID].OnMoveAwayEvent -= mo.UpdateMoveAwayStatus;
  287. }
  288. }
  289. if (!loadMode)
  290. foreach (ItemBase item in quest.MQuestReward.Items)
  291. {
  292. BagManager.Instance.GetItem(item);
  293. }
  294. //TODO 经验和金钱的处理
  295. return true;
  296. }
  297. return false;
  298. }
  299. public bool HasQuest(Quest quest)
  300. {
  301. return QuestsOngoing.Contains(quest);
  302. }
  303. public bool HasCompleteQuest(Quest quest)
  304. {
  305. return QuestsComplete.Contains(quest);
  306. }
  307. public bool HasCompleteQuestWithID(string questID)
  308. {
  309. return QuestsComplete.Exists(x => x._ID == questID);
  310. }
  311. #endregion
  312. #region UI相关
  313. public void ShowDescription(Quest quest)
  314. {
  315. if (!quest) return;
  316. QuestAgent qa = QuestAgents.Find(x => x.MQuest == quest);
  317. if (qa)
  318. {
  319. if (SelectedQuest && SelectedQuest != quest)
  320. {
  321. QuestAgent tqa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
  322. tqa.Title.color = Color.black;
  323. }
  324. qa.Title.color = Color.blue;
  325. }
  326. SelectedQuest = quest;
  327. UpdateObjectivesText();
  328. money_EXP.text = string.Format("[奖励]\n<size=14>经验:\n{0}\n金币:\n{1}</size>", quest.MQuestReward._EXP, quest.MQuestReward.Money);
  329. foreach (ItemAgent rwc in rewardCells)
  330. rwc.Item = null;
  331. foreach (ItemBase item in quest.MQuestReward.Items)
  332. foreach (ItemAgent rw in rewardCells)
  333. {
  334. if (rw.Item == null)
  335. {
  336. rw.Item = item;
  337. rw.Icon.sprite = item.Icon;
  338. break;
  339. }
  340. }
  341. }
  342. public void UpdateObjectivesText()
  343. {
  344. if (SelectedQuest == null) return;
  345. string objectives = string.Empty;
  346. for (int i = 0; i < SelectedQuest.Objectives.Count; i++)
  347. objectives += SelectedQuest.Objectives[i].DisplayName +
  348. "[" + SelectedQuest.Objectives[i].CurrentAmount + "/" + SelectedQuest.Objectives[i].Amount + "]" +
  349. (SelectedQuest.Objectives[i].IsComplete ? "(达成)\n" : "\n");
  350. description.text = string.Format("<size=16><b>{0}</b></size>\n[委托人: {1}]\n{2}\n\n<size=16><b>任务目标{3}</b></size>\n{4}",
  351. SelectedQuest.Tittle,
  352. SelectedQuest.MOriginQuestGiver.Name,
  353. SelectedQuest.Description,
  354. SelectedQuest.IsComplete ? "(完成)" : SelectedQuest.IsOngoing ? "(进行中)" : "",
  355. objectives);
  356. }
  357. public void CloseDescriptionWindow()
  358. {
  359. QuestAgent qa = QuestAgents.Find(x => x.MQuest == SelectedQuest);
  360. if (qa) qa.Title.color = Color.black;
  361. SelectedQuest = null;
  362. descriptionWindow.alpha = 0;
  363. descriptionWindow.blocksRaycasts = false;
  364. }
  365. public void OpenDescriptionWindow(QuestAgent questAgent)
  366. {
  367. QuestGiverQuestManager.Instance.CloseDescriptionWindow();
  368. ShowDescription(questAgent.MQuest);
  369. descriptionWindow.alpha = 1;
  370. descriptionWindow.blocksRaycasts = true;
  371. }
  372. public void CloseQuestWindow()
  373. {
  374. questsWindow.alpha = 0;
  375. questsWindow.blocksRaycasts = false;
  376. CloseDescriptionWindow();
  377. }
  378. public void OpenQuestWindow()
  379. {
  380. questsWindow.alpha = 1;
  381. questsWindow.blocksRaycasts = true;
  382. QuestGiverQuestManager.Instance.CloseDescriptionWindow();
  383. }
  384. #endregion
  385. }

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

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

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. public delegate void ItemInfoListener(string itemID, int amount);
  4. public class BagManager : MonoBehaviour {
  5. private static BagManager instance;
  6. public static BagManager Instance
  7. {
  8. get
  9. {
  10. if (instance == null)
  11. instance = FindObjectOfType<BagManager>();
  12. return instance;
  13. }
  14. }
  15. public event ItemInfoListener OnGetItemEvent;
  16. public event ItemInfoListener OnLoseItemEvent;
  17. private Dictionary<string, List<ItemBase>> items = new Dictionary<string, List<ItemBase>>();
  18. public Dictionary<string, List<ItemBase>> Items
  19. {
  20. get
  21. {
  22. return items;
  23. }
  24. }
  25. public void GetItem(ItemBase item, int amount = 1)
  26. {
  27. if (!item) return;
  28. int originAmount = GetItemAmountByID(item._ID);
  29. for (int i = 0; i < amount; i++)
  30. {
  31. if (Items.ContainsKey(item._ID)) Items[item._ID].Add(item);
  32. else
  33. {
  34. Items.Add(item._ID, new List<ItemBase>());
  35. Items[item._ID].Add(item);
  36. }
  37. }
  38. if (OnGetItemEvent != null) OnGetItemEvent(item._ID, GetItemAmountByID(item._ID) - originAmount);
  39. PlayerQuestManager.Instance.UpdateObjectivesText();
  40. QuestGiverQuestManager.Instance.UpdateObjectivesText();
  41. }
  42. public int GetItemAmountByID(string id)
  43. {
  44. if (Items.ContainsKey(id))
  45. {
  46. return Items[id].Count;
  47. }
  48. return 0;
  49. }
  50. public bool HasItemWithID(string id)
  51. {
  52. return GetItemAmountByID(id) > 0;
  53. }
  54. public void LoseItem(ItemBase item)
  55. {
  56. if (!HasItemWithID(item._ID)) return;
  57. if (!item || ThereIsQuestRequiredItem(item._ID, GetItemAmountByID(item._ID) - 1) || GetItemAmountByID(item._ID) < 1) return;
  58. items[item._ID].Remove(item);
  59. if (Items[item._ID].Count <= 0) Items.Remove(item._ID);
  60. if (OnLoseItemEvent != null) OnLoseItemEvent(item._ID, GetItemAmountByID(item._ID));
  61. PlayerQuestManager.Instance.UpdateObjectivesText();
  62. QuestGiverQuestManager.Instance.UpdateObjectivesText();
  63. }
  64. public void LoseItemByID(string itemID, int amount = 1)
  65. {
  66. if (!HasItemWithID(itemID)) return;
  67. if (itemID == string.Empty || ThereIsQuestRequiredItem(itemID, GetItemAmountByID(itemID) - amount) || GetItemAmountByID(itemID) < amount) return;
  68. for (int i = 0; i < amount; i++)
  69. {
  70. Items[itemID].RemoveAt(Items[itemID].Count - 1);
  71. if (Items[itemID].Count <= 0)
  72. {
  73. Items.Remove(itemID);
  74. break;
  75. }
  76. }
  77. if (OnLoseItemEvent != null) OnLoseItemEvent(itemID, GetItemAmountByID(itemID));
  78. PlayerQuestManager.Instance.UpdateObjectivesText();
  79. QuestGiverQuestManager.Instance.UpdateObjectivesText();
  80. }
  81. /// <summary>
  82. /// 判定是否有某个任务需要某数量的某个道具
  83. /// </summary>
  84. /// <param name="itemID">要判定的道具</param>
  85. /// <param name="amount">要判定的数量</param>
  86. /// <returns>是否需要该道具</returns>
  87. bool ThereIsQuestRequiredItem(string itemID, int amount)
  88. {
  89. foreach (Quest quest in PlayerQuestManager.Instance.QuestsOngoing)
  90. if (quest.RequiredItem(itemID, amount))
  91. return true;
  92. return false;
  93. }
  94. }

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

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

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. public delegate void EnermyDeathListener();
  5. public class Enermy : MonoBehaviour {
  6. [SerializeField]
  7. private string ID;
  8. public string _ID
  9. {
  10. get
  11. {
  12. return ID;
  13. }
  14. set
  15. {
  16. ID = value;
  17. }
  18. }
  19. [SerializeField]
  20. private string _name;
  21. public string Name
  22. {
  23. get
  24. {
  25. return _name;
  26. }
  27. set
  28. {
  29. _name = value;
  30. }
  31. }
  32. public event EnermyDeathListener OnDeathEvent;
  33. public void Death()
  34. {
  35. if (OnDeathEvent != null)
  36. OnDeathEvent();
  37. PlayerQuestManager.Instance.UpdateObjectivesText();
  38. QuestGiverQuestManager.Instance.UpdateObjectivesText();
  39. //TODO
  40. }
  41. }
  1. using UnityEngine;
  2. public delegate void MoveToPointListener(QuestPoint point);
  3. public class QuestPoint : MonoBehaviour {
  4. [SerializeField]
  5. private string ID;
  6. public string _ID
  7. {
  8. get
  9. {
  10. return ID;
  11. }
  12. }
  13. public event MoveToPointListener OnMoveIntoEvent;
  14. public event MoveToPointListener OnMoveAwayEvent;
  15. private void OnTriggerEnter(Collider other)
  16. {
  17. if (OnMoveIntoEvent != null) OnMoveIntoEvent(this);
  18. }
  19. private void OnTriggerStay(Collider other)
  20. {
  21. //TODO
  22. }
  23. private void OnTriggerExit(Collider other)
  24. {
  25. if (OnMoveAwayEvent != null) OnMoveAwayEvent(this);
  26. }
  27. private void OnTriggerEnter2D(Collider2D collision)
  28. {
  29. if (OnMoveIntoEvent != null) OnMoveIntoEvent(this);
  30. }
  31. private void OnTriggerStay2D(Collider2D collision)
  32. {
  33. //TODO
  34. }
  35. private void OnTriggerExit2D(Collider2D collision)
  36. {
  37. if (OnMoveAwayEvent != null) OnMoveAwayEvent(this);
  38. }
  39. }

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

  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. public class GameManager : MonoBehaviour {
  4. private static GameManager instance;
  5. public static GameManager Instance
  6. {
  7. get
  8. {
  9. if (instance == null || !instance.gameObject)
  10. instance = FindObjectOfType<GameManager>();
  11. return instance;
  12. }
  13. }
  14. [SerializeField]
  15. private string itemInfosPath = "1";
  16. private Dictionary<string, ItemBase> itemDataBase;
  17. public Dictionary<string, ItemBase> ItemDataBase
  18. {
  19. get
  20. {
  21. return itemDataBase;
  22. }
  23. }
  24. private Dictionary<string, List<Enermy>> allEnermy = new Dictionary<string, List<Enermy>>();
  25. public Dictionary<string, List<Enermy>> AllEnermy
  26. {
  27. get
  28. {
  29. allEnermy.Clear();
  30. Enermy[] enermies = FindObjectsOfType<Enermy>();
  31. foreach (Enermy enermy in enermies)
  32. {
  33. if (!allEnermy.ContainsKey(enermy._ID))
  34. {
  35. allEnermy.Add(enermy._ID, new List<Enermy>());
  36. }
  37. allEnermy[enermy._ID].Add(enermy);
  38. }
  39. return allEnermy;
  40. }
  41. }
  42. private Dictionary<string, QuestGiver> allQuestGiver = new Dictionary<string, QuestGiver>();
  43. public Dictionary<string, QuestGiver> AllQuestGiver
  44. {
  45. get
  46. {
  47. allQuestGiver.Clear();
  48. QuestGiver[] questGivers = FindObjectsOfType<QuestGiver>();
  49. foreach (QuestGiver giver in questGivers)
  50. {
  51. try
  52. {
  53. allQuestGiver.Add(giver._ID, giver);
  54. }
  55. catch
  56. {
  57. Debug.LogWarningFormat("[Add quest giver error] ID: {0} Name: {1}", giver._ID, giver.Name);
  58. }
  59. }
  60. return allQuestGiver;
  61. }
  62. }
  63. private Dictionary<string, QuestPoint> allQuestPoint = new Dictionary<string, QuestPoint>();
  64. public Dictionary<string, QuestPoint> AllQuestPoint
  65. {
  66. get
  67. {
  68. allQuestPoint.Clear();
  69. QuestPoint[] questPoints = FindObjectsOfType<QuestPoint>();
  70. foreach (QuestPoint point in questPoints)
  71. {
  72. try
  73. {
  74. allQuestPoint.Add(point._ID, point);
  75. }
  76. catch
  77. {
  78. Debug.LogWarningFormat("[Add quest point error] ID: {0}", point._ID);
  79. }
  80. }
  81. return allQuestPoint;
  82. }
  83. }
  84. public void Init()
  85. {
  86. itemDataBase = new Dictionary<string, ItemBase>();
  87. ItemBase[] items = Resources.LoadAll<ItemBase>(itemInfosPath);
  88. foreach (ItemBase item in items)
  89. {
  90. try
  91. {
  92. itemDataBase.Add(item._ID, item);
  93. }
  94. catch
  95. {
  96. Debug.LogWarningFormat("[Add item error] ID: {0} Name: {1}", item._ID, item.Name);
  97. continue;
  98. }
  99. }
  100. foreach (KeyValuePair<string, QuestGiver> kvp in AllQuestGiver)
  101. kvp.Value.Init();
  102. }
  103. private void Start()
  104. {
  105. Init();
  106. }
  107. public ItemBase GetItemInstanceByID(string id)
  108. {
  109. ItemBase item = Instantiate(itemDataBase[id]);
  110. if(item != null)
  111. switch (item.ItemType)
  112. {
  113. case ItemType.武器: return item as WeaponItem;
  114. default:return item;
  115. }
  116. return item;
  117. }
  118. }

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

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

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

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

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

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

 

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

 

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

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

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

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

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

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

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

  1. using System.Collections.Generic;
  2. [System.Serializable]
  3. public class SaveData
  4. {
  5. public List<ItemData> itemDatas = new List<ItemData>();
  6. public List<QuestData> ongoingQuestDatas = new List<QuestData>();
  7. public List<QuestData> completeQuestDatas = new List<QuestData>();
  8. }
  9. [System.Serializable]
  10. public class ItemData
  11. {
  12. public string itemID;
  13. public int itemAmount;
  14. public ItemData(string id, int amount)
  15. {
  16. itemID = id;
  17. itemAmount = amount;
  18. }
  19. }
  20. [System.Serializable]
  21. public class QuestData
  22. {
  23. public string questID;
  24. public string originGiverID;
  25. public List<ObjectiveData> objectiveDatas = new List<ObjectiveData>();
  26. public QuestData(Quest quest)
  27. {
  28. questID = quest._ID;
  29. originGiverID = quest.MOriginQuestGiver._ID;
  30. foreach(Objective o in quest.Objectives)
  31. {
  32. objectiveDatas.Add(new ObjectiveData(o));
  33. }
  34. }
  35. }
  36. [System.Serializable]
  37. public class ObjectiveData
  38. {
  39. public string runtimeID;
  40. public int currentAmount;
  41. public ObjectiveData(Objective objective)
  42. {
  43. runtimeID = objective.runtimeID;
  44. currentAmount = objective.CurrentAmount;
  45. }
  46. }

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

  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Runtime.Serialization.Formatters.Binary;
  5. using UnityEngine;
  6. using UnityEngine.SceneManagement;
  7. public class SaveManager : MonoBehaviour
  8. {
  9. private static SaveManager instance;
  10. public static SaveManager Instance
  11. {
  12. get
  13. {
  14. if (instance == null || !instance.gameObject)
  15. instance = FindObjectOfType<SaveManager>();
  16. return instance;
  17. }
  18. }
  19. private static bool DontDestroyOnLoadOnce;
  20. public string dataName = "SaveData.zdat";
  21. private void Awake()
  22. {
  23. if (!DontDestroyOnLoadOnce)
  24. {
  25. DontDestroyOnLoad(this);
  26. DontDestroyOnLoadOnce = true;
  27. }
  28. else
  29. {
  30. Destroy(gameObject);
  31. }
  32. }
  33. public bool Save()
  34. {
  35. FileStream fs = OpenFile(Application.persistentDataPath + "/" + dataName, FileMode.Create);
  36. try
  37. {
  38. BinaryFormatter bf = new BinaryFormatter();
  39. SaveData data = new SaveData();
  40. SaveBag(data);
  41. SavePlayerQuest(data);
  42. bf.Serialize(fs, data);
  43. fs.Close();
  44. return true;
  45. }
  46. catch (System.Exception ex)
  47. {
  48. if (fs != null) fs.Close();
  49. Debug.LogError(ex.Message);
  50. return false;
  51. }
  52. }
  53. void SaveBag(SaveData data)
  54. {
  55. foreach (KeyValuePair<string, List<ItemBase>> itemList in BagManager.Instance.Items)
  56. {
  57. data.itemDatas.Add(new ItemData(itemList.Key, itemList.Value.Count));
  58. }
  59. }
  60. void SavePlayerQuest(SaveData data)
  61. {
  62. foreach (Quest quest in PlayerQuestManager.Instance.QuestsOngoing)
  63. {
  64. data.ongoingQuestDatas.Add(new QuestData(quest));
  65. }
  66. foreach (Quest quest in PlayerQuestManager.Instance.QuestsComplete)
  67. {
  68. data.completeQuestDatas.Add(new QuestData(quest));
  69. }
  70. }
  71. public bool Load()
  72. {
  73. try
  74. {
  75. StartCoroutine(LoadAsync());
  76. return true;
  77. }
  78. catch (System.Exception ex)
  79. {
  80. Debug.LogError(ex.Message);
  81. return false;
  82. }
  83. }
  84. IEnumerator LoadAsync()
  85. {
  86. AsyncOperation ao = SceneManager.LoadSceneAsync("QuestTest");
  87. ao.allowSceneActivation = false;
  88. yield return new WaitUntil(() => { return ao.progress >= 0.9f; });
  89. ao.allowSceneActivation = true;
  90. yield return new WaitUntil(() => { return ao.isDone; });
  91. FileStream fs = OpenFile(Application.persistentDataPath + "/" + dataName, FileMode.Open);
  92. try
  93. {
  94. GameManager.Instance.Init();
  95. BinaryFormatter bf = new BinaryFormatter();
  96. SaveData data = new SaveData();
  97. data = bf.Deserialize(fs) as SaveData;
  98. fs.Close();
  99. LoadBag(data);
  100. LoadPlayerQuest(data);
  101. }
  102. catch
  103. {
  104. if (fs != null) fs.Close();
  105. StopCoroutine(LoadAsync());
  106. throw;
  107. }
  108. }
  109. void LoadBag(SaveData data)
  110. {
  111. foreach (ItemData itemData in data.itemDatas)
  112. {
  113. BagManager.Instance.GetItem(GameManager.Instance.GetItemInstanceByID(itemData.itemID), itemData.itemAmount);
  114. }
  115. }
  116. void LoadPlayerQuest(SaveData data)
  117. {
  118. foreach (QuestData questData in data.ongoingQuestDatas)
  119. {
  120. HandlingQuestData(questData);
  121. }
  122. foreach (QuestData questData in data.completeQuestDatas)
  123. {
  124. Quest quest = HandlingQuestData(questData);
  125. PlayerQuestManager.Instance.CompleteQuest(quest, true);
  126. }
  127. }
  128. Quest HandlingQuestData(QuestData questData)
  129. {
  130. QuestGiver questGiver = GameManager.Instance.AllQuestGiver[questData.originGiverID];
  131. Quest quest = questGiver.QuestInstances.Find(x => x._ID == questData.questID);
  132. PlayerQuestManager.Instance.AcceptQuest(quest);
  133. foreach (ObjectiveData od in questData.objectiveDatas)
  134. {
  135. foreach (Objective o in quest.Objectives)
  136. {
  137. if (o.runtimeID == od.runtimeID)
  138. {
  139. o.CurrentAmount = od.currentAmount;
  140. break;
  141. }
  142. }
  143. }
  144. return quest;
  145. }
  146. FileStream OpenFile(string path, FileMode fileMode)
  147. {
  148. try
  149. {
  150. return new FileStream(path, fileMode);
  151. }
  152. catch
  153. {
  154. return null;
  155. }
  156. }
  157. }

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


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

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

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

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

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号