第三部分:任务系统

        实现一个简易的任务系统,与对话系统相对接,包含任务的接取,追踪,完成等功能。

        前提:完成对话系统  第三部分:对话系统-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
      };
   }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值