Unity——可复用背包工具
Unity可复用背包工具
Demo展示
设计思路
游戏中有非常多的背包样式,比如玩家道具背包,商城,装备栏,技能栏等;每个形式的背包都单独写一份逻辑会非常繁琐,所以需要有一套好用的背包工具;
这些背包有几个共同的特点:
1.有多个排列好的方格子;
2.每个方格子中有内容时,可被拖动且拖动逻辑相同;
3.可添加使用删除格子中的物品;
因此根据这些特点,使用ScrollView等组件,提取两个类,分别负责数据管理和拖动逻辑;
前期准备
1.界面设置
制作三个界面,一个滚动背包面板,一个丢弃面板,一个单独物品的预制体;
关键组件:ScrollView,content中添加GridLayoutGroup;
2.物品配表
1.使用Excel配置物品属性表,同时创建字段和excel标签相同的类,用于json序列化;
2.Excel转Json,最简单方式;
之后将转成功的Json内容存到txt文本中,并导入项目;
3.LitJson库
我这里使用的LitJson,一个非常简单轻量的库;https://litjson.net/
直接导入项目或者打包成dll放进项目;
使用时只需要读取Txt文本,转成string,直接调用Api即可,支持数组;
关键基类设计
1.Item
物品属性基类,规定物品属性,需要字段名和json中的关键字相同才能被json序列化;
Clone方法用来深拷贝,需要重写,因为我深拷贝使用的内存拷贝,所以必须加[Serializable];
ItemKind类,单纯是为了不用每次判断时手动打“string",个人觉得麻烦Orz;
2.InventoryItem
挂在物品的预制体模板上,负责拖拽和刷新逻辑;
该类继承拖拽相关的三个接口;
IBeginDragHandler //开始拖拽IDragHandler //拖拽中IEndDragHandler //拖拽结束
字段:
private Transform parentTf; //开始拖动前,Item的父节点;private Transform canvasTf; //画布uiRoot;private CanvasGroup blockRaycast; //该组件可以禁用该UI的射线检测,这样在拖拽过程中可以识别下面uipublic GameObject panelDrop; //丢弃物品叛变;
方法:
Start:其中给canvasTf和blockRaycast赋值;
OnBeginDrag:拖拽开始,记录Item的父节点后,将Item的父节点改为canvsTf(避免拖拽过程中遮挡),屏蔽item射线检测;
OnDrag:Item位置和鼠标位置一致;
OnEndDrag:
检测拖拽结束时,Item下方的UI是什么类型;我这里设置了三个Tag;
item—下方为有物品的格子,两个互换位置;
box—为空的格子,Item移位;
background—弹出丢弃物品面板,同时隐藏当前Item;
其他—返回原位置;
判断结束后将位置归零,关闭射线屏蔽;
RefreshItem:根据数据更新Item的icon,名称,数量之类,需要重写;
ReturnPos:丢弃面板中点击取消,返回原位置;
GetNumber(string str):提取字符串中的数字,正则表达式;
全部代码如下:
using System;using System.Collections;using System.Collections.Generic;using System.Text.RegularExpressions;using UnityEngine;using UnityEngine.EventSystems;public class InventoryItem : MonoBehaviour,IBeginDragHandler,IEndDragHandler,IDragHandler{ private Transform parentTf; private Transform canvasTf; private CanvasGroup blockRaycast; public GameObject panelDrop; private void Start() { canvasTf = GameObject.FindGameObjectWithTag("UiRoot").transform; blockRaycast = GetComponent<CanvasGroup>(); } public void OnBeginDrag(PointerEventData eventData) { parentTf = transform.parent; transform.SetParent(canvasTf); blockRaycast.blocksRaycasts = false; } public void OnDrag(PointerEventData eventData) { transform.position = Input.mousePosition; } public void OnEndDrag(PointerEventData eventData) { GameObject go = eventData.pointerEnter; Debug.Log(go.tag); //Debug.Log(go.transform.parent.gameObject.name); if (go.CompareTag("item")) { int pos1 = GetNumber(parentTf.gameObject.name); int pos2 = GetNumber(go.transform.parent.gameObject.name); transform.SetParent(go.transform.parent); go.transform.SetParent(parentTf); go.transform.localPosition = new Vector3(0, 0, 0); //交换数据 BagPanel.I.bagData.SwitchItem(pos1, pos2); } else if (go.CompareTag("box")) { int pos1 = GetNumber(parentTf.gameObject.name); int pos2 = GetNumber(go.name); transform.SetParent(go.transform); BagPanel.I.bagData.SwitchItem(pos1, pos2); } else if (go.CompareTag("background")) { Debug.Log("丢弃物品"); gameObject.SetActive(false); //弹出新UI是否丢弃 //panelDrop.gameObject.SetActive(true); GameObject temp = Instantiate<GameObject>(panelDrop, UIMa.I.uiRoot); int pos = GetNumber(parentTf.gameObject.name); temp.GetComponent<PanDrop>().SetInventoryItem(this, pos); } else { transform.SetParent(parentTf); } transform.localPosition = new Vector3(0, 0, 0); blockRaycast.blocksRaycasts = true; } public virtual void RefreshItem(Item data) { } public void ReturnPos() { gameObject.SetActive(true); transform.SetParent(parentTf); transform.localPosition = new Vector3(0, 0, 0); blockRaycast.blocksRaycasts = true; } private int GetNumber(string str) { return int.Parse(Regex.Replace(str, @"[^0-9]+", "")); } }
3.InventoryData
数据管理类,负责背包信息管理,增删查改,泛型可复用不同Item类,入物品,技能等;
字段:
protected InventoryPanel mPanel:数据控制的哪个背包面板;
protected GameObject itemGo:物品模板;
protected int count = 0 :背包格子使用数;
protected Dictionary<int, T> allItemData :游戏中所有物品key是id,T为存放的物品实例;
private int capacity :背包容量,我这里设置了默认25,初始化时可修改;
protected List itemList :背包中存放的物品实例,index代表在背包中的位置;
方法:
1.public void InitData(string path, InventoryPanel panel, GameObject itemgo, int capacity);
初始化数据,path为之前jsonTxt的路径;
根据容量,将背包格子实例化满空对象;
2.private void LoadAllData(string path);
根据路径加载所有物品数据到allItemData;
我这里是假设资源都存放在Resources中,实际情况自行替换这段读取代码;
这里的json序列化有个坑,如果类中字段为string,excel中为纯数字会报错;
3.public void AddItem(int id, int num);
根据物品id添加物品;
这里分多种情况,背包中是否存在该物品,该物品种类是否为装备,装备是不能叠加存放的;
添加物品时,必须从allItemData中深拷贝,否则会导致该一个数据所有都变;
4.public void UseItem(int index, int num);
根据物品在背包中的位置,使用物品;
使用后判断数量是否为0,为0删除;
5.public void SwitchItem(int pos1, int pos2);
交换物品位置,简单的交换赋值;
6.public void DropItem(int index);
根据物品位置删除;
7.public void RefreshPanel();
刷新背包面板;
8.public void LoadPanel();
加载背包数据,第一次加载背包时调用;
全部代码如下:
using System;using System.Collections;using System.Collections.Generic;using System.IO;using System.Reflection;using System.Runtime.Serialization.Formatters.Binary;using LitJson;using UnityEngine;public class InventoryData<T> where T : Item{ protected InventoryPanel mPanel; protected GameObject itemGo; protected int count = 0; protected Dictionary<int, T> allItemData = new Dictionary<int, T>(); private int capacity = 25; protected List<T> itemList = new List<T>(); //初始化接口继承后调用 public void InitData(string path, InventoryPanel panel, GameObject itemgo, int capacity) { this.capacity = capacity; this.itemGo = itemgo; LoadAllData(path); for (int i = 0; i < capacity; ++i) { T temp = null; itemList.Add(temp); } mPanel = panel; } //初始化所有物品信息 private void LoadAllData(string path) { //假设资源都存放在Resources中,实际情况自行替换这段读取代码 //string str = File.ReadAllText(path); TextAsset data = Resources.Load<TextAsset>(path); if (data == null) return; string str = data.ToString(); Debug.Log(typeof(T).Name); //坑点:纯数字无法转为string; List<T> itDa = JsonMapper.ToObject<List<T>>(str); foreach (var it in itDa) { allItemData.Add(it.id, it); } Debug.Log("初始化物品信息成功"); } //添加物品 public void AddItem(int id, int num) { //背包已有 foreach (T it in itemList) { if (it == null) continue; if (it.id == id) { if (it.kind != ItemKind.equip) { it.num += num; RefreshPanel(); return; } else if (it.kind == ItemKind.equip) { for (int i = 0; i < capacity; i++) { if (itemList[i] == null) { //T t = ObjectDeepCopy(it); //T t = (T)it.Clone(); T t = DeepCopy(it); itemList[i] = t; RefreshPanel(); return; } } } } } //背包中无 int index = -1; for (int i = 0; i < itemList.Count; ++i) { if (itemList[i] == null) index = i; } if (index == -1) { Debug.Log("背包已满!"); return; } T t1 = (T) allItemData[id].Clone(); //T t1 = DeepCopy(allItemData[id]); t1.num = num; itemList[index] = t1; count++; //更新界面 RefreshPanel(); } //使用物品 public void UseItem(int index, int num) { if (itemList[index] == null) return; T item = itemList[index]; item.num -= num; if (item.num <= 0) { itemList[index] = null; } //更新界面 RefreshPanel(); } public void DropItem(int index) { itemList[index] = null; RefreshPanel(); } //掉换位置 public void SwitchItem(int pos1, int pos2) { T item = itemList[pos1]; itemList[pos1] = itemList[pos2]; itemList[pos2] = item; //更新界面 RefreshPanel(); } //更新背包面板 public void RefreshPanel() { Transform tf = mPanel.content; int count = tf.childCount; for (int i = 0; i < capacity; ++i) { Transform boxTf = tf.GetChild(i); if (itemList[i] != null) { if (boxTf.childCount > 0) { boxTf.GetChild(0).GetComponent<InventoryItem>().RefreshItem(itemList[i]); } else if (boxTf.childCount <= 0) { GameObject it = GameObject.Instantiate(itemGo, boxTf); it.GetComponent<InventoryItem>().RefreshItem(itemList[i]); break; } } else { if (boxTf.childCount > 0) { GameObject.Destroy(boxTf.GetChild(0).gameObject); } } } } public void LoadPanel() { Transform tf = mPanel.content; int count = tf.childCount; for (int i = 0; i < capacity; ++i) { if (itemList[i] != null) { int tempIndex = 0; for (int j = 0; j < count; ++j) { Transform boxTf = tf.GetChild(j); if (boxTf.childCount <= 0) { GameObject it = GameObject.Instantiate(itemGo, boxTf); it.GetComponent<InventoryItem>().RefreshItem(itemList[i]); break; } tempIndex = j; } if (tempIndex == count) { Debug.Log("背包已满"); break; } } } } }
4.InventoryPanel
使用这个父类,单纯为了让InventoryData中的字段有父类指向,content为所有box格子的父节点;
继承后的子类,需要在改面板中添加打开,关闭,数量显示,金钱等其他逻辑;
如果有UI框架,该类需要继承UI基类;
public class InventoryPanel : MonoBehaviour{ public Transform content; }
Test类
四个类分别继承四个关键基类;
GoodInfo:
继承自Item可添加需要字段,比如gold,cost等;
重写深拷贝方法;
using System;using System.Collections;using System.Collections.Generic;using System.IO;using System.Runtime.Serialization.Formatters.Binary;using UnityEngine; [Serializable]public class GoodsInfo : Item{ public string xxx; public override object Clone() { MemoryStream stream = new MemoryStream(); BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, this); stream.Position = 0; var obj = formatter.Deserialize(stream); return obj; } }
BagItem:
继承自InventoryItem;重写了刷新方法;
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class BagItem : InventoryItem{ public Image icon; public Text num; public override void RefreshItem(Item data) { GoodsInfo itData = (GoodsInfo) data; string path = $"icon/{itData.id}"; Sprite spTemplate = Resources.Load(path, typeof(Sprite)) as Sprite; Sprite sp = Instantiate<Sprite>(spTemplate); icon.sprite = sp; num.text = data.num.ToString(); } }
BagData:
继承了InventroyData,同时泛型替换成GoodsInfo;
添加了两个测试方法,初始化背包数据;
using System.Collections;using System.Collections.Generic;using UnityEngine;public class BagData : InventoryData<GoodsInfo> { public void TestInit() { addTestData(8, 1,ItemKind.equip); addTestData(5, 1,ItemKind.equip); addTestData(0, 5,ItemKind.material); addTestData(1, 21,ItemKind.drug); } private void addTestData(int id, int num,string kind) { GoodsInfo it = new GoodsInfo(); it.num = num; it.id = id; it.kind = kind; itemList[count] = it; count++; } }
BagPanel:
继承InventroyPanel,单例;
与bagData组合,存放bagData数据的实例;
添加了两个测试按钮,添加物品,和使用物品;
Start中,初始化BagData数据;加载背包;根据index修改content中box的名称(上面我改成正则表达式提取数字,这里可以不用改了);
using System;using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class BagPanel : InventoryPanel{ private static BagPanel instance; public static BagPanel I { get { if (instance == null) { instance = new BagPanel(); } return instance; } } private BagPanel() { } public GameObject itemGo; public BagData bagData = new BagData(); public Button btnAdd; public Button btnUse; private void Start() { instance = this; bagData.InitData("ItemData", this, itemGo, 25); bagData.TestInit(); bagData.LoadPanel(); btnAdd.onClick.AddListener(OnAddItem); btnUse.onClick.AddListener(OnUseItem); for (int i = 0; i < content.childCount; ++i) { content.GetChild(i).gameObject.name = i.ToString(); } } public void OnAddItem() { bagData.AddItem(9,1); } public void OnUseItem() { bagData.UseItem(3,1); } }
DropPanel:
丢弃面板,是否丢弃;是:删除数据,否:物品取消隐藏返回父节点;
using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;public class PanDrop : MonoBehaviour{ public Button btnYes; public Button btnNo; private InventoryItem it; private int pos; void Start() { btnYes.onClick.AddListener(OnBtnYes); btnNo.onClick.AddListener(OnBtnNo); } private void OnBtnYes() { //数据删除 BagPanel.I.bagData.DropItem(pos); Destroy(it.gameObject); gameObject.SetActive(false); } private void OnBtnNo() { it.ReturnPos(); gameObject.SetActive(false); } public void SetInventoryItem(InventoryItem it,int pos) { this.it = it; this.pos = pos; } }
UIMa:
初始化,提供canvasTf节点;
UI框架部分,用于存储各个背包面板的对象,由于之前写过UI框架所以这里没有展开写;
有需求可以看之前的文章《Unity——基于UGUI的UI框架》;
坑点
泛型对象创建
泛型对象T是不能被new 出来的,这里就需要使用反射或内存拷贝;
反射:有时候会失效,原因未知;
public T ObjectDeepCopy(T inM){ Type t = inM.GetType(); T outM = (T)Activator.CreateInstance(t); foreach (PropertyInfo p in t.GetProperties()) { t.GetProperty(p.Name).SetValue(outM, p.GetValue(inM)); } return outM; }
内存拷贝:序列化的类必须有[Serializable]
public static T DeepCopy(T obj){ object retval; using (MemoryStream ms = new MemoryStream()) { BinaryFormatter bf = new BinaryFormatter(); //序列化成流 bf.Serialize(ms, obj); ms.Seek(0, SeekOrigin.Begin); //反序列化成对象 retval = bf.Deserialize(ms); ms.Close(); } return (T) retval; }
以上是我对背包工具的总结,如果有更好的意见,欢迎给作者评论留言;
分类: Unity
来源https://www.cnblogs.com/littleperilla/p/15380732.html