Unity——可複用揹包工具

小紫蘇xw發表於2021-10-08

Unity可複用揹包工具

Demo展示

1


設計思路

遊戲中有非常多的揹包樣式,比如玩家道具揹包,商城,裝備欄,技能欄等;每個形式的揹包都單獨寫一份邏輯會非常繁瑣,所以需要有一套好用的揹包工具;

這些揹包有幾個共同的特點:

1.有多個排列好的方格子;

2.每個方格子中有內容時,可被拖動且拖動邏輯相同;

3.可新增使用刪除格子中的物品;

因此根據這些特點,使用ScrollView等元件,提取兩個類,分別負責資料管理和拖動邏輯;


前期準備

1.介面設定

製作三個介面,一個滾動揹包皮膚,一個丟棄皮膚,一個單獨物品的預製體;

關鍵元件:ScrollView,content中新增GridLayoutGroup;

image-20210926101014836

image-20210925222627318

2.物品配表

1.使用Excel配置物品屬性表,同時建立欄位和excel標籤相同的類,用於json序列化;

image-20210925222717143

2.Excel轉Json,最簡單方式;

image-20210926093739019

之後將轉成功的Json內容存到txt文字中,並匯入專案;

3.LitJson庫

我這裡使用的LitJson,一個非常簡單輕量的庫;https://litjson.net/

直接匯入專案或者打包成dll放進專案;

使用時只需要讀取Txt文字,轉成string,直接呼叫Api即可,支援陣列;

image-20210926094154640


關鍵基類設計

1.Item

物品屬性基類,規定物品屬性,需要欄位名和json中的關鍵字相同才能被json序列化;

Clone方法用來深拷貝,需要重寫,因為我深拷貝使用的記憶體拷貝,所以必須加[Serializable];

ItemKind類,單純是為了不用每次判斷時手動打“string",個人覺得麻煩Orz;

image-20210926094410027

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:

  1. 檢測拖拽結束時,Item下方的UI是什麼型別;我這裡設定了三個Tag;
  2. item—下方為有物品的格子,兩個互換位置;
  3. box—為空的格子,Item移位;
  4. background—彈出丟棄物品皮膚,同時隱藏當前Item;
  5. 其他—返回原位置;
  6. 判斷結束後將位置歸零,關閉射線遮蔽;

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;
}

以上是我對揹包工具的總結,如果有更好的意見,歡迎給作者評論留言;

相關文章