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的射線檢測,這樣在拖拽過程中可以識別下面ui
public 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
方法:
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;
}
以上是我對背包工具的總結,如果有更好的意見,歡迎給作者評論留言;