Unity-背包系统

发布时间 2023-11-14 19:20:08作者: Daliuteliu

Unity-背包系统

简介

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

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

简单构思

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

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

背包的数据库

数据库逻辑

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

创建数据库的步骤及细节

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

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

(还需要注意以下细节:

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

代码样例_物品数据

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

代码样例_背包数据

////////////////////////////////////////////////////////////
////////////////为记录背包中的物品个数创建该类/////////////////
///////////////////////////////////////////////////////////
[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;
                }
            }
        }
    }
}

代码样例_背包脚本

public class InventroyManager : Singleton<InventroyManager> //继承单例模式
{
    //TODO:最后复制模板保存数据
    [Header("Inventory Data")]	//背包数据
    public InventoryData_So inventoryData;
	//剩余不同背包........

    void Start() 
    {
        inventoryUI.RefreshUI();
        //剩余不同背包.........
    }
    
    //UI部分代码
}

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

代码样例_物品脚本

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 上供其他类调用

  2. 根据背包UI代码所分的三层,背包UI的 GameObject 同样需要分成三层。第一层 挂载ItemUI 作为物品UI层,同时子物体需要有 图片(Image)文本(Text);第二层 挂载SlotHolder 作为物品栏,子物体需要 ItemUI;第三层 挂载Container 作为背包层,子物体需要 n个 SlotHolder

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

代码样例_ItemUI

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

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

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自带的拖拽物品函数,只需要实现以下几个接口:

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. 在拖动结束后,需要进行一系列判断,保证物品交换无误(直接交换、堆叠 或 回到原位)

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

代码样例

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