Demo展示

1

设计思路

游戏中有非常多的背包样式,比如玩家道具背包,商城,装备栏,技能栏等;每个形式的背包都单独写一份逻辑会非常繁琐,所以需要有一套好用的背包工具;

这些背包有几个共同的特点:

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

2.每个方格子中有内容时,可被拖动且拖动逻辑相同;

3.可添加使用删除格子中的物品;

因此根据这些特点,使用ScrollView等组件,提取两个类,分别负责数据管理和拖动逻辑;

前期准备

界面设置

制作三个界面,一个滚动背包面板,一个丢弃面板,一个单独物品的预制体;

关键组件:ScrollView,content中添加GridLayoutGroup;

image-20210926101014836

image-20210925222627318

物品配表

1.使用Excel配置物品属性表,同时创建字段和excel标签相同的类,用于json序列化;

image-20210925222717143

2.Excel转Json,最简单方式;

image-20210926093739019

之后将转成功的Json内容存到txt文本中,并导入项目;

LitJson库

我这里使用的LitJson,一个非常简单轻量的库;https://litjson.net/

直接导入项目或者打包成dll放进项目;

使用时只需要读取Txt文本,转成string,直接调用Api即可,支持数组;

image-20210926094154640

关键基类设计

Item

物品属性基类,规定物品属性,需要字段名和json中的关键字相同才能被json序列化;

Clone方法用来深拷贝,需要重写,因为我深拷贝使用的内存拷贝,所以必须加[Serializable];

ItemKind类,单纯是为了不用每次判断时手动打“string”,个人觉得麻烦Orz;

image-20210926094410027

InventoryItem

挂在物品的预制体模板上,负责拖拽和刷新逻辑;

该类继承拖拽相关的三个接口;

1
2
3
IBeginDragHandler	//开始拖拽
IDragHandler //拖拽中
IEndDragHandler //拖拽结束

字段:

1
2
3
4
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):提取字符串中的数字,正则表达式;

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();

加载背包数据,第一次加载背包时调用;

InventoryPanel

使用这个父类,单纯为了让InventoryData中的字段有父类指向,content为所有box格子的父节点;

继承后的子类,需要在改面板中添加打开,关闭,数量显示,金钱等其他逻辑;

如果有UI框架,该类需要继承UI基类;

1
2
3
4
public class InventoryPanel : MonoBehaviour
{
public Transform content;
}

Test类

四个类分别继承四个关键基类;

GoodInfo:

继承自Item可添加需要字段,比如gold,cost等;

重写深拷贝方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;重写了刷新方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

添加了两个测试方法,初始化背包数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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的名称(上面我改成正则表达式提取数字,这里可以不用改了);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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:

丢弃面板,是否丢弃;是删除数据,否物品打开隐藏返回父节点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 出来的,这里就需要使用反射或内存拷贝;

反射:有时候会失效,原因未知;

1
2
3
4
5
6
7
8
9
10
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}