Unity实现简易背包系统

Unity中使用ScriptableObject和UI实现背包系统

ScriptableObject:

  • ScriptableObject 是 Unity 提供的一个数据配置存储基类,它是一个可以用来保存大量数据的数据容器,我们可以将它保存为自定义的数据资源文件。
  • ScriptableObject 是一个类似 MonoBehaviour 的基类,继承自 UnityEngine.Object 。要想使用它,需要我们写个脚本去继承 ScriptableObject 。需要注意的是,继承自 SctiptableObject 的脚本无法挂载到游戏物体上,毕竟它不是继承自 MonoBehaviour。
  • ScriptableObject 类的实例会被保存成资源文件(.asset文件),和预制体,材质球,音频文件等类似,都是一种资源文件,存放在 Assets 文件夹下,创建出来的实例也是唯一存在的
  • 详细链接:Unity进阶:ScriptableObject使用指南

背包系统详细实现链接:背包系统制作

背包功能实现

核心代码和想法:

两个ScriptableObject: 分别代表物品和背包

Item.cs 物品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="New Item",menuName ="Inventory/New Item")]
public class Item : ScriptableObject
{
public string itemName; //物品名称
public Sprite itemImage; //物品图片
public int itemHeld; //物品数量
[TextArea]
public string itemInfo; //物品信息
public bool equip; //物品是否可装备

}

Inventory.cs 背包

1
2
3
4
5
6
7
8
9
10
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="New Inventory",menuName ="Inventory/New Inventory")]
public class Inventory : ScriptableObject
{
public List<Item> itemList=new List<Item>(); //存放物品
}

让我们按照事件触发的逻辑走起。

首先我们是要碰撞到世界中的一个物体,然后会调用背包管理脚本添加物品。

那么世界中的物体需要脚本来控制,在触发碰撞且碰撞对象为玩家(Player)时就要调用背包管理脚本添加物品,同时销毁自己。

当前物体在挂载脚本之后需要绑定两个object:Item和Inventory,表示是哪种类型的物体,该放在哪一个背包中。

让我们来看代码实现吧:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemOnWorld : MonoBehaviour
{
public Item thisItem;
public Inventory playerInventory;
private void OnTriggerEnter2D(Collider2D other) {
if(other.gameObject.CompareTag("Player"))
{
AddNewItem();
Destroy(gameObject);
}
}
public void AddNewItem(){
//判断是否在列表中,在 先加1
if(!playerInventory.itemList.Contains(thisItem)){
// playerInventory.itemList.Add(thisItem);
for(int i=0;i<playerInventory.itemList.Count;i++){
if(playerInventory.itemList[i]==null){
playerInventory.itemList[i]=thisItem;
break;
}
}
}else{
thisItem.itemHeld+=1;
}
InventoryManger.RefreshItem();
}
}

先修改背包的数据,然后调用RefreshItem重新生成每一个背包槽

在一开始游戏初始化的时候,我们需要预先生成一些空槽位来填充grid panel,方便之后实现拖拽功能。空槽位的大小需要我们预先在Inventory对象中定义。修改myBag的itemList大小即可。

InventoryManger.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class InventoryManger : MonoBehaviour
{
static InventoryManger instance;
public Inventory myBag;
public GameObject slotGrid;
// public Slot slotPrefab;
public GameObject emptySlot;
public TMP_Text itemInformation;
public List<GameObject> slots=new List<GameObject>();

private void Awake()
{
if (instance != null)
Destroy(this);
instance = this;
}
private void OnEnable()
{
RefreshItem();
itemInformation.text = "";
}

public static void UpdateItemInformation(string itemDescription)
{
instance.itemInformation.text = itemDescription;
}

public static void RefreshItem()
{
for (int i = 0; i < instance.slotGrid.transform.childCount; i++)
{
if (instance.slotGrid.transform.childCount == 0)
break;
Destroy(instance.slotGrid.transform.GetChild(i).gameObject);
instance.slots.Clear();
}
for (int i = 0; i < instance.myBag.itemList.Count; i++)
{
instance.slots.Add(Instantiate(instance.emptySlot));
instance.slots[i].transform.SetParent(instance.slotGrid.transform);
instance.slots[i].GetComponent<Slot>().slotID=i;
instance.slots[i].GetComponent<Slot>().SetupSlot(instance.myBag.itemList[i]);
}
}
}

可以看到这个背包管理脚本的核心就是控制销毁或实例化slot物体。
slot是什么呢?

Slot本身不再承载装备的素材图片,仅呈现出背包栏背景图片样式,实际的图片以及数字由Slot下的相关子物体承载。这样在没有物品时,只需将子物体的active属性设置为false即可正常显示背包栏背景,而对于物品的移动和交换,只需要将两个Slot下的子物体交换即可。

Slot.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class Slot : MonoBehaviour
{
public int slotID; //空格ID=物体ID
public Item slotItem;
public Image slotImage;
public TMP_Text slotNum;
public string slotInfo;
public GameObject itemInSlot;
public void ItemOnClicked()
{
InventoryManger.UpdateItemInformation(slotInfo);
}
public void SetupSlot(Item item){
if(item==null){
itemInSlot.SetActive(false);
return;
}else{
slotImage.sprite=item.itemImage;
slotNum.text=item.itemHeld.ToString();
slotInfo=item.itemInfo;
}
}
}

这里的绑定关系如图所示:


当调用SetupSlot的时候就会显示当前类型物品的图片和数量,并更新描述信息。

实现拖拽功能,主要靠交换slot下的item来实现,因此需要创建ItemOnDrag脚本并赋给item对象。

注意点:

  1. 拖拽的时候解除item和slot的父子关系,但是此时它还是位于Gird布局下,因此会“溢出”,所以要给item添加组件,忽略布局
  2. 如何判读拖拽后的位置可以放物体进去。使用UI射线检测,为了避免item挡住检测,要给它添加CanvasGroup组件,通过设置blocksRaycasts来控制是否阻止射线检测。
  3. 如果只是交换item的位置并不能实际改变数据,下次调用RefreshItem()又会重置会原来的样子,所以需要实际交换背包的itemList对应的内容才行。
    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
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    public class ItemOnDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
    {
    public Transform originalParent;
    public Inventory myBag;
    private int currentItemID; //当前物品ID
    public void OnBeginDrag(PointerEventData eventData)
    {
    originalParent=transform.parent;
    currentItemID=originalParent.GetComponent<Slot>().slotID;
    transform.SetParent(transform.parent.parent);
    transform.position=eventData.position;
    GetComponent<CanvasGroup>().blocksRaycasts=false;
    }

    public void OnDrag(PointerEventData eventData)
    {
    transform.position=eventData.position;
    Debug.Log(eventData.pointerCurrentRaycast.gameObject.name);

    }

    public void OnEndDrag(PointerEventData eventData)
    {
    if(eventData.pointerCurrentRaycast.gameObject!=null)
    if(eventData.pointerCurrentRaycast.gameObject.name=="ItemImage"){
    transform.SetParent(eventData.pointerCurrentRaycast.gameObject.transform.parent.parent);
    transform.position=eventData.pointerCurrentRaycast.gameObject.transform.parent.parent.position;

    //实际数据改变
    var tmp=myBag.itemList[currentItemID];
    myBag.itemList[currentItemID]=myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID];
    myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID]=tmp;


    eventData.pointerCurrentRaycast.gameObject.transform.parent.position=originalParent.position;
    eventData.pointerCurrentRaycast.gameObject.transform.parent.SetParent(originalParent);
    GetComponent<CanvasGroup>().blocksRaycasts=true;
    return;
    }
    if(eventData.pointerCurrentRaycast.gameObject.name=="slot(Clone)")
    {
    transform.SetParent(eventData.pointerCurrentRaycast.gameObject.transform);
    transform.position=eventData.pointerCurrentRaycast.gameObject.transform.position;
    myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID]=myBag.itemList[currentItemID];
    if(eventData.pointerCurrentRaycast.gameObject.GetComponent<Slot>().slotID!=currentItemID)
    myBag.itemList[currentItemID]=null;
    GetComponent<CanvasGroup>().blocksRaycasts=true;
    return;
    }
    //其他任何位置都归位
    transform.SetParent(originalParent);
    transform.position=originalParent.position;
    GetComponent<CanvasGroup>().blocksRaycasts=true;
    }

    }

移动背包

最后需要做是移动背包整体界面的功能,通过在一个简单的脚本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class MoveBag : MonoBehaviour, IDragHandler
{
public Canvas canvas;
RectTransform currentRect;
private void Awake()
{
currentRect = GetComponent<RectTransform>();
}
public void OnDrag(PointerEventData eventData)
{
currentRect.anchoredPosition+=eventData.delta;
}
}

保存背包数据到本地,下次加载

在场景中新建空物体,挂载GameSaveManger脚本,绑定想保存的背包

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
public class GameSaveManger : MonoBehaviour
{
public Inventory myInventory;
public void SaveGame(){
if(!Directory.Exists(Application.persistentDataPath+"/game_SaveData")){}
Directory.CreateDirectory(Application.persistentDataPath+"/game_SaveData");
BinaryFormatter formatter=new BinaryFormatter(); //二进制转换
FileStream file=File.Create(Application.persistentDataPath+"/game_SaveData/inventory.txt");
var json=JsonUtility.ToJson(myInventory);
formatter.Serialize(file,json);
file.Close();
}
public void LoadGame(){
BinaryFormatter binaryFormatter=new BinaryFormatter();
if(File.Exists(Application.persistentDataPath+"/game_SaveData/inventory.txt")){
FileStream file=File.Open(Application.persistentDataPath+"/game_SaveData/inventory.txt",FileMode.Open);
JsonUtility.FromJsonOverwrite((string)binaryFormatter.Deserialize(file),myInventory);
file.Close();
}
}

}

Unity实现简易背包系统
https://shanhainanhua.github.io/2023/03/25/Unity实现简易背包系统/
作者
wantong
发布于
2023年3月25日
许可协议