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

您所在的位置:网站首页 游戏任务清单怎么做图片 【游戏开发实战】Unity从零做一个任务系统,人生如梦,毕业大学生走上人生巅峰(含源码工程

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

2024-03-16 01:14| 来源: 网络整理| 查看: 265

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

工程源码见文章末尾。

文章目录 一、前言二、什么是任务系统三、需求文档1、故事背景2、任务链设计3、任务规则4、界面样式设计 四、从哪里开始着手五、任务配置表1、定义表头字段2、配置表格数据3、转表工具:Excel转Json 六、读取配置表1、资源加载:Resources.Load2、C#的json库:LitJson3、任务配置配置读取:TaskCfg.cs脚本3.1、创建TaskCfg.cs脚本3.2、定义任务配置结构:TaskCfgItem3.3、定义存储配置的容器3.4、读取配置:LoadCfg3.5、索引任务配置项:GetCfgItem3.6、使用单例模式3.7、思维导图3.8、TaskCfg.cs完整代码 七、任务数据增删改查:TaskData.cs脚本1、创建TaskData.cs脚本2、定义任务数据:TaskDataItem3、本地数据读写:PlayerPrefs4、定义存储数据的容器5、从本地读取任务数据6、写任务数据到本地7、查询指定任务的数据8、任务数据增加或更新9、任务数据删除10、思维导图11、TaskData.cs完整代码 八、任务逻辑:TaskLogic.cs1、创建TaskLogics脚本2、成员:TaskData3、获取任务数据4、更新任务进度5、领取任务奖励6、一键领取任务的奖励7、开启下一个任务(含支线)8、思维导图9、TaskLogic.cs完整代码 九、UI界面制作1、UI资源获取2、场景界面制作:Main场景,人生如梦3、制作列表界面预设:TaskPanel.prefab4、制作提示框界面预设:TipsPanel.prefab5、制作奖励界面预设:AwardPanel.prefab 十、编写界面代码1、入口脚本:Main.cs2、任务列表界面2.1、循环复用列表2.2、列表项脚本:TaskItemUI.cs2.3、列表界面脚本:TaskPanel.cs 3、提示框界面:TipsPanel.cs4、奖励界面:AwardPanel.cs5、精灵资源管理器 十一、运行测试十二、工程源码十三、完毕

一、前言

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

二、什么是任务系统

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

三、需求文档

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

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篇的目标数量就是100awardstring奖励,json格式,例:{"gold":1000},表示奖励1000金币open_chainstring要打开的支线任务,格式:链id|任务id,开启多个链以英文逗号隔开。例:2|1,4|1表示打开 链2的子任务1和打开链4的子任务1 2、配置表格数据

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

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

在这里插入图片描述

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

3、转表工具:Excel转Json

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 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 m_cfg; 3.4、读取配置:LoadCfg

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

// TaskCfg.cs /// /// 读取配置 /// public void LoadCfg() { m_cfg = new Dictionary(); var txt = Resources.Load("task_cfg").text; var jd = JsonMapper.ToObject(txt); for (int i = 0, cnt = jd.Count; i m_cfg[cfgItem.task_chain_id] = new Dictionary(); } m_cfg[cfgItem.task_chain_id].Add(cfgItem.task_sub_id, cfgItem); } } 3.5、索引任务配置项:GetCfgItem

为了索引任务配置项,我们再封装一个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; } 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完整代码如下:

/// /// 任务配置读取与查询 /// 作者:林新发,博客:https://blog.csdn.net/linxinfa /// using System.Collections.Generic; using UnityEngine; using LitJson; public class TaskCfg { /// /// 读取配置 /// public void LoadCfg() { m_cfg = new Dictionary(); var txt = Resources.Load("task_cfg").text; var jd = JsonMapper.ToObject(txt); for (int i = 0, cnt = jd.Count; i m_cfg[cfgItem.task_chain_id] = new Dictionary(); } 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 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; } 七、任务数据增删改查:TaskData.cs脚本 1、创建TaskData.cs脚本

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

2、定义任务数据:TaskDataItem

我们要读写任务数据,需要先定义任务数据结构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; } 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 m_taskDatas; 5、从本地读取任务数据

使用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(jsonStr); for (int i = 0, cnt = taskList.Count; i var jsonStr = JsonMapper.ToJson(m_taskDatas); PlayerPrefs.SetString("TASK_DATA", jsonStr); } 7、查询指定任务的数据 /// /// 获取某个任务数据 /// /// 链id /// 任务子id /// public TaskDataItem GetData(int chainId, int subId) { for (int i = 0, cnt = m_taskDatas.Count; i bool isUpdate = false; for (int i = 0, cnt = m_taskDatas.Count; i // 更新数据 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、任务数据删除 /// /// 移除任务数据 /// /// 链id /// 任务子id public void RemoveData(int chainId, int subId) { for (int i = 0, cnt = m_taskDatas.Count; i m_taskDatas.Remove(item); SaveDataToDB(); return; } } } 10、思维导图

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

11、TaskData.cs完整代码

最终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(); } /// /// 从数据库读取任务数据 /// /// 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(jsonStr); for (int i = 0, cnt = taskList.Count; i bool isUpdate = false; for (int i = 0, cnt = m_taskDatas.Count; i // 更新数据 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 for (int i = 0, cnt = m_taskDatas.Count; i 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 taskDatas { get { return m_taskDatas; } } // 任务数据 private List 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; } 八、任务逻辑:TaskLogic.cs 1、创建TaskLogics脚本

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

2、成员:TaskData

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

private TaskData m_taskData; public List taskDatas { get { return m_taskData.taskDatas; } } public TaskLogic() { m_taskData = new TaskData(); } 3、获取任务数据 /// /// 获取任务数据 /// /// 回调 public void GetTaskData(Action cb) { m_taskData.GetTaskDataFromDB(cb); } 4、更新任务进度

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

/// /// 更新任务进度 /// /// 链id /// 任务子id /// 进度增加量 /// 回调 public void AddProgress(int chainId, int subId, int deltaProgress, Action 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表示已领过奖。 领完奖的任务从列表中移除,并开启下一个任务(如果配置了开启支线任务,则还需要配套开启对应的支线任务),

/// /// 领取任务奖励 /// /// 链id /// 任务子id /// 回调 public void GetAward(int chainId, int subId, Action 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、一键领取任务的奖励

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

/// /// 一键领取所有任务的奖励 /// /// public void OneKeyGetAward(Action cb) { int totalGold = 0; var tmpTaskDatas = new List(m_taskData.taskDatas); for (int i = 0, cnt = tmpTaskDatas.Count; i 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,

/// /// 开启下一个任务(含支线) /// /// 链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 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 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 cb) { int totalGold = 0; var tmpTaskDatas = new List(m_taskData.taskDatas); for (int i = 0, cnt = tmpTaskDatas.Count; i 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 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 m_taskData.ResetData(cb); } public List 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; /// /// 入口脚本 /// 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; /// /// 更新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 && 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(); }; } /// /// 刷新整个列表 /// 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("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("TipsPanel"); var panelObj = Instantiate(s_tipsPanelPrefab); panelObj.transform.SetParent(GlobalObj.s_canvasTrans, false); var panelBhv = panelObj.GetComponent(); 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; /// /// 显示奖励界面 /// public static void Show(string award) { if (null == s_awardPanelPrefab) s_awardPanelPrefab = Resources.Load("AwardPanel"); var panelObj = Instantiate(s_awardPanelPrefab); panelObj.transform.SetParent(GlobalObj.s_canvasTrans, false); var panelBhv = panelObj.GetComponent(); 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 { /// /// 根据名字加载精灵资源 /// public Sprite GetSprite(string name) { if (m_sprites.ContainsKey(name)) return m_sprites[name]; var sprite = Resources.Load("Sprites/" + name); m_sprites.Add(name, sprite); return sprite; } private Dictionary m_sprites = new Dictionary(); 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 原创不易,若转载请注明出处,感谢大家~ 喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信,拜拜~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3