Unity2D背包系统

概述

本文中介绍的方法是对麦扣老师发布的视频教程“Unity教程:背包系统”的记录和总结。

这里是麦扣老师的链接:https://m-studio-m.github.io/

准备工作

该项目用到了三份美术素材

  • FreePixelGear:装备图片
  • Simple Fantasy GUI:背包UI图片
  • Tiny RPG Forest:游戏场景及人物图片

这三份美术素材可以在UnityAssetStore免费下载。

下面是游戏场景和背包UI截图:

  • 玩家在游戏世界中碰到道具/装备后,会将该物品放入背包的物品栏中,并在格子左下角显示当前物品的持有数量。
  • 当玩家点击物品栏中的物品时,会在背包UI左下方显示该物品的详细介绍。
  • 玩家可以单击键盘“O”键来打开和关闭背包UI,在背包UI打开时,点击其右上角的“关闭按钮”也可以关闭背包。
  • 玩家可以用鼠标拖动物品栏中的物品,将它放置到其他格子里;如果格子里有物品,则交换两个物品位置。
  • 玩家点击游戏界面左上角的Save按钮,可以将现在背包数据和物品数据作为存档保存起来;点击Load按钮,则将保存的存档加载到目前游戏中。

由于游戏的输入检测,人物的移动、动画控制不在文章的讨论范围内,因此这部分代码直接给出。

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

public class PlayerMovement : MonoBehaviour
{
    Rigidbody2D rb;
    Collider2D coll;
    Animator anim;

    public GameObject myBag;//myBag的UI
    private bool bagIsOpen;//myBag是打开还是关闭状态

    public float speed;
    Vector2 movement;

    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        coll = GetComponent<Collider2D>();
        anim = GetComponent<Animator>();
    }

    private void Update()
    {
        MoveInput();
        SwitchAnim();
        OpenAndCloseMyBag();
    }

    private void FixedUpdate()
    {
        Movement();
    }

    void MoveInput()
    {
        movement.x = Input.GetAxisRaw("Horizontal");
        movement.y = Input.GetAxisRaw("Vertical");
    }

    void Movement()//移动
    {
        rb.MovePosition(rb.position + movement * speed * Time.fixedDeltaTime);
    }

    void SwitchAnim()//切换动画
    {
        if (movement != Vector2.zero)//保证Horizontal归0时,保留movment的值来切换idle动画的blend tree
        {
            anim.SetFloat("horizontal", movement.x);
            anim.SetFloat("vertical", movement.y);
        }
        //magnitude 也可以用 sqrMagnitude 具体可以参考Api 默认返回值永远>=0
        //返回向量movement的长度
        anim.SetFloat("speed", movement.magnitude);
    }

    //打开和关闭myBag
    private void OpenAndCloseMyBag()
    {
        //获取myBag是打开还是关闭状态
        bagIsOpen = myBag.activeSelf;

        //按下O键
        //在myBag关闭时,打开它;在myBag打开时,关闭它
        if(Input.GetKeyDown(KeyCode.O))
        {
            bagIsOpen = !bagIsOpen;
            myBag.SetActive(bagIsOpen);
            InventoryManager.instance.RefreshItem();
        }
    }
}

背包UI组件层次结构

背包UI组件结构概览图:

下面对Canvas下的UI组件进行说明:

  • BagPanel游戏物体,背包UI的主体,承载背包下的其他子级游戏物体。

  • TitleImage游戏物体,背包的Title,位于Bag的正上方,其子级下还有一个Text

  • CloseBtnButton游戏物体,位于Bag的右上方,功能是点击后关闭背包。

  • descriptionTxtText游戏物体,位于Bag的下方,用于显示物品的详细介绍。

  • UseBtnButton游戏物体,位于Bag的右下方,用于使用物品,其子级下还有一个Text

  • GridPanel游戏物体,位于Bag中心,用于存放物品。其有Grid Layout Group组件,该组件能够让Grid子级下的游戏物体的位置按照每行每列的顺序排列好。

我们可以设置Cell SizeSpacing这两项属性来改变Grid子物体的大小和它们的间距。

  • slot:Image游戏物体,是Grid的子级物体,上述提到的Grid Layout Group组件就是用来排列多个slot的,该物体是背包UI里最重要的物体。

    slot的作用是显示背包里的物品的,包括物品图片、持有数量,并且当鼠标点击物品后,会在descriptionTxt中显示其详细介绍。

    slot的默认Image是空slot图片

这样在slot中没有物品时,看上去是空的。
如:

为了实现上述slot的功能,我们在其下添加了三个子物体:

    • ItemBtnButton游戏物体,实现点击该slot后,在Grid下方显示详细介绍。

    • ItemImageImage游戏物体,显示物品的图片。

    • numberText游戏物体,显示该物品的持有数量。

其中ItemImagenumber两个物体是位于ItemBtn下方的,这样做的目的是:在后面我们要实现用鼠标拖动物品将其移动到其他slot中,因此我们直接交换slot下的ItemBtn,就可以将ItemImage和number一同交换了。

后文04.显示背包物品中将详细讨论slot是如何在游戏里工作的。


数据存储

这一节将介绍如何用ScriptableObject来创建自定义Unity文件。

游戏中的装备、药水等物品,其可能包含有物品名字、图片、持有个数、详细介绍以及是否能被玩家装备在身上等属性。

游戏中的背包(可能还有NPC的背包),其也需要记录其中装了哪些游戏物品。

Unity中有许多可创建的文件,这些文件可以保存相应的数据,如:Script文件保存代码,Material文件保存材质数据,Sprite文件保存图片等。我们可以直接在Unity中设置这些文件的属性,并且无论游戏是否在运行,它们的数据都永久地保存着,不会因游戏退出导致数据丢失。

那么我们可以通过自定义Unity文件,创建我们需要的ItemInventory文件,用来保存游戏物品信息和背包信息。

方法如下:

  • 创建Item脚本,编写代码:

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 itemImageSprite;
    public int itemHeld;  //持有个数
    [TextArea]
    public string itemInfo;

    //装备可以equip,药水不能equip
    public bool equip;
}
    • 该类继承自ScriptableObject,它将帮助我们创建自定义文件。

    • 在类名上方,使用CreateAssetMenu,让我们可以直接在Asset面板中右键创建该文件,fileName为该文件的名字,menuName为右键菜单中的路径。

    • 在类中,我们定义了物品一般包含的属性,将它们访问权限设置为public,让我们可以在Inspector面板中设置它们。

  • 创建Inventory脚本,编写代码:

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>();
}
    • 相同的,它继承自ScriptableObject

    • CreateAssetMenu设置为对应的Inventory

    • 其成员只有一个itemList列表,用于存储该背包所存放的Item

保存两个脚本,我们便成功定义了自定义文件。

Asset中右键,便可以直接创建该两项文件:

下面我们创建两个Item文件,分别将它们设置为SwordShoes

  • 两个Item文件,我们将它设置为SwordShoes

注:默认的ItemHeld为1,每拾取一个+1。

再创建一个Inventory文件,将它设置为玩家的背包,取名为myBag

  • inventory文件,玩家背包

由于现在玩家背包还是空的,其List也就为空。

 

至此,我们就已经完成了:定义自定义文件类型,以及给我们项目设置物品和背包的工作。

定义自定义文件类型的方法非常方便,不仅可以用在背包系统中,在其他需要保存某些物品数据的场景中也可以使用该方法。


显示背包物品

这一节将介绍如何将我们创建的Item显示在UI中。

注:为了能够通过代码生成slot,我们先将它保存为Prefab

  • 在玩家捡(碰撞)到物品后,判断背包里是否已经包含了该物品;

    • 若没有包含该物品,则在背包中按从头到尾的顺序找一个空slot,将该物品放入该slot中,即将item文件中的数据,如图片、持有数量和详细介绍设置给slot

    • 若背包里已经包含了该物品,则直接将该物品的持有数量,即itemHeld+1即可。

上面是这一节需要实现的大概功能,我们将创建三个脚本代码文件:ItemOnWorldSlotInventoryManager

  • ItemOnWorld脚本挂载在游戏中的物体上,如SwrodShoes

  • Slot脚本挂载在slot游戏物体上,每个slot都有该脚本;

  • InventoryManager挂载在Canvas游戏物体上,采用单例模式。

下面的图展示了这三个脚本所包含的成员以及一些说明:

  • thisItem:该物品是什么Item,如:是Sword还是Shoes,还是其他什么玩意儿。

  • playerInventory:该物品属于哪个背包,如:属于玩家的背包,还是商店的背包或者是某个NPC的背包。(本项目只有玩家的背包)

这两个成员变量需要我们在Inspector中设置它们,下面是Sword的示例:

将前面我们创建的自定义文件Sword(item)MyBag(Inventory)拖给对应的变量就OK

其他Item也是一样的设置方法,这里我们在游戏中添加两把剑和一双鞋方便测试。

  • OnTriggerEnter2D(Collider2D collision):当物体上的触发器碰到玩家后将该物品添加进MyBag中(调用AddNewItem方法),并Destroy该物品。

  • AddNewItem():实现将物品添加进MyBag的功能,并调用刷新物品界面的方法,刷新背包物品栏。该方法需要判断背包里是否已经有该物品了,以此来执行不同的添加物品代码。

  • slotID:记录当前slot是Grid中第几个,该变量用在05.实现拖拽物品效果中将使用到,这一节中可忽略。

  • slotImage:用于保存物品item的图片,在方法SetupSlot方法中会用到。

  • slotNum:用于保存物品item持有数量,在方法SetupSlot方法中会用到。

  • slotInfo:用于保存物品item详细介绍,在方法SetupSlot方法中会用到。

  • itemInSlot:是该slot下的itemBtn游戏物体,在方法SetupSlot方法中会用到。

在slot预制体中设置脚本中用到的变量:

SlotImage为slot下的ItemImage游戏物体;SlotNum为slot下的number游戏物体;ItemInSlot为slot下的ItemBtn游戏物体。

  • ItemOnClicked():实现当slot被点击时,显示该slot下物品的详细介绍。它会调用InventoryManager中的UpdateItemInfo方法,并将自己的slotInfo当成实参传进去,在InventoryManager中更改descriptionTxt的值。注:别忘了在Unity中将该方法绑定给按键。

  • SetupSlot(Item item)

    • 该方法接收一个Item类型的形参,实现设置slot的slotImage、slotNum和slotInfo成员变量的功能。

    • slot在有物品时显示物品的图片和持有数量,以及拥有物品的详细介绍;而当该slot没有物品时,则将itemInSlot隐藏掉,让它看起来是空的。

    • 该方法只会在InventoryManager脚本里被调用,调用时将MyBagitemList的每个元素当作实参传递进来,无论该元素位置是否有item:若为空,则传递进来的是null,就将itemInSlot隐藏。

  • instance:该类的静态实例,将该类设置为单例,在其他类里可通过该成员instance访问InventoryManager里的其他成员。

  • myBag:玩家背包,在RefreshItem()方法中将用到它。

  • slotGridGrid游戏物体。

  • emptySlot:slot游戏物体、预制体。

  • itemInformation:descriptionTxt的显示文本。

  • slots:存放slot的列表,当前游戏中,Grid下的slots都存放在这里面。

  • Awake():实现单例。

  • OnEnable():当该游戏物体Enable后,调用RefreshItem()方法刷新一次背包物品栏,并将descriptionTxt的显示文本设置为空。

  • UpdateItemInfo(string itemDescription):更新descriptionTxt的显示文本,它在Slot脚本里的ItemOnClicked()方法中被调用,并接收一个物品详细介绍的字符串形参,将该字符串赋值给itemInformation.text

  • RefreshItem():用于刷新背包物品栏:

    • 当玩家捡到物品后,如捡到Sword:我们会修改对应itemSword的持有数量和myBag中列表的元素,这些功能已经在ItemOnWorld::AddNewItem()方法中实现了;但我们背包UI里还没有将更新过后的数据显示出来,因此我们需要在ItemOnWorld::AddNewItem()方法结束前调用RefreshItem()方法来刷新背包UI,这也是该方法的主要作用。

    • 由于物品持有数量的变化不仅有增加,也可能有减少,并且增减量不一定都是1;若单独更改某个Slottext,则逻辑比较复杂,这里我们通过两个循环来实现“刷新”Grid;

    • 第一个循环:将Grid中的Slot都给清除掉,并将slots列表也给清空;

    • 第二个循环:将背包(itemList)里的物品再添加回来。在这里,之前定义的slots列表就派上用场了:先生成空slot,即emptySlot并添加进slots列表,同时将生成的空slot放在Grid子级下;然后我们获取到该slot的Slot脚本,调用其中的SetupSlot()方法,来设置该slot的属性。

至此,已经大概介绍完上述三个脚本代码的成员,捋一捋它们之间的逻辑关系:

  • ItemOnWorld挂载在每个可拾取物品上,当物品碰到玩家后,设置对应ItemItem::itemHeld值和Inventory::itemList中的元素。
  • Slot挂载在Grid下每一个slot上,主要有两个功能:点击显示详细介绍(调用InventoryManager::UpdateItemInfo)和设置该slot的属性。

  • InventoryManager:挂载在Canvas上,单例。主要功能有两个:修改详细介绍文本和刷新背包物品栏(调用Slot::SetupSlot)。

ItemOnWorld.cs 脚本代码

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

//该脚本挂载在游戏世界里的物品上
//thisItem存储当前物品,如Sword、Shoes等的信息
//playerInventory表示当前物品属于玩家背包,即当玩家捡到该物品后,该物品被放入玩家的背包
public class ItemOnWorld : MonoBehaviour
{
    public Item thisItem;
    public Inventory playerInventory;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.CompareTag("Player"))
        {
            AddNewItem();
            Destroy(gameObject);
        }
    }

    //将碰到的装备加入背包里
    //如果背包里没有该装备,则在背包里添加该装备
    //如果背包里已经有该装备,则将该装备的持有数量+1
    //之后,刷新背包的Grid,显示背包里新的物品信息
    private void AddNewItem()
    {
        if (!playerInventory.itemList.Contains(thisItem))
        {
            //playerInventory.itemList.Add(thisItem);
            //InventoryManager.instance.CreateNewItem(thisItem);

            //在itemList列表中找一个空位给新物品,找到后将新物品放到空位上
            for(int i=0;i<playerInventory.itemList.Count;++i)
            {
                if(playerInventory.itemList[i] == null)
                {
                    playerInventory.itemList[i] = thisItem;
                    break;
                }
            }
        }
        else
        {
            thisItem.itemHeld++;
        }

        InventoryManager.instance.RefreshItem();
    }
}

Slot.cs 脚本代码

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

public class Slot : MonoBehaviour
{
    public int slotID;//空格ID,也等于物品ID
    //public Item slotItem;//这个好像没有用到诶
    public Image slotImage;
    public Text slotNum;
    public string slotInfo;

    //slot下的ItemBtn,其不但是Button,还包含了物品的图片和持有数量text
    public GameObject itemInSlot;

    //当点击Slot后,显示该物品的文本描述
    public void ItemOnClicked()
    {
        InventoryManager.instance.UpdateItemInfo(slotInfo);
    }

    //设置slot的图片和持有数量,在InventoryManager的RefreshItem()里调用
    //调用该方法时,如果item==null,则slot没有物品,将slot的itemInSlot禁用掉,让它看起来是空的
    //如果item != null,则设置slot的属性,这样slot就会显示物品图片和持有数量
    public void SetupSlot(Item item)
    {
        if(item ==null)
        {
            itemInSlot.SetActive(false);
            return;
        }

        slotImage.sprite = item.itemImageSprite;
        slotNum.text = item.itemHeld.ToString();
        slotInfo = item.itemInfo;
    }
}

InventoryManager.cs 脚本代码

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

//该脚本挂载在UI组件Canvas上
public class InventoryManager : MonoBehaviour
{
    public static InventoryManager instance;

    public Inventory myBag;
    public GameObject slotGrid;
    //public Slot slotPrefab;
    public GameObject emptySlot;
    public Text itemInformation;

    //slot列表,用来存储Slot
    public List<GameObject> slots = new List<GameObject>();

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

    //当挂载到的GameObject Enable后调用
    private void OnEnable()
    {
        //当游戏开始时,也要刷新背包里的物品
        RefreshItem();
        instance.itemInformation.text = "";
    }

    public void UpdateItemInfo(string itemDescription)
    {
        instance.itemInformation.text = itemDescription;
    }
    /*
    //当玩家捡到新物品,即原背包里没有的物品时,调用该方法,将新物品的信息显示在Grid里
    //由于负责在Grid显示物品的是Slot,而存储物品信息的是Item
    //因此我们需要将Item中的物品信息告诉给Slot
    //并且在Grid中生成Slot,即不但在Grid的位置生成,也要将其transform的父级设置为Grid的transform
    
    public void CreateNewItem(Item item)
    {
        Slot newItem = Instantiate(instance.slotPrefab, instance.slotGrid.transform.position, Quaternion.identity);
        newItem.gameObject.transform.SetParent(instance.slotGrid.transform);

        newItem.slotItem = item;
        newItem.slotImage.sprite = item.itemImageSprite; //右值是Sprite类型
        newItem.slotNum.text = item.itemHeld.ToString();
    }
    */

    //当玩家捡到背包里已经拥有的物品后,直接将持有数量+1
    //将持有数量+1的操作在 ItemOnWord脚本中的AddNewItem方法里实现了
    //我们在这里需要将更改后的持有数量刷新,即在Grid里,让Slot显示当前的持有数量
    //由于物品持有数量的变化不仅有增加,也有减少,并且增减量不一定都是1
    //若单独更改某个Slot的text(持有数量文本text),则逻辑比较复杂
    //这里我们通过两个循环来实现“刷新”Grid
    //首先第一个循环将Grid中的Slot都给清楚掉,再用第二个循环将它们添加回来
    //添加回来后,新Slot的text就会顺带更新
    public void RefreshItem()
    {
        //第一个循环,将Grid中的Slot都给清除掉
        //由于Slot处于Grid的子级上,我们可以通过遍历Grid的Child来达到检查每一个Slot
        for (int i = 0; i < instance.slotGrid.transform.childCount; ++i)
        {
            if (instance.slotGrid.transform.childCount == 0)
                return;
            Destroy(instance.slotGrid.transform.GetChild(i).gameObject);
            instance.slots.Clear();
        }

        //第二个循环,将背包的物品再添加回来
        //这里直接用CreateNewItem方法添加
        // ↑↑↑↑大人,时代变了,不用CreateNewItem了
        //InventoryManager会维护一个slots列表,里面放背包的slots们,这个列表只有在游戏运行时才有用
        //而真正保存背包里物品信息的是Inventory里的itemList列表,即使在游戏退出后,其列表中的信息也不会被清除
        //刷新背包物品时先通过上面的循环清除Grid里的slots,然后用下面的循环添加当前itemList列表里的物品到slots列表:
        //首先生成一个新的空的slot,并添加进slots列表,并设置好它与Grid的位置
        //然后再设置刚刚生成的slot的属性——图片、持有数量等信息
        for (int i = 0; i < instance.myBag.itemList.Count; ++i)
        {
            //CreateNewItem(instance.myBag.itemList[i]);
            instance.slots.Add(Instantiate(instance.emptySlot));//生成空slot,并加入到slots列表里
            instance.slots[i].transform.SetParent(instance.slotGrid.transform);//将生成的空slot放进slotGrid
            instance.slots[i].GetComponent<Slot>().slotID = i;//给slotID赋值
            instance.slots[i].GetComponent<Slot>().SetupSlot(instance.myBag.itemList[i]);//设置slot的图片、持有数量的信息
        }
    }
}

实现拖拽物品效果

这一节将介绍实现鼠标拖拽物品栏里物品放到其他slot中的方法。

我们想要的是,slot不变,而是交换slot下的ItemBtn来实现将物品放入其他slot中的功能。

因此我们需要为预制体Slot下的ItemBtn编写一个脚本ItemOnDrag

注:需要引用命名空间:UnityEngine.EventSystems;

先介绍三个成员变量:

  • originalParent:由于涉及到两个物品交换的情况,因此要记录当前拖拽物品的父级,即SlotTansform,交换完毕时,要将被交换的物品的位置和父子级关系与originalParent绑定。

  • myBag:玩家的背包。当在背包UI界面上移动物品位置后,也要重新设置该物品存放在背包里的位置。别忘了在Unity中将myBag拖给它。

  • currentItemID:当前Item在myBag里的索引值,为了实现更新物品在myBag中的存放位置,所以需要它。
    Slot脚本中,有一个成员变量slotID,该成员在InventoryManager::RefreshItem()方法中的第二个循环中被赋值,因此Grid下的每一个slot都有自己的索引,并且该索引与myBag::itemList的索引一一对应:于是我们只要知道该slot的索引(slotID),就知道这个slot所存放的物品在myBag中列表的索引。

三个额外继承类:

  • IBeginDragHandler:定义了OnBeginDrag(PointerEventData eventData)方法,当鼠标刚开始拖拽时调用。

  • IDragHandler:定义了OnDrag(PointerEventData eventData)方法,在鼠标拖拽过程中调用。

  • IEndDragHandler:定义了OnEndDrag(PointerEventData eventData)方法,当鼠标结束拖拽时调用。

在我们拖动ItemBtn时,需要通过鼠标射线来判断鼠标光标指向的是已经有物品的slot还是空slot,由此来执行交换物品操作或者直接将物品放入空slot。

我们可以通过eventData.pointerCurrentRaycast.gameObject.name来判断鼠标射线照射到的UI游戏物体的名字(若射线照射到的不是UI游戏物体,则返回null):如果返回的是“ItemImage”,表示鼠标停留在有物品的slot上,如果返回的是“slot(Clone)”(由于是通过预制体生成,后面带有Clone),则表示该slot上没有物品,是空的。

但还有一个问题:当我们拖拽物品时,被拖拽物品是一直跟随鼠标的,因此上述API返回值永远是“ItemImage”。为了解决这个问题,我们给ItemBtn添加一个游戏组件:CanvasGroup

该组件中有BlocksRaycasts选项:当该选项设置为fasle时,鼠标射线将穿透被拖拽物品。所以我们可以在开始拖拽时,将该属性设置为false,让我们的鼠标射线能够判断下一层UI的name,最后结束拖拽时再将它设置为true。

成员方法:

  • OnBeginDrag(PointerEventData eventData):刚开始拖拽时会被调用。

    • 设置originalParent、currentItemID以及CanvasGroup::blocksRaycasts的值

    • ItemBtn的位置等于鼠标光标的位置

  • OnDrag(PointerEventData eventData):拖拽过程中会一直调用该方法,我们只需要在该方法中让ItemBtn的位置一直与鼠标光标的位置一致就好。

  • OnEndDrag(PointerEventData eventData):拖拽结束时会被调用。

    • 若当结束拖拽时,鼠标放在另一个道具上边,则交换两个道具的位置。

      • 将被拖拽物品放在该slot下。

      • 更改itemList中物品的存储位置。

      • 将被交换物品放在originalParent下。

      • 将blocksRaycasts设置为true。

    • 拖拽结束时,鼠标放在空slot上,则直接将道具放在这个slot下面。

      • 将被拖拽物品放在该slot下。

      • 更改itemList中物品的存储位置,别忘了将之前物品存放的位置清空。

      • blocksRaycasts设置为true。

    • 拖拽到其他任何位置都归位物品

      • 将被拖拽物品与originalParent重新绑定。

      • blocksRaycasts设置为true。

注:将物品与slot“绑定”时,不仅要将物品的position设置为与slot相同,也要将slot设置为该物品的父级游戏物体。

我们可以用同样的方法,实现拖动背包BagUI的功能。

 

ItemOnDrag.cs 脚本代码

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

//该脚本挂载在Slot预制体下的ItemBtn上
//实现用鼠标拖拽ItemBtn,从而实现更改背包道具存放位置的功能
public class ItemOnDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public Transform originalParent;
    public Inventory myBag;
    public int currentItemID;//当前物品ID

    public void OnBeginDrag(PointerEventData eventData)//开始拖拽
    {
        originalParent = transform.parent;//存放当前ItemBtn的slot,在后面交换两个物品的ItemBtn会用到
        currentItemID = originalParent.GetComponent<Slot>().slotID;//获得当前被拖拽物品的ID
        transform.SetParent(transform.parent.parent);//将ItemBtn放在更上一级(Grid下),防止被下面的ItemBtn挡住
        transform.position = eventData.position;//让ItemBtn跟随鼠标位置
        GetComponent<CanvasGroup>().blocksRaycasts = false;//让鼠标射线穿透ItemBt
    }

    public void OnDrag(PointerEventData eventData)//拖拽中
    {
        transform.position = eventData.position;//让ItemBtn跟随鼠标位置
        Debug.Log(eventData.pointerCurrentRaycast.gameObject.name);//显示当前鼠标射线碰到的游戏物体的名字
        //注:eventData.pointerCurrentRaycast.gameObject只会判断UI游戏物体,若射线照射到的不是UI游戏物体,则返回null
    }

    public void OnEndDrag(PointerEventData eventData)//拖拽结束
    {
        //当拖拽到场景地图上时(不在CanvasUI组件上),下面的if判断为false
        if (eventData.pointerCurrentRaycast.gameObject != null)
        {
            //当结束拖拽时,鼠标放在另一个道具上边,则交换两个道具的位置
            //重新设置它们的父级对象和位置
            if (eventData.pointerCurrentRaycast.gameObject.name == "ItemImage")
            {
                //将拖拽中的ItemBtn的父级设置为当前射线照射到的ItemImage的slot
                transform.SetParent(eventData.pointerCurrentRaycast.gameObject.transform.parent.parent);
                //将拖拽中的ItemBtn的位置放在新slot的地方
                transform.position = eventData.pointerCurrentRaycast.gameObject.transform.parent.parent.position;

                //Inventory(myBag)中itemList的物品的存储位置改变 ↓↓↓ 注:下面着三段代码要放在设置被交换的ItemBtn代码之前
                //先把当前Item保存进temp
                var temp = myBag.itemList[currentItemID];
                //将被交换的Item放进当前Item的坑中;GetComponentInParent能找所有父级中的游戏组件,因此不用 .parent
                myBag.itemList[currentItemID] = myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID];
                //将temp中保存的当前Item放进被交换的Item的坑中
                myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID] = temp;

                //将被交换的ItemBtn放到它的位置上
                eventData.pointerCurrentRaycast.gameObject.transform.parent.position = originalParent.position;
                //将被交换的ItemBtn的父级设置好
                eventData.pointerCurrentRaycast.gameObject.transform.parent.SetParent(originalParent);

                GetComponent<CanvasGroup>().blocksRaycasts = true;//记得设置回来
                return;
            }
            else if (eventData.pointerCurrentRaycast.gameObject.name == "slot(Clone)")
            {
                //拖拽结束时,鼠标放在空slot上,则直接将道具放在这个slot下面
                transform.SetParent(eventData.pointerCurrentRaycast.gameObject.transform);
                transform.position = eventData.pointerCurrentRaycast.gameObject.transform.position;

                //Inventory(myBag)中itemList的物品的存储位置改变 ↓↓↓(没有被交换物品时)
                //当放进去的坑与取出来的坑不同才可以执行,不然会被设置为null
                if (currentItemID != eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID)
                {
                    myBag.itemList[eventData.pointerCurrentRaycast.gameObject.GetComponentInParent<Slot>().slotID] = myBag.itemList[currentItemID];
                    myBag.itemList[currentItemID] = null;//放进去之后,之前的坑变成空的了
                }

                GetComponent<CanvasGroup>().blocksRaycasts = true;//不让鼠标射线穿透ItemBtn
                return;
            }
        }
        //拖拽到其他任何位置都归位物品
        transform.SetParent(originalParent);
        transform.position = originalParent.position;
        GetComponent<CanvasGroup>().blocksRaycasts = true;
    }
}

MoveBag.cs 脚本代码

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

//挂载在Bag上,实现鼠标拖拽Bag的功能
public class MoveBag : MonoBehaviour, IDragHandler
{
    private RectTransform currentRect;

    private void Awake()
    {
        currentRect = GetComponent<RectTransform>();
    }

    public void OnDrag(PointerEventData eventData)
    {
        //拖拽时currentRect中心锚点的位置等于鼠标移动的变化量
        currentRect.anchoredPosition += eventData.delta;
    }
}

实现游戏存档的功能

我们给游戏添加两个按钮,分别是Save按钮和Load按钮。

当玩家点击Save按钮后,系统将玩家背包里存放物品的信息和物品的持有数量信息保存成文件;当玩家点击Load按钮后,系统将读取保存的文件,将玩家当前的背包信息修改为文件里的信息。

该项目只有玩家的背包,因此我们只用一个文件就能保存背包信息。

但物品持有数量itemHeld是保存在对应的物品对象中的,如果我们也想同时保存和加载itemHeld这一项的话,还需要将玩家背包里拥有的物品也给保存下来。一个item保存为一个文件,那么物品最多的情况下,需要保存物品栏最大值的文件数。

我们创建一个新空游戏物体,命名为GameSaveManager,并为它添加脚本GameSaveManager

该脚本只有一个成员变量:public Inventory myInventory; 要用到玩家背包,因此要获取它。

该脚本中有四个成员方法:

  • SaveGame():点击Save按钮后调用该方法,主要功能是保存背包数据:

    • 检查该游戏绝对路径下是否有“game_SaveData”文件夹,若没有,则创建该文件夹,用于存储我们的存档。(文件夹的名字可以随意取);

    • 在该文件夹里创建文件,我们用该文件存储背包数据。比如我可以创建名为“inventory.txt的文件,其中文件名和扩展名可以是其他的;
    • 将myInventory转换为Json格式的变量(取名为json)保存起来;

    • 最后将json通过二进制序列化的方式写进“inventory.txt”

  • LoadGame():点击Load按钮后调用该方法,主要功能是将“inventory.txt”文件里的数据读取出来,并写进myInventory(mybag)里:

    • 首先检查绝对路径下有没有“inventory.txt”文件,若有才执行下面两步;

    • 打开“inventory.txt文件;

    • “inventory.txt”文件中的数据反序列化后重写进myInventory中;

  • SaveItem():用于存储背包里的物品,主要作用是保存物品中itemHeld这个成员变量。实现方法与SaveGame()类似,区别在于一个文件只保存一种物品。该方法在SaveGame()中被调用。

  • LoadItem():读取SaveItem()保存的文件,将文件中物品的数据重写进背包里的物品中。该方法在LoadGame()中被调用。

 

GameSaveManager.cs 脚本代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

//用来保存背包文件
//SaveGame和LoadGame主要功能是保存和加载myBag,即玩家背包里的itemList
//SaveItem和LoadItem功能是保存和加载Item的信息,为了解决SaveGame和LoadGame没有保存ItemHeld的问题
//myBag信息保存在inventory.txt文件中
//一个item保存在一个Item[i].txt文件中
public class GameSaveManager : MonoBehaviour
{
    public Inventory myInventory;
    //public Item thisItem;

    public void SaveGame()
    {
        Debug.Log(Application.persistentDataPath);//显示保存路径

        //如果程序路径下没有game_SaveData这个路径(文件夹),则创建文件夹
        if(!Directory.Exists(Application.persistentDataPath+"/game_SaveData"))
        {
            Directory.CreateDirectory(Application.persistentDataPath + "/game_SaveData");//创建文件夹
        }

        SaveItem();//Item也要保存哟,主要是保存ItemHeld

        //用于二进制转化
        BinaryFormatter formatter = new BinaryFormatter();

        //创建存储文件,扩展名随便
        FileStream file = File.Create(Application.persistentDataPath + "/game_SaveData/inventory.txt");

        //用json存储
        var json = JsonUtility.ToJson(myInventory);

        //写进去
        formatter.Serialize(file, json);

        file.Close();
    }

    public void LoadGame()
    {
        LoadItem();//也要加载Item,主要是加载ItemHeld

        //用于反序列化
        BinaryFormatter bf = 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)bf.Deserialize(file), myInventory);

            file.Close();
        }

        InventoryManager.instance.RefreshItem();
    }

    //保存Item,目的是保存ItemHeld
    private void SaveItem()
    {
        //用于二进制转化
        BinaryFormatter formatter = new BinaryFormatter();

        for (int i = 0; i != myInventory.itemList.Count; ++i)
        {
            if (myInventory.itemList[i] == null)//当前这个坑是空的话就不保存了
                continue;
            //创建存储文件,扩展名随便
            FileStream file = File.Create(Application.persistentDataPath + "/game_SaveData/Item"+i+".txt");

            //用json存储
            var json = JsonUtility.ToJson(myInventory.itemList[i]);

            //写进去
            formatter.Serialize(file, json);

            file.Close();
        }
    }

    private void LoadItem()
    {
        //用于反序列化
        BinaryFormatter bf = new BinaryFormatter();

        for (int i = 0; i != myInventory.itemList.Count; ++i)
        {
            if (myInventory.itemList[i] == null)//当前这个坑是空的话就不加载了
                continue;
            //如果存储文件存在
            if (File.Exists(Application.persistentDataPath + "/game_SaveData/Item"+i+".txt"))
            {
                //打开存储文件
                FileStream file = File.Open(Application.persistentDataPath + "/game_SaveData/Item"+i+".txt", FileMode.Open);

                //反序列化后写进Item文件
                JsonUtility.FromJsonOverwrite((string)bf.Deserialize(file), myInventory.itemList[i]);

                file.Close();
            }
        }
    }
}

上一篇
下一篇