本文最終效果如下:



工程思維導圖:

工程原始碼見文章末尾,
文章目錄
- 一、前言
- 二、什么是任務系統
- 三、需求檔案
- 1、故事背景
- 2、任務鏈設計
- 3、任務規則
- 4、界面樣式設計
- 四、從哪里開始著手
- 五、任務配置表
- 1、定義表頭欄位
- 2、配置表格資料
- 3、轉表工具:Excel轉Json
- 六、讀取配置表
- 1、資源加載:Resources.Load
- 2、C#的json庫:LitJson
- 3、任務配置配置讀取:TaskCfg.cs腳本
- 3.1、創建TaskCfg.cs腳本
- 3.2、定義任務配置結構:TaskCfgItem
- 3.3、定義存盤配置的容器
- 3.4、讀取配置:LoadCfg
- 3.5、索引任務配置項:GetCfgItem
- 3.6、使用單例模式
- 3.7、思維導圖
- 3.8、TaskCfg.cs完整代碼
- 七、任務資料增刪改查:TaskData.cs腳本
- 1、創建TaskData.cs腳本
- 2、定義任務資料:TaskDataItem
- 3、本地資料讀寫:PlayerPrefs
- 4、定義存盤資料的容器
- 5、從本地讀取任務資料
- 6、寫任務資料到本地
- 7、查詢指定任務的資料
- 8、任務資料增加或更新
- 9、任務資料洗掉
- 10、思維導圖
- 11、TaskData.cs完整代碼
- 八、任務邏輯:TaskLogic.cs
- 1、創建TaskLogics腳本
- 2、成員:TaskData
- 3、獲取任務資料
- 4、更新任務進度
- 5、領取任務獎勵
- 6、一鍵領取任務的獎勵
- 7、開啟下一個任務(含支線)
- 8、思維導圖
- 9、TaskLogic.cs完整代碼
- 九、UI界面制作
- 1、UI資源獲取
- 2、場景界面制作:Main場景,人生如夢
- 3、制作串列界面預設:TaskPanel.prefab
- 4、制作提示框界面預設:TipsPanel.prefab
- 5、制作獎勵界面預設:AwardPanel.prefab
- 十、撰寫界面代碼
- 1、入口腳本:Main.cs
- 2、任務串列界面
- 2.1、回圈復用串列
- 2.2、串列項腳本:TaskItemUI.cs
- 2.3、串列界面腳本:TaskPanel.cs
- 3、提示框界面:TipsPanel.cs
- 4、獎勵界面:AwardPanel.cs
- 5、精靈資源管理器
- 十一、運行測驗
- 十二、工程原始碼
- 十三、完畢
一、前言
嗨,大家好,我是新發,
事情是這樣的,有小朋友微信問我如何做任務系統,作為一個熱心的技術博主,我都是能幫就幫,今天,我就來做一個任務系統吧,

二、什么是任務系統
任務系統就是一個有明確目標性的系統,通過設定任務來引導玩家進行游戲,讓玩家更快的融入游戲中,
可以說任務系統幾乎是游戲必備的模塊,我們隨便找個游戲都可以看到任務系統,

根據這位小朋友的需求,是要做 主線任務/支線任務 的系統,
簡單的說,就是有一條 主線任務鏈,在完成主線任務鏈上的某個節點時,開啟下一個任務,并可以開啟一潭訓多條 支線任務鏈,主線任務和多條支線任務并行,畫個圖,方便大家理解:

三、需求檔案
由于只有我一個人,沒有策劃,那我就先充當策劃,給自己寫個需求檔案吧~
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轉為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來讀取檔案,
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目錄,我們的資料代碼、邏輯代碼和界面代碼要分開,所以建立Data、Logic和View三個子目錄,

在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_id為1)的任務排在最前面,
/// <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的值來進行處理,一般約定ErrorCode為0表示成功,回呼函式第二個引數是是否任務進度已達成,如果任務達成,客戶端需要顯示領獎按鈕,
/// <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界面,我們封裝一個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();
});
}
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
原創不易,若轉載請注明出處,感謝大家~
喜歡我的可以點贊、關注、收藏,如果有什么技術上的疑問,歡迎留言或私信,拜拜~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/290371.html
標籤:其他
上一篇:「 硬核分享」 ?? FPS類游戲CSGO透視外掛完整原始碼??「 復制即用」
下一篇:亂數的生成+猜數字游戲
