文章目录
- 前言
- 素材
- 开始
- 一、绘制背包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()
{
// 初始化物品列表并赋值为 null
for (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; // 鼠标上的物品UI
public 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的物品信息赋值给槽位A
slotA.item = slotB.item;
slotA.stacks = slotB.stacks;
// 将暂存的物品信息赋值给槽位B
slotB.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()
{
// 初始化物品列表并赋值为 null
for (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; // 物品数量,默认为1
private 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
{
//更新text
text.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。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~