实现一个简易的任务系统,与对话系统相对接,包含任务的接取,追踪,完成等功能。
前提:完成对话系统 第三部分:对话系统-CSDN博客
一:数据定义
1:TaksData_SO
(1)创建了内部类TaskRequire表示具体的需求:每项需求包含 需求目标的名字、需求数目、当前数目。
(2)TaksData包含了任务名字,任务描述,系列的(List)任务需求和奖励,以及任务的状态,剩余需求的数目。
[CreateAssetMenu(fileName = "New Task",menuName = "Task/Task Data")]
public class TaskData_SO : ScriptableObject
{
[System.Serializable]
public class TaskRequire
{
//暂时以目标的名字作为判断
public string requireName;
public int requireAmount;
public int currentAmount;
}
public string taskName;
[TextArea]
public string taskDescription;
public TaskStateType TaskState { get; set; }
public List<TaskRequire> taskRequires = new();
public List<InventoryItem> rewards = new();
private int restRequireAmount;
public int RestRequireAmount
{
get => restRequireAmount;
set
{
restRequireAmount = value;
if (restRequireAmount == 0) TaskState = TaskStateType.Completed;
else TaskState = TaskStateType.Started;
}
}
public void GiveRewards()
{
foreach (var reward in rewards)
{
if (reward.itemAmount < 0)
{
int costAmount = -reward.itemAmount;
//先从背包中寻找 再从快捷栏中寻找 直到满足costAmount
}
else
{
switch (reward.itemData.itemType)
{
case ItemType.Consumable:
InventoryManager.Instance.consumableInventory.AddItem(reward.itemData, reward.itemAmount);
break;
case ItemType.PrimaryWeapon:
case ItemType.SecondaryWeapon:
InventoryManager.Instance.equipmentsInventory.AddItem(reward.itemData, reward.itemAmount);
break;
}
}
InventoryManager.Instance.RefreshAllContainer();
}
}
}
2:与对话的对接部分
(1)在DialoguePiece中新建TaskDataSO字段。
(2)在OptionUI中新建TakeTask的bool类型字段。
(3)在接受任务的选项中设置TakeTask字段为true,在OptionCliked中判断接受任务。
private void OnOptionClicked()
{
//判断是否接受了任务
if (takeTask && currentDialoguePiece.taskData != null)
{
var gameTask = TaskManager.Instance.GetTask(currentDialoguePiece.taskData);
//如果任务列表中已经有该Task 则要判断任务是否完成 并且给予奖励
if (gameTask != null)
{
if (gameTask.TaskState == TaskStateType.Completed)
{
Debug.Log("Give Rewards");
gameTask.TaskState = TaskStateType.Finished;
//给予任务的奖励
gameTask.GiveRewards();
}
}
//任务列表中没有该Task 则直接加入
else
{
TaskManager.Instance.AddTask(currentDialoguePiece.taskData);
//初始化进度:对于所有的需求,检测背包中的物品
foreach (var taskRequire in currentDialoguePiece.taskData.taskRequires)
{
InventoryManager.Instance.CheckTaskItemInBag(taskRequire.requireName);
}
}
}
//跳转到选项的下一句对话
if (nextPieceID == string.Empty)
{
DialogueUIManager.Instance.CloseDialogue();
}
else
{
var targetPlace = DialogueUIManager.Instance.GetDialoguePiecesByName(nextPieceID);
DialogueUIManager.Instance.UpdateMainDialogue(currentDialogueData.dialoguePieces[targetPlace]);
}
}
3:TaskManager
(1)存储所有的任务。提供添加任务,更新进度,得到任务等方法。
(2)实现 ISavble接口,简单完成任务的保存和加载。
(3)编辑器中拖拽赋值的TaskDataSO属于模板,在加入到TaksManager中要新建实例(使用Instantitate),在保存和加载时都要新建实例。
public class TaskManager : Singleton<TaskManager>,ISavable
{
public List<TaskData_SO> tasks = new();
public void AddTask(TaskData_SO taskData)
{
var newTask = Instantiate(taskData);
newTask.TaskState = TaskStateType.Started;
newTask.RestRequireAmount = newTask.taskRequires.Count;
tasks.Add(newTask);
}
//敌人死亡 或 拾取物品的时候调用
public void UpdateTaskProgress(string requireName, int amount = 1)
{
foreach (var task in tasks)
{
if (task.TaskState == TaskStateType.Finished) continue;
var matchTask = task.taskRequires.Find(r => r.requireName == requireName);
if (matchTask!= null)
{
//如果是减少了物品,则要检测任务的要求是否变得不满足了
if (matchTask.currentAmount >= matchTask.requireAmount &&
matchTask.currentAmount + amount <= matchTask.requireAmount)
{
++task.RestRequireAmount;
}
//正常流程 物品变化后判断是否满足了条件要求
matchTask.currentAmount += amount;
if (matchTask.currentAmount >= matchTask.requireAmount)
{
--task.RestRequireAmount;
}
}
}
}
public bool HaveTask(TaskData_SO targetTaskData)
{
return tasks.Find(task => task.taskName == targetTaskData.taskName) != null;
}
public TaskData_SO GetTask(TaskData_SO targetTaskData)
{
return tasks.Find(task => task.taskName == targetTaskData.taskName);
}
#region 任务的保存接口
public string GetDataID()
{
return "Task";
}
public void SaveData(Data data)
{
data.tasks.Clear();
foreach (var task in tasks)
{
data.tasks.Add(Instantiate(task));
}
}
public void LoadData(Data data)
{
tasks.Clear();
foreach (var task in data.tasks)
{
tasks.Add(Instantiate(task));
}
}
#endregion
}
4:TaskUIManager
(1)完成UI部分的更新——具体来说,UI左侧部分:任务按钮列表,每个按钮上会显示任务的名字和状态,每当点击一个任务按钮时,在UI右侧更新具体说明部分。UI右侧部分:包含了任务的详情说明,需求列表,奖励列表。
(2)将每个部分解耦合(任务按钮,任务需求,任务奖励等),然后统一设置即可。
(3)在更新UI右侧时:需要先删除原有列表下的子物体,然后重新生成。
public class TaskUIManager : Singleton<TaskUIManager>
{
[Header("引用")]
[SerializeField] private GameObject taskPanel;
[SerializeField] public Tooltip itemTooltip;
[Header("任务Button")]
[SerializeField] private Transform taskListTransform;
[SerializeField] private TaskNameButton taskNameButtonPrefab;
[Header("任务Requirement")]
[SerializeField] private Transform requirementListTransform;
[SerializeField] private TaskRequirement requirementPrefab;
[Header("任务Reward")]
[SerializeField] private Transform rewardListTransform;
[SerializeField] private ItemUI rewardUIPrefab;
[Header("任务Description")]
[SerializeField] private TextMeshProUGUI taskDescriptionText;
public void OpenTaskPanel()
{
taskPanel.SetActive(true);
taskDescriptionText.text=string.Empty;
//显示面板内容
SetupTaskList();
}
//设置UI左侧任务名称列表
private void SetupTaskList()
{
//先删除所有不该显示的物体:ButtonList RequireList RewardList
foreach (Transform taskButton in taskListTransform)
Destroy(taskButton.gameObject);
foreach (Transform requirement in requirementListTransform)
Destroy(requirement.gameObject);
foreach (Transform reward in rewardListTransform)
Destroy(reward.gameObject);
//通过TaskManager 新生成所有的task 并且进行初始化
foreach (var task in TaskManager.Instance.tasks)
{
var taskButton = Instantiate(taskNameButtonPrefab, taskListTransform);
taskButton.SetupNameButton(task);
}
}
//设置UI右侧的任务详情
public void SetUpTaskDescription(TaskData_SO taskData)
{
taskDescriptionText.text = taskData.taskDescription;
}
//设置UI右侧任务需求列表
public void SetupRequirementList(TaskData_SO taskData)
{
foreach (Transform requirement in requirementListTransform)
Destroy(requirement.gameObject);
foreach (var requirement in taskData.taskRequires)
{
var requirementUI = Instantiate(requirementPrefab, requirementListTransform);
if (taskData.TaskState == TaskStateType.Finished)
requirementUI.SetupRequirement(requirement.requireName, true);
else
requirementUI.SetupRequirement(requirement.requireName, requirement.requireAmount, requirement.currentAmount);
}
}
//设置UI右侧任务奖励列表
public void SetupRewardList(TaskData_SO taskData)
{
foreach (Transform reward in rewardListTransform)
Destroy(reward.gameObject);
foreach (var reward in taskData.rewards)
{
var rewardItem = Instantiate(rewardUIPrefab, rewardListTransform);
rewardItem.SetUpItemUI(reward.itemData, reward.itemAmount);
}
}
}
5:TaskNameButton
public class TaskNameButton : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI taskNameText;
private TaskData_SO currentTaskData;
private void Awake()
{
GetComponent<Button>().onClick.AddListener(UpdateTaskContent);
}
public void SetupNameButton(TaskData_SO taskData)
{
taskNameText.text = taskData.taskName;
currentTaskData = taskData;
//根据任务状态给予不同显示
switch (currentTaskData.TaskState)
{
case TaskStateType.Started:
taskNameText.text = taskData.taskName + "(进行中)";
break;
case TaskStateType.Completed:
taskNameText.text = taskData.taskName + "(已完成)";
break;
case TaskStateType.Finished:
taskNameText.text = taskData.taskName + "(已结束)";
break;
}
}
private void UpdateTaskContent()
{
//设置右侧的文本内容:任务详情,任务要求,任务奖励
TaskUIManager.Instance.SetUpTaskDescription(currentTaskData);
TaskUIManager.Instance.SetupRequirementList(currentTaskData);
TaskUIManager.Instance.SetupRewardList(currentTaskData);
}
}
6:TaskRequirement
public class TaskRequirement : MonoBehaviour
{
[SerializeField]private TextMeshProUGUI requireNameText;
[SerializeField]private TextMeshProUGUI progressNumberText;
/// <summary>
///
/// </summary>
/// <param name="name">需求目标的名字</param>
/// <param name="needAmount">需求的数量</param>
/// <param name="currentAmount">现有的数量</param>
public void SetupRequirement(string name, int needAmount, int currentAmount)
{
requireNameText.text = name;
progressNumberText.text = currentAmount + "/" + needAmount;
}
public void SetupRequirement(string name, bool isFinished)
{
if (isFinished)
{
requireNameText.text = name;
progressNumberText.text = "已完成";
requireNameText.color = Color.gray;
progressNumberText.color = Color.gray;
}
}
}
7:TaskReward:由ItemUI和ShowTooltip组合而成。
8:ShowTooltip
(1)奖励采用ItemUI(背包系统中实现的物品图片类)显示(但是没有SlotHolder)。
(2)奖励的说明,和背包系统中的Tooltip实现差不多,但由于挂载在不同的物体上,所以简单重新实现以下。(利用IPointerEnterHandler和IPointerExitHandler接口)
public class ShowTooltip :MonoBehaviour, IPointerEnterHandler,IPointerExitHandler
{
private ItemUI currentItemUI;
private void Awake()
{
currentItemUI = GetComponent<ItemUI>();
}
public void OnPointerEnter(PointerEventData eventData)
{
//设置task中的tooltip
TaskUIManager.Instance.itemTooltip.gameObject.SetActive(true);
TaskUIManager.Instance.itemTooltip.SetItemText(currentItemUI.GetItemData);
}
public void OnPointerExit(PointerEventData eventData)
{
//设置task中的tooltip
TaskUIManager.Instance.itemTooltip.gameObject.SetActive(false);
TaskUIManager.Instance.itemTooltip.SetItemText(null);
}
}
二:逻辑实现
1:接受任务与提交任务
(1)前面有提到,在每一条对话DialoguePiece中新增TaskDataSO字段,在每一条选项DialogueOption中新增 TakeTask(bool类型)字段(即可代表接受任务,也代表提交任务)。
2:追踪任务进度
(1)目前游戏中的任务目标涉及到了 收集物品/杀死怪物内容,因此在拾取物品/使用物品/击杀怪物的时候通过物品的ItemName/敌人的EnemyTypeName来实现人物的更新。
(2)物品的ItemName是自带的;敌人的EnemyController类中要新增字段EnemyTypeName,在Awake中调用纯虚函数SettingEnemyName()(在子类中实现)。
//设置敌人类型的名字
protected abstract void SettingEnemyName();
(3)在具体的更新中,要遍历所有任务,并查找每个任务的需求列表中是否包含这个需求目标,如果包含,则进行更新操作,同时更新任务的进度状态。
public void UpdateTaskProgress(string requireName, int amount = 1)
{
foreach (var task in tasks)
{
if (task.TaskState == TaskStateType.Finished) continue;
var matchTask = task.taskRequires.Find(r => r.requireName == requireName);
if (matchTask!= null)
{
//如果是减少了物品,则要检测任务的要求是否变得不满足了
if (matchTask.currentAmount >= matchTask.requireAmount &&
matchTask.currentAmount + amount <= matchTask.requireAmount)
{
++task.RestRequireAmount;
}
//正常流程 物品变化后判断是否满足了条件要求
matchTask.currentAmount += amount;
if (matchTask.currentAmount >= matchTask.requireAmount)
{
--task.RestRequireAmount;
}
}
}
}
(4)在任务获取的时候,也要进行一个初始的更新进度:遍历当前任务的所有需求,并且在背包中进行查找,有无需求目标的物品,并且进行更新操作。
public void CheckTaskItemInBag(string requireItemName)
{
foreach (var inventoryItem in consumableInventory.items)
{
if (inventoryItem.itemData != null)
{
if (inventoryItem.itemData.itemName == requireItemName)
{
TaskManager.Instance.UpdateTaskProgress(requireItemName,inventoryItem.itemAmount);
}
}
}
foreach (var inventoryItem in actionInventory.items)…………
foreach (var inventoryItem in equipmentsInventory.items)…………
}
(5)获取奖励:如果任务目标需要提交物品,则转换成奖励物品个数为负数。
对于实际奖励的物品:直接添加到背包中,如果加不下,则掉落到世界上(可自行定义规则)。
对于需要提交的物品:从背包中寻找物品并更新,直到满足costAmount。
public void GiveRewards()
{
foreach (var reward in rewards)
{
if (reward.itemAmount < 0)
{
int costAmount = -reward.itemAmount;
InventoryManager.Instance.TaskCostItem(reward, costAmount);
}
else
{
InventoryManager.Instance.TaskRewardItem(reward, reward.itemAmount);
}
}
}
//消耗物品
public void TaskCostItem(InventoryItem costItem, int costAmount)
{
switch (costItem.itemData.itemType)
{
case ItemType.Consumable:
//先从消耗物品背包中查找,再从快捷栏物品中查找
TaskCostItem(costItem, costAmount, consumableInventory);
if (costAmount != 0)
TaskCostItem(costItem, costAmount, actionInventory);
consumableContainer.RefreshContainerUI();
actionContainer.RefreshContainerUI();
break;
case ItemType.PrimaryWeapon:
case ItemType.SecondaryWeapon:
//从装备背包中查找
TaskCostItem(costItem, costAmount, equipmentsInventory);
equipmentsContainer.RefreshContainerUI();
break;
}
}
private void TaskCostItem(InventoryItem costItem, int costAmount, InventoryData_SO inventory)
{
foreach (var item in inventory.items.Where(item => item.itemData == costItem.itemData))
{
if (item.itemAmount >= costAmount)
{
item.itemAmount -= costAmount;
costAmount = 0;
break;
}
else
{
costAmount -= item.itemAmount;
item.itemAmount = 0;
}
}
}
public void TaskRewardItem(InventoryItem rewardItem, int rewardAmount)
{
switch (rewardItem.itemData.itemType)
{
case ItemType.Consumable:
consumableInventory.AddItem(rewardItem.itemData, rewardItem.itemAmount);
consumableContainer.RefreshContainerUI();
break;
case ItemType.PrimaryWeapon:
case ItemType.SecondaryWeapon:
equipmentsInventory.AddItem(rewardItem.itemData, rewardItem.itemAmount);
equipmentsContainer.RefreshContainerUI();
break;
}
}
(6)对话变更:当任务处于不同的状态下,需要NPC(给予任务的人物)有不同的对话内容,这通过脚本TaskGiver来实现。
TaskGiver中包含了对话控制器DialogueController ,以及在不同任务状态下的的对话DialogueData_SO (没有接受任务时,任务进行中,任务完成,提交任务后)。
由于编辑器中的TaskDataSO是模板而不是游戏中具体的任务,因此TaskState要在TaskManager中的Tasks列表中获取对应的Task的TaskState,并且暂时简单在Update中根据任务的状态更新DialogueController中的对话内容。//效率可能偏低 改进方法:在TaskManager中改用哈希表来存储任务,Key值为任务名string,value为TaskDataSO。
[RequireComponent(typeof(DialogueController))]
public class TaskGiver : MonoBehaviour
{
private DialogueController dialogueController;
[SerializeField]private TaskData_SO currentTaskData;
[Header("不同任务状态下的对话")]
[SerializeField] private DialogueData_SO startDialogueData;
[SerializeField] private DialogueData_SO progressDialogueData;
[SerializeField] private DialogueData_SO CompleteDialogueData;
[SerializeField] private DialogueData_SO FinishDialogueData;
private TaskData_SO taskOnGame = null;
public TaskStateType TaskState
{
get
{
if (taskOnGame != null) return taskOnGame.TaskState;
taskOnGame = TaskManager.Instance.GetTask(currentTaskData);
return taskOnGame == null ? TaskStateType.NotStarted : taskOnGame.TaskState;
}
}
private void Awake()
{
dialogueController=GetComponent<DialogueController>();
}
private void Start()
{
dialogueController.currentDialogueData = startDialogueData;
}
//TODO:暂时在Update中检测任务的状态
private void Update()
{
dialogueController.currentDialogueData = TaskState switch
{
TaskStateType.NotStarted => startDialogueData,
TaskStateType.Started => progressDialogueData,
TaskStateType.Completed => CompleteDialogueData,
TaskStateType.Finished => FinishDialogueData,
_ => null
};
}
}