工程源码见文章末尾。
嗨,大家好,我是新发。
事情是这样的,有小朋友微信问我如何做任务系统,作为一个热心的技术博主,我都是能帮就帮。今天,我就来做一个任务系统吧。
任务系统就是一个有明确目标性的系统。通过设置任务来引导玩家进行游戏,让玩家更快的融入游戏中。
可以说任务系统几乎是游戏必备的模块,我们随便找个游戏都可以看到任务系统。
根据这位小朋友的需求,是要做 主线任务/支线任务 的系统。
简单的说,就是有一条 主线任务链,在完成主线任务链上的某个节点时,开启下一个任务,并可以开启一条或多条 支线任务链,主线任务和多条支线任务并行。画个图,方便大家理解:
由于只有我一个人,没有策划,那我就先充当策划,给自己写个需求文档吧~
主人公林新发刚刚大学毕业,开始面临一个人生难题:如何走上人生巅峰!
现在我们为林新发设计一套任务,帮助他走上人生巅峰吧~
主线任务必须按顺序完成;
主线任务与支线任务可以并行;
支线任务并不影响主线任务;
每完成一个任务都可以得到相应的奖励;
任务界面只显示当前要执行或已完成但还未领取奖励的任务;
任务界面中要显示每个任务当前的进度;
每个任务有个前往按钮,点击前往按钮触发任务执行或跳转到相应的界面;
每个任务有对应的图标,可配置;
界面底部有一键领奖按钮,点击一键领奖领取所有可以领奖的任务奖励。
对于萌新来说,拿到需求时可能不知道从哪里开始做,是先写代码还是先做界面?代码又是从哪里开始写?
字段解释:
字段 | 数据类型 | 说明 |
---|---|---|
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 |
根据我们上面设计的任务链,在配置表中配置任务数据,入下:
注:黄色的是主线任务,每条支线任务我都单独标了颜色方便阅读。
表格保存为链式任务.xlsx
,如下
Excel
表格是方便策划进行配置数值,游戏中并不是直接读取Excel
配置,实际项目中一般都是将Excel
转为xml
、json
、lua
或自定义的文本格式的配置。
我这里就以Excel
转Json
为例,处理Excel
我推荐大家使用python
来写工具,我之前写过一篇文章:《教你使用python读写Excel表格(增删改查操作),使用openpyxl库》,里面我详细介绍了使用python
的openpyxl
库来读写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
来读取文件。
先新建一个Resources
文件夹,
把task_cfg.bytes
放在Resources
目录中,
这样我们就可以直接使用Resources.Load
来读取task_cfg.bytes
文件了,如下:
string txt = Resources.Load<TextAsset>("task_cfg").text;
因为我们使用的是json
格式的文本,要解析它我们需要使用json
库,这里我推荐使用LitJson
,可以在GitHub
中找到LitJson
的开源项目,
地址:https://hub.fastgit.org/LitJSON/litjson
我们下载下来后,把src
目录中的LitJson
文件夹整个拷贝到我们Unity
工程中,如下:
这样我们就可以在C#
中使用LitJson
了。
使用时引入命名空间:
using LitJson;
现在我们开始写C#
代码,养成好习惯,先建好Scripts
目录。我们的数据代码、逻辑代码和界面代码要分开,所以建立Data
、Logic
和View
三个子目录,
在Data
目录中新建一个TaskCfg.cs
脚本,
LitJson
提供了一个JsonMapper.ToObject
方法,可以直接将json
字符串转为类对象,前提是类的字段名要与json
的字段相同,所以我们先定义一个与json
字段名相同的类TaskCfgItem
,如下:
// TaskCfg.cs
///
/// 任务配置结构
///
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;
}
为了方便在内存中索引配置表,我们使用字典来存储,定义一个用来存放配置数据的字典:
// TaskCfg.cs
// 任务配置,(链id : 子任务id : TaskCfgItem)
private Dictionary<int, Dictionary<int, TaskCfgItem>> m_cfg;
我们封装一个LoadCfg
方法来读取配置,如下:
// TaskCfg.cs
///
/// 读取配置
///
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);
}
}
为了索引任务配置项,我们再封装一个GetCfgItem
方法,
// TaskCfg.cs
///
/// 获取配置项
///
/// 链id
/// 任务子id
///
public TaskCfgItem GetCfgItem(int chainId, int taskSubId)
{
if (m_cfg.ContainsKey(chainId) && m_cfg[chainId].ContainsKey(taskSubId))
return m_cfg[chainId][taskSubId];
return null;
}
我们希望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();
最终,TaskCfg.cs
完整代码如下:
///
/// 任务配置读取与查询
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
///
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class TaskCfg
{
///
/// 读取配置
///
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);
}
}
///
/// 获取配置项
///
/// 链id
/// 任务子id
///
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;
}
}
}
///
/// 任务配置结构
///
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;
}
严格来说,我们需要在服务端存储任务数据、更新任务进度等,这里我就只是在客户端进行模拟,不做服务端了。
在Scripts / Data
目录中新建一个TaskData.cs
脚本,来实现任务数据增删改查的功能。
我们要读写任务数据,需要先定义任务数据结构TaskDataItem
,
// TaskData.cs
///
/// 任务数据
///
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;
}
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");
定义一个容器用于内存中存储数据,
private List<TaskDataItem> m_taskDatas;
使用PlayerPrefs.GetString
接口从本地读取数据,使用Action cb
回调是为了模拟实际场景中从服务端数据库读取数据(异步)的过程,
///
/// 从数据库读取任务数据
///
///
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();
}
使用PlayerPrefs.SetString
接口写数据到本地,
///
/// 写数据到数据库
///
private void SaveDataToDB()
{
var jsonStr = JsonMapper.ToJson(m_taskDatas);
PlayerPrefs.SetString("TASK_DATA", jsonStr);
}
///
/// 获取某个任务数据
///
/// 链id
/// 任务子id
///
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;
}
新增任务时,需要对列表进行重新排序,确保主线任务(即task_chain_id
为1
)的任务排在最前面,
///
/// 添加或更新任务
///
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();
}
///
/// 移除任务数据
///
/// 链id
/// 任务子id
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;
}
}
}
最终TaskData.cs
完整代码如下:
///
/// 任务数据增删改查
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
///
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LitJson;
using System;
public class TaskData
{
public TaskData()
{
m_taskDatas = new List<TaskDataItem>();
}
///
/// 从数据库读取任务数据
///
///
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();
}
///
/// 添加或更新任务
///
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();
}
///
/// 获取某个任务数据
///
/// 链id
/// 任务子id
///
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;
}
///
/// 移除任务数据
///
/// 链id
/// 任务子id
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;
}
}
}
///
/// 写数据到数据库
///
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;
}
///
/// 任务数据
///
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;
}
在Scripts / Logic
目录中创建TaskLogic.cs
脚本,
任务的逻辑其实就是进度更新、任务完成后领奖、开启下一个任务、开启分支任务等,我们挨个来实现。
先把TaskData
作为成员变量,并提供一个数据属性taskDatas
,方便访问,
private TaskData m_taskData;
public List<TaskDataItem> taskDatas
{
get { return m_taskData.taskDatas; }
}
public TaskLogic()
{
m_taskData = new TaskData();
}
///
/// 获取任务数据
///
/// 回调
public void GetTaskData(Action cb)
{
m_taskData.GetTaskDataFromDB(cb);
}
使用Action
回调是为了模拟实际场景中与服务端通信(异步),处理结果会有个返回码ErrorCode
(回调函数第一个参数),客户端需判断ErrorCode
的值来进行处理,一般约定ErrorCode
为0
表示成功,回调函数第二个参数是是否任务进度已达成,如果任务达成,客户端需要显示领奖按钮,
///
/// 更新任务进度
///
/// 链id
/// 任务子id
/// 进度增加量
/// 回调
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);
}
}
是否领奖的状态字段为award_is_get
,为0
表示未领奖,为1
表示已领过奖。
领完奖的任务从列表中移除,并开启下一个任务(如果配置了开启支线任务,则还需要配套开启对应的支线任务),
///
/// 领取任务奖励
///
/// 链id
/// 任务子id
/// 回调
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, "{}");
}
}
遍历所有达成进度且未领奖的任务,支线领奖,并开开启每个领完奖的任务的下一个任务(如果配置了开启支线任务,则还需要配套开启对应的支线任务),
///
/// 一键领取所有任务的奖励
///
///
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);
}
}
约定任务id
递增,开启下一个任务就是查找id+1
的任务并开启。
支线任务的开启是open_chain
字段,格式链id|任务子id
,多个支线以,
号隔开,例:3|1,5|1
表示开启链3的子任务1
和链5的子任务1
,
///
/// 开启下一个任务(含支线)
///
/// 链id
/// 任务子id
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);
}
}
}
}
最终TaskLogic.cs
完整代码如下
///
/// 任务逻辑
/// 作者:林新发,博客:https://blog.csdn.net/linxinfa
///
using System.Collections.Generic;
using UnityEngine;
using LitJson;
using System;
public class TaskLogic
{
public TaskLogic()
{
m_taskData = new TaskData();
}
///
/// 获取任务数据
///
/// 回调
public void GetTaskData(Action cb)
{
m_taskData.GetTaskDataFromDB(cb);
}
///
/// 更新任务进度
///
/// 链id
/// 任务子id
/// 进度增加量
/// 回调
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);
}
}
///
/// 一键领取所有任务的奖励
///
///
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);
}
}
///
/// 领取任务奖励
///
/// 链id
/// 任务子id
/// 回调
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, "{}");
}
}
///
/// 触发下一个任务,并开启支线任务
///
/// 链id
/// 任务子id
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
资源我是在阿里巴巴矢量图库上找,地址:https://www.iconfont.cn/
比如搜索按钮
,
找一个形状合适的,可以进行调色,我一般是调成白色,
因为Unity
中可以设置Color
,这样我们只需要一个白色按钮就可以在Unity
中创建不同颜色的按钮了。
弄点基础的美术资源,
注:那个头像是我自己用PhotoShop
画的哦,我之前用PhotoShop
画过一幅原创连环画,如下:
同时,我们还需要任务图标,也找一些,
注意所有要使用UGUI
来展示资源都设置为Sprite (2D and UI)
格式。
注,关于资源的获取,我之前写过一篇文章:《Unity游戏开发——新发教你做游戏(二):60个Unity免费资源获取网站》,感兴趣的同学可以看下,
养成好习惯,不管你是单场景还是多场景,入口场景命名为Main
。
在场景中使用UGUI
简单做下入口界面:MainPanel
,
这个任务系统的主题我定为:人生如梦。
根据需求,我们的任务要以列表的显示展示,使用UGUI
制作任务列表界面,
如下,
界面保存为TaskPanel.prefab
,放在Resources
目录中,
为了在客户端模拟测试,做一个提示框界面,
如下:
界面保存为TipsPanel.prefab
,放在Resources
目录中,
嘛,顺手做个界面动画吧,
注:关于动画相关的教程,我之前写过一些,感兴趣的同学可以看下:
《Unity使用Animator控制动画播放,皮皮猫打字机游戏》
《Unity动画状态机Animator使用》
《Unity动画使用混合树BlendTree实现动画过渡控制》
《新发教你做游戏(七):Animator控制角色动画播放》
领取任务奖励要有个奖励UI
展示,做一个,
界面保存为AwardPanel.prefab
,放在Resources
目录中,
界面预设制作好了,接下来就是写界面交互的代码了。
像C/C++
有Main
入口函数一样,我们的游戏也需要有一个脚本作为入口脚本。
我们创建一个Main.cs
脚本,挂到场景中的MainPanel
节点上,
Main.cs
脚本代码如下,主要是做一些全局变量、配置、数据等的初始化,然后显示界面,不过我们任务界面代码还没写,先留个TODO
,
using UnityEngine;
///
/// 入口脚本
///
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;
}
任务界面以列表展示任务,我之前做过一个循环复用列表的功能,可以参见我之前这篇文章:《Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码》
我把之前写的RecyclingList
脚本拷贝过来,
RecyclingList
脚本地址:https://codechina.csdn.net/linxinfa/UnityRecyclingListDemo/-/tree/master/Assets/Scripts/RecyclingList
给ScrollVIew
挂上RecyclingListView
脚本,脚本的ChildObj
对象需要是RecyclingListViewItem
类型的,我们下面会写一个TaskItemUI
继承RecyclingListViewItem
,这里ChildObj
先留空,
在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;
///
/// 更新UI
///
///
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 领奖要显示奖励界面。
现在,我们继续往下做。
在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();
};
}
///
/// 刷新整个列表
///
private void RefreshAll()
{
scrollList.RowCount = TaskLogic.instance.taskDatas.Count;
scrollList.Refresh();
}
我们需要告诉RecyclingListView
我们的列表的item
的数量,方便它进行计算,
// TaskPanel.cs
// 设置数据,此时列表会执行更新
scrollList.RowCount = TaskLogic.instance.taskDatas.Count;
为了便于显示TaskPanel
界面,我们封装一个static
的Show
方法,
// 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();
});
}
在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);
});
});
在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;
///
/// 显示奖励界面
///
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();
}
});
});
在Scripts / View
目录中创建一个SpriteManager.cs
脚本,
代码如下:
// SpriteManager.cs
using System.Collections.Generic;
using UnityEngine;
public class SpriteManager
{
///
/// 根据名字加载精灵资源
///
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
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,拜拜~