【游戏开发实战】Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程 | 链式任务 | 主线支线)

本文最终效果如下:
请添加图片描述
请添加图片描述
请添加图片描述
工程思维导图:
在这里插入图片描述

工程源码见文章末尾。

文章目录

一、前言

嗨,大家好,我是新发。
事情是这样的,有小朋友微信问我如何做任务系统,作为一个热心的技术博主,我都是能帮就帮。今天,我就来做一个任务系统吧。
在这里插入图片描述

二、什么是任务系统

任务系统就是一个有明确目标性的系统。通过设置任务来引导玩家进行游戏,让玩家更快的融入游戏中。
可以说任务系统几乎是游戏必备的模块,我们随便找个游戏都可以看到任务系统。
在这里插入图片描述
根据这位小朋友的需求,是要做 主线任务/支线任务 的系统。
简单的说,就是有一条 主线任务链,在完成主线任务链上的某个节点时,开启下一个任务,并可以开启一条或多条 支线任务链,主线任务和多条支线任务并行。画个图,方便大家理解:
在这里插入图片描述

三、需求文档

由于只有我一个人,没有策划,那我就先充当策划,给自己写个需求文档吧~

1、故事背景

主人公林新发刚刚大学毕业,开始面临一个人生难题:如何走上人生巅峰!
现在我们为林新发设计一套任务,帮助他走上人生巅峰吧~

2、任务链设计

下面,就是走上人生巅峰的任务链啦~
请添加图片描述

3、任务规则

主线任务必须按顺序完成;
主线任务与支线任务可以并行;
支线任务并不影响主线任务;
每完成一个任务都可以得到相应的奖励;
任务界面只显示当前要执行或已完成但还未领取奖励的任务;
任务界面中要显示每个任务当前的进度;
每个任务有个前往按钮,点击前往按钮触发任务执行或跳转到相应的界面;
每个任务有对应的图标,可配置;
界面底部有一键领奖按钮,点击一键领奖领取所有可以领奖的任务奖励。

4、界面样式设计

使用 原型图设计 软件制作界面样式,如下:
在这里插入图片描述

四、从哪里开始着手

对于萌新来说,拿到需求时可能不知道从哪里开始做,是先写代码还是先做界面?代码又是从哪里开始写?

我总结了一个客户端开发流程,大家可以按这个流程执行,
在这里插入图片描述

五、任务配置表

1、定义表头字段

根据需求,我们先定义表头字段,
在这里插入图片描述

字段解释:

字段 数据类型 说明
task_chain_id int 链id,每个任务都有它对应的链id,同一条链上的任务的链id相同
task_sub_id int 任务id,它是链上的任务id,不同链的任务id可以重复,从1开始往下自增
icon string 任务图标
desc string 任务描述,这个会显示到界面中
task_target string 任务目标,定义一个字符串来表示任务的目标类别,比如加班5次加班10次的任务目标是一样的,只是数量不同,同理,写博客5篇写博客100篇的任务目标也是一样的
target_amount int 目标数量,比如加班5次的目标数量就是5,写博客100篇的目标数量就是100
award string 奖励,json格式,例:{"gold":1000},表示奖励1000金币
open_chain string 要打开的支线任务,格式:链id|任务id,开启多个链以英文逗号隔开。例:2|1,4|1表示打开 链2的子任务1和打开链4的子任务1

2、配置表格数据

根据我们上面设计的任务链,在配置表中配置任务数据,入下:

注:黄色的是主线任务,每条支线任务我都单独标了颜色方便阅读。

在这里插入图片描述

表格保存为链式任务.xlsx,如下
在这里插入图片描述

3、转表工具:Excel转Json

Excel表格是方便策划进行配置数值,游戏中并不是直接读取Excel配置,实际项目中一般都是将Excel转为xmljsonlua或自定义的文本格式的配置。
我这里就以ExcelJson为例,处理Excel我推荐大家使用python来写工具,我之前写过一篇文章:《教你使用python读写Excel表格(增删改查操作),使用openpyxl库》,里面我详细介绍了使用pythonopenpyxl库来读写Excel,建议大家先认真看一下这篇文章。
这里我就直接把最终我写好的python代码贴出来,代码也很简单,这里不赘述了~

import openpyxl
import json

# excel表格转json文件
def excel_to_json(excel_file, json_f_name):
    jd = []
    heads = []
    book = openpyxl.load_workbook(excel_file)
    sheet = book[u'Sheet1']
    
    max_row = sheet.max_row
    max_column = sheet.max_column
    # 解析表头
    for column in range(max_column):
        heads.append(sheet.cell(1, column + 1).value)
    # 遍历每一行
    for row in range(max_row):
        if row < 2:
        	# 前两行跳过
            continue
        one_line = {
    
    }
        # 遍历一行中的每一个单元格
        for column in range(max_column): 
            k = heads[column]
            v = sheet.cell(row + 1, column + 1).value
            one_line[k] = v
        jd.append(one_line)
    book.close()
    # 将json保存为文件
    save_json_file(jd, json_f_name)

# 将json保存为文件
def save_json_file(jd, json_f_name):
    f = open(json_f_name, 'w', encoding='utf-8')
    txt = json.dumps(jd, indent=2, ensure_ascii=False)
    f.write(txt)
    f.close()

if '__main__' == __name__:
     excel_to_json(u'链式任务.xlsx', 'task_cfg.bytes')

上面的python代码保存为excel_to_json.py,如下
在这里插入图片描述
excel_to_json.py放在上面的链式任务.xlsx文件的同级目录中,执行excel_to_json.py,生成task_cfg.bytes
在这里插入图片描述
使用文本编辑器打开task_cfg.bytes,看下生成效果,如下,格式正确:
在这里插入图片描述

六、读取配置表

上面配置表做好了,接下来就可以开始动手Unity部分了。
Unity中如何读取配置表呢?其实配置表也是一种资源,关于资源读取我之前写过相关文章:
《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》
在这里插入图片描述

这里我就简单处理,通过Resources.Load来读取文件。

1、资源加载:Resources.Load

先新建一个Resources文件夹,
在这里插入图片描述
task_cfg.bytes放在Resources目录中,
在这里插入图片描述
这样我们就可以直接使用Resources.Load来读取task_cfg.bytes文件了,如下:

string txt = Resources.Load<TextAsset>("task_cfg").text;

2、C#的json库:LitJson

因为我们使用的是json格式的文本,要解析它我们需要使用json库,这里我推荐使用LitJson,可以在GitHub中找到LitJson的开源项目,
地址:https://hub.fastgit.org/LitJSON/litjson
在这里插入图片描述
我们下载下来后,把src目录中的LitJson文件夹整个拷贝到我们Unity工程中,如下:
在这里插入图片描述
这样我们就可以在C#中使用LitJson了。
使用时引入命名空间:

using LitJson;

3、任务配置配置读取:TaskCfg.cs脚本

3.1、创建TaskCfg.cs脚本

现在我们开始写C#代码,养成好习惯,先建好Scripts目录。我们的数据代码、逻辑代码和界面代码要分开,所以建立DataLogicView三个子目录,
在这里插入图片描述
Data目录中新建一个TaskCfg.cs脚本,
在这里插入图片描述

3.2、定义任务配置结构:TaskCfgItem

LitJson提供了一个JsonMapper.ToObject<T>(jsonString)方法,可以直接将json字符串转为类对象,前提是类的字段名要与json的字段相同,所以我们先定义一个与json字段名相同的类TaskCfgItem,如下:

// TaskCfg.cs

/// <summary>
/// 任务配置结构
/// </summary>
public class TaskCfgItem
{
    
    
    public int task_chain_id;
    public int task_sub_id;
    public string icon;
    public string desc;
    public string task_target;
    public int target_amount;
    public string award;
    public string open_chain;
}
3.3、定义存储配置的容器

为了方便在内存中索引配置表,我们使用字典来存储,定义一个用来存放配置数据的字典:

// TaskCfg.cs

// 任务配置,(链id : 子任务id : TaskCfgItem)
private Dictionary<int, Dictionary<int, TaskCfgItem>> m_cfg;
3.4、读取配置:LoadCfg

我们封装一个LoadCfg方法来读取配置,如下:

// TaskCfg.cs

/// <summary>
/// 读取配置
/// </summary>
public void LoadCfg()
{
    
    
    m_cfg = new Dictionary<int, Dictionary<int, TaskCfgItem>>();
    var txt = Resources.Load<TextAsset>("task_cfg").text;
    var jd = JsonMapper.ToObject<JsonData>(txt);


    for (int i = 0, cnt = jd.Count; i < cnt; ++i)
    {
    
    
        var itemJd = jd[i] as JsonData;
        TaskCfgItem cfgItem = JsonMapper.ToObject<TaskCfgItem>(itemJd.ToJson());

        if (!m_cfg.ContainsKey(cfgItem.task_chain_id))
        {
    
    
            m_cfg[cfgItem.task_chain_id] = new Dictionary<int, TaskCfgItem>();
        }
        m_cfg[cfgItem.task_chain_id].Add(cfgItem.task_sub_id, cfgItem);
    }

}
3.5、索引任务配置项:GetCfgItem

为了索引任务配置项,我们再封装一个GetCfgItem方法,

// TaskCfg.cs

/// <summary>
/// 获取配置项
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="taskSubId">任务子id</param>
/// <returns></returns>
public TaskCfgItem GetCfgItem(int chainId, int taskSubId)
{
    
    
    if (m_cfg.ContainsKey(chainId) && m_cfg[chainId].ContainsKey(taskSubId))
        return m_cfg[chainId][taskSubId];
    return null;
}
3.6、使用单例模式

我们希望TaskCfg全局只有一个对象,我们使用单例模式,

// TaskCfg.cs

// 单例模式
private static TaskCfg s_instance;
public static TaskCfg instance
{
    
    
    get
    {
    
    
        if (null == s_instance)
            s_instance = new TaskCfg();
        return s_instance;
    }
}

这样我们就可以通过TaskCfg.instance来调用它的public方法了,如下

// 调用读取配置的方法
TaskCfg.instance.LoadCfg();
3.7、思维导图

画个图,
在这里插入图片描述

3.8、TaskCfg.cs完整代码

最终,TaskCfg.cs完整代码如下:

/// <summary>
/// 任务配置读取与查询
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
/// </summary>

using System.Collections.Generic;
using UnityEngine;
using LitJson;

public class TaskCfg
{
    
    
    /// <summary>
    /// 读取配置
    /// </summary>
    public void LoadCfg()
    {
    
    
        m_cfg = new Dictionary<int, Dictionary<int, TaskCfgItem>>();
        var txt = Resources.Load<TextAsset>("task_cfg").text;
        var jd = JsonMapper.ToObject<JsonData>(txt);
      
        for (int i = 0, cnt = jd.Count; i < cnt; ++i)
        {
    
    
            var itemJd = jd[i] as JsonData;
            TaskCfgItem cfgItem = JsonMapper.ToObject<TaskCfgItem>(itemJd.ToJson());

            if (!m_cfg.ContainsKey(cfgItem.task_chain_id))
            {
    
    
                m_cfg[cfgItem.task_chain_id] = new Dictionary<int, TaskCfgItem>();
            }
            m_cfg[cfgItem.task_chain_id].Add(cfgItem.task_sub_id, cfgItem);
        }
    }

    /// <summary>
    /// 获取配置项
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="taskSubId">任务子id</param>
    /// <returns></returns>
    public TaskCfgItem GetCfgItem(int chainId, int taskSubId)
    {
    
    
        if (m_cfg.ContainsKey(chainId) && m_cfg[chainId].ContainsKey(taskSubId))
            return m_cfg[chainId][taskSubId];
        return null;
    }

    // 任务配置,(链id : 子任务id : TaskCfgItem)
    private Dictionary<int, Dictionary<int, TaskCfgItem>> m_cfg;

    private static TaskCfg s_instance;
    public static TaskCfg instance
    {
    
    
        get
        {
    
    
            if (null == s_instance)
                s_instance = new TaskCfg();
            return s_instance;
        }
    }
}

/// <summary>
/// 任务配置结构
/// </summary>
public class TaskCfgItem
{
    
    
    public int task_chain_id;
    public int task_sub_id;
    public string icon;
    public string desc;
    public string task_target;
    public int target_amount;
    public string award;
    public string open_chain;
}

七、任务数据增删改查:TaskData.cs脚本

1、创建TaskData.cs脚本

严格来说,我们需要在服务端存储任务数据、更新任务进度等,这里我就只是在客户端进行模拟,不做服务端了。
Scripts / Data目录中新建一个TaskData.cs脚本,来实现任务数据增删改查的功能。
在这里插入图片描述

2、定义任务数据:TaskDataItem

我们要读写任务数据,需要先定义任务数据结构TaskDataItem

// TaskData.cs

/// <summary>
/// 任务数据
/// </summary>
public class TaskDataItem
{
    
    
    // 链id
    public int task_chain_id;
    // 任务子id
    public int task_sub_id;
    // 进度
    public int progress;
    // 奖励是否被领取了,0:未被领取,1:已被领取
    public short award_is_get;
}

3、本地数据读写:PlayerPrefs

Unity提供了一个PlayerPrefs类给我们,可以很方便进行本地持久化数据读写。
在这里插入图片描述
读:

string defaultJson = "[{'task_chain_id':1,'task_sub_id':1,'progress':0,'award_is_get':0}]";
string jsonStr = PlayerPrefs.GetString("TASK_DATA", defaultJson);

写:

string jsonStr = "[{'task_chain_id':1,'task_sub_id':1,'progress':0,'award_is_get':0}]";
PlayerPrefs.SetString("TASK_DATA", jsonStr);

清空:

PlayerPrefs.DeleteKey("TASK_DATA");

4、定义存储数据的容器

定义一个容器用于内存中存储数据,

private List<TaskDataItem> m_taskDatas;

5、从本地读取任务数据

使用PlayerPrefs.GetString接口从本地读取数据,使用Action cb回调是为了模拟实际场景中从服务端数据库读取数据(异步)的过程,

/// <summary>
/// 从数据库读取任务数据
/// </summary>
/// <param name="cb"></param>
public void GetTaskDataFromDB(Action cb)
{
    
    
    // 正规是与服务端通信,从数据库中读取,这里纯客户端进行模拟,直接使用PlayerPrefs从客户端本地读取
    var jsonStr = PlayerPrefs.GetString("TASK_DATA", "[{'task_chain_id':1,'task_sub_id':1,'progress':0,'award_is_get':0}]");
    var taskList = JsonMapper.ToObject<List<TaskDataItem>>(jsonStr);
    for (int i = 0, cnt = taskList.Count; i < cnt; ++i)
    {
    
    
        AddOrUpdateData(taskList[i]);
    }
    cb();
}

6、写任务数据到本地

使用PlayerPrefs.SetString接口写数据到本地,

/// <summary>
/// 写数据到数据库
/// </summary>
private void SaveDataToDB()
{
    
    
    var jsonStr = JsonMapper.ToJson(m_taskDatas);
    PlayerPrefs.SetString("TASK_DATA", jsonStr);
}

7、查询指定任务的数据

/// <summary>
/// 获取某个任务数据
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="subId">任务子id</param>
/// <returns></returns>
public TaskDataItem GetData(int chainId, int subId)
{
    
    
    for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
    {
    
    
        var item = m_taskDatas[i];
        if (chainId == item.task_chain_id && subId == item.task_sub_id)
            return item;
    }
    return null;
}

8、任务数据增加或更新

新增任务时,需要对列表进行重新排序,确保主线任务(即task_chain_id1)的任务排在最前面,

/// <summary>
/// 添加或更新任务
/// </summary>
public void AddOrUpdateData(TaskDataItem itemData)
{
    
    
    bool isUpdate = false;
    for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
    {
    
    
        var item = m_taskDatas[i];
        if (itemData.task_chain_id == item.task_chain_id && itemData.task_sub_id == item.task_sub_id)
        {
    
    
            // 更新数据
            m_taskDatas[i] = itemData;
            isUpdate = true;
            break;
        }
    }
    if(!isUpdate)
        m_taskDatas.Add(itemData);
    // 排序,确保主线在最前面
    m_taskDatas.Sort((a, b) => 
    {
    
    
        return a.task_chain_id.CompareTo(b.task_chain_id);
    });
    SaveDataToDB();
}

9、任务数据删除

/// <summary>
/// 移除任务数据
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="subId">任务子id</param>
public void RemoveData(int chainId, int subId)
{
    
    
    for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
    {
    
    
        var item = m_taskDatas[i];
        if (chainId == item.task_chain_id && subId == item.task_sub_id)
        {
    
    
            m_taskDatas.Remove(item);
            SaveDataToDB();
            return;
        }
    }
}

10、思维导图

按照惯例,画个图:
在这里插入图片描述

11、TaskData.cs完整代码

最终TaskData.cs完整代码如下:

/// <summary>
/// 任务数据增删改查
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
/// </summary>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LitJson;
using System;

public class TaskData
{
    
    
    public TaskData()
    {
    
    
        m_taskDatas = new List<TaskDataItem>();
    }

    /// <summary>
    /// 从数据库读取任务数据
    /// </summary>
    /// <param name="cb"></param>
    public void GetTaskDataFromDB(Action cb)
    {
    
    
        // 正规是与服务端通信,从数据库中读取,这里纯客户端进行模拟,直接使用PlayerPrefs从客户端本地读取
        var jsonStr = PlayerPrefs.GetString("TASK_DATA", "[{'task_chain_id':1,'task_sub_id':1,'progress':0,'award_is_get':0}]");
        var taskList = JsonMapper.ToObject<List<TaskDataItem>>(jsonStr);
        for (int i = 0, cnt = taskList.Count; i < cnt; ++i)
        {
    
    
            AddOrUpdateData(taskList[i]);
        }
        cb();
    }

    /// <summary>
    /// 添加或更新任务
    /// </summary>
    public void AddOrUpdateData(TaskDataItem itemData)
    {
    
    
        bool isUpdate = false;
        for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
        {
    
    
            var item = m_taskDatas[i];
            if (itemData.task_chain_id == item.task_chain_id && itemData.task_sub_id == item.task_sub_id)
            {
    
    
                // 更新数据
                m_taskDatas[i] = itemData;
                isUpdate = true;
                break;
            }
        }
        if(!isUpdate)
            m_taskDatas.Add(itemData);
        // 排序,确保主线在最前面
        m_taskDatas.Sort((a, b) => 
        {
    
    
            return a.task_chain_id.CompareTo(b.task_chain_id);
        });
        SaveDataToDB();
    }

    /// <summary>
    /// 获取某个任务数据
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="subId">任务子id</param>
    /// <returns></returns>
    public TaskDataItem GetData(int chainId, int subId)
    {
    
    
        for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
        {
    
    
            var item = m_taskDatas[i];
            if (chainId == item.task_chain_id && subId == item.task_sub_id)
                return item;
        }
        return null;
    }

    /// <summary>
    /// 移除任务数据
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="subId">任务子id</param>
    public void RemoveData(int chainId, int subId)
    {
    
    
        for (int i = 0, cnt = m_taskDatas.Count; i < cnt; ++i)
        {
    
    
            var item = m_taskDatas[i];
            if (chainId == item.task_chain_id && subId == item.task_sub_id)
            {
    
    
                m_taskDatas.Remove(item);
                SaveDataToDB();
                return;
            }
        }
    }

    /// <summary>
    /// 写数据到数据库
    /// </summary>
    private void SaveDataToDB()
    {
    
    
        var jsonStr = JsonMapper.ToJson(m_taskDatas);
        PlayerPrefs.SetString("TASK_DATA", jsonStr);
    }

    public void ResetData(Action cb)
    {
    
    
        PlayerPrefs.DeleteKey("TASK_DATA");
        m_taskDatas.Clear();
        GetTaskDataFromDB(cb);
    }

    public List<TaskDataItem> taskDatas
    {
    
    
        get {
    
     return m_taskDatas; }
    }

    // 任务数据
    private List<TaskDataItem> m_taskDatas;
}

/// <summary>
/// 任务数据
/// </summary>
public class TaskDataItem
{
    
    
    // 链id
    public int task_chain_id;
    // 任务子id
    public int task_sub_id;
    // 进度
    public int progress;
    // 奖励是否被领取了,0:未被领取,1:已被领取
    public short award_is_get;
}

八、任务逻辑:TaskLogic.cs

1、创建TaskLogics脚本

Scripts / Logic目录中创建TaskLogic.cs脚本,
在这里插入图片描述
任务的逻辑其实就是进度更新、任务完成后领奖、开启下一个任务、开启分支任务等,我们挨个来实现。

2、成员:TaskData

先把TaskData作为成员变量,并提供一个数据属性taskDatas,方便访问,

private TaskData m_taskData;

public List<TaskDataItem> taskDatas
{
    
    
    get {
    
     return m_taskData.taskDatas; }
}

public TaskLogic()
{
    
    
    m_taskData = new TaskData();
}

3、获取任务数据

/// <summary>
/// 获取任务数据
/// </summary>
/// <param name="cb">回调</param>
public void GetTaskData(Action cb)
{
    
    
    m_taskData.GetTaskDataFromDB(cb);
}

4、更新任务进度

使用Action<int, bool>回调是为了模拟实际场景中与服务端通信(异步),处理结果会有个返回码ErrorCode(回调函数第一个参数),客户端需判断ErrorCode的值来进行处理,一般约定ErrorCode0表示成功,回调函数第二个参数是是否任务进度已达成,如果任务达成,客户端需要显示领奖按钮,

/// <summary>
/// 更新任务进度
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="subId">任务子id</param>
/// <param name="deltaProgress">进度增加量</param>
/// <param name="cb">回调</param>
public void AddProgress(int chainId, int subId, int deltaProgress, Action<int, bool> cb)
{
    
    
    var data = m_taskData.GetData(chainId, subId);
    if (null != data)
    {
    
    
        data.progress += deltaProgress;
        m_taskData.AddOrUpdateData(data);
        var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
        if (null != cfg)
            cb(0, data.progress >= cfg.target_amount);
        else
            cb(-1, false);
    }
    else
    {
    
    
        cb(-1, false);
    }
}

5、领取任务奖励

是否领奖的状态字段为award_is_get,为0表示未领奖,为1表示已领过奖。
领完奖的任务从列表中移除,并开启下一个任务(如果配置了开启支线任务,则还需要配套开启对应的支线任务),

/// <summary>
/// 领取任务奖励
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="subId">任务子id</param>
/// <param name="cb">回调</param>
public void GetAward(int chainId, int subId, Action<int, string> cb)
{
    
    
    var data = m_taskData.GetData(chainId, subId);
    if (null == data)
    {
    
    
        cb(-1, "{}");
        return;
    }
    if (0 == data.award_is_get)
    {
    
    
        data.award_is_get = 1;
        m_taskData.AddOrUpdateData(data);
        GoNext(chainId, subId);
        var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
        cb(0, cfg.award);
    }
    else
    {
    
    
        cb(-2, "{}");
    }
}

6、一键领取任务的奖励

遍历所有达成进度且未领奖的任务,支线领奖,并开开启每个领完奖的任务的下一个任务(如果配置了开启支线任务,则还需要配套开启对应的支线任务),

/// <summary>
/// 一键领取所有任务的奖励
/// </summary>
/// <param name="cb"></param>
public void OneKeyGetAward(Action<int, string> cb)
{
    
    
    int totalGold = 0;
    var tmpTaskDatas = new List<TaskDataItem>(m_taskData.taskDatas);

    for (int i = 0, cnt = tmpTaskDatas.Count; i < cnt; ++i)
    {
    
    
        var oneTask = tmpTaskDatas[i];
        var cfg = TaskCfg.instance.GetCfgItem(oneTask.task_chain_id, oneTask.task_sub_id);
        if (oneTask.progress >= cfg.target_amount && 0 == oneTask.award_is_get)
        {
    
    
            oneTask.award_is_get = 1;
            m_taskData.AddOrUpdateData(oneTask);
            var awardJd = JsonMapper.ToObject(cfg.award);
            totalGold += int.Parse(awardJd["gold"].ToString());
            GoNext(oneTask.task_chain_id, oneTask.task_sub_id);
        }
    }
    if (totalGold > 0)
    {
    
    
        JsonData totalAward = new JsonData();
        totalAward["gold"] = totalGold;
        cb(0, JsonMapper.ToJson(totalAward));
    }
    else
    {
    
    
        cb(-1, null);
    }
}

7、开启下一个任务(含支线)

约定任务id递增,开启下一个任务就是查找id+1的任务并开启。
支线任务的开启是open_chain字段,格式链id|任务子id,多个支线以,号隔开,例:3|1,5|1表示开启链3的子任务1链5的子任务1

/// <summary>
/// 开启下一个任务(含支线)
/// </summary>
/// <param name="chainId">链id</param>
/// <param name="subId">任务子id</param>
private void GoNext(int chainId, int subId)
{
    
    
    var data = m_taskData.GetData(chainId, subId);
    var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
    var nextCfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id + 1);

    if (1 == data.award_is_get)
    {
    
    
        // 移除掉已领奖的任务
        m_taskData.RemoveData(chainId, subId);

        // 开启下一个任务
        if (null != nextCfg)
        {
    
    
            TaskDataItem dataItem = new TaskDataItem();
            dataItem.task_chain_id = nextCfg.task_chain_id;
            dataItem.task_sub_id = nextCfg.task_sub_id;
            dataItem.progress = 0;
            dataItem.award_is_get = 0;
            m_taskData.AddOrUpdateData(dataItem);
        }

        // 开启支线任务
        if (!string.IsNullOrEmpty(cfg.open_chain))
        {
    
    
            // 开启新分支
            var chains = cfg.open_chain.Split(',');
            for (int i = 0, len = chains.Length; i < len; ++i)
            {
    
    
                var task = chains[i].Split('|');
                TaskDataItem subChainDataItem = new TaskDataItem();
                subChainDataItem.task_chain_id = int.Parse(task[0]);
                subChainDataItem.task_sub_id = int.Parse(task[1]);
                subChainDataItem.progress = 0;
                subChainDataItem.award_is_get = 0;
                m_taskData.AddOrUpdateData(subChainDataItem);
            }
        }
    }
}

8、思维导图

画一下图,
在这里插入图片描述

9、TaskLogic.cs完整代码

最终TaskLogic.cs完整代码如下

/// <summary>
/// 任务逻辑
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
/// </summary>

using System.Collections.Generic;
using UnityEngine;
using LitJson;
using System;

public class TaskLogic
{
    
    
    public TaskLogic()
    {
    
    
        m_taskData = new TaskData();
    }

    /// <summary>
    /// 获取任务数据
    /// </summary>
    /// <param name="cb">回调</param>
    public void GetTaskData(Action cb)
    {
    
    
        m_taskData.GetTaskDataFromDB(cb);
    }

    /// <summary>
    /// 更新任务进度
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="subId">任务子id</param>
    /// <param name="deltaProgress">进度增加量</param>
    /// <param name="cb">回调</param>
    public void AddProgress(int chainId, int subId, int deltaProgress, Action<int, bool> cb)
    {
    
    
        var data = m_taskData.GetData(chainId, subId);
        if (null != data)
        {
    
    
            data.progress += deltaProgress;
            m_taskData.AddOrUpdateData(data);
            var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
            if (null != cfg)
                cb(0, data.progress >= cfg.target_amount);
            else
                cb(-1, false);
        }
        else
        {
    
    
            cb(-1, false);
        }
    }

    /// <summary>
    /// 一键领取所有任务的奖励
    /// </summary>
    /// <param name="cb"></param>
    public void OneKeyGetAward(Action<int, string> cb)
    {
    
    
        int totalGold = 0;
        var tmpTaskDatas = new List<TaskDataItem>(m_taskData.taskDatas);

        for (int i = 0, cnt = tmpTaskDatas.Count; i < cnt; ++i)
        {
    
    
            var oneTask = tmpTaskDatas[i];
            var cfg = TaskCfg.instance.GetCfgItem(oneTask.task_chain_id, oneTask.task_sub_id);
            if (oneTask.progress >= cfg.target_amount && 0 == oneTask.award_is_get)
            {
    
    
                oneTask.award_is_get = 1;
                m_taskData.AddOrUpdateData(oneTask);
                var awardJd = JsonMapper.ToObject(cfg.award);
                totalGold += int.Parse(awardJd["gold"].ToString());
                GoNext(oneTask.task_chain_id, oneTask.task_sub_id);
            }
        }
        if (totalGold > 0)
        {
    
    
            JsonData totalAward = new JsonData();
            totalAward["gold"] = totalGold;
            cb(0, JsonMapper.ToJson(totalAward));
        }
        else
        {
    
    
            cb(-1, null);
        }
    }

    /// <summary>
    /// 领取任务奖励
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="subId">任务子id</param>
    /// <param name="cb">回调</param>
    public void GetAward(int chainId, int subId, Action<int, string> cb)
    {
    
    
        var data = m_taskData.GetData(chainId, subId);
        if (null == data)
        {
    
    
            cb(-1, "{}");
            return;
        }
        if (0 == data.award_is_get)
        {
    
    
            data.award_is_get = 1;
            m_taskData.AddOrUpdateData(data);
            GoNext(chainId, subId);
            var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
            cb(0, cfg.award);
        }
        else
        {
    
    
            cb(-2, "{}");
        }
    }

    /// <summary>
    /// 触发下一个任务,并开启支线任务
    /// </summary>
    /// <param name="chainId">链id</param>
    /// <param name="subId">任务子id</param>
    private void GoNext(int chainId, int subId)
    {
    
    
        var data = m_taskData.GetData(chainId, subId);
        var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
        var nextCfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id + 1);

        if (1 == data.award_is_get)
        {
    
    
            // 移除掉已领奖的任务
            m_taskData.RemoveData(chainId, subId);

            // 开启下一个任务
            if (null != nextCfg)
            {
    
    
                TaskDataItem dataItem = new TaskDataItem();
                dataItem.task_chain_id = nextCfg.task_chain_id;
                dataItem.task_sub_id = nextCfg.task_sub_id;
                dataItem.progress = 0;
                dataItem.award_is_get = 0;
                m_taskData.AddOrUpdateData(dataItem);
            }

            // 开启支线任务
            if (!string.IsNullOrEmpty(cfg.open_chain))
            {
    
    
                // 开启新分支
                var chains = cfg.open_chain.Split(',');
                for (int i = 0, len = chains.Length; i < len; ++i)
                {
    
    
                    var task = chains[i].Split('|');
                    TaskDataItem subChainDataItem = new TaskDataItem();
                    subChainDataItem.task_chain_id = int.Parse(task[0]);
                    subChainDataItem.task_sub_id = int.Parse(task[1]);
                    subChainDataItem.progress = 0;
                    subChainDataItem.award_is_get = 0;
                    m_taskData.AddOrUpdateData(subChainDataItem);
                }
            }
        }
    }

    public void ResetAll(Action cb)
    {
    
    
        m_taskData.ResetData(cb);
    }

    public List<TaskDataItem> taskDatas
    {
    
    
        get {
    
     return m_taskData.taskDatas; }
    }

    private TaskData m_taskData;
    private static TaskLogic s_instance;
    public static TaskLogic instance
    {
    
    
        get
        {
    
    
            if (null == s_instance)
                s_instance = new TaskLogic();
            return s_instance;
        }
    }
}

九、UI界面制作

1、UI资源获取

简单的UI资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮
在这里插入图片描述
找一个形状合适的,可以进行调色,我一般是调成白色,
在这里插入图片描述

因为Unity中可以设置Color,这样我们只需要一个白色按钮就可以在Unity中创建不同颜色的按钮了。
弄点基础的美术资源,
在这里插入图片描述
注:那个头像是我自己用PhotoShop画的哦,我之前用PhotoShop画过一幅原创连环画,如下:
在这里插入图片描述
同时,我们还需要任务图标,也找一些,
在这里插入图片描述
注意所有要使用UGUI来展示资源都设置为Sprite (2D and UI)格式。
在这里插入图片描述

注,关于资源的获取,我之前写过一篇文章:《Unity游戏开发——新发教你做游戏(二):60个Unity免费资源获取网站》,感兴趣的同学可以看下,

2、场景界面制作:Main场景,人生如梦

养成好习惯,不管你是单场景还是多场景,入口场景命名为Main
在场景中使用UGUI简单做下入口界面:MainPanel
在这里插入图片描述
这个任务系统的主题我定为:人生如梦。
在这里插入图片描述

3、制作列表界面预设:TaskPanel.prefab

根据需求,我们的任务要以列表的显示展示,使用UGUI制作任务列表界面,
在这里插入图片描述
如下,
在这里插入图片描述
界面保存为TaskPanel.prefab,放在Resources目录中,
在这里插入图片描述

4、制作提示框界面预设:TipsPanel.prefab

为了在客户端模拟测试,做一个提示框界面,
在这里插入图片描述
如下:
在这里插入图片描述
界面保存为TipsPanel.prefab,放在Resources目录中,
在这里插入图片描述
嘛,顺手做个界面动画吧,

注:关于动画相关的教程,我之前写过一些,感兴趣的同学可以看下:
《Unity使用Animator控制动画播放,皮皮猫打字机游戏》
《Unity动画状态机Animator使用》
《Unity动画使用混合树BlendTree实现动画过渡控制》
《新发教你做游戏(七):Animator控制角色动画播放》

请添加图片描述

5、制作奖励界面预设:AwardPanel.prefab

领取任务奖励要有个奖励UI展示,做一个,
在这里插入图片描述
界面保存为AwardPanel.prefab,放在Resources目录中,
在这里插入图片描述

也顺手做个动画,
请添加图片描述

十、编写界面代码

界面预设制作好了,接下来就是写界面交互的代码了。

1、入口脚本:Main.cs

C/C++Main入口函数一样,我们的游戏也需要有一个脚本作为入口脚本。
我们创建一个Main.cs脚本,挂到场景中的MainPanel节点上,
在这里插入图片描述
Main.cs脚本代码如下,主要是做一些全局变量、配置、数据等的初始化,然后显示界面,不过我们任务界面代码还没写,先留个TODO

using UnityEngine;

/// <summary>
/// 入口脚本
/// </summary>
public class Main : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        GlobalObj.s_canvasTrans = GameObject.Find("Canvas").transform;
        // 加载任务配置
        TaskCfg.instance.LoadCfg();
        // 获取任务数据
        TaskLogic.instance.GetTaskData(()=> 
        {
    
    
            // TODO: 显示任务界面
            
        });
    }
}

public class GlobalObj
{
    
    
    public static Transform s_canvasTrans;
}

2、任务列表界面

2.1、循环复用列表

任务界面以列表展示任务,我之前做过一个循环复用列表的功能,可以参见我之前这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》
在这里插入图片描述
我把之前写的RecyclingList脚本拷贝过来,
RecyclingList脚本地址:https://codechina.csdn.net/linxinfa/UnityRecyclingListDemo/-/tree/master/Assets/Scripts/RecyclingList
在这里插入图片描述
ScrollVIew挂上RecyclingListView脚本,脚本的ChildObj对象需要是RecyclingListViewItem类型的,我们下面会写一个TaskItemUI继承RecyclingListViewItem,这里ChildObj先留空,
在这里插入图片描述

2.2、列表项脚本:TaskItemUI.cs

Scripts / View目录中创建TaskItemUI.cs脚本,它要继承RecyclingListViewItem

public class TaskItemUI : RecyclingListViewItem

定义一些UI对象,

// 描述
public Text desText;
// 进度
public Text progressText;
// 前往按钮
public Button goAheadBtn;
// 领奖按钮
public Button getAwardBtn;
// 进度条
public Slider progressSlider;
// 任务图标
public Image icon;
// 任务类型标记,主线/支线 
public Image taskType;

TaskItemUI脚本挂到ChildItem节点上,并赋值各个UI对象,
在这里插入图片描述
现在我们可以给RecyclingListView脚本赋值ChildObj对象了,
在这里插入图片描述
TaskItemUI.cs脚本唯一要做的事情就是根据数据更新UI
在这里插入图片描述

// TaskItemUI.cs

public Action updateListCb;

/// <summary>
/// 更新UI
/// </summary>
/// <param name="data"></param>
public void UpdateUI(TaskDataItem data)
{
    
    
    var cfg = TaskCfg.instance.GetCfgItem(data.task_chain_id, data.task_sub_id);
    if (null != cfg)
    {
    
    
        desText.text = cfg.desc;
        // TODO 设置图标
        // icon.sprite 
        
        // TODO 设置主线/支线图标
        // var taskTypeSpriteName = 1 == cfg.task_chain_id ? "zhu" : "zhi";
        // taskType.sprite
        
        progressText.text = data.progress + "/" + cfg.target_amount;
        progressSlider.value = (float)data.progress / cfg.target_amount;
        // 前往按钮
        goAheadBtn.onClick.RemoveAllListeners();
        goAheadBtn.onClick.AddListener(() =>
        {
    
    
            // TODO 前往任务
        });

		// 领奖按钮
        getAwardBtn.onClick.RemoveAllListeners();
        getAwardBtn.onClick.AddListener(() =>
        {
    
    
            TaskLogic.instance.GetAward(data.task_chain_id, data.task_sub_id, (errorCode, award) => 
            {
    
    
                if(0 == errorCode)
                {
    
    
                    // TODO 领奖界面
                    
                    updateListCb();
                }
            });
        });

        goAheadBtn.gameObject.SetActive(data.progress < cfg.target_amount);
        getAwardBtn.gameObject.SetActive(data.progress >= cfg.target_amount && 0 == data.award_is_get);
    }
}

上面代码有几个TODO
1 设置图标我们等下写个图标资源管理器;
2 任务的前往逻辑,我们要弹出提示框;
3 领奖要显示奖励界面。
现在,我们继续往下做。

2.3、列表界面脚本:TaskPanel.cs

Scripts / View目录中创建TaskPanel.cs脚本,把它挂到TaskPanel界面的根节点上,
在这里插入图片描述
最关键的,定义RecyclingListView成员对象,

// TaskPanel.cs

public RecyclingListView scrollList;

我们的列表更新就是监听它的ItemCallback回调的,

// TaskPanel.cs

// 列表item更新回调
scrollList.ItemCallback = PopulateItem;

// ...

private void PopulateItem(RecyclingListViewItem item, int rowIndex)
{
    
    
	var child = item as TaskItemUI;
	// 刷新某个item
	child.UpdateUI(TaskLogic.instance.taskDatas[rowIndex]);
	child.updateListCb = () =>
    {
    
    
    	// 刷新整个列表
        RefreshAll();
    };
}

/// <summary>
/// 刷新整个列表
/// </summary>
private void RefreshAll()
{
    
    
    scrollList.RowCount = TaskLogic.instance.taskDatas.Count;
    scrollList.Refresh();
}

我们需要告诉RecyclingListView我们的列表的item的数量,方便它进行计算,

// TaskPanel.cs

// 设置数据,此时列表会执行更新
scrollList.RowCount = TaskLogic.instance.taskDatas.Count;

为了便于显示TaskPanel界面,我们封装一个staticShow方法,

// TaskPanel.cs

private static GameObject s_taskPanelPrefab;

// 显示任务界面
public static void Show()
{
    
    
    if (null == s_taskPanelPrefab)
        s_taskPanelPrefab = Resources.Load<GameObject>("TaskPanel");
    var panelObj = Instantiate(s_taskPanelPrefab);
    panelObj.transform.SetParent(GlobalObj.s_canvasTrans, false);
}

这样,我们就可以在Main.cs脚本中加上这个TaskPanel.Show()的调用了,

// Main.cs

void Start()
{
    
    
	// ...
	
    // 获取任务数据
    TaskLogic.instance.GetTaskData(()=> 
    {
    
    
        // 显示任务界面
        TaskPanel.Show();
    });
}

3、提示框界面:TipsPanel.cs

Scripts / View目录中创建TipsPanel.cs脚本,先定义三个按钮对象,

public Button closeBtn;
public Button addProgressBtn;
public Button onekeyBtn;

在这里插入图片描述
TipsPanel预设跟节点挂上TipsPanel.cs脚本,赋值按钮对象,
在这里插入图片描述
分别写三个按钮的点击逻辑。
关闭按钮:

// TipsPanel.cs

// 关闭按钮
closeBtn.onClick.AddListener(() =>
{
    
    
    Destroy(gameObject);
});

进度+1按钮:

// TipsPanel.cs

private int m_taskChainId;
private int m_tasksubId;
private Action updateTaskDataCb;

// 进度+1
addProgressBtn.onClick.AddListener(() =>
{
    
    
    Destroy(gameObject);
    TaskLogic.instance.AddProgress(m_taskChainId, m_tasksubId, 1, (errorCode, finishTask) =>
    {
    
    
        updateTaskDataCb();
    });
});

一键完成按钮:

// TipsPanel.cs

// 一键完成
onekeyBtn.onClick.AddListener(() =>
{
    
    
    Destroy(gameObject);
    var cfg = TaskCfg.instance.GetCfgItem(m_taskChainId, m_tasksubId);
    TaskLogic.instance.AddProgress(m_taskChainId, m_tasksubId, cfg.target_amount, (errorCode, finishTask) =>
    {
    
    
        updateTaskDataCb();
    });
});

同理,为了方便显示,也封装一个静态的Show方法:

// TipsPanel.cs

private static GameObject s_tipsPanelPrefab;
// 显示任务界面
public static void Show(int chainId, int subId, Action cb)
{
    
    
	if (null == s_tipsPanelPrefab)
	    s_tipsPanelPrefab = Resources.Load<GameObject>("TipsPanel");
	var panelObj = Instantiate(s_tipsPanelPrefab);
	panelObj.transform.SetParent(GlobalObj.s_canvasTrans, false);
	var panelBhv = panelObj.GetComponent<TipsPanel>();
	panelBhv.Init(chainId, subId, cb);
}

public void Init(int chainId, int subId, Action cb)
{
    
    
   	m_taskChainId = chainId;
   	m_tasksubId = subId;
   	updateTaskDataCb = cb;
}

TaskItemUI.cs脚本的前往按钮补上TipsPanel.Show调用,

// TaskItemUI.cs

goAheadBtn.onClick.AddListener(() =>
{
    
    
   TipsPanel.Show(data.task_chain_id, data.task_sub_id, () =>
   {
    
    
       UpdateUI(data);
   });
});

在这里插入图片描述

4、奖励界面:AwardPanel.cs

Scripts / View目录中创建AwardPanel.cs脚本,
定义UI对象,

public Text goldText;
public Button bgBtn;
private GameObject m_selfGo;

private void Awake()
{
    
    
    m_selfGo = gameObject;
}

AwardPanel.cs脚本挂到AwardPanel预设跟节点上,赋值UI对象,
在这里插入图片描述
逻辑很简单,显示金币奖励,加个1.5秒自动销毁,点击空白处销毁的逻辑,如下:

// AwardPanel.cs

public void Init(string award)
{
    
    
    var jd = JsonMapper.ToObject(award);
    goldText.text = jd["gold"].ToString();
    bgBtn.onClick.AddListener(() =>
    {
    
    
        SelfDestroy();
    });

    // 3秒后自动销毁
    Invoke("SelfDestroy", 1.5f);
}

private void Awake()
{
    
    
    m_selfGo = gameObject;
}

private void SelfDestroy()
{
    
    
    if (null != m_selfGo)
    {
    
    
        Destroy(m_selfGo);
        m_selfGo = null;
    }
}

也封装一个静态的Show方法,

private static GameObject s_awardPanelPrefab;

/// <summary>
/// 显示奖励界面
/// </summary>
public static void Show(string award)
{
    
    
    if (null == s_awardPanelPrefab)
        s_awardPanelPrefab = Resources.Load<GameObject>("AwardPanel");
    var panelObj = Instantiate(s_awardPanelPrefab);
    panelObj.transform.SetParent(GlobalObj.s_canvasTrans, false);
    var panelBhv = panelObj.GetComponent<AwardPanel>();
    panelBhv.Init(award);
}

TaskItemUI.cs脚本的领奖按钮补上AwardPanel.Show调用,

// TaskItemUI.cs

getAwardBtn.onClick.AddListener(() =>
{
    
    
    TaskLogic.instance.GetAward(data.task_chain_id, data.task_sub_id, (errorCode, award) => 
    {
    
    
        Debug.Log("errorCode: " + errorCode + ", award: " + award);
        if(0 == errorCode)
        {
    
    
            AwardPanel.Show(award);
            updateListCb();
        }
    });
});

在这里插入图片描述

5、精灵资源管理器

我们需要根据任务配置来显示任务的图标,封装一个精灵管理器。
在这里插入图片描述

Scripts / View目录中创建一个SpriteManager.cs脚本,
代码如下:

// SpriteManager.cs

using System.Collections.Generic;
using UnityEngine;

public class SpriteManager
{
    
    
    /// <summary>
    /// 根据名字加载精灵资源
    /// </summary>
    public Sprite GetSprite(string name)
    {
    
    
        if (m_sprites.ContainsKey(name))
            return m_sprites[name];
        var sprite = Resources.Load<Sprite>("Sprites/" + name);
        m_sprites.Add(name, sprite);
        return sprite;
    }


    private Dictionary<string, Sprite> m_sprites = new Dictionary<string, Sprite>();
    private static SpriteManager s_instance;
    public static SpriteManager instance
    {
    
    
        get
        {
    
    
            if (null == s_instance)
                s_instance = new SpriteManager();
            return s_instance;
        }
    }
}

回到TaskItemUI.cs脚本中,补上精灵设置的调用:

// TaskItemUI.cs

public void UpdateUI(TaskDataItem data)
{
    
    
	// ...
	
	// 图标
	icon.sprite = SpriteManager.instance.GetSprite(cfg.icon);
	// 主线/支线标记
	var taskTypeSpriteName = 1 == cfg.task_chain_id ? "zhu" : "zhi";
	taskType.sprite = SpriteManager.instance.GetSprite(taskTypeSpriteName);
	
}

这样就可以根据配置显示不同的图标了,
在这里插入图片描述

十一、运行测试

代码写完了,一切就绪,运行Unity,测试效果如下:
请添加图片描述
请添加图片描述
人生如梦,究竟是要选梦醒来还是继续做梦呢?
请添加图片描述

十二、工程源码

本文的工程我一上传到CODE CHINA,感兴趣的同学可以自行下载下来学习。
工程地址:https://codechina.csdn.net/linxinfa/UnityChainTaskDemo
注:我的Unity版本是Unity 2020.1.14f1c1 (64-bit)
在这里插入图片描述

十三、完毕

好了,写得有点多了,就写到这里吧~
人生如梦,祝大家都能走上人生巅峰~
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,拜拜~

猜你喜欢

转载自blog.csdn.net/linxinfa/article/details/119023984