zoukankan      html  css  js  c++  java
  • 【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哈哈~


  • 相关阅读:
    getopt函数
    Pac的OI回忆录
    Python embed包使用
    Git add . Git add * 的区别
    Linux Python升级版本至2.7.5
    Linux6.3升级zlib
    一: Centos 虚拟机安装
    4.4 SpringCloud__服务注册与发现Eureka__自我保护机制
    4.3 SpringCloud__服务注册与发现Eureka__高可用集群配置
    4.2 SpringCloud__服务注册与发现Eureka__搭建注册中心
  • 原文地址:https://www.cnblogs.com/BFXYMY/p/9690846.html
Copyright © 2011-2022 走看看