Unity UGUI有趣应用 -------------------- 背包系统(上)之简易单页背包系统及检索功能的实现

Posted bfxymy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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>();
技术分享图片

  以每个道具的名字为 key。同时为了方便管理,我把DateManager做成了单例模式。不熟悉单例模式的朋友可以参考一下这篇博文    https://www.cnblogs.com/liaoguipeng/p/5130144.html

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

技术分享图片技术分享图片?

 



















以上是关于Unity UGUI有趣应用 -------------------- 背包系统(上)之简易单页背包系统及检索功能的实现的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D-UGUI应用篇UGUI实现窗口的自由拖拽

Unity3D-UGUI应用篇UGUI实现窗口的自由拖拽

Unity3D-UGUI应用篇使用UGUI弹窗显示模型及弹窗模型交互

Unity3D - UGUI的初级应用

Unity3D-UGUI应用篇使用Image实现进度条动画

Unity3D-UGUI应用篇使用Text实现进度等待动画