220425 【程式筆記】寫一個通用的任務系統

提醒一下自己這一個是原唱的版本
但說真的兩個版本都很棒

來更新一下已經長草很久的這裡

剛好公司最近需要做一個像是成就系統的東西

不過這裡的專案用到的東西又跟一般遊戲的不太一樣

不過也就剛好藉著這一個機會來寫一個通用的成就系統

當然這裡也就是盡量留著擴充性

架構構思

在成就系統中大多都是跟一個個事件的完成有關係

例如

打死了一隻Boss

可以跟以下相關成就有關

  • 累計打死的怪物數量
  • 劇情是否推進

也就是說當玩家在遊戲裡做出了一個動作有可能同時牽涉到多個事件

因此在設計上我們應該要關注的是完成的這一個動作有了什麼資料

並由各個不同的成就各自取用對應的資料去做計算

QuestManager

主要是負責管理Quest本身的存在與否

並且同時負責提供一個事件好讓其它的類別可以得知現在Quest的狀態

Quest & QuestData

Quest 定義Quest的實際內容 也就是Quest是否完成

QuestData 則是Quest在執行上所需要使用到的資料

QuestDataQuest 不是一一對應的關係

因為一個QuestData可能會牽涉到多個任務

像是怪物擊殺的Data就可能會同時影響到多個Quest

實作

QuestManager

裡面使用Dictionary作為容器 並且使用 QuestData 的Type作為Key

目前可以透過Manager做到以下的事情

  • 新增一個Quest
  • 移除特定的Quest
  • 更新Quest

下面省略大部分實作 不過要注意的是因為實作上為了外部方便使用所以實際上有使用到 System.Reflection

為的是從Generic的 QuestType 反推回 DataType

或許這裡還是在一開始就把DataType傳進來可能會是比較好的作法

using System;
using System.Collections.Generic;
namespace CJStudio.Quest
{
	public static class QuestManager
	{
		public static event Action<IQuest> OnQuestCompleted;
		public static Dictionary<Type, Dictionary<int, IQuest>> Quests = new Dictionary<Type, Dictionary<int, IQuest>>();

		public static void AddQuest<QuestType>(QuestType quest) where QuestType : IQuest;
		public static void AddQuest<QuestType, DataType>(QuestType quest) where QuestType : IQuest<DataType> where DataType : IQuestData;

		public static void RemoveQuest(IQuest quest);
		public static void RemoveQuest<QuestType, DataType>(QuestType quest) where QuestType : IQuest<DataType> where DataType : IQuestData;
		public static void RemoveQuest<QuestType>(int id) where QuestType : IQuest;
		public static void RemoveQuest<QuestType, DataType>(int id) where QuestType : IQuest<DataType> where DataType : IQuestData;

		public static void RefreshQuest<DataType>(DataType data) where DataType : IQuestData
		{
			//...
			for (int i = completeQuest.Count - 1; i >= 0; i--)
			{
				OnQuestCompleted?.Invoke(completeQuest[i]);
			}
		}

		public static bool TryGetQuest<QuestType, DataType>(int id, out QuestType result) where QuestType : IQuest<DataType> where DataType : IQuestData;

		public static bool TryGetQuest<QuestType>(int id, out QuestType result) where QuestType : IQuest;

		#region PRIVATE_METHOD
		private static void AddQuestImpl<QuestType>(QuestType quest, Type keyType) where QuestType : IQuest;
		private static void RemoveQuestImpl(int id, Type type);
		private static bool TryGetQuestImpl<QuestType>(int id, Type dataType, out QuestType result) where QuestType : IQuest;
		private static Type GetDataType<QuestType>() where QuestType : IQuest;
		private static Type GetDataType(Type questType);
		private static Type GetDataTypeImpl(Type questType);
		#endregion PRIVATE_METHOD
	}
}

Quest & QuestData

主要是定義其所擁有的介面

namespace CJStudio.Quest
{
	public interface IQuest
	{
		int Id { get; }
		bool IsComplete { get; }
	}
	public interface IQuest<T> : IQuest where T : IQuestData
	{
		void Refresh(T data);
	}

	public interface IQuestData { }
}

實際使用

這裡是定義Quest跟其所使用的Data

在關係上就像上面所說的不會是完全一對一的關係

namespace CJStudio.Quest
{
	public class TestQuest : IQuest<TestQuestData>
	{
		public int Id { get; private set; }
		public bool IsComplete => Remain <= 0;

		public int Remain { get; private set; }
		public TestQuest(int remain)
		{
			Id = this.GetHashCode();
			Remain = remain;
		}
		public void Refresh(TestQuestData data)
		{
			Remain -= data.Count;	
		}
	}

	public class TestQuestData : IQuestData
	{
		public int Count { get; private set; }
		public TestQuestData(int count)
		{
			Count = count;
		}
	}

	public class TestQuest2 : IQuest<TestQuest2Data>
	{
		public int Id { get; private set; }
		public bool IsComplete => State == "Complete";
		public string State { get; private set; } = string.Empty;

		public TestQuest2(string initState)
		{
			Id = this.GetHashCode();
			State = initState;
		}
		public void Refresh(TestQuest2Data data)
		{
			State = data.State;
		}
	}

	public class TestQuest2Data : IQuestData
	{
		public string State { get; private set; }
		public TestQuest2Data(string state)
		{
			State = state;
		}
	}
}

private void Awake()
	{
		QuestManager.OnQuestCompleted += OnQuestCompleted;
		QuestManager.AddQuest(new TestQuest(10));
		QuestManager.AddQuest(new TestQuest2("Doing"));
	}

	private void OnDestroy()
	{
		QuestManager.OnQuestCompleted -= OnQuestCompleted;
	}

	private void OnQuestCompleted(IQuest mission)
	{
		Debug.LogError($"{mission.GetType()} is complete");
		QuestManager.RemoveQuest(mission);
	}

	public void Update()
	{
		if (Input.GetKeyDown(KeyCode.E))
		{
			QuestManager.RefreshQuest(new TestQuestData(2));
		}
		if (Input.GetKeyDown(KeyCode.R))
		{
			QuestManager.RefreshQuest(new TestQuest2Data("Complete"));
		}
	}

實際上使用時則是在對應的時機 將對應的資料new出來

則Manager就會把所有對應這一個QuestData的Quest都會更新一遍

實際使用時關注是Data不是Quest本身

結論

開發到了一定的程度就會開始想要把這一些通用的大概念都使用通用的解法

不過實際在製作上並不一定有這麼多的時間

所以要重零開始開發一款遊戲是需要花非常多的時間的

感覺一個穩建的公司也是只能透過一個又一個的作品去疊代

不過也不是每一間公司都有這一種遠見就是了

發表留言