Unity-背包系统

简介

​ 背包是每个成功游戏中不可缺少的,玩家获取的装备与道具将会放入背包,需要时再拿出来使用。如果没有背包来储存玩家在游戏中获得的武器和道具,或许游戏将会变得十分单一枯燥,出招方式一成不变。

​ 有了背包系统,玩家才可以使用不同的武器,搭配不同的道具,使出不同的攻击搭配,从而提高游戏的多样性。

简单构思

​ 首先我们思考一下背包系统的简单逻辑,玩家拾取物品后,背包中出现该物品,点击该物品之后又可以使用。

​ 上述步骤的实现,需要我们完成两个层面的工作,一个是 背包的数据库 ,一个是 背包的UI

背包的数据库

数据库逻辑

​ 现在需要构思如何拾取物品后记录数据。这里可以使用 ScriptableObject 来记录各个物品以及背包的数据,当玩家拾取物品后,将物品的信息传入背包的数据库进行记录。

创建数据库的步骤及细节

​ 根据上述简单逻辑,可以得到下列创建数据库的步骤:

  1. 需要给 每个物品每个背包 都创建自己的 ScriptableObject 数据
  2. 物品数据中包含自身的各项参数;背包数据需要有将物品数据存进背包的函数
  3. 同时需要创建 MonoBehavior脚本,并将其挂载在 物品和背包 上用于 控制自身读取数据
  4. 物品脚本需要获得物品数据,并拥有将该数据放入背包的函数;背包脚本只需要拥有背包数据即可
  5. Player在触碰到物体后,物体本身的脚本将会触发,并通过函数将自己的数据加入背包中

(还需要注意以下细节:

  1. 物品数据中只有自身的个数,无法记录相同物品在背包中的个数,需要 创建一个类来保存物品数据及个数
  2. 不同的物品会需要不同的物品数据,需根据需求获取不同物品独有的 ScriptableObject数据
  3. 背包数据中,存储数据的算法需要做到最简(?)

代码样例_物品数据

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
public enum ItemType{ Useable, Weapon, Armor }//控制物品类型

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item Data")]
public class ItemData_SO : ScriptableObject
{
public bool stackable; //判断物品是否可以堆叠
public ItemType itemType; //物品对应类型
public string itemName; //物品名字
public Sprite itemIcon; //物品图片
public int itemAmount; //物品个数

[TextArea]
public string description = ""; //用于描述物品信息

////////////////////////////////////////////////////////
/////////////////以下是各个物品的独有数据/////////////////
////////////////////////////////////////////////////////

[Header("Useable")] //使用品
public UseableItemData_SO useableItemData;

[Header("Weapon")] //武器
public GameObject weaponPrefabs;
public AttackData_SO attackData;
}

代码样例_背包数据

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
////////////////////////////////////////////////////////////
////////////////为记录背包中的物品个数创建该类/////////////////
///////////////////////////////////////////////////////////
[System.Serializable] //为使该类在Unity窗口中显示,需要序列化
public class InventroyItem
{
public ItemData_SO itemData; //物品信息
public int amount; //物品数量
}
////////////////////////////////////////////////////////////
////////////////为记录背包中的物品个数创建该类/////////////////
///////////////////////////////////////////////////////////

[CreateAssetMenu(fileName = "New Inventory", menuName = "Inventory/Inventory Data")]
public class InventoryData_So : ScriptableObject
{
public List<InventroyItem> items = new List<InventroyItem>();
//将物品数据保存进背包
public void AddItem(ItemData_SO itemData, int amount){
bool found = false; //判断物品是否被找到
//如果物品是可堆叠的,则先遍历列表
if(itemData.stackable){
foreach (var item in items){
if(item.itemData == itemData){
//如果背包中有同类型道具,则直接增加数量,并标明物品已经找到
item.amount += amount;
found = true;
break;
}
}
}
if(!found){ //如果物品没有被找到
for(int i = 0; i < items.Count; i++){
//则同样遍历整个列表,找到第一个空位并且添加
if(items[i].itemData == null){
items[i].itemData = itemData;
items[i].amount = amount;
break;
}
}
}
}
}

代码样例_背包脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class InventroyManager : Singleton<InventroyManager> //继承单例模式
{
//TODO:最后复制模板保存数据
[Header("Inventory Data")] //背包数据
public InventoryData_So inventoryData;
//剩余不同背包........

void Start()
{
inventoryUI.RefreshUI();
//剩余不同背包.........
}

//UI部分代码
}

/*该类还有很多尚未完成的部分,未完成的部分会在下面讲述背包UI时完善*/

代码样例_物品脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ItemPickUp : MonoBehaviour
{
public ItemData_SO itemData;
void OnTriggerEnter(Collider other)
{
if(other.CompareTag("Player"))
{
//TODO:将物品添加到背包
InventroyManager.Instance.inventoryData.AddItem(itemData, itemData.itemAmount);
//........

//TODO:装备武器
// GameManager.Instance.playerStats.EquipWeapon(itemData);

Destroy(gameObject);
}
}
}

背包的UI

​ 完成上述背包的数据库后,还需要根据数据库完成背包的UI部分,让玩家能够更直观的管理背包

背包UI逻辑

​ 要完成背包UI,就需要 从背包的数据库中获取背包中各项物品的各项数据(图片、数量……),然后在背包UI上显示。

创建背包UI的步骤及细节

  1. 编写背包UI的 代码 需要三层,第一层是 物品UI层(ItemUI),第二层是 物品栏(SlotHolder),第三层是背包层(ContainerUI),最后需要将背包层挂再在 InventoryManager 上供其他类调用
  1. 根据背包UI代码所分的三层,背包UI的 GameObject 同样需要分成三层。第一层 挂载ItemUI 作为物品UI层,同时子物体需要有 图片(Image)文本(Text);第二层 挂载SlotHolder 作为物品栏,子物体需要 ItemUI;第三层 挂载Container 作为背包层,子物体需要 n个 SlotHolder

(下面将会通过代码实例更详细的讲述

代码样例_ItemUI

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
public class ItemUI : MonoBehaviour
{
public Image icon = null; //物品图片
public Text amount = null; //物品个数
public InventoryData_So Bag { get; set; } //物品所属背包,在SlotHolder中赋值
public int Index { get; set; } = -1; //物品代号

public void SetupItemUI(ItemData_SO item, int itemAmount) //更新图片数据
{
if(itemAmount == 0) //当数量为零时,清除背包中该位置的物品数据
{
Bag.items[Index].itemData = null;
icon.gameObject.SetActive(false);
return;
}

if(item != null) //如果Item不等于空, 则根据item数据显示物品
{
icon.sprite = item.itemIcon;
amount.text = itemAmount.ToString();
icon.gameObject.SetActive(true);
}
else
{
icon.gameObject.SetActive(false);
}
}
}

代码样例_SlotHolder

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
public enum SlotType { BAG, WEAPON, ARMOR, ACTION }	//记录物品栏类型(背包、武器栏、防具栏、道具栏)
public class SlotHolder : MonoBehaviour, IPointerClickHandler
{
public SlotType slotType; //背包栏类型
public ItemUI itemUI; //外部赋值

//鼠标点击使用物品函数........

public void UpdateItem() //更新物品
{
switch(slotType)
{
//根据背包栏类型找对应数据库
case SlotType.BAG:
itemUI.Bag = InventroyManager.Instance.inventoryData;
break;
case SlotType.WEAPON:
itemUI.Bag = InventroyManager.Instance.equipmentData;
//需判断武器栏内是否为空,不为空则装备对应武器,2D不需要装备武器
break;
case SlotType.ARMOR:
itemUI.Bag = InventroyManager.Instance.equipmentData;
break;
case SlotType.ACTION:
itemUI.Bag = InventroyManager.Instance.actionData;
break;
}

//根据背包数据库中的信息更改背包UI
var item = itemUI.Bag.items[itemUI.Index];
itemUI.SetupItemUI(item.itemData, item.amount);
}
}

代码样例_ContainerUI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ContainerUI : MonoBehaviour
{
public SlotHolder[] slotHolders;

public void RefreshUI() //刷新UI
{
for(int i = 0; i < slotHolders.Length; i++)
{
//根据SlotHolder类的数组的序号,刷新各个背包栏的标号
slotHolders[i].itemUI.Index = i;
slotHolders[i].UpdateItem();
}
}
}

实现拖拽物品

​ 实现物品拖拽,可以直接使用Unity自带的拖拽物品函数,只需要实现以下几个接口:

1
2
3
4
5
6
7
8
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public void OnBeginDrag(PointerEventData eventData){} //开始拖拽

public void OnDrag(PointerEventData eventData){} //拖拽中

public void OnEndDrag(PointerEventData eventData){} //拖拽结束
}

(这几个接口中传入的参数 eventData 包含了关于鼠标的各项参数,有需要的可以查阅Unity官方手册

​ 需要知道的是:

  1. 需要拖拽的物品是挂载有 ItemUI 的 GameObject ,所以该类需要挂载在上述 GameObject 上
  2. 为了防止玩家将物品拖拽进错的位置,需要 记录物品原本的位置 并在其 位置错误之后,将物品复原
  3. 在拖动结束后,需要进行一系列判断,保证物品交换无误(直接交换、堆叠 或 回到原位)

(下面用代码样例进行详细说明

代码样例

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
[RequireComponent(typeof(ItemUI))]
public class DragItem : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
ItemUI currentItemUI;
SlotHolder currentHolder;
SlotHolder targetHolder;
void Awake()
{
currentItemUI = GetComponent<ItemUI>();
currentHolder = GetComponentInParent<SlotHolder>();
}

////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////// 以下各个PointerEventData类会有与鼠标相关的各个数据///////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////


public void OnBeginDrag(PointerEventData eventData) //开始拖拽
{
//记录原始数据
InventroyManager.Instance.currentDrag = new InventroyManager.DragData();
InventroyManager.Instance.currentDrag.originalHolder = GetComponentInParent<SlotHolder>();
InventroyManager.Instance.currentDrag.originalParent = (RectTransform)transform.parent;

//为了使物品不被物品栏遮挡,将其设置为一个层级更高的画布的子物体
transform.SetParent(InventroyManager.Instance.DragCanvas.transform, true);
}

public void OnDrag(PointerEventData eventData) //拖拽中
{
//跟随鼠标位置
transform.position = eventData.position;
}

public void OnEndDrag(PointerEventData eventData) //拖拽结束
{
//放下物品 交换数据
if(EventSystem.current.IsPointerOverGameObject()) //判断是否在UI上
{
/*判断是否在对应的物品栏中,因为InventoryManager中包含了各个背包UI的数据
所以将判断函数写在InventoryManager中,方便调用*/
if(InventroyManager.Instance.CheckInActionUI(eventData.position) || InventroyManager.Instance.CheckInEquipmentUI(eventData.position) ||
InventroyManager.Instance.CheckInInventoryUI(eventData.position))
{
//给targetHolder赋值,如果找到了SlotHolder组件,则直接赋值,如果没有找到,则在父级中找
if(eventData.pointerEnter.gameObject.GetComponent<SlotHolder>())
{
targetHolder = eventData.pointerEnter.gameObject.GetComponent<SlotHolder>();
}
else
{
targetHolder = eventData.pointerEnter.gameObject.GetComponentInParent<SlotHolder>();
}

switch(targetHolder.slotType) //判断targetHolder的物品栏类型
{
case SlotType.BAG:
SwapItem();
break;
case SlotType.WEAPON:
if(currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Weapon)
{
SwapItem();
}
break;
case SlotType.ARMOR:
if(currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Armor)
{
SwapItem();
}
break;
case SlotType.ACTION:
if(currentItemUI.Bag.items[currentItemUI.Index].itemData.itemType == ItemType.Useable)
{
SwapItem();
}
break;
}

currentHolder.UpdateItem();
targetHolder.UpdateItem();
}
}
//重置物品的层级关系
transform.SetParent(InventroyManager.Instance.currentDrag.originalParent);

RectTransform t = transform as RectTransform;

//为了防止图片出现堆放位置错误的问题
t.offsetMax = Vector2.one * 40;
t.offsetMin = -Vector2.one * 40;
}

//交换物品
public void SwapItem()
{
//先获得两个 slotHolder 对应的物品
var targetItem = targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index];
var tempItem = currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index];

//判断两个物品是否一样
bool isSameItem = tempItem.itemData == targetItem.itemData;

if(isSameItem && targetItem.itemData.stackable)
{
targetItem.amount += tempItem.amount;
tempItem.itemData = null;
tempItem.amount = 0;
}
else
{
currentHolder.itemUI.Bag.items[currentHolder.itemUI.Index] = targetItem;
targetHolder.itemUI.Bag.items[targetHolder.itemUI.Index] = tempItem;
}
}
}