普通背包的实现
系列文章:
1.Unity 简易背包系统:简单的拖动,储存和回退;
2.Unity 简易背包系统:物品展示框;
3.Unity 简易背包系统:工作台合成和配方;
4.Unity 简易背包系统:可数物品,数值化储存;
〇. 实现原理
- 运行视频
拖动单元
:负责储存单元的信息,和基础的友元操作。
储存单元
:负责储存和管理拖动单元,并接受鼠标行为信息,再把信息转化为单元管理者的操作。
单元管理者
:负责将储存单元发送的信息整理和加工,再反馈给储存单元,以达到对拖动单元实现间接控制的效果;并储存一些基本的对储存单元的操作(比如:选择框显示,开始和结束对单元的操作,丢弃或保留单元内物品的指令的发送等等)。
回退板
:负责给单元管理者发送回退信息,如果鼠标进入回退板,则在鼠标松开后能够实现拖动单元的回退,否则不回退。
单元容器板
:负责储存储存单元和管理自己负责的储存单元,并储存基本的对储存单元的操作(比如:把所有储存单元储存的拖动单元转移到另一个单元容器板内)。
一. 具体实现步骤
1. 拖动单元
- 仅仅储存数据和简单的自毁方法
using UnityEngine;
/// <summary>
/// 仅用于移动的 UIDragSlot
/// </summary>
public class UIDragSlot : MonoBehaviour
{
// 储存位置信息
[HideInInspector] public Vector2 startOffset;
// 储存 RectTranform
public RectTransform rectTrans;
// 初始化
public virtual void Start()
{
rectTrans = this.GetComponent<RectTransform>();
}
/// <summary>
/// 销毁操作
/// </summary>
public void Dissolve()
{
// 这个是我自己写的拓展销毁方法
// 可以改成对象池的回池操作或者直接 Destory(this.gameObject);
gameObject.Destroy();
}
}
2. 储存单元
- 为了安全性,里面用的都是私有访问修饰符来修饰变量。
- 为了可拓展性,里面有很多方法是可以继承重写的。
using UnityEngine;
using UnityEngine.EventSystems;
public delegate void StoreDo(UIStoreSlot slot);
public delegate void RemoveDo(UIStoreSlot slot);
/// <summary>
/// 必须需要一个 '回退板' 在后面,不然会当做丢掉拖动单元
/// </summary>
public class UIStoreSlot : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler,IPointerEnterHandler,IPointerExitHandler
{
[SerializeField] private UIDragSlot hold;
protected UIDragSlot Hold
{
get => hold;
set => SetHold(value);
}
[SerializeField] private Vector2 m_StoreOffset;
// 事件区 // 用在需要 '单元容器板' 操作的时候
public StoreDo storeDo;
public RemoveDo removeDo;
public bool IsEmpty => hold == null;
public virtual void Start()
{
if(UISlotManager.manager == null)
UISlotManager.Create<UISlotManager>();
}
/// <summary>
/// 将 hold 物品移动到指定位置
/// <para> ==不安全== 需要外部检验 IsEmpty </para>
/// </summary>
public void Reback()
{
hold.transform.SetParent(this.transform);
hold.rectTrans.anchoredPosition = m_StoreOffset;
storeDo?.Invoke(this);
SetHoldDo();
}
/// <summary>
/// 将 hold 物品父集关系从 '储存单元' 脱离出来
/// </summary>
/// <param name="parent"> 脱离到的 parent </param>
public void TakeOutTo(Transform parent)
{
hold.transform.SetParent(parent);
}
/// <summary>
/// <para> 与另一个 '储存单元' 交换 hold </para>
/// <para> ==不安全== 如果出现有一个 hold 为 null 则交换失败</para>
/// </summary>
/// <param name="other"></param>
private void SwapWith(UIStoreSlot other)
{
UISlotManager.Swap(ref hold, ref other.hold);
this.Reback();
other.Reback();
}
/// <summary>
/// <para> 把 hold 交给另一个 '储存单元'(强加给 other),然后自己丢失 </para>
/// <para> ==不安全== 如果 otherSlot 存在 hold 则会丢弃原有的 hold,数据丢失! </para>
/// </summary>
/// <param name="other"></param>
public void ImposeTo(UIStoreSlot other)
{
other.hold = this.hold;
SetNull();
other.Reback();
}
/// <summary>
/// <para> 拿取一个 hold </para>
/// <para> ==安全== 会在拿取前检验自身有无 hold,并 Reback() </para>
/// <para> ==不安全== 如果凭空生成物体,则相当于数据滋生 </para>
/// </summary>
/// <param name="hold"></param>
/// <returns> 自己有 hold 则拿取失败,返回 false;否则返回 true </returns>
public bool SetHold(UIDragSlot hold)
{
if (this.hold == null && hold != null)
{
this.hold = hold;
Reback();
SetHoldDo();
return true;
}
else
return false;
}
/// <summary>
/// <para> 强制失去 hold </para>
/// <para> ==不安全== 数据上的置空,显示上 '拖拽单元' 不会消失 </para>
/// </summary>
private void SetNull()
{
hold = null;
removeDo?.Invoke(this);
SetNullDo();
}
/// <summary>
/// <para> 当此 hold 不为 null 时,可以进行交换 </para>
/// <para> ==稍微安全== 当此 hold 不为 null 时,可以进行交换 </para>
/// </summary>
/// <param name="other"></param>
public void ExchangeWith(UIStoreSlot other)
{
if (other.IsEmpty)
ImposeTo(other);
else
SwapWith(other);
}
#region 继承方法区
/// <summary>
/// 设置 hold 后要做的事情
/// </summary>
protected virtual void SetHoldDo() { }
/// <summary>
/// 设置 hold 为 null 后要做的事情
/// </summary>
protected virtual void SetNullDo() { }
/// <summary>
/// 比较两个 '储存单元' 的 hold 是否是一样的(不同的继承可以有不同的返回值)
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public virtual bool IsSameHold(UIStoreSlot other) => false;
public virtual void Check() { }
#endregion
/// <summary>
/// 丢弃 hold 物品
/// </summary>
public void Drop()
{
// 和外界的互动
// ...
Destory();
}
/// <summary>
/// 销毁 hold 物品
/// </summary>
public void Destory()
{
hold.Dissolve();
SetNull();
}
#region 事件区
public void OnPointerDown(PointerEventData eventData)
{
UISlotManager.manager.Start(this);
if (hold != null)
{
hold.startOffset = hold.rectTrans.anchoredPosition - (Vector2)Input.mousePosition;
BeginDragDo();
}
}
public void OnDrag(PointerEventData eventData)
{
if (hold != null && UISlotManager.manager.IsStartWith(this))
{
hold.rectTrans.anchoredPosition = (Vector2)Input.mousePosition + hold.startOffset;
OnDragDo();
}
}
public void OnPointerUp(PointerEventData eventData)
{
UISlotManager.manager.End();
if (hold != null && UISlotManager.manager.IsStartWith(this))
{
EndDragDo();
}
}
public void OnPointerEnter(PointerEventData eventData)
{
UISlotManager.manager.SlotChoose(this);
}
public void OnPointerExit(PointerEventData eventData)
{
UISlotManager.manager.SlotDischoose();
}
/// <summary>
/// 开始拖动时调用的方法(在移动后调用)
/// </summary>
public virtual void BeginDragDo() { }
/// <summary>
/// 拖动时调用的方法(在移动后调用)
/// </summary>
public virtual void OnDragDo() { }
/// <summary>
/// 结束拖动时调用的方法(在移动后调用)
/// </summary>
public virtual void EndDragDo() { }
#endregion
}
注意:里面大部分方法都是缺少安全性的,但是换来的是更精确的分类,需要对准需求来使用方法。而为了保障方法安全使用,会在单元管理者中将方法进行分类使用。
3. 单元管理者
- 关于单元管理者的脚本编写
- 细节解释:
内部包括了选择选择框的使用,而其相关代码在这段代码结束的下面,其他的详细相关解释都在注解中。
using UnityEngine;
public class UISlotManager : MonoBehaviour
{
public static UISlotManager manager;
// 回退布尔
public bool reback = false;
[Header("关于选择 '储存单元' 的部分")]
[SerializeField] private UISlotSelectShow m_SelectShow;
protected UISlotSelectShow SelectShow => m_SelectShow;
public UIStoreSlot SelectSlot => m_SelectShow.SelectSlot;
[SerializeField] private UIStoreSlot m_StartSlot, m_EndSlot;
protected UIStoreSlot StartSlot => m_StartSlot;
protected UIStoreSlot EndSlot => m_EndSlot;
[Header("要被拿出来时候放在的 parent")]
[SerializeField] private Transform outestPanel;
public void Awake()
{
if (manager != null && manager != this)
{
this.gameObject.Destroy();
return;
}
manager = this;
}
public void Start()
{
outestPanel ??= GameObject.Find("Canvas").transform;
m_SelectShow.Init();
}
#region 外部使用(选取和抓取)方法
/// <summary>
/// 记录下第一次点击拖拽的 '储存单元',开始移动操作
/// </summary>
/// <param name="slot"></param>
public void Start(UIStoreSlot slot)
{
if (!slot.IsEmpty)
{
slot.TakeOutTo(outestPanel); // 将拖动的物品置顶,以防被挡到
m_StartSlot = slot;
}
else if (m_SelectShow.IsOnSelect)
{
m_SelectShow.StartSelect(slot);
MultiSelectDo();
}
}
/// <summary>
/// 结束移动操作
/// </summary>
public void End()
{
if (m_SelectShow.IsSelectingSlots)
{
if (m_SelectShow.SelectSlot.IsEmpty)
m_SelectShow.UnSelect(outestPanel);
m_SelectShow.EndSelect();
return;
}
else if (m_StartSlot == null)
{
Debug.Log("没有选中 StartSlot");
return;
}
else if (m_StartSlot.IsEmpty)
{
Debug.Log("选中的 StartSlot 是空的");
m_SelectShow.UnSelect(outestPanel);
return;
}
if (!reback) // 丢弃物品
{
m_StartSlot.Drop();
m_SelectShow.UnSelect(outestPanel);
}
else if (m_EndSlot != null) // 移动到合法的格子,两个格子的物品互相交换
{
if (m_StartSlot == m_EndSlot)
SelectSwitch();
else
{
EndDo();
m_SelectShow.Select(m_EndSlot);
}
}
else if (reback) // 没有移动到合法的格子,物品回到原来的格子
{
m_StartSlot.Reback();
}
// 清除初始记录状态
m_StartSlot = null;
}
/// <summary>
/// 选择最后要放在的 '储存单元'
/// </summary>
/// <param name="slot"></param>
public void SlotChoose(UIStoreSlot slot)
{
m_EndSlot = slot;
if (m_SelectShow.IsSelectingSlots && slot.IsEmpty)
{
if (m_SelectShow.SelectSlot.IsEmpty)
{
End();
return;
}
m_SelectShow.AppendSelect(slot);
MultiSelectDo();
}
}
/// <summary>
/// 取消选择最后要放在的 slot
/// </summary>
public void SlotDischoose()
{
m_EndSlot = null;
}
/// <summary>
/// 在外部强制取消显示框的显示
/// </summary>
public void UnSelect()
=> m_SelectShow.UnSelect(outestPanel);
/// <summary>
/// 检验鼠标点下的是不是 other '储存单元'
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool IsStartWith(UIStoreSlot other)
=> m_StartSlot == other;
#endregion
// 子类继承和使用的方法
protected virtual void EndDo()
{
m_StartSlot.ExchangeWith(m_EndSlot);
}
protected virtual void MultiSelectDo() { }
protected void SelectSwitch()
{
if (m_SelectShow.SelectSame(m_StartSlot))
m_SelectShow.UnSelect(outestPanel);
else
m_SelectShow.Select(m_StartSlot);
m_StartSlot.Reback();
}
#region 其他静态方法
public enum SlotsState
{
Error, // 移动主体是空
Empty, // 移动到空位
Equal, // 移动到的位置具有一样的物体
Ocupy, // 移动到的位置被占领,且不相同
}
/// <summary>
/// 移动物体
/// </summary>
/// <param name="startSlot"> 开始移动的位置 </param>
/// <param name="endSlot"> 移动到的位置 </param>
/// <returns>
/// <para> 如果有效物体移动到空位,并返回 SlotsState.Empty; </para>
/// <para> 如果有效物体要移动的位置为值一样的非空位,并返回 SlotsState.Equal; </para>
/// <para> 如果有效物体要移动的位置为非空位,则返回 SlotsState.Ocupy; </para>
/// <para> 如果移动无效物体,则返回 SlotsState.Error; </para>
/// </returns>
public static SlotsState MoveTo(UIStoreSlot startSlot, UIStoreSlot endSlot)
{
if (startSlot.IsEmpty) // 移动无效物体
return SlotsState.Error;
else if (endSlot.IsEmpty) // 有效物体移动到空位
return SlotsState.Empty;
else if (startSlot.IsSameHold(endSlot)) // 移动到的位置被占领,且不相同
return SlotsState.Equal;
else // 移动到的位置具有一样的物体
return SlotsState.Ocupy;
}
public static void Swap<T>(ref T item1,ref T item2)
{
T tmpItem = item1;
item1 = item2;
item2 = tmpItem;
}
#endregion
#region 强制初始化
public static void Create<T>() where T :UISlotManager
{
GameObject slotManagerGO = new GameObject("UISlotManager");
UISlotManager slotManager = slotManagerGO.AddComponent<T>();
manager = slotManager;
}
#endregion
}
- 关于选择框 ‘
UISlotSelectShow
’ 的脚本编写: - 细节解释:
内部包括了选择框显示的代码和多物体选择的代码,其中多物体选择部分暂时不会用到(和后面可数储存单元有关)。
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class UISlotSelectShow
{
public void Init()
{
if (m_SelectTrans.gameObject.activeInHierarchy)
m_SelectTrans.gameObject.SetActive(false);
}
#region 选择框部分
[Header("选择展示框部分")]
[SerializeField] private RectTransform m_SelectTrans;
[SerializeField] private Vector2 m_SelectOffset;
private UIStoreSlot m_SelectSlot;
public UIStoreSlot SelectSlot => m_SelectSlot;
public bool IsOnSelect => m_SelectSlot != null;
public void Select(UIStoreSlot slot)
{
if (m_SelectSlot == null)
m_SelectTrans.gameObject.SetActive(true);
m_SelectSlot = slot;
m_SelectTrans.SetParent(slot.transform);
m_SelectTrans.SetAsFirstSibling();
m_SelectTrans.anchoredPosition = m_SelectOffset;
}
public void UnSelect(Transform outestPanel)
{
m_SelectSlot = null;
//m_SelectTrans.SetParent(outestPanel);
m_SelectTrans.gameObject.SetActive(false);
}
public bool SelectSame(UIStoreSlot slot)
=> m_SelectSlot == slot;
#endregion
#region 选择多个物体部分
[Header("选择多个 '储存单元' 部分")]
private List<UIStoreSlot> m_SelectSlots;
private bool m_SelectingSlots;
public bool IsSelectingSlots => m_SelectingSlots;
/// <summary>
/// 创建一个新的 '储存单元' 数组储存 selectSlots
/// </summary>
/// <typeparam name="T"> UIStoreSlot 继承类 </typeparam>
/// <returns> 新的 Slot 数组 </returns>
public T[] GetSelectSlots<T>()where T : UIStoreSlot
{
T[] selectSlots = new T[m_SelectSlots.Count];
for(int i = 0; i < m_SelectSlots.Count; ++i)
selectSlots[i] = m_SelectSlots[i] as T;
return selectSlots;
}
/// <summary>
/// 开始多个选择 '储存单元'
/// <para> ==不安全== 不会检验是否在多选前,满不满足 IsOnSelect </para>
/// </summary>
/// <returns></returns>
public void StartSelect(UIStoreSlot appendSlot)
{
m_SelectSlots = new List<UIStoreSlot>() { appendSlot };
m_SelectingSlots = true;
}
/// <summary>
/// 添加 selectSlot
/// <para> ==安全== 不会重复储存已经有的 selectSlot </para>
/// </summary>
/// <param name="slot"></param>
public void AppendSelect(UIStoreSlot slot)
{
if (!m_SelectSlots.Contains(slot))
m_SelectSlots.Add(slot);
}
/// <summary>
/// 添加 selectSlot
/// <para> ==!注意!== 会再次储存已经有的 selectSlot </para>
/// </summary>
/// <param name="slot"></param>
public void AddSelect(UIStoreSlot slot)
{
m_SelectSlots.Add(slot);
}
/// <summary>
/// 移除 selectSlot
/// </summary>
/// <param name="slot"></param>
public void RemoveSelect(UIStoreSlot slot)
{
m_SelectSlots.Remove(slot);
}
/// <summary>
/// 置空 selectSlot 列表
/// </summary>
public void EndSelect()
{
m_SelectSlots = null;
m_SelectingSlots = false;
m_SelectSlot?.Check();
}
#endregion
}
4. 回退板
- 它的操作就是直接对单元管理者的
回退布尔
进行更改。
using UnityEngine;
using UnityEngine.EventSystems;
public class UIRebackSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
public void OnPointerEnter(PointerEventData eventData)
=> UISlotManager.manager.reback = true;
public void OnPointerExit(PointerEventData eventData)
=> UISlotManager.manager.reback = false;
}
5. 单元容器板
- 一个储存储存单元的容器(不止可以当背包)
using UnityEngine;
using System;
public class UISlotContainer : MonoBehaviour
{
[SerializeField] protected UIStoreSlot[] slots;
public int Count
{
get
{
int count = 0;
foreach (UIStoreSlot slot in slots)
if (!slot.IsEmpty)
++count;
return count;
}
}
public bool IsEmpty => Count == 0;
public bool IsFull => Count >= slots.Length;
/// <summary>
/// 将物品转移给另一个容器
/// </summary>
/// <param name="otherContainer"> 要转移物品给的容器 </param>
public virtual void TransferTo(UISlotContainer otherContainer)
{
UISlotManager.manager.UnSelect();
for (int i = 0, j = 0; i < slots.Length && j < otherContainer.slots.Length;)
{
UISlotManager.SlotsState move = UISlotManager.MoveTo(slots[i], otherContainer.slots[j]);
switch (move)
{
case UISlotManager.SlotsState.Empty:
slots[i].ImposeTo(otherContainer.slots[j]);
++i;
++j;
break;
case UISlotManager.SlotsState.Ocupy:
++j;
break;
case UISlotManager.SlotsState.Error:
++i;
break;
default:
return;
}
}
}
/// <summary>
/// 压缩自身物品(不按排序顺序排序背包内物品)
/// </summary>
public virtual void TransferSelf()
{
UISlotManager.manager.UnSelect();
for (int i = 0; i < slots.Length; ++i)
{
for (int j = i + 1; j < slots.Length;++j)
{
UISlotManager.SlotsState move = UISlotManager.MoveTo(slots[j], slots[i]);
switch (move)
{
case UISlotManager.SlotsState.Empty:
slots[j].ImposeTo(slots[i]);
break;
case UISlotManager.SlotsState.Ocupy:
break;
case UISlotManager.SlotsState.Error:
continue;
case UISlotManager.SlotsState.Equal:
break;
default:
return;
}
}
}
}
/// <summary>
/// 向空格子添加物体
/// </summary>
/// <param name="slot"></param>
public void Add(params UIDragSlot[] dragSlots)
{
for (int i = 0, j = 0; i < slots.Length && j < dragSlots.Length; ++i)
if (slots[i].SetHold(dragSlots[j]))
++j;
}
/// <summary>
/// 向空格子添加物体
/// </summary>
/// <param name="dragSlot"></param>
public void Add(UIDragSlot dragSlot)
{
for (int i = 0; i < slots.Length; ++i)
if (slots[i].SetHold(dragSlot))
break;
}
#if UNITY_EDITOR
public virtual void OnTransformChildrenChanged()
{
slots = new UIStoreSlot[transform.childCount];
for (int i = 0; i < transform.childCount; ++i)
if (transform.GetChild(i).TryGetComponent(out UIStoreSlot slot))
slots[i] = slot;
}
/// <summary>
/// 完完全全复制一个 dragSlot,数据泄露(作弊器)
/// </summary>
/// <param name="dragSlot"></param>
public void Create(UIDragSlot dragSlot)
{
Create(dragSlot, null);
}
protected void Create<T>(T dragSlot, Action<T> createDo) where T : UIDragSlot
{
if (!IsFull)
{
T newSlot = Instantiate(dragSlot);
if (!newSlot.gameObject.activeInHierarchy)
newSlot.gameObject.SetActive(true);
createDo?.Invoke(newSlot);
newSlot.Start();
Add(newSlot);
}
}
#endif
}
二. 脚本附着
普通操作
-
拖动单元
-
储存单元
-
单元容器板和回退板
-
单元管理者
-
运行视频
更多操作
- 转移物品按钮
- 转移回来:
- 转移出去:
- 转移回来:
- 假排序按钮
- 创建物品按钮
- 展示视频
最后,有什么不足的请大佬们在评论区分享分享观点~