文章目录
- 前言
- 素材
- 开始
- 配置不同物品信息
- 实例化物品
- 拾取物品
- 物品栏、库存大小
- 寻找物品栏并可以添加物品
- 库存已满问题解决
- 库存UI脚本
- 显示物品信息
- 切换指示器
- 丢弃物品
- 添加丢弃弹出效果
- 最终效果
- 源码
- 完结
前言
其实前面我已经做过了很多次背包库存系统了,背包系统实现方法千千万,条条大陆通罗马。因为背包系统在游戏中实在太常见和关键了。我相信,新的实现探究总会给我们不一样的启发,所以我还是会一直关注实现他们的不同方式,我希望你们也是。
之前实现的背包系统,有兴趣的可以去看看:
复刻类泰瑞利亚生存建造游戏——包括建造系统和库存系统
从零手戳一个库存背包系统
从零手戳一个背包系统
先看看最终效果
素材
https://cupnooble.itch.io
开始
配置不同物品信息
自定义物品菜单添加脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
[CreateAssetMenu(menuName = "库存/物品定义", fileName = "新物品定义")]
public class ItemDefinition : ScriptableObject
{
[SerializeField]
private string _name; // 物品名称
[SerializeField]
private bool _isStackable; // 是否可堆叠
[SerializeField]
private Sprite _inGameSprite; // 游戏内显示的精灵图像
[SerializeField]
private Sprite _uiSprite; // UI界面显示的精灵图像
public string Name => _name; // 获取物品名称
public bool IsStackable => _isStackable; // 获取是否可堆叠
public Sprite InGameSprite => _inGameSprite; // 获取游戏内显示的精灵图像
public Sprite UiSprite => _uiSprite; // 获取UI界面显示的精灵图像
}
}
创建几个不同的物品
实例化物品
物品堆栈,限制物品数量设置
using System;
using UnityEngine;
namespace InventorySystem
{
[Serializable]
public class ItemStack
{
[SerializeField]
private ItemDefinition _item; // 物品定义
[SerializeField]
private int _numberOfItems; // 物品数量
public bool IsStackable => _item.IsStackable; // 是否可堆叠
public ItemDefinition Item => _item; // 物品定义
public int NumberOfItems
{
get => _numberOfItems;
set
{
value = value < 0 ? 0 : value; // 确保物品数量不小于零
_numberOfItems = IsStackable ? value : 1; // 如果不可堆叠,将物品数量限制在 1 以内
}
}
//构造方法
public ItemStack(ItemDefinition item, int numberOfItems)
{
_item = item;
NumberOfItems = numberOfItems;
}
}
}
物品挂载脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
public class GameItem : MonoBehaviour
{
[SerializeField]
private ItemStack _stack; // 物品堆栈
private SpriteRenderer _spriteRenderer; // 精灵渲染器
// 当对象检验时被调用,只要程序员在这个对象上修改了一个字段,Unity就会自动地调用这个函数。
private void OnValidate()
{
SetupGameObject(); // 设置游戏对象
}
// 设置游戏对象
private void SetupGameObject()
{
if (_stack.Item == null) return; // 如果物品为空,返回
SetGameSprite(); // 设置游戏精灵
AdjustNumberOfItems(); // 调整物品数量
UpdateGameObjectName(); // 更新游戏对象名称
}
// 设置游戏精灵
private void SetGameSprite()
{
_spriteRenderer = GetComponent<SpriteRenderer>();
_spriteRenderer.sprite = _stack.Item.InGameSprite;
}
// 更新游戏对象名称
private void UpdateGameObjectName()
{
var name = _stack.Item.Name;
var number = _stack.IsStackable ? _stack.NumberOfItems.ToString() : "ns"; // 判断物品是否可堆叠
gameObject.name = $"{name} ({number})"; // 修改游戏对象名称
}
// 调整物品数量
private void AdjustNumberOfItems()
{
_stack.NumberOfItems = _stack.NumberOfItems;
}
}
}
挂载脚本
效果
拾取物品
修改GameItem,新增拾取方法
//拾取物品方法
public ItemStack Pick()
{
Destroy(gameObject);
return _stack;
}
人物挂载碰撞检测脚本
namespace InventorySystem
{
public class ItemCollisionHandler : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D col)
{
// 检测碰撞的对象是否拥有 GameItem 组件
var gameItem = col.GetComponent<GameItem>();
if (gameItem == null) return;
gameItem.Pick();
}
}
}
效果
物品栏、库存大小
InventorySlot
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
[System.Serializable]
public class InventorySlot
{
// 物品槽状态改变事件
public event EventHandler<(ItemStack,bool)> StatChanged;
public ItemDefinition Item{
get => _state.Item;
}
// 物品堆栈状态
[SerializeField]
private ItemStack _state;
// 物品槽是否处于激活状态
private bool _active;
// 物品堆栈状态属性
public ItemStack State
{
get => _state;
set
{
_state = value;
NotifyAboutStateChange();
}
}
// 物品槽激活状态属性
public bool Active
{
get => _active;
set
{
_active = value;
NotifyAboutStateChange();
}
}
// 物品数量属性
public int NumberOfItems
{
get => _state.NumberOfItems;
set
{
_state.NumberOfItems = value;
NotifyAboutStateChange();
}
}
// 判断物品槽是否有物品(即物品堆栈中的物品不为空)
public bool HasItem => _state?.Item != null;
// 清空物品槽
public void Clear()
{
State = null;
}
// 通知物品槽状态改变
private void NotifyAboutStateChange()
{
StatChanged?.Invoke(this, (_state, _active));
}
}
}
namespace InventorySystem
{
public class Inventory : MonoBehaviour
{
[SerializeField]
private int _size = 8; // 背包大小
[SerializeField]
private List<InventorySlot> _slots; // 物品槽列表
private void OnValidate()
{
AdjustSize(); // 检查并调整背包大小
}
/// <summary>
/// 检查并调整背包大小
/// </summary>
private void AdjustSize()
{
_slots ??= new List<InventorySlot>(); // 如果物品槽列表为空,则初始化为一个空的列表
if (_slots.Count > _size) // 如果物品槽数量大于背包大小
_slots.RemoveRange(_size, _slots.Count - _size); // 移除多余的物品槽
if (_slots.Count < _size) // 如果物品槽数量小于背包大小
_slots.AddRange(new InventorySlot[_size - _slots.Count]); // 添加空的物品槽,直到数量达到背包大小
}
}
}
效果
寻找物品栏并可以添加物品
修改inventory代码
/// <summary>
/// 判断背包是否已满
/// </summary>
public bool IsFull()
{
return _slots.Count(slot => slot.HasItem) >= _size; // 如果已占用的物品槽数量大于等于背包大小,则背包已满
}
/// <summary>
/// 判断是否可以接收物品堆叠
/// </summary>
/// <param name="itemStack">物品堆叠</param>
/// <returns>如果可以接收,则返回true;否则返回false</returns>
public bool CanAcceptItem(ItemStack itemStack)
{
var slotWithStackableItem = FindSlot(itemStack.Item, onlyStackable: true); // 查找可以堆叠的物品槽
return !IsFull() || slotWithStackableItem != null; // 如果背包未满或找到了一个可以堆叠的物品槽,就可以接收该物品
}
/// <summary>
/// 查找具有指定物品的物品槽
/// </summary>
/// <param name="item">物品定义</param>
/// <param name="onlyStackable">是否只查找可堆叠的物品槽</param>
/// <returns>如果找到,返回具有指定物品的物品槽;否则返回null</returns>
private InventorySlot FindSlot(ItemDefinition item, bool onlyStackable = false)
{
// 遍历物品槽列表,找到第一个物品槽的Item属性等于指定物品的Item属性,并且该物品符合是否只查找可堆叠的物品槽的要求
return _slots.FirstOrDefault(slot => slot.Item == item && (item.IsStackable || !onlyStackable));
}
/// <summary>
/// 向背包中添加物品堆叠
/// </summary>
/// <param name="itemStack">物品堆叠</param>
/// <returns>实际添加到背包中的物品堆叠</returns>
public ItemStack AddItem(ItemStack itemStack)
{
var relevantSlot = FindSlot(itemStack.Item, true); // 查找具有相同物品的物品槽
if (IsFull() && relevantSlot == null) // 如果背包已满并且无法找到可以堆叠的物品槽,就抛出异常
{
throw new InventoryException(InventoryOperation.Add, "Inventory is full");
}
if (relevantSlot != null) // 如果找到了可以堆叠的物品槽
{
relevantSlot.NumberOfItems += itemStack.NumberOfItems; // 合并物品堆叠数量
}
else // 如果没有找到可以堆叠的物品槽
{
relevantSlot = _slots.First(slot => !slot.HasItem); // 找到第一个空的物品槽
relevantSlot.State = itemStack; // 设置该物品槽的物品堆叠为指定的物品堆叠
}
return relevantSlot.State; // 返回实际添加到背包中的物品堆叠
}
背包异常类InventoryException
using System;
namespace InventorySystem
{
// 背包操作枚举类型
public enum InventoryOperation
{
Add,// 添加物品
Remove// 移除物品
}
// 背包异常类
public class InventoryException : Exception
{
public InventoryOperation Operation { get; }// 引发异常时的背包操作
public InventoryException(InventoryOperation operation, string msg) : base($"{operation} Error: {msg}")// 构造函数
{
Operation = operation;
}
}
}
修改ItemCollisionHandler代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
public class ItemCollisionHandler : MonoBehaviour
{
private Inventory _inventory;
private void Awake()
{
_inventory = GetComponentInParent<Inventory>();
}
private void OnTriggerEnter2D(Collider2D col)
{
// 检测碰撞的对象是否拥有 GameItem 组件
var gameItem = col.GetComponent<GameItem>();
if (gameItem == null) return;
_inventory.AddItem(gameItem.Pick());
}
}
}
效果
库存已满问题解决
修改GameItem
public ItemStack Stack =>_stack;
修改
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
public class ItemCollisionHandler : MonoBehaviour
{
private Inventory _inventory;
private void Awake()
{
_inventory = GetComponentInParent<Inventory>();
}
private void OnTriggerEnter2D(Collider2D col)
{
// 检测碰撞的对象是否拥有 GameItem 组件
var gameItem = col.GetComponent<GameItem>();
if (gameItem == null || !_inventory.CanAcceptItem(gameItem.Stack)) return;
_inventory.AddItem(gameItem.Pick());
}
}
}
库存UI脚本
绘制插槽ui
文字粗细和轮廓
新增UI_InventorySlot脚本
public class UI_InventorySlot : MonoBehaviour
{
}
新增UI_Inventory脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem.UI
{
// UI物品栏界面类
public class UI_Inventory : MonoBehaviour
{
// 物品槽预制体
[SerializeField]
private GameObject _inventorySlotPrefab;
// 对应的物品栏
[SerializeField]
private Inventory _inventory;
// 物品槽列表
[SerializeField]
private List<UI_InventorySlot> _slots;
// 物品栏属性
public Inventory Inventory => _inventory;
// 初始化物品栏UI
[ContextMenu("初始化库存")]
private void InitializeInventoryUi()
{
if (_inventory == null || _inventorySlotPrefab == null)
return;
_slots = new List<UI_InventorySlot>(_inventory.Size);
for (var i = 0; i < _inventory.Size; i++)
{
// 实例化物品槽预制体
var uiSlot = Instantiate(_inventorySlotPrefab, transform);
var uiSlotScript = uiSlot.GetComponent<UI_InventorySlot>();
// 将实例化生成的物品槽脚本添加到列表中
_slots.Add(uiSlotScript);
}
}
}
}
修改Inventory
public int Size =>_size;
效果
显示物品信息
修改插槽脚本UI_InventorySlot
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace InventorySystem.UI
{
public class UI_InventorySlot : MonoBehaviour
{
// 物品栏
[SerializeField]
private Inventory _inventory;
// 物品槽索引
[SerializeField]
private int _inventorySlotIndex;
// 物品图标
[SerializeField]
private Image _itemIcon;
// 激活指示器
[SerializeField]
private Image _activeIndicator;
// 物品数量文本
[SerializeField]
private TMP_Text _numberOfItems;
// 物品槽
private InventorySlot _slot;
private void Start()
{
AssignSlot(_inventorySlotIndex);
}
// 分配物品槽
public void AssignSlot(int slotIndex)
{
// 取消之前的事件监听
if (_slot != null) _slot.StateChanged -= OnStateChanged;
// 更新物品槽索引
_inventorySlotIndex = slotIndex;
// 获取所属的物品栏
if (_inventory == null) _inventory = GetComponentInParent<UI_Inventory>().Inventory;
// 获取物品槽
_slot = _inventory.Slots[_inventorySlotIndex];
// 添加事件监听
_slot.StateChanged += OnStateChanged;
// 更新视图状态
UpdateViewState(_slot.State, _slot.Active);
}
// 更新视图状态
private void UpdateViewState(ItemStack state, bool active)
{
// 更新激活指示器
_activeIndicator.enabled = active;
var item = state?.Item;
var hasItem = item != null;
var isStackable = hasItem && item.IsStackable;
// 更新物品图标显示
_itemIcon.enabled = hasItem;
// 更新物品数量文本显示
_numberOfItems.enabled = isStackable;
if (!hasItem) return;
// 更新物品图标
_itemIcon.sprite = item.UiSprite;
if (isStackable)
{
// 更新物品数量文本
_numberOfItems.SetText(state.NumberOfItems.ToString());
}
}
// 物品槽状态改变事件处理方法
// private void OnStateChanged(object sender, InventorySlotStateChangedArgs args)
// {
// UpdateViewState(args.NewState, args.Active);
// }
private void OnStateChanged(object sender, (ItemStack, bool) e)
{
UpdateViewState(e.Item1,e.Item2);
}
}
}
修改UI_Inventory
private void InitializeInventoryUi()
{
//。。。
for (var i = 0; i < _inventory.Size; i++)
{
// 。。。
uiSlotScript.AssignSlot(i);
// 。。。
}
}
修改Inventory
public List<InventorySlot> Slots => _slots;
效果
切换指示器
新增InventoryInputHandler脚本,挂载在任务身上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace InventorySystem
{
public class InventoryInputHandler : MonoBehaviour
{
private Inventory _inventory;
// 初始化_inventory变量
private void Awake()
{
_inventory = GetComponent<Inventory>();
}
// 每帧检查是否响应按键
void Update()
{
if (Input.GetKeyDown(KeyCode.Q))
{
OnPreviousItem(); // 选择上一项
}
if (Input.GetKeyDown(KeyCode.E))
{
OnNextItem(); // 选择上一项
}
}
// 选择下一项物品
private void OnNextItem()
{
_inventory.ActivateSlot(_inventory.ActiveSlotIndex + 1);
}
// 选择上一项物品
private void OnPreviousItem()
{
_inventory.ActivateSlot(_inventory.ActiveSlotIndex - 1);
}
}
}
修改Inventory
private void Awake()
{
if (_size > 0)
{
_slots[0].Active = true;// 激活第一个物品槽
}
}
public void ActivateSlot(int atIndex)
{
ActiveSlotIndex = atIndex;
}
效果
丢弃物品
修改InventoryInputHandler
void Update()
{
if (Input.GetKeyDown(KeyCode.G))
{ // 如果按下G键
OnThrowItem(); // 丢弃物品
}
}
// 丢弃当前物品
private void OnThrowItem()
{
_inventory.RemoveItem(_inventory.ActiveSlotIndex, true);
}
新增GameItemSpawner代码,生成物品,挂载在人物身上
namespace InventorySystem
{
public class GameItemSpawner : MonoBehaviour
{
[SerializeField]
private GameObject _itemBasePrefab;
// 生成物品
public void SpawnItem(ItemStack itemStack)
{
// 如果物品基础预制体为空,直接返回
if (_itemBasePrefab == null) return;
Debug.Log(transform.position);
// 实例化物品基础预制体,并将其设置为当前对象的子对象
var item = Instantiate(_itemBasePrefab, transform.position, Quaternion.identity);
item.transform.SetParent(null);
// 获取物品的GameItem组件
var gameItemScript = item.GetComponent<GameItem>();
// 设置堆叠数量和物品定义到GameItem组件
gameItemScript.SetStack(new ItemStack(itemStack.Item, itemStack.NumberOfItems));
}
}
}
修改Inventory
private int _activeSlotIndex;
public int ActiveSlotIndex
{
get => _activeSlotIndex;
private set
{
_slots[_activeSlotIndex].Active = false;
_activeSlotIndex = value < 0 ? _size - 1 : value % Size;// 如果索引小于0,循环到最后一个物品槽;否则取模获取合法索引
_slots[_activeSlotIndex].Active = true;
}
}
public bool HasItem(ItemStack itemStack, bool checkNumberOfItems = false)
{
var itemSlot = FindSlot(itemStack.Item);
if (itemSlot == null) return false;
if (!checkNumberOfItems) return true;
if (itemStack.Item.IsStackable)
{
return itemSlot.NumberOfItems >= itemStack.NumberOfItems; // 检查物品数量是否足够
}
return _slots.Count(slot => slot.Item == itemStack.Item) >= itemStack.NumberOfItems;// 检查物品槽数量是否足够
}
public ItemStack RemoveItem(int atIndex, bool spawn = false)
{
if (!_slots[atIndex].HasItem)
throw new InventoryException(InventoryOperation.Remove, "槽位为空");
if (spawn && TryGetComponent<GameItemSpawner>(out var itemSpawner))
{
// 添加生成物品的逻辑
itemSpawner.SpawnItem(_slots[atIndex].State);
}
ClearSlot(atIndex);
return new ItemStack(null, 0);
}
public void ClearSlot(int atIndex)
{
_slots[atIndex].Clear();
}
修改GameItem
// 设定物品堆栈
public void SetStack(ItemStack itemStack)
{
_stack = itemStack;
}
效果
添加丢弃弹出效果
修改GameItemSpawner
// 生成物品
public void SpawnItem(ItemStack itemStack)
{
// 。。。
// 抛掷物品(根据当前对象的缩放比例来确定抛掷方向)
gameItemScript.Throw(transform.localScale.x);
}
修改GameItem
[Header("丢弃设置")]
[SerializeField]
private float _colliderEnabledAfter = 1f; // 多少秒后启用碰撞器
[SerializeField]
private float _throwGravity = 2f; // 扔出的物品受到的重力系数
[SerializeField]
private float _minThrowXForce = 3f; // 扔出物品时在X轴方向最小的力量值
[SerializeField]
private float _maxThrowXForce = 5f; // 扔出物品时在X轴方向最大的力量值
[SerializeField]
private float _throwYForce = 5f; // 扔出物品时在Y轴方向的力量值
private Collider2D _collider; // 碰撞器
private Rigidbody2D _rb; // 刚体
// 初始化
private void Awake()
{
_collider = GetComponent<Collider2D>();
_rb = GetComponent<Rigidbody2D>();
_collider.enabled = false; // 防止在还没扔出时被玩家拾取
}
// 开始游戏时的初始化
private void Start()
{
SetupGameObject(); // 设置游戏对象
StartCoroutine(EnableCollider(_colliderEnabledAfter)); // 在设定的时间后启用碰撞器
}
// 等待一段时间后启用碰撞器
private IEnumerator EnableCollider(float afterTime)
{
yield return new WaitForSeconds(afterTime);
_collider.enabled = true;
}
// 扔出物品
public void Throw(float xDir)
{
_rb.gravityScale = _throwGravity; // 设定重力值
var throwXForce = Random.Range(_minThrowXForce, _maxThrowXForce); // 随机力量值
_rb.velocity = new Vector2(Mathf.Sign(xDir) * throwXForce, _throwYForce); // 设定物体的速度向量
StartCoroutine(DisableGravity(_throwYForce));
}
// 一定的高度后禁用重力,让物品飞行更自然
private IEnumerator DisableGravity(float atYVelocity)
{
yield return new WaitUntil(() => _rb.velocity.y < -atYVelocity);
_rb.velocity = Vector2.zero;
_rb.gravityScale = 0;
}
效果
最终效果
源码
整理好后我会放上来
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。点赞越多,更新越快哦!当然,如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~