[Unity背包系统]1.脚本化对象与GUI显示

脚本化对象与GUI显示

提要

  • 背包系统是游戏中非常重要的一个功能,我们需要实现一个涵盖多种类型物品(比如玩家装备,BUFF物品等)的背包系统。 本节需要做的就是先将所谓的背包(Inventory)实例化出来,并且使用GUI显示背包的物品情况
  • 在此之前,我们需要学会使用Unity的脚本化对象-Scriptable Object

Scriptable Object

什么是Scriptable Object

  • 首先看看官方文档中的描述:

一个类,如果需要创建无需附加到游戏对象的对象时,可从该类派生。
它对仅用于存储数据的资源最有用。

  • 简单来说,我们平时写的游戏脚本,都会挂载到相应的游戏对象中,比如PlayerController之类的,而ScriptableObject就是不用挂载到游戏对象的脚本,这个类的实例是作为资源文件(.asset)存在的,当我们在背包中存放物品时,这些物品就是这样的资源文件,也就是实例化的ScriptableObject

初探-ItemObject

  • 下面我们直接来上手使用,首先创建一个脚本,命名为ItemObject,我们希望用它来创建各种各样不同类型的物品资源文件,注意,这里需要更改ItemObject继承的类,它不再是MonoBehaviour,而是Scriptable Object

  • 这里需要思考一下,加入背包里的物品可能有这么几种类型:默认,食物,装备,所以我们可能需要区分一下,让ItemObject作为一个父类,把它们的共同点都写在父类,然后分别在写三个子类继承,为什么要这样做呢?仔细想想,食物可能有恢复的血量是多少,装备可能有防御值,速度等不同的属性,所以这里我们增加一个枚举类,表示这个物品的类型

public enum ItemType
{
    Food,
    Equipment,
    Default,
}

public  class ItemObject : ScriptableObject
{
    public GameObject prefab;//这里是物品的实际对象,比如缩略图,模型等
    public ItemType type;//物品的类型
    [TextArea(15,20)]//让物品描述的文本框变大,可以写更多描述
    public string description;
}
  • 接下来,分别创建DefaultObject,FoodObject,EquipmentObject,这里可以根据不同物品自由增加属性,比如装备的防御力等,我在这就直接省略了,以后有需要再写:
public class EquipmentObject : ItemObject
{
    private void Awake()
    {
       type = ItemType.Equipment;// 赋予对应类型
    }
}
  • 编写好后,重要的一步来了,要开始创建对应的资源对象了,也就是实例化这些ScriptableObject,在此之前,我们需要在刚刚的那三个Object脚本上增添这么一行
[CreateAssetMenu(fileName = "new Equipment Object", menuName = "Inventory System/Items/Equipment")]
public class EquipmentObject : ItemObject
{
    private void Awake()
    {
       type = ItemType.Equipment;
    }
}
  • 这行代码的意思就是,让这个ScriptableObject添加到创建资源(右键菜单)的面板上,其中filename就是新创建出一个资源时默认显示的名字,menuName指定了一个路径,我们直接去试试就知道了:

  • 显而易见,现在我们可以直接创建这些ScriptableObject的资源了,我们尝试创建一个装备(Equipment),命名为盔甲-armour

  • 成功了,可以在Inspector里面更改我们刚刚设定的一些属性,如果你加入了自己的一些想法比如盔甲的防御力,也可以在这里更改

  • 下面可以为每种类型的ItemObject都创建一些对应的物品资源玩玩(这里我是将刚刚的ScriptableObject脚本都放入了Scripts文件夹,这里就不强调分类存放的了,相信各位都有自己的想法 ):

存储ScriptableObject-InventoryObject

  • 我们已经初步使用了ScriptableObject,并且创建了几个可以存放在背包里的物品资源,现在来具体制作一个背包,当然,其实也可以不用ScriptableObject,有许多方法实现。这里也是为了更熟悉,并且之后会制作存档背包功能,我们再次创建一个继承了ScriptableObject的脚本对象:InventoryObject,用来表示背包:
[CreateAssetMenu(fileName = "new Inventory",menuName = "Inventory System/Inventory/BackPack")]
public class InventoryObject : ScriptableObject
{

}
  • 下面来思考如何表示背包中存放的物品:可以用一个List来存放,但同时也要注意,我们需要记录的不只是物品,通常在游戏中的背包,还可以看到每个物品对应的数量,这里可以换成Dictionary来存放,但是要知道,Dictionary在Unity中是不会被Unity自动序列化的,需要写一些额外的代码(接下来会用到的但可能不是现在),这里就编写一个类:InventorySlot,来记录这些相关的信息,每个物品资源都会对应一个这样的InventorySlot,其实按照字面意思就是背包中的一个物品槽:
[System.Serializable]//序列化
public class InventorySlot
{
   public ItemObject item;
   public int amount;

   public InventorySlot(ItemObject item,int amount)
   {
       this.item = item;
       this.amount = amount;
   }//构造函数

   public void AddAmount(int value)
   {
       amount += value;
   }//增加这个物品槽物品的数量
}
  • 现在可以使用List来存储了:
public List<InventorySlot> BackPack;
  • 现在来考虑如何向这个背包增加指定数量的物品,第一肯定能想到的是BackPack是个List,每次只要BackPack.Add(new Inventory(item,amount))就行了,但假若有重复的物品(比如背包里已经有fish了,探索游戏的过程中有拾取到了fish),我们应该先看List里是否已经有这个物品对应的InventorySlot,如果有直接调用刚刚的AddAmount()即可,在这里就直接遍历一遍了:
    (其实你可以发现AddAmount()这个方法不是必要的,这里就相当于规范一下 了)
       public void AddItem(ItemObject item,int amount)
   {
       for(int i = 0 ; i < BackPack.Count; i ++ )
       {
           if(BackPack[i].item == item)
           {
               BackPack[i].AddAmount(amount);
               return;
           }
       }//遍历,如果发现已经有这个物品了,就直接加上数量
   		
       BackPack.Add(new InventorySlot(item,amount));//如果没有,则调用List的Add()方法

   }
  • 下面是完整代码,这样我们的InventoryObject就算是写好了:
[CreateAssetMenu(fileName = "new Inventory",menuName = "Inventory System/Inventory/BackPack")]
public class InventoryObject : ScriptableObject
{
    public List<InventorySlot> BackPack;


    public void AddItem(ItemObject item,int amount)
    {
        for(int i = 0 ; i < BackPack.Count; i ++ )
        {
            if(BackPack[i].item == item)
            {
                BackPack[i].AddAmount(amount);
                return;
            }
        }
        BackPack.Add(new InventorySlot(item,amount));

    }
}


[System.Serializable]
public class InventorySlot
{
    public ItemObject item;
    public int amount;

    public InventorySlot(ItemObject item,int amount)
    {
        this.item = item;
        this.amount = amount;
    }

    public void AddAmount(int value)
    {
        amount += value;
    }

}

  • 然后来创建玩家使用的背包资源,也是一样的方式,取名为PlayerInventory:

  • 可以看到序列化后的BackPack,还可以直接从这个界面对BackPack进行一些增删改操作

拾取物品

  • 我们已经创建好了物品,背包的资源,下面就要进行一些实际的操作了,首先在Scene里创建好玩家对象(可以直接用Cube或者你喜欢的模型代替),再创建好两个“拾取物”,接下来要给它们编写一些脚本:

  • 我们先编写对玩家拾取物品的脚本:PlayerInventoryController(自己取名习惯)
public class PlayerInventoryController : MonoBehaviour
{
   
   public InventoryObject inventory;//引入玩家的背包资源


   public void PickUp(ItemObject item,int amount)
   {
       inventory.AddItem(item, amount);
   }//拾取到物品的方法,加入到玩家的背包里面
}
  • 将这个脚本挂载到Player游戏对象上,并绑定之前创建的PlayerInventory背包资源

  • 接下来编写对拾取物的控制脚本,我们想让Player对象接触到拾取物时,触发PickUp方法,参数即是拾取物所代表的item,因此先将拾取物(这里我用的是Cube)的碰撞器设定为触发器:

  • 然后编写脚本ItemController:

public class ItemController : MonoBehaviour
{
    public ItemObject item;

    private void OnTriggerEnter(Collider other)
    {
        if(other.gameObject.name == "Player")//也可以用Tag等,这里随便判断了,其实在这个demo里甚至不需要判断
        {
            other.gameObject.GetComponent<PlayerInventoryController>().PickUp(item, 1);
            Destroy(this.gameObject);
        }

    }
}
  • 挂载到拾取物上后,给它们不同的item:

  • 测试后,让Player对象接触两个拾取物(没有写控制的方法的话可以在Scene中直接拖拽),可以看到拾取物消失,运行时查看PlayerInventory,可以看到BackPack中增加了对应的item。

  • 不过也发现了一个问题:当退出游戏再进入时,BackPack没有清空,还保留着之前的数据,这是因为ScriptableObject的持久化能力,不过这里我们不需要,可以在退出游戏时清空BackPack,在PlayerInventoryController中增加这个方法:

    private void OnApplicationQuit()
    {
        inventory.BackPack.Clear();
    }

UI显示背包内容

制作物品资源的Prefab

  • 做好了基本的资源和拾取功能,下面来用UI显示背包内容,这也包括当拾取物品后,UI需要更新显示的内容。首先创建一个Canvas,再创一个Panel作为其子物体,将其放置在屏幕右侧(这里就不做快捷键弹出背包啥的了,可以自己写)

  • 下面来思考如何显示背包里的物品,这里将采用Image组件,然后没有准备素材什么的,每个物品就用一种颜色来代替,这里的操作可能有点繁杂:

  • 先在Inventory Panel下创建Image组件

  • 调整位置 大小等,放在我们设想好的Panel里的第一格

  • 我们要改变的是颜色属性,这里设定fish这个物品是绿色

  • 为了显示物品的数量,再添加TextMeshPro组件,放到中间先写上0,一会就用这个来更新物品数量

  • 还记得吗,之前的itemObject里,有一个属性是Prefab,这里我们要使用的就是这个Prefab,先把Image设定为不同的颜色(刚刚有三个物品资源),分别做成Prefab(这里使用了变体Prefab,按自己方式都可以)

  • 分别绑定

脚本显示并更新物品

  • 现在删除之前的Image(不过我建议记下第一个格子的坐标:Pos X,Pos Y,一会要用到),来思考如何如何编写脚本:
    1.游戏初始化时,要读取已有背包内容,并且立即显示当前背包的物品,也就是创建Image对象
    2.在该脚本的Update()中,实时更新背包的内容,如果有新增或者物品数量改变,要及时更新(事实上可能还有删除内容,这样的话逻辑要多写一些,可以自己实现试试)。其实也可以不用每帧都执行更新背包内容的方法,我们可以使用观察者模式,当背包内容有改变时触发事件,执行更新背包内容的方法,这里可以自己尝试改写
  • 不过在此之前,还要先思考物品的排列,先创建好脚本,可能要用到这些变量来控制物品排列:
    //每个物体之间的间隔
    public int X_SPACE_BETWEEN_ITEM;
    public int Y_SPACE_BETWEEN_ITEM;

    //第一个物体的起始位置
    public float X_Start = -339;
    public float Y_Start = 170;

    //每行显示多少物体
    public int NUMBER_OF_COLUMN;
  • 所以在创建Image组件时,它的位置需要根据对应的物品资源在当前背包的BackPack这个List里的位置索引来决定,直接来写出GetPosition方法,返回Vector3的位置值:
    public Vector3 GetPosition(int index)
    {
        Vector3 pos = new Vector3(X_Start + X_SPACE_BETWEEN_ITEM * (index % NUMBER_OF_COLUMN),Y_Start - Y_SPACE_BETWEEN_ITEM * (index / NUMBER_OF_COLUMN),0);
        return pos; 
    }
  • 解决好排列这个问题,现在来搞定显示与更新,这回我们使用一个Dictionary结构来表示Panel中显示的物品,它的Key是InventorySlot,Value是在Panel中生成好的Image组件,我们希望创建一个一一对应关系,这样在更新物品数量的时候,只需要对比Key是否相同,然后改变Image组件的子组件TextMeshPro的内容即可
  • 注意Dictionary比较特殊,Unity对于一些类型不会进行自动序列化等操作
 Dictionary<InventorySlot, GameObject> itemsDisplayed = new Dictionary<InventorySlot, GameObject>();
  • 编写CreateDisplay()方法,在游戏初始化时就创建好背包内容,这是为了我们之后的存档功能铺垫:
    public void CreateDisplay()
    {
        for(int i = 0; i < inventory.BackPack.Count; i ++ )
        {
            var obj = Instantiate(inventory.BackPack[i].item.prefab, Vector3.zero, Quaternion.identity, transform);//生成物品资源对应的Image,并返回给obj
            obj.GetComponent<RectTransform>().localPosition = GetPosition(i);//设定位置
            obj.GetComponentInChildren<TextMeshProUGUI>().text = inventory.BackPack[i].amount.ToString("n0");//最后面的ToString是设定了一下格式
            itemsDisplayed.Add(inventory.BackPack[i],obj);//加入刚刚的Dictionary中,便于我们更新数量
        }
    }
  • 编写UpdateDisplay()方法
    public void UpdateDisplay()
    {
        for(int i = 0; i < inventory.BackPack.Count; i ++ )
        {
            if(itemsDisplayed.ContainsKey(inventory.BackPack[i]))
            {
                itemsDisplayed[inventory.BackPack[i]].GetComponentInChildren<TextMeshProUGUI>().text = inventory.BackPack[i].amount.ToString("n0");
            }//如果已经有该物品存在,就直接更改显示的数量
            else
            {
                var obj = Instantiate(inventory.BackPack[i].item.prefab, Vector3.zero, Quaternion.identity, transform);
                obj.GetComponent<RectTransform>().localPosition = GetPosition(i);
                obj.GetComponentInChildren<TextMeshProUGUI>().text = inventory.BackPack[i].amount.ToString("n0");
                itemsDisplayed.Add(inventory.BackPack[i], obj);
            }//这个else里的可以直接复制刚刚的,其实就是如果没有该物品,那就生成
        }

    }
  • 到这里就基本结束了,可以看到拾取两个Cube后:
posted @ 2023-05-20 20:21  月牙同学  阅读(497)  评论(0)    收藏  举报