文章目录
- 前言
- 素材
- 开始
- 一、绘制背包UI
- 二、背包开启关闭
- 三、初始化背包网格
- 四、 添加物品
- 五、 拖拽交换功能物品
- 六、 物品拆分
- 七、 物品堆叠
- 八、 拖拽还原
- 九、 引入字典存储数据
- 十、 拾取物品
- 十一、 丢弃物品
- 最终效果
- 源码
- 完结
前言
库存背包系统是大多数游戏的关键部分,几乎在每种类型的游戏都可能会用到,今天我将带你从零实现一个能够进行拖放的库存拆分、堆叠和丢弃的背包系统,你可以将它轻松的移植到你的游戏中。
老粉丝应该知道我之前有做过一个背包系统,当时做的比较简单,很多功能都没有实现,所以这次算一个重置版,当然你有兴趣可以看看之前实现的背包系统:https://blog.csdn.net/qq_36303853/article/details/129962414
先来看看实现的最终效果
素材
开始
一、绘制背包UI
物品列表网格添加Grid Layout Group组件控制物品格子排序
效果
二、背包开启关闭
物品槽信息类,用于存储每个物品槽的信息
// 物品槽信息类,用于存储每个物品槽的信息
[System.Serializable]
public class ItemSlotInfo
{public Item item;// 物品对象public string name;// 物品名称public int stacks;// 堆叠数量public ItemSlotInfo(Item newItem, int newstacks){item = newItem;stacks = newstacks;}
}
库存系统
using System.Collections.Generic;
using UnityEngine;public class Inventory : MonoBehaviour
{[SerializeReference] public List<ItemSlotInfo> items = new List<ItemSlotInfo>();// 物品列表[Space][Header("库存菜单组件")]public GameObject inventoryMenu;// 背包菜单public GameObject itemPanel;// 物品列表容器public GameObject itemPanelGrid;// 物品列表网格布局[Space]public int inventorySize = 24;// 背包容量大小void Start(){// 初始化物品列表并赋值为 nullfor (int i = 0; i < inventorySize; i++){items.Add(new ItemSlotInfo(null, 0));}}void Update(){// 按下 Tab 键显示或隐藏背包界面if (Input.GetKeyDown(KeyCode.Tab)){if (inventoryMenu.activeSelf){inventoryMenu.SetActive(false);Cursor.lockState = CursorLockMode.Locked;// 隐藏光标并锁定在屏幕中心}else{inventoryMenu.SetActive(true);Cursor.lockState = CursorLockMode.Confined;// 显示光标并限制在屏幕内部移动}}}
}
挂载Inventory 脚本在canvas,配置数据
运行效果
三、初始化背包网格
新建物品容器脚本
using UnityEngine;
using UnityEngine.UI;
using TMPro;public class ItemPanel : MonoBehaviour
{public Inventory inventory;// 背包脚本引用public ItemSlotInfo itemSlot;// 物品槽信息public Image itemImage;// 物品图像组件public TextMeshProUGUI stacksText;// 堆叠数量文本组件
}
挂载代码
新建物品抽象类,所有物品类型都需要继承此类
using UnityEngine;
//物品抽象类,所有物品类型都需要继承此类
[System.Serializable]
public abstract class Item
{public abstract string GiveName();// 获取物品名称public virtual int MaxStacks()// 获取每个物品槽的最大堆叠数量{return 30;// 默认为 30}public virtual Sprite GiveItemImage()// 获取物品图片{return Resources.Load<Sprite>("UI/Item Images/No Item Image Icon");// 默认图片}
}
Inventory新增刷新背包功能,每次打开背包时调用RefreshInventory();
private List<ItemPanel> existingPanels = new List<ItemPanel>();//物品容器列表//刷新背包
public void RefreshInventory()
{//物品容器列表existingPanels = itemPanelGrid.GetComponentsInChildren<ItemPanel>().ToList();//如果物品列表容器不足,创建物品面板if (existingPanels.Count < inventorySize){int amountToCreate = inventorySize - existingPanels.Count;for (int i = 0; i < amountToCreate; i++){GameObject newPanel = Instantiate(itemPanel, itemPanelGrid.transform);existingPanels.Add(newPanel.GetComponent<ItemPanel>());}}int index = 0;foreach (ItemSlotInfo i in items){//给物品列表元素命名i.name = "" + (index + 1);if (i.item != null) i.name += ":" + i.item.GiveName();else i.name += ":-";//更新物品面板ItemPanel panel = existingPanels[index];panel.name = i.name + " Panel";if (panel != null){panel.inventory = this;panel.itemSlot = i;if (i.item != null){panel.itemImage.gameObject.SetActive(true); // 显示物品图标panel.itemImage.sprite = i.item.GiveItemImage(); // 设置物品图标的精灵panel.stacksText.gameObject.SetActive(true); // 显示物品叠加数量panel.stacksText.text = "" + i.stacks; // 设置物品叠加数量的文本}else{panel.itemImage.gameObject.SetActive(false); // 隐藏物品图标panel.stacksText.gameObject.SetActive(false); // 隐藏物品叠加数量}}index++;}
}
效果
四、 添加物品
Inventory实现添加和清除物品功能
//添加物品
public int AddItem(Item item, int amount)
{// 检查已有物品槽中是否有空余位置foreach (ItemSlotInfo i in items){if (i.item != null){if (i.item.GiveName() == item.GiveName()){if (amount > i.item.MaxStacks() - i.stacks){amount -= i.item.MaxStacks() - i.stacks;i.stacks = i.item.MaxStacks();}else{i.stacks += amount;//如果背包菜单处于激活状态,刷新背包显示if (inventoryMenu.activeSelf) RefreshInventory();return 0;}}}}//将剩余的物品放入空的物品槽中foreach (ItemSlotInfo i in items){if (i.item == null){if (amount > item.MaxStacks()){i.item = item;i.stacks = item.MaxStacks();amount -= item.MaxStacks();}else{i.item = item;i.stacks = amount;//如果背包菜单处于激活状态,刷新背包显示if (inventoryMenu.activeSelf) RefreshInventory();return 0;}}}Debug.Log("库存中没有空间:" + item.GiveName());//如果背包菜单处于激活状态,刷新背包显示if (inventoryMenu.activeSelf) RefreshInventory();return amount;
}//清空指定物品槽中的物品和叠加数量
public void ClearSlot(ItemSlotInfo slot)
{slot.item = null;slot.stacks = 0;
}
新增第一个物品,木头脚本,继承Item基类,重新对应参数
using UnityEngine;
public class WoodItem : Item
{public override string GiveName()// 获取物品名称{return "Wood";}public override int MaxStacks()// 获取每个物品槽的最大堆叠数量{return 10;}public override Sprite GiveItemImage()// 获取物品图片{return Resources.Load<Sprite>("UI/Item Images/Wood Icon");}
}
添加40个木头进库测试
public class Inventory : MonoBehaviour
{void Start(){// 。。。AddItem(new WoodItem(), 40);}
}
效果
五、 拖拽交换功能物品
先实现指针跟随鼠标,新增脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;public class Mouse : MonoBehaviour
{public GameObject mouseItemUI; // 鼠标上的物品UIpublic Image mouseCursor; // 鼠标光标public ItemSlotInfo itemSlot; // 物品槽信息public Image itemImage; // 物品图像public TextMeshProUGUI stacksText; // 叠加数量文本void Update(){// 将鼠标指针位置设置为当前鼠标位置transform.position = Input.mousePosition;// 如果鼠标被锁定if (Cursor.lockState == CursorLockMode.Locked){mouseCursor.enabled = false; // 隐藏鼠标光标mouseItemUI.SetActive(false); // 隐藏鼠标上的物品UI}else{mouseCursor.enabled = true; // 显示鼠标光标// 如果物品槽中有物品if (itemSlot.item != null){mouseItemUI.SetActive(true); // 显示鼠标上的物品UI}else{mouseItemUI.SetActive(false); // 隐藏鼠标上的物品UI}}}public void SetUI(){stacksText.text = "" + itemSlot.stacks; // 设置叠加数量文本itemImage.sprite = itemSlot.item.GiveItemImage(); // 设置物品图像}public void EmptySlot(){itemSlot = new ItemSlotInfo(null, 0);// 清空物品槽}
}
新增拖拽,跟随鼠标显示的UI
效果,就和一个格子效果一致
给Mouse挂载mouse脚本,并添加Canvas Group组件,将这个组件的blocksRaycasts属性设置为false,表示在我们刚开始拖拽的整个过程当中,鼠标不会再去把这个UI物品当作一个阻挡物来看待,包括他的子物体的所有的UI对象
ps:当然,选择去除各个子组件Image里的射线投射目标,也能实现相同的效果
记得默认先隐藏mouse下的物品列表容器
修改ItemPanel脚本实现物品拖拽和交换
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.EventSystems;public class ItemPanel : MonoBehaviour, IPointerEnterHandler, IPointerDownHandler, IPointerUpHandler, IDragHandler, IDropHandler
{public Inventory inventory; // 背包脚本引用public ItemSlotInfo itemSlot; // 物品槽信息public Image itemImage; // 物品图像组件public TextMeshProUGUI stacksText; // 堆叠数量文本组件private bool click; // 当前是否点击private Mouse mouse; // 鼠标// 当鼠标进入时调用的方法public void OnPointerEnter(PointerEventData eventData){eventData.pointerPress = this.gameObject;}// 当鼠标按下时调用的方法public void OnPointerDown(PointerEventData eventData){click = true;}// 当鼠标抬起时调用的方法public void OnPointerUp(PointerEventData eventData){if (click){OnClick();click = false;}}// 在拖拽结束时调用public void OnDrop(PointerEventData eventData){OnClick();click = false;}// 在拖拽过程中连续调用public void OnDrag(PointerEventData eventData){if (click){OnClick();click = false;OnDrop(eventData);}}// 将物品拾取到鼠标槽位public void PickupItem(){mouse.itemSlot = itemSlot;mouse.SetUI();}// 物品面板淡出效果public void Fadeout(){itemImage.CrossFadeAlpha(0.3f, 0.05f, true);//0.05 秒itemImage透明度渐变到0.3}// 将物品放下到当前物品面板上public void DropItem(){itemSlot.item = mouse.itemSlot.item;itemSlot.stacks = mouse.itemSlot.stacks;inventory.ClearSlot(mouse.itemSlot);}// 交换两个物品槽位的物品public void SwapItem(ItemSlotInfo slotA, ItemSlotInfo slotB){// 暂存槽位A的物品信息ItemSlotInfo tempItem = new ItemSlotInfo(slotA.item, slotA.stacks);// 将槽位B的物品信息赋值给槽位AslotA.item = slotB.item;slotA.stacks = slotB.stacks;// 将暂存的物品信息赋值给槽位BslotB.item = tempItem.item;slotB.stacks = tempItem.stacks;}// 当物品面板被点击时调用的方法void OnClick(){if (inventory != null){mouse = inventory.mouse;// 如果鼠标槽位为空,将物品拾取到鼠标槽位if (mouse.itemSlot.item == null){if (itemSlot.item != null){PickupItem();Fadeout();}}else{// 点击的是原始槽位if (itemSlot == mouse.itemSlot){inventory.RefreshInventory();}// 点击的是空槽位else if (itemSlot.item == null){DropItem();inventory.RefreshInventory();}// 点击的是不同物品类型的已占用槽位else if (itemSlot.item.GiveName() != mouse.itemSlot.item.GiveName()){SwapItem(itemSlot, mouse.itemSlot);inventory.RefreshInventory();}}}}
}
修改Inventory,初始化清空鼠标物品槽
public Mouse mouse;void Update()
{// 按下 Tab 键显示或隐藏背包界面if (Input.GetKeyDown(KeyCode.Tab)){if (inventoryMenu.activeSelf){//。。。mouse.EmptySlot();}else{//。。。}}
}
//刷新背包
public void RefreshInventory()
{//。。。mouse.EmptySlot();
}
为了方便交换测试,我们新增一个石头的物品脚本
using UnityEngine;
public class StoneItem : Item
{public override string GiveName()// 获取物品名称{return "Stone";}public override int MaxStacks()// 获取每个物品槽的最大堆叠数量{return 5;}public override Sprite GiveItemImage()// 获取物品图片{return Resources.Load<Sprite>("UI/Item Images/Stone Icon");}
}
在Inventory中生成
void Start()
{//。。。AddItem(new WoodItem(), 40);AddItem(new StoneItem(), 40);
}
运行效果
修复问题,基本拖到和交换功能是做好了,但是你会发现物品放下时物品透明的并没有还原,那是因为前面我们拖动时调用了Fadeout方法,实现了物品面板淡出效果,透明度变为了0.3,我们放置物品时没有还原物品面板透明度
在Inventory新增代码
//刷新背包
public void RefreshInventory()
{//。。。foreach (ItemSlotInfo i in items){//。。。if (panel != null){//。。。if (i.item != null){//。。。panel.itemImage.CrossFadeAlpha(1, 0.05f, true);//0.05 秒itemImage透明度渐变到1完全不透明}//。。。}}
}
效果
六、 物品拆分
实现滚轮拆分物品,shift对半分物品效果
修改Mouse代码,实现滚轮拆分物品
public ItemPanel sourceItemPanel; // 源物品面板对象
public int splitSize; // 拆分数量void Update()
{// ...if (itemSlot.item != null) // 如果物品槽中有物品{if (Input.GetAxis("Mouse ScrollWheel") > 0 && splitSize < itemSlot.stacks){// 当鼠标向上滚动并且拆分数量小于物品槽剩余堆叠数量时splitSize++; // 增加拆分数量}if (Input.GetAxis("Mouse ScrollWheel") < 0 && splitSize > 1){// 当鼠标向下滚动并且拆分数量大于1时splitSize--; // 减少拆分数量}stacksText.text = "" + splitSize; // 在UI中显示拆分数量if (splitSize == itemSlot.stacks)// 如果拆分数量等于物品的堆叠数量{ // 将源物品面板的堆叠数量文本组件设置为不可见sourceItemPanel.stacksText.gameObject.SetActive(false); }else{sourceItemPanel.stacksText.gameObject.SetActive(true);// 在文本组件中显示物品的剩余堆叠数量sourceItemPanel.stacksText.text = "" + (itemSlot.stacks - splitSize);}}
} public void SetUI()
{// stacksText.text = "" + itemSlot.stacks; // 设置叠加数量文本stacksText.text = "" + splitSize;// 在UI中显示拆分数量itemImage.sprite = itemSlot.item.GiveItemImage();
}
修改ItemPanel,实现shift对半分物品效果
// 将物品拾取到鼠标槽位
public void PickupItem()
{mouse.itemSlot = itemSlot;mouse.sourceItemPanel = this;if (Input.GetKey(KeyCode.LeftShift) && itemSlot.stacks > 1){mouse.splitSize = itemSlot.stacks / 2;}else{mouse.splitSize = itemSlot.stacks;}mouse.SetUI();
}
// 将物品放下到当前物品面板上
public void DropItem()
{itemSlot.item = mouse.itemSlot.item;if (mouse.splitSize < mouse.itemSlot.stacks){itemSlot.stacks = mouse.splitSize;mouse.itemSlot.stacks -= mouse.splitSize;mouse.EmptySlot();}else{itemSlot.stacks = mouse.itemSlot.stacks;inventory.ClearSlot(mouse.itemSlot);}
}
效果
七、 物品堆叠
修改ItemPanel代码,新增物品堆叠方法
//物品堆叠
public void StackItem(ItemSlotInfo source, ItemSlotInfo destination, int amount)
{// 计算目标物品槽中可用的堆叠空间int slotsAvailable = destination.item.MaxStacks() - destination.stacks;// 如果目标物品槽没有可用的堆叠空间,则直接返回if (slotsAvailable == 0) return;if (amount > slotsAvailable){// 堆叠数量超过可用空间时,从源物品槽中减去可用空间source.stacks -= slotsAvailable;// 目标物品槽的堆叠数量设置为最大堆叠数destination.stacks = destination.item.MaxStacks();}if (amount <= slotsAvailable){// 堆叠数量小于可用空间时,将堆叠数量加到目标物品槽中destination.stacks += amount;// 如果源物品槽中剩余的堆叠数量等于堆叠数量(即所有物品都被堆叠完),则清空源物品槽if (source.stacks == amount) inventory.ClearSlot(source);// 否则,从源物品槽中减去堆叠数量else source.stacks -= amount; }
}// 当物品面板被点击时调用的方法
void OnClick()
{//。。。// 点击的是同物品类型的已占用槽位else if (itemSlot.stacks < itemSlot.item.MaxStacks()){StackItem(mouse.itemSlot, itemSlot, mouse.splitSize);inventory.RefreshInventory();}
}
运行效果
八、 拖拽还原
实现拖拽物品时按鼠标右键
还原物品
修改Inventory代码
void Update()
{//。。。//拖拽物品时按鼠标右键还原物品if (Input.GetKeyDown(KeyCode.Mouse1) && mouse.itemSlot.item != null){RefreshInventory();}
}
效果
九、 引入字典存储数据
创建一个用于存储所有物品的字典allItemsDictionary
allItemsDictionary 字典是为了方便根据物品名称来查找对应的物品对象。通常情况下,当你需要添加某个物品时,首先需要根据物品名称从字典中获取对应的物品对象,然后再将其添加到 items 列表中。
使用字典的好处是可以通过物品名称快速索引到对应的物品对象,而不需要遍历整个 items 列表。这在处理大量物品的时候可以提高性能和效率。
修改Inventory代码
// 创建一个用于存储所有物品的字典,其中键为物品名称,值为对应的物品对象
Dictionary<string, Item> allItemsDictionary = new Dictionary<string, Item>();void Start()
{// 初始化物品列表并赋值为 nullfor (int i = 0; i < inventorySize; i++){items.Add(new ItemSlotInfo(null, 0));}// 获取所有可用的物品,并将它们添加到物品字典中List<Item> allItems = GetAllItems().ToList();string itemsInDictionary = "字典条目:";foreach (Item i in allItems){if (!allItemsDictionary.ContainsKey(i.GiveName())){allItemsDictionary.Add(i.GiveName(), i);itemsInDictionary += "," + i.GiveName();}else{// 如果字典中已存在同名的物品,则输出调试信息Debug.Log("" + i + "已存在于与之共享名称的字典中 " + allItemsDictionary[i.GiveName()]);}}itemsInDictionary += ".";Debug.Log(itemsInDictionary);// 添加一些初始物品AddItem("Wood", 40);AddItem("Stone", 40);
}// 获取所有可用的物品
IEnumerable<Item> GetAllItems()
{List<Item> allItems = new List<Item>();// 获取当前应用程序域中的所有程序集Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();// 遍历每个程序集foreach (Assembly assembly in assemblies){// 获取程序集中定义的所有类型Type[] types = assembly.GetTypes();// 遍历每个类型foreach (Type type in types){// 检查类型是否是 Item 类的子类if (type.IsSubclassOf(typeof(Item))){// 创建该类型的实例,并将其转换为 Item 对象Item item = Activator.CreateInstance(type) as Item;// 将物品添加到列表中allItems.Add(item);}}}return allItems;
} //添加物品
public int AddItem(string itemName, int amount)
{//查找要添加的项目Item item = null;allItemsDictionary.TryGetValue(itemName, out item);//如果未找到任何项,则退出方法if (item == null){Debug.Log("无法在字典中找到要添加到库存中的物品");return amount;}//。。。
}
十、 拾取物品
新增可拾取物品脚本
using UnityEngine;
using TMPro;//物品拾取脚本
public class ItemPickup : MonoBehaviour
{public string itemToDrop; // 需要拾取的物品名称public int amount = 1; // 物品数量,默认为1private TextMeshPro text; //物品文本Inventory playerInventory; //玩家的背包组件void Start(){text = transform.GetComponentInChildren<TextMeshPro>();text.text = amount.ToString();}void Update(){if (Input.GetKeyDown(KeyCode.C)){PickUpItem();}}// 当碰撞体进入触发器时调用private void OnTriggerStay2D(Collider2D other){if (other.tag == "Player"){// 获取玩家的背包组件playerInventory = other.GetComponent<Inventory>();}}//当碰撞体离开触发器时调用private void OnTriggerExit2D(Collider2D other){if (other.tag == "Player"){// 获取玩家的背包组件playerInventory = null;}}// 拾取物品的方法public void PickUpItem(){// 如果玩家背包组件存在,则拾取物品if (playerInventory == null) return;// 将物品添加到背包,并返回剩余的物品数量amount = playerInventory.AddItem(itemToDrop, amount);// 如果数量小于1,销毁该拾取物品的游戏对象if (amount < 1){Destroy(this.gameObject);}else{//更新texttext.text = amount.ToString();}}
}
创建一个Player主角和可拾取物品
给Player添加Player标签,并把Inventory脚本移到Player下
可拾取物品添加脚本,配置参数
效果
十一、 丢弃物品
绘制丢弃物品预制体
挂载ItemPickup脚本
修改Inventory代码
public GameObject dropObject;//丢弃物品预制体//丢弃物品
public void DropItem(string itemName)
{Item item = null;// 从字典中查找物品allItemsDictionary.TryGetValue(itemName, out item);if (item == null){Debug.Log("在字典中找不到要添加到掉落的物品");return;}// 在当前位置实例化一个掉落物体GameObject droppedItem = Instantiate(dropObject, transform.position, Quaternion.identity);//修改图片droppedItem.GetComponent<SpriteRenderer>().sprite = item.GiveItemImage();ItemPickup ip = droppedItem.GetComponent<ItemPickup>();if (ip != null){// 设置掉落物品的属性ip.itemToDrop = itemName;ip.amount = mouse.splitSize;mouse.itemSlot.stacks -= mouse.splitSize;//更新物品槽中该物品的剩余数量,及减去将要丢弃的物品数量}if (mouse.itemSlot.stacks < 1) ClearSlot(mouse.itemSlot);// 清空物品槽mouse.EmptySlot();// 清空鼠标上的物品RefreshInventory();// 刷新背包显示
}
Update中调用
void Update()
{//控制丢弃物品 EventSystem.current.IsPointerOverGameObject():该条件判断鼠标当前是否位于UI元素之上if (Input.GetKeyDown(KeyCode.Mouse0) && mouse.itemSlot.item != null && !EventSystem.current.IsPointerOverGameObject()){DropItem(mouse.itemSlot.item.GiveName());}
}
效果
最终效果
源码
整理好后,我会放上来
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。点赞越多,更新越快哦!当然,如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~