【Unity UGUI有趣應用 】 (三)-------------------- 揹包系統(上)之簡易單頁揹包系統及檢索功能的實現
揹包系統,無論是遊戲還是應用,都是常常見到的功能,其作用及重要性不用我多說,玩過遊戲的朋友都應該明白。
在Unity中實現一個簡易的揹包系統其實並不是太過複雜的事。本文要實現的是一個帶檢索功能的揹包系統。先看一下我們要完成的效果 。由於上傳的gif圖不能大於5M,所以錄製的質量比較一般。大家先將就看一下吧
那現在,我們就開始動手了~~
一. 拼UI
由於本文著重講述的是揹包系統的運作,所以,揹包裡面的元素獲取就簡潔地做成這個樣子
事實上,一般遊戲內獲取道具的途徑是敵人掉落,完成任務,商城購買等,而這裡簡化了這一步,右邊的加號代表向背包裡新增一個該元素。好了,現在開始實際製作。
1.1 在 Resources 資料夾下建立兩個子資料夾 Prefab 和 Sprite ,用於存放資源
,
1.2 建立一個Canvas;
建立一個空物體,命名為Options;
建立一個 Image,用來顯示道具,命名為 X1-裝備
在上一步建立的 Image 下建立一個子 Image。
然後給這兩張 Image 賦值
![]()
![]()
這裡需要說明的是,命名為 X1-裝備 是為了更方便的說明揹包如何運作,事實上一般遊戲的道具都是有著名字,種類等屬性,而這些屬性應該儲存在 配置表 裡面。本文不會涉及到配置表的操作,所以這裡就簡潔處理。還有這些UI資源都是隨意的,讀者自行選擇自己的圖片資源。
1.3 進行第②中的操作,實現如下效果
![]()
1.4 製作揹包
在Canvas下建立一個空物體,命名為BG,賦上一張背景圖,調整尺寸大小
![]()
在BG下建立一個Panel,調整其尺寸大小。並新增 Content Size Fitter 和 Grid Layout Group 元件,修改其中引數
1.5 檢索UI
在BG下建立一個空物體,命名為 Toggles ,並在其下建立3個子Toggle,分別命名為 Med,Eqi,Goods。
再建立一個 InputField。如圖
![]()
![]()
1.6 單個道具Item
建立一張Image, 命名為Cell。在Cell下建立一個子Text。調整兩者大小。做成prefab,放到prefab資料夾中
至此,UI 拼接就完成了。只缺邏輯了~~~
二. UI邏輯
2.1 物品類
首先建立一個C#指令碼,命名為 CellItem,代表物品類。一個道具(物品),通常都會有 名字,所屬種類,數量,價格等屬性,而這裡的話,只取前三種屬性,完整程式碼:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class CellItem { private string name; private string tag; private int number; public CellItem(string name,string tag,int num) { Name = name; Tag = tag; Number = num; } public string Name { get { return name; } set { name = value; } } public string Tag { get { return tag; } set { tag = value; } } public int Number { get { return number; } set { number = value; } } }
2.2 資料管理類
當我們的角色獲得了一個道具之後,理應有一個管理系統將新獲得的道具資訊新增進去。所以建立一個C#指令碼,命名為DataManager 。
在本文中實現的揹包系統中,只管理了三種道具:藥品,裝備,物品(消耗品)。對應著前文所描述的 UI拼接 。所以在 DataManager 中建立3個 Dictionary ,用來儲存道具資訊。
/// <summary> /// 藥品 /// </summary> public Dictionary<string, CellItem> Medicine = new Dictionary<string, CellItem>(); /// <summary> /// 裝備 /// </summary> public Dictionary<string, CellItem> Equipment = new Dictionary<string, CellItem>(); /// <summary> /// 物品(消耗品) /// </summary> public Dictionary<string, CellItem> Goods = new Dictionary<string, CellItem>();
DataManager完整程式碼:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class DataManager { /// <summary> /// 藥品 /// </summary> public Dictionary<string, CellItem> Medicine = new Dictionary<string, CellItem>(); /// <summary> /// 裝備 /// </summary> public Dictionary<string, CellItem> Equipment = new Dictionary<string, CellItem>(); /// <summary> /// 物品(消耗品) /// </summary> public Dictionary<string, CellItem> Goods = new Dictionary<string, CellItem>(); // 定義一個靜態變數來儲存類的例項 private static DataManager _Instance; // 定義一個標識確保執行緒同步 private static readonly object locker = new object(); public static DataManager GetInstance() { // 當第一個執行緒執行到這裡時,此時會對locker物件 "加鎖", // 當第二個執行緒執行該方法時,首先檢測到locker物件為"加鎖"狀態,該執行緒就會掛起等待第一個執行緒解鎖 // lock語句執行完之後(即執行緒執行完之後)會對該物件"解鎖" // 雙重鎖定只需要一句判斷就可以了 if (_Instance == null) { lock (locker) { // 如果類的例項不存在則建立,否則直接返回 if (_Instance == null) { _Instance = new DataManager(); } } } return _Instance; } }
2.3 UI管理類
這個類是整個揹包的重點,用於管理第一步中建立的所以UI。
建立一個C#指令碼,命名為UIManager。在場景中建立一個空物體,命名為GameManager,並把UIManager掛載上去。
為了獲取第一步中所建立的UI,先定義與之相匹配的變數
private Toggle selectMed; private Toggle selectEqi; private Toggle selectGoods; private InputField SearchBox; private bool isSelectMed; private bool isSelectEqi; private bool isSelectGoods; private Button[] buttons; private GameObject lattice; private GameObject content; private Dictionary<string, CellItem> whole = new Dictionary<string, CellItem>();
其中,toggle 和 InputField 對應之前所製作的 toggle 和輸入框;
中間三個 bool 變量表示檢索時是否選擇的toggle代表的種類;
buttons 代表左邊6個新增按鈕; lattice 用來得到之前製作的 Cell 的prefab;
content 代表前文的Panel;
whole則會記錄所以的道具資訊;
先給這些變數賦值 。我會先給出每個UI所對應的響應方法,方法體中會牽涉其它核心的方法,這個稍後會講。
private void Awake() { SearchBox = GameObject.Find("Canvas/BG/InputField").GetComponent<InputField>(); SearchBox.onValueChanged.AddListener(SearchItem); lattice = Resources.Load<GameObject>("Prefab/Cell"); content = GameObject.Find("Canvas/BG/Panel"); selectMed = GameObject.Find("Canvas/BG/Toggles/Med").GetComponent<Toggle>(); selectEqi = GameObject.Find("Canvas/BG/Toggles/Eqi").GetComponent<Toggle>(); selectGoods = GameObject.Find("Canvas/BG/Toggles/Goods").GetComponent<Toggle>(); selectMed.onValueChanged.AddListener(OnMedicineToggleClick); selectEqi.onValueChanged.AddListener(OnEquipmentToggleClick); selectGoods.onValueChanged.AddListener(OnGoodsToggleClick); GameObject buts = GameObject.Find("Canvas/Options"); for (int count = 0; count < buts.transform.childCount; count++) { GameObject kid = buts.transform.GetChild(count).gameObject; kid.transform.GetChild(0).GetComponent<Button>().onClick.AddListener(() => OnAddButtonClick(kid.name)); } isSelectMed = true; isSelectEqi = true; isSelectGoods = true; }
而3個bool值變數對應著三個方法。這三個方法作用於檢索
private string IsSelectMed() { if (isSelectMed) return "藥品"; return " "; } private string IsSelectEqi() { if (isSelectEqi) return "裝備"; return " "; } private string IsSelectGoods() { if (isSelectGoods) return "物品"; return " "; }
3個Toggle也對應了3個響應方法
public void OnMedicineToggleClick(bool isOn) { if (isOn) isSelectMed = true; else isSelectMed = false; ReflashBackup(); } public void OnEquipmentToggleClick(bool isOn) { if (isOn) isSelectEqi = true; else isSelectEqi = false; ReflashBackup(); } public void OnGoodsToggleClick(bool isOn) { if (isOn) isSelectGoods = true; else isSelectGoods = false; ReflashBackup(); }
6個button對應著同一個方法,不過引數不同,引數為每個button所對應的名字,如“X1-裝備”,“X2-藥品”等
public void OnAddButtonClick(string Name) { string[] parts = Name.Split('-'); string name = parts[0]; string tag = parts[1]; switch(tag) { case "藥品": ModifyKey(DataManager.GetInstance().Medicine, name,"藥品"); break; case "裝備": ModifyKey(DataManager.GetInstance().Equipment, name,"裝備"); break; case "物品": ModifyKey(DataManager.GetInstance().Goods, name,"物品"); break; } ModifyKey(whole, name,tag); ReflashBackup(); }
接下來就是比較重要的方法:
一. 修改鍵對值
當新增一個道具進來時,對其進行管理。應該先檢索當前的 Dictionary 中是否有這個道具(key),如果有,則這個道具的數量屬性 + 1;如果沒有,則新增一個鍵對值。
public void ModifyKey(Dictionary<string, CellItem> cells,string name,string tag) { //先判斷是否存在這個Key,沒有則add if (cells.ContainsKey(name)) { int count = cells[name].Number; cells[name].Number = count + 1; } else { CellItem cellItem = new CellItem(name,tag,1); cells.Add(name, cellItem); } }
二. 建立道具單元
在預設顯示所有物品的情況下,只要遍歷 Dictionary whole就可以創建出所有的道具。而在加入了檢索功能後,則需要多進行一個判斷,提取出符合檢索標準的鍵對值,這一步我們用 Linq 可以快速獲取。然後建立所有道具。
public void InstantiateCell(Dictionary<string, CellItem> dictionary) { Dictionary<string, CellItem> SelectObjs = new Dictionary<string, CellItem>(); var selects = from cell in dictionary where (cell.Value.Tag == IsSelectMed() || cell.Value.Tag == IsSelectEqi() || cell.Value.Tag == IsSelectEqi()) select cell; foreach(var select in selects) { SelectObjs.Add(select.Key, select.Value); } foreach (KeyValuePair<string, CellItem> value in SelectObjs) { GameObject cell = GameObject.Instantiate(lattice, content.transform); SetPicture(value.Key, cell); cell.transform.GetChild(0).GetComponent<Text>().text = value.Value.Number.ToString(); } }
建立單元時需要把道具相應的圖片顯示出來,需要注意的是,我把我的資源放在了Resources/Sprite下,讀者的資源路徑與我不一致時,僅修改一行程式碼即可
public void SetPicture(string name,GameObject cell) { //圖片資源路徑 string path = "Sprite/" + name; Texture2D tex = Resources.Load<Texture2D>(path); Image img = cell.transform.GetComponent<Image>(); Sprite sp = Sprite.Create(tex, new Rect(new Vector2(0, 0), new Vector2(tex.width, tex.height)), new Vector2(0, 0)); img.rectTransform.sizeDelta = new Vector2(sp.rect.width, sp.rect.height); img.sprite = sp; }
三. 重新整理揹包
當我們新增一個道具或者按種類檢索時,都應該重新整理揹包。而重新整理的思路也很簡單:先把 Panel 下的所以 CellItem 刪除,然後根據新的 Dictionary 建立新的一批道具單元。
刪除 方法
public void ClearContent() { Transform[] lattices = content.GetComponentsInChildren<Transform>(); foreach (Transform lattice in lattices) { if (lattice.name != "Panel") { lattice.DetachChildren(); GameObject.Destroy(lattice.gameObject); } } }
所以重新整理函式如下
public void ReflashBackup() { ClearContent(); InstantiateCell(whole); }
四. 按名字搜尋
按名字搜尋對應著前文建立的 InputField。思路也是比較簡單的,如果字典中有這個道具key,就創建出來
public void SearchItem(string itemName) { ClearContent(); if (whole.ContainsKey(itemName)) { GameObject cell = GameObject.Instantiate(lattice, content.transform); SetPicture(itemName, cell); cell.transform.GetChild(0).GetComponent<Text>().text = whole[itemName].Number.ToString(); } }
完整程式碼:
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; using System.Linq; public class UIManager : MonoBehaviour { private Toggle selectMed; private Toggle selectEqi; private Toggle selectGoods; private InputField SearchBox; private bool isSelectMed; private bool isSelectEqi; private bool isSelectGoods; private Button[] buttons; private GameObject lattice; private GameObject content; private Dictionary<string, CellItem> whole = new Dictionary<string, CellItem>(); private void Awake() { SearchBox = GameObject.Find("Canvas/BG/InputField").GetComponent<InputField>(); SearchBox.onValueChanged.AddListener(SearchItem); lattice = Resources.Load<GameObject>("Prefab/Cell"); content = GameObject.Find("Canvas/BG/Panel"); selectMed = GameObject.Find("Canvas/BG/Toggles/Med").GetComponent<Toggle>(); selectEqi = GameObject.Find("Canvas/BG/Toggles/Eqi").GetComponent<Toggle>(); selectGoods = GameObject.Find("Canvas/BG/Toggles/Goods").GetComponent<Toggle>(); selectMed.onValueChanged.AddListener(OnMedicineToggleClick); selectEqi.onValueChanged.AddListener(OnEquipmentToggleClick); selectGoods.onValueChanged.AddListener(OnGoodsToggleClick); GameObject buts = GameObject.Find("Canvas/Options"); for (int count = 0; count < buts.transform.childCount; count++) { GameObject kid = buts.transform.GetChild(count).gameObject; kid.transform.GetChild(0).GetComponent<Button>().onClick.AddListener(() => OnAddButtonClick(kid.name)); } isSelectMed = true; isSelectEqi = true; isSelectGoods = true; } private string IsSelectMed() { if (isSelectMed) return "藥品"; return " "; } private string IsSelectEqi() { if (isSelectEqi) return "裝備"; return " "; } private string IsSelectGoods() { if (isSelectGoods) return "物品"; return " "; } public void OnAddButtonClick(string Name) { string[] parts = Name.Split('-'); string name = parts[0]; string tag = parts[1]; switch(tag) { case "藥品": ModifyKey(DataManager.GetInstance().Medicine, name,"藥品"); break; case "裝備": ModifyKey(DataManager.GetInstance().Equipment, name,"裝備"); break; case "物品": ModifyKey(DataManager.GetInstance().Goods, name,"物品"); break; } ModifyKey(whole, name,tag); ReflashBackup(); } public void OnMedicineToggleClick(bool isOn) { if (isOn) isSelectMed = true; else isSelectMed = false; ReflashBackup(); } public void OnEquipmentToggleClick(bool isOn) { if (isOn) isSelectEqi = true; else isSelectEqi = false; ReflashBackup(); } public void OnGoodsToggleClick(bool isOn) { if (isOn) isSelectGoods = true; else isSelectGoods = false; ReflashBackup(); } public void ModifyKey(Dictionary<string, CellItem> cells,string name,string tag) { //先判斷是否存在這個Key,沒有則add if (cells.ContainsKey(name)) { int count = cells[name].Number; cells[name].Number = count + 1; } else { CellItem cellItem = new CellItem(name,tag,1); cells.Add(name, cellItem); } } public void ReflashBackup() { ClearContent(); InstantiateCell(whole); } public void InstantiateCell(Dictionary<string, CellItem> dictionary) { Dictionary<string, CellItem> SelectObjs = new Dictionary<string, CellItem>(); var selects = from cell in dictionary where (cell.Value.Tag == IsSelectMed() || cell.Value.Tag == IsSelectEqi() || cell.Value.Tag == IsSelectEqi()) select cell; foreach(var select in selects) { SelectObjs.Add(select.Key, select.Value); } foreach (KeyValuePair<string, CellItem> value in SelectObjs) { GameObject cell = GameObject.Instantiate(lattice, content.transform); SetPicture(value.Key, cell); cell.transform.GetChild(0).GetComponent<Text>().text = value.Value.Number.ToString(); } } public void SetPicture(string name,GameObject cell) { //圖片資源路徑 string path = "Sprite/" + name; Texture2D tex = Resources.Load<Texture2D>(path); Image img = cell.transform.GetComponent<Image>(); Sprite sp = Sprite.Create(tex, new Rect(new Vector2(0, 0), new Vector2(tex.width, tex.height)), new Vector2(0, 0)); img.rectTransform.sizeDelta = new Vector2(sp.rect.width, sp.rect.height); img.sprite = sp; } public void ClearContent() { Transform[] lattices = content.GetComponentsInChildren<Transform>(); foreach (Transform lattice in lattices) { if (lattice.name != "Panel") { lattice.DetachChildren(); GameObject.Destroy(lattice.gameObject); } } } public void SearchItem(string itemName) { ClearContent(); if (whole.ContainsKey(itemName)) { GameObject cell = GameObject.Instantiate(lattice, content.transform); SetPicture(itemName, cell); cell.transform.GetChild(0).GetComponent<Text>().text = whole[itemName].Number.ToString(); } } }
有個問題是,當按名字搜尋時,新增道具會執行相應的程式碼。事實上,當遊戲中處於檢索時一般是不會觸發新增物體這種事件的。
三. 總結
總體來說,本文實現的這個揹包系統不算複雜,也有人會問,本文一直在使用 Dictionary whole。而在 DataManager 中定義的三個字典除了儲存了資訊之外,並沒有用到別的地方去,似乎是有點多餘。
事實上,本文只是著重介紹整個揹包系統的原理才使用了whole,可以更為簡潔地說明。讀者可以思考一下,如果我有著 10個種類的道具,每種道具都需要一頁表來顯示。如果我使用whole,那麼對whole篩選10遍,重複操作多,浪費資源,而如果我有10個於種類對應的字典則可以直接使用,無需篩選。所以,分類儲存是很有必要的。
在下一篇中,我會對這個揹包系統進行優化和升級
- 採用讀取配置表來錄入道具資訊
- 從單頁揹包升級分頁揹包
- 更為完善的檢索系統
碼字不易,希望這篇文章能對各位讀者有所幫助O(∩_∩)O哈哈~