1. 程式人生 > >【Unity UGUI有趣應用 】 (三)-------------------- 揹包系統(上)之簡易單頁揹包系統及檢索功能的實現

【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 FitterGrid 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哈哈~