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

Posted 林新发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发实战Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程 | 链式任务 | 主线支线)相关的知识,希望对你有一定的参考价值。

本文最终效果如下:



工程思维导图:

工程源码见文章末尾。

文章目录

一、前言

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

二、什么是任务系统

任务系统就是一个有明确目标性的系统。通过设置任务来引导玩家进行游戏,让玩家更快的融入游戏中。
可以说任务系统几乎是游戏必备的模块,我们随便找个游戏都可以看到任务系统。

根据这位小朋友的需求,是要做 主线任务/支线任务 的系统。
简单的说,就是有一条 主线任务链,在完成主线任务链上的某个节点时,开启下一个任务,并可以开启一条或多条 支线任务链,主线任务和多条支线任务并行。画个图,方便大家理解:

三、需求文档

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

1、故事背景

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

2、任务链设计

下面,就是走上人生巅峰的任务链啦~

3、任务规则

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

4、界面样式设计

使用 原型图设计 软件制作界面样式,如下:

四、从哪里开始着手

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

我总结了一个客户端开发流程,大家可以按这个流程执行,

五、任务配置表

1、定义表头字段

根据需求,我们先定义表头字段,

字段解释:

字段数据类型说明
task_chain_idint链id,每个任务都有它对应的链id,同一条链上的任务的链id相同
task_sub_idint任务id,它是链上的任务id,不同链的任务id可以重复,从1开始往下自增
iconstring任务图标
descstring任务描述,这个会显示到界面中
task_targetstring任务目标,定义一个字符串来表示任务的目标类别,比如加班5次加班10次的任务目标是一样的,只是数量不同,同理,写博客5篇写博客100篇的任务目标也是一样的
target_amountint目标数量,比如加班5次的目标数量就是5,写博客100篇的目标数量就是100
awardstring奖励,json格式,例:{"gold":1000},表示奖励1000金币
open_chainstring要打开的支线任务,格式:链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;
        }
    }<

以上是关于游戏开发实战Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程 | 链式任务 | 主线支线)的主要内容,如果未能解决你的问题,请参考以下文章

游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)

游戏开发实战Unity从零开发多人视频聊天功能,无聊了就和自己视频聊天(附源码 | Mirror | 多人视频 | 详细教程)

后端开发者从零做一个移动应用(后端篇)

从零开始学Unity游戏开发

游戏开发实战手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)

游戏开发实战手把手教你从零跑一个Skynet,详细教程,含案例讲解(服务端 | Skynet | Ubuntu)