Unity2D背包系统的搭建

0. 前言

这篇文章参考了麦扣老师@M_Studio的教程Unity教程:背包系统01:概览简介_哔哩哔哩_bilibili

我做了许多修改和调整。项目的完整代码我放在了这里LLLLLevi/2DPlatform: 2D平台横版战斗 (github.com)

希望可以帮到你😉

1. GUI 图形界面设置

本节知识点:

  1. Grid Layout Group组件:该组件用于对子对象进行网格状的排列。它可以将子对象按照指定的行数、列数以及间距进行自动布局

我想做的是类似泰拉瑞亚的库存系统,有始终在场景中的物品栏(ToolBar),可按快捷键12345使用物品;也有需按键调出的主背包(MainInventory)。

UI层级结构如下:

image-20231119215330579

SlotBackground:空物品槽的父类,物品槽在其下面生成。给他添加Grid Layout Group组件

image-20231115223748261

效果如下:

image-20231115223833763

主仓库要按Tab键才能唤出,代码逻辑如下(放在人物控制脚本内):

bool isOpen = false;
[SerializeField] GameObject bag;

public void OpenBag(InputAction.CallbackContext context)
{
    if (context.started)
    {
        isOpen = !isOpen;   //赋反值
        bag.SetActive(isOpen);
    }

}

用的是新输入系统,设置好按键后将按键绑定在Player输入系统上。

2. ScriptableObject保存物品属性

ScriptableObject是Unity中的一个特殊类,用于创建可序列化的自定义数据对象。它主要用于存储和管理在游戏中需要持久化的数据,例如游戏设置、角色属性、关卡信息等。

与其他Unity组件不同,ScriptableObject不依赖于游戏对象,因此它可以独立存在。这意味着它可以在脚本之间共享和传递,并且可以在不同场景中重复使用。

创建一个ScriptableObject对象非常简单。只需创建一个继承自ScriptableObject的脚本类,并在编辑器中创建一个新的ScriptableObject实例。

使用ScriptableObject的好处包括:

  1. 可以在编辑器中创建和编辑自定义数据对象,避免了手动编写和解析数据的麻烦。
  2. ScriptableObject的数据是可序列化的,因此可以在不同平台和场景间进行持久化保存和加载。
  3. 可以通过引用来共享和传递ScriptableObject对象,方便数据的复用和管理。

所谓背包系统,当然得有背包类和物品类,我们使这两个类继承自ScriptableObject,从而实现可在Unity中创建多种物品、多种仓库(如主背包、物品栏、商店)的目的。

  • 物品类中定义物品的普遍拥有的属性
  • 仓库类定义物品的列表
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 itemHeldNum;    //持有的物品数
    [TextArea]              //使得输入为多行文本框而不是单行
    public string itemInfo; //物品介绍
    public ItemType type;
}

//物品种类枚举,用于判断物品特性
public enum ItemType{
    key,
    food,
    equipment
}

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

在Unity右键菜单中的右键菜单中创建物品鸡腿、主仓库和物品栏:

image-20231114213511415

()[https://gitee.com/lingweizhenshuai/typora-image/raw/master/img/image-20231114213530667.png]

image-20231114213635720

image-20231114213644702

OK,接下来就是写脚本使得当玩家碰撞到世界中的物品时,将此物品添加进主背包:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemInWorld : MonoBehaviour
{
    //物品数据和要保存到的物品仓库
    public Item item;
    public Inventory mainInventory;

    public void AddNewItem()
    {
        //不存在就找到最前面的一个空位然后填入物品到仓库,存在就物品数量加1
        if (!mainInventory.itemList.Contains(item))
        {
            for (int i = 0; i < mainInventory.itemList.Count; i++)
            {
                if (mainInventory.itemList[i] == null)
                {
                    mainInventory.itemList[i] = item;
                    break;
                }
            }
        }
        else
        {
            item.itemHeldNum++;
        }

        InventoryManager.RefreshItem();     //刷新UI的物品数
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        //碰到玩家则添加物品
        if (collision.gameObject.CompareTag("Player"))
        {
            AddNewItem();
            Destroy(gameObject);
        }
    }
}

在场景中添加鸡腿(不是数据文件),将上面这个脚本挂载到鸡腿物品身上,同时添加触发器:

image-20231114214006659

将MainInventory的列表大小改成你背包格子的数量:

image-20231118204508682

当吃到鸡腿后,主背包的列表第一个值改变,且结束游戏后数据依旧存在。当再次运行游戏再吃鸡腿,持有的鸡腿数变为2:
人物与鸡腿

image-20231118204632835

image-20231114214331777

3. 将物品显示在背包中

声明slot类,用于定义物品槽的属性值,如物品的属性值、物品图片、物品持有数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class Slot : MonoBehaviour
{
    public int slotID;      //格子的编号
    public Item slotItem;   //物品的属性
    public Image slotImage;
    public Text slotNum;     //持有数
    public GameObject Description;
    public Text DescriptionText;

    public GameObject itemInSlot;
    //生成格子的数据
    public void SetupSlot(Item item)
    {
        if(item == null)
        {
            itemInSlot.SetActive(false);    //如果当前格子没物品数据,那就不显示该物品(不然就会显示一张白图)
            return;
        }

        //物品数据显示
        slotImage.sprite = item.itemImage;
        slotNum.text = item.itemHeldNum.ToString();
        DescriptionText.text = item.itemInfo;
    }
}

ItemOnPointer用于当鼠标悬停时,显示物品的描述信息,挂载到Item上。

继承IPointerEnterHandler和IPointerExitHandler,用于实现鼠标悬停在格子上时显示物品的描述信息。

实现鼠标悬停的方法也蛮多的,可以看这个UGUI鼠标悬停事件-CSDN博客)的回答。

using UnityEngine.EventSystems;
using UnityEngine.UI;

public class ItemOnPointer : MonoBehaviour, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler
{
    public GameObject Description;
    public Text DescriptionText;

    public Vector2 vec = new Vector2(200, -130);     //与鼠标的偏移值

    public void OnPointerEnter(PointerEventData eventData)
    {
        Description.SetActive(true);
    }
    public void OnPointerMove(PointerEventData eventData)
    {
        Description.transform.position = eventData.position + vec;//物品描述框跟随鼠标
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        Description.SetActive(false);
    }
}

创建InventorySlot预制体,结构如下,用于生成出现在UI上,将Slot脚本挂载到声明好的InventorySlot预制体上:

image-20231118205022960

其中Description要有Canvas组件:

image-20231119155710913

声明InventoryManager类,用于管理库存系统的数据

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class InventoryManager : MonoBehaviour
{
    //用单例模式 方便管理
    static InventoryManager instance;

    public Inventory mainInventory;
    public GameObject slotParent;   //格子的爹  
    public GameObject emptySlot;    //空格子

    public List<GameObject> slotsList = new List<GameObject>();     //用来存放空格子的列表


    void Awake()
    {
        if(instance != null)
        {
            Destroy(this);
        }
        instance = this;
    }

    private void OnEnable()
    {
        RefreshItem();      //游戏开始时刷新背包
    }

    //刷新当前背包的物品数:先删后创等于刷新
    public static void RefreshItem()
    {
        //根据子类的数量遍历删除
        for(int i = 0; i < instance.slotParent.transform.childCount; i++)
        {
            if (instance.slotParent.transform.childCount == 0)
                break;
            Destroy(instance.slotParent.transform.GetChild(i).gameObject);
            instance.slotsList.Clear(); //清空列表
        }

        //重新创建物品
        for(int i = 0; i < instance.mainInventory.itemList.Count; i++)
        {
            //生成空格子且添加到空格子列表中
            GameObject newSlot = Instantiate(instance.emptySlot);
            //newSlot.transform.localScale = new Vector2(2, 2);
            instance.slotsList.Add(newSlot);    
            instance.slotsList[i].transform.SetParent(instance.slotParent.transform);   //静态方法的变量一定是静态值
            instance.slotsList[i].GetComponent<Slot>().slotID = i;  //赋予编号
            instance.slotsList[i].GetComponent<Slot>().SetupSlot(instance.mainInventory.itemList[i]);   //赋值生成物品的数据
        }
    }
}

将InventoryManager挂载到Canvas上:

image-20231118205149262

效果如下:

image-20231119215716624

4. 物品的拖拽与交换

原理讲解:

实现更改物品在背包中的位置和交换物品

给Item添加组件Canvas Group和Layout Element,我们逐一解释为什么添加:

image-20231119220037238

我们拖拽的原理是从鼠标位置向屏幕内方向发射射线去识别物品。当我们拖拽物品的时候,物品会跟着鼠标移动嘛,所以就会使得鼠标发出的射线无法透过当前拖拽的物品,从而识别不到我们该放到哪个物品槽中。

怎么办呢?就是用这个Canvas Group的Blocks Raycasts功能,它可以暂时屏蔽挂载了这个组件的物品,使得射线可以探测它下面的UI。

在层级关系中我们的Item是位于InventorySlot的子层级下面的,所以说我们想要实现更改物品UI的位置就得和原来的Slot脱离父子关系,再去到其他的Slot成为它的儿子,从而实现换位置。那么别忘了我们SlotBackground是有Grid Layout Group的,也就是说当和原来的父级脱离关系时,有一瞬间这个Item会顺位的跑到SlotBackground下成为最后一个元素,就会有图片在背包左下角闪一下的问题。

为了解决这个小bug,我们就掏出了Layout Element中的ignore Layout去忽略布局,就这么简单。如果没听懂的话可以去看麦扣的视频:Unity教程:背包系统07:优化代码&解决bugs(也有小技巧哦)_哔哩哔哩_bilibili

下面我们就来实现它:

实现:

ItemOnDrag实现物品拖拽和交换,与显示物品的描述信息原理相似,继承IBeginDragHandler, IDragHandler, IEndDragHandler。将它挂载到Item上。

这部分代码可能会比较绕,但其实没多大事情,就是更新UI位置和mainInventory中itemList的元素位置,使其同步

using UnityEngine.EventSystems;

//挂载到物品身上,实现拖拽效果
public class ItemOnDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public Transform originalParent;    //记录起始的父级位置
    public Inventory mainInventory;
    private int curItemID;

    public void OnBeginDrag(PointerEventData eventData)
    {
        originalParent = transform.parent;
        curItemID = 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)
    {
        GameObject itemOnRaycast = eventData.pointerCurrentRaycast.gameObject;
        Transform itemTransOnRaycast = eventData.pointerCurrentRaycast.gameObject.transform;

        //当拖拽终点格子有物品时,交换两个物品的位置
        if(itemOnRaycast != null)
        {
            if (itemOnRaycast.CompareTag("Item"))
            {
                //改变父级和位置
                transform.SetParent(itemTransOnRaycast.parent);     //itemTransOnRaycast.parent就是格子
                transform.position = itemTransOnRaycast.parent.position;

                //刷新itemList的物品存储位置
                var temp = mainInventory.itemList[curItemID];
                mainInventory.itemList[curItemID] = mainInventory.itemList[itemOnRaycast.GetComponentInParent<Slot>().slotID];
                mainInventory.itemList[itemOnRaycast.GetComponentInParent<Slot>().slotID] = temp;

                itemTransOnRaycast.position = originalParent.position;
                itemTransOnRaycast.SetParent(originalParent);

                GetComponent<CanvasGroup>().blocksRaycasts = true;
                return;
            }

            //没有物品时射线射到的是个格子,直接赋值给这个格子
            else if (itemOnRaycast.CompareTag("Slot"))
            {
                transform.SetParent(itemTransOnRaycast);
                transform.position = itemTransOnRaycast.position;
                //空格子的位置也得换
                itemTransOnRaycast.Find("Item").position = originalParent.position;
                itemTransOnRaycast.Find("Item").SetParent(originalParent);
                

                mainInventory.itemList[itemOnRaycast.GetComponent<Slot>().slotID] = mainInventory.itemList[curItemID];
                //变位置了的话就将原来的改为空
                if (itemOnRaycast.GetComponent<Slot>().slotID != curItemID)
                    mainInventory.itemList[curItemID] = null;

                GetComponent<CanvasGroup>().blocksRaycasts = true;
                return;
            }
        }
        
        //射到其他东西时回到原位置
        transform.SetParent(originalParent);
        transform.position = originalParent.position;
        GetComponent<CanvasGroup>().blocksRaycasts = true; 
    }
}

效果如下:

动画

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值