Unity 简易背包系统系列
入门背包系统的实现
系列文章:
1.Unity 简易背包系统:简单的拖动,储存和回退;
2.Unity 简易背包系统:物品展示框;
3.Unity 简易背包系统:工作台合成和配方;
4.Unity 简易背包系统:可数物品,数值化储存;
〇. 实现原理
- 演示视频
可数型拖动单元
:负责储存单元数值的信息,继承自数据型拖动单元。
可数型储存单元
:负责储存和管理可数型拖动单元,继承自数据型储存单元,能够对可数型拖动单元实现与 “ 数据型储存单元对数据型拖动单元 ” 相同的基本操作。
可数型单元管理者
:不仅实现对可数型储存单元基本管理操作,还实现了对可数型储存单元局限的多物体管理,继承自单元管理者。
可数型单元容器板
:除了继承自单元容器板,实现对可数型储存单元基本操作外,还实现了只对可数型储存单元有效的操作(分堆,集块等等)。
一. 具体实现步骤
- 总体都是在分堆、堆叠方法上修改。
1. 储存单元(修改)
- 添加储存单元消耗一个拖动单元的虚拟方法。
/// <summary>
/// 消耗一个 '拖动单元'
/// </summary>
public virtual void Consume()
{
Destory();
}
2. 单元工作台(修改)
- 将消耗材料方法的销毁材料改成消耗一个材料
/// <summary>
/// 工作台消耗合成材料
/// </summary>
protected virtual void ConsumeMaterial()
{
foreach (UIStoreSlot slot in slots)
if (!slot.IsEmpty)
slot.Consume();
}
3. 可数型拖动单元
- 添加分堆(一半一半地分),多重分堆(我的世界鼠标左键滑动分堆),叠加(普通物体堆叠),消耗(-1)函数。
public class UIDragSlot_Countable : UIDragSlot_Data
{
[Header("显示相关")]
[SerializeField] private Text m_QuantityDisplayText;
[Header("数值相关")]
[SerializeField] private int quantity;
public bool IsEmpty => quantity == 0;
public bool IsOnlyOne => quantity == 1;
public bool IsFull => quantity == UISlotSettings.settings.UltimateStack;
public int Quantity => quantity;
/// <summary>
/// 拆分物体给 another
/// </summary>
/// <param name="another"></param>
public void SplitFor(UIDragSlot_Countable another)
{
another.quantity = Split();
this.DisplayQuantity();
another.DisplayQuantity();
}
/// <summary>
/// 拆分物体分给多个 another
/// <para> ==!注意!== 只会在自己的 quantity 基础上进行拆分 </para>
/// </summary>
/// <param name="anothers"></param>
public void SplitFor(params UIDragSlot_Countable[] anothers)
{
int quantityLeft = this.quantity;
int eachQuantity = Split(anothers.Length);
this.DisplayQuantity();
foreach (UIDragSlot_Countable dragSlot in anothers)
{
if (quantityLeft > this.quantity)
{
dragSlot.quantity = eachQuantity;
dragSlot.DisplayQuantity();
quantityLeft -= eachQuantity;
}
else
break;
}
}
/// <summary>
/// 只改变 quantity 数值,将 quantity 除半(向上进位),并返回失去的值
/// </summary>
/// <returns> 失去的值(另一半,向下进位) </returns>
private int Split()
{
if (quantity == 1)
return 0;
int rightQuantity = quantity / 2;
int leftQuantity = quantity - rightQuantity;
quantity = leftQuantity;
return rightQuantity;
}
/// <summary>
/// 只改变 quantity 数值,将 quantity 除 totalSlotCount(向上进位),返回每个 anotherSlot 可以获得的 quantity
/// </summary>
/// <returns> 每个 anotherSlot 可以获得的 quantity </returns>
public int Split(int totalSlotCount)
{
if (quantity < totalSlotCount)
{
quantity = 0;
return 1;
}
else
{
int eachQuantity = quantity / totalSlotCount;
int left = quantity - eachQuantity * totalSlotCount;
quantity = left;
return eachQuantity;
}
}
/// <summary>
/// 只改变 quantity 数值,追加数值,如果超出,则归还超出部分
/// <para> ==不安全== 如果自身和自身交换,则结果为 0 </para>
/// <para> -- 不处理 Destory -- </para>
/// </summary>
/// <param name="other"></param>
/// <returns> 如果超出最大界限(不包含相等),则返回 false </returns>
public void Append(UIDragSlot_Countable other)
{
// 1.先把 other 的所有 quantity 赋予给 this
this.quantity += other.quantity;
other.quantity = 0;
// 2.检验是否超出容量,如果超出容量则返还超出部分给 other
int ultimateStack = UISlotSettings.settings.UltimateStack;
if (this.quantity > ultimateStack)
{
other.quantity += this.quantity - ultimateStack;
this.quantity = ultimateStack;
}
// ==!== 这一步在 Store 里面进行
// 3.如果 other 容量为 0,则销毁
}
/// <summary>
/// 消耗一个
/// ==非常不安全== 只进行 --quantity,没有检验
/// </summary>
public void Consume()
{
--quantity;
this.DisplayQuantity();
}
#region 显示部分
public void DisplayQuantity()
{
m_QuantityDisplayText.text = quantity.ToString();
}
#endregion
}
4. 可数型储存单元
- 分堆,叠加方法(注解都有详细的解释),都是在可数型拖动单元的方法基础上加以修改。
public class UIStoreSlot_Countable : UIStoreSlot_Data
{
// 缓存 '可数型' 为了避免每次都转换
private UIDragSlot_Countable m_CacheSlot;
public override void Start()
{
if (UISlotManager.manager == null)
UISlotManager.Create<UISlotManager_Countable>();
if (IsEmpty)
SetNullDo();
else
SetHoldDo();
}
/// <summary>
/// 把 this.hold 分两半,然后把另一半给 another
/// <para> ==安全== 如果 another 已经存在物体,则不会进行任何操作 </para>
/// </summary>
/// <param name="another"></param>
public void Split(UIStoreSlot_Countable another)
{
if (another.IsEmpty && !this.m_CacheSlot.IsOnlyOne)
{
UIDragSlot_Countable newSlot = Instantiate(m_CacheSlot);
another.SetHold(newSlot);
m_CacheSlot.SplitFor(newSlot);
another.Reback();
}
}
/// <summary>
/// 把 this.hold 分 n+1 份,然后把剩下的给 this
/// <para> ==安全== 如果 another 已经存在物体,则不会进行任何操作 </para>
/// </summary>
/// <param name="anothors"></param>
public void Split(params UIStoreSlot_Countable[] anothors)
{
if (m_CacheSlot == null)
return;
// 1.把所有被分开的值重新合并起来
foreach (UIStoreSlot_Countable storeSlot in anothors)
{
if (!storeSlot.IsEmpty)
this.m_CacheSlot.Append(storeSlot.m_CacheSlot);
}
int rawQuantity = this.m_CacheSlot.Quantity;
// 2.创建大小为 Quantity 的数组,并转移数据
List<UIStoreSlot_Countable> storeSlots = new List<UIStoreSlot_Countable>();
int minLength = m_CacheSlot.Quantity < anothors.Length ? m_CacheSlot.Quantity : anothors.Length;
for (int i = 0; i < minLength; ++i)
storeSlots.Add(anothors[i]);
// 3.把 storeSlot 全部改成 dragSlot 形式
List<UIDragSlot_Countable> dragSlots = new List<UIDragSlot_Countable>();
foreach (UIStoreSlot_Countable storeSlot in storeSlots)
{
if (storeSlot.IsEmpty)
{
UIDragSlot_Countable newSlot = Instantiate(m_CacheSlot); // 这个操作与后面数值分配无关,所以不用调零
storeSlot.SetHold(newSlot);
dragSlots.Add(newSlot);
}
else
{
dragSlots.Add(storeSlot.m_CacheSlot);
}
}
// 4.分配数据
this.m_CacheSlot.SplitFor(dragSlots.ToArray());
// 5.检验是否有空的或 Quantity 为 0 的数据,然后销毁,并重置 dragSlot 的位置
foreach (UIStoreSlot_Countable storeSlot in storeSlots)
{
if (storeSlot.IsEmpty || storeSlot.m_CacheSlot.IsEmpty)
storeSlot.Destory();
else
storeSlot.Reback();
}
if (anothors.Length >= rawQuantity)
{
this.Destory();
UISlotManager.manager.End(); //强制结束
}
}
/// <summary>
/// 把 other.hold 数值加给 this.hold,如果 other.hold 数值为 0,则销毁 hold
/// <para> ==不安全== 如果 this 移动,则不会吧 this.hold 归位 </para>
/// </summary>
/// <param name="other"></param>
public void Append(UIStoreSlot_Countable other)
{
// 数据处理部分
this.m_CacheSlot.Append(other.m_CacheSlot);
if (other.m_CacheSlot.IsEmpty)
other.Destory();
else
other.Reback();
// 显示处理部分
this.m_CacheSlot.DisplayQuantity();
other.m_CacheSlot?.DisplayQuantity();
}
/// <summary>
/// 把物体加给 this.hold
/// </summary>
/// <param name="dragSlot"></param>
/// <returns> 加给后,多出来的部分 </returns>
public int Append(UIDragSlot_Countable dragSlot)
{
// 数据处理部分
m_CacheSlot.Append(dragSlot);
// 显示处理部分
if (dragSlot.IsEmpty)
{
this.m_CacheSlot.DisplayQuantity();
dragSlot.Dissolve();
return 0;
}
else
{
this.m_CacheSlot.DisplayQuantity();
dragSlot.DisplayQuantity();
return dragSlot.ID;
}
}
#region 继承方法区
// 都是更新 '缓存'
protected override void SetHoldDo()
{
m_CacheSlot = Hold as UIDragSlot_Countable;
}
protected override void SetNullDo()
{
m_CacheSlot = null;
}
public override bool IsSameHold(UIStoreSlot other)
=> m_CacheSlot.ID == (other as UIStoreSlot_Countable).m_CacheSlot.ID;
public override void Check()
{
if (m_CacheSlot.IsEmpty)
{
this.Destory();
UISlotManager.manager.UnSelect();
}
}
#endregion
public bool IsSameID(UIDragSlot_Countable dragSlot)
=> m_CacheSlot.ID == dragSlot.ID;
/// <summary>
/// 继承方法,消耗一个物品
/// </summary>
public override void Consume()
{
if (!m_CacheSlot.IsEmpty)
m_CacheSlot.Consume();
}
// 这些后来有用
#region 事件区
public override void BeginDragDo() { }
public override void OnDragDo() { }
public override void EndDragDo() { }
#endregion
}
5. 可数型单元管理者
- 继承方法,做小修改,就不做注释了。
public class UISlotManager_Countable : UISlotManager
{
public virtual void OnEnable()
{
SelectMultiDo += OnMultiSelectDo;
}
public virtual void OnDisable()
{
SelectMultiDo -= OnMultiSelectDo;
}
protected override void EndDo()
{
SlotsState move = MoveTo(StartSlot, EndSlot);
switch (move)
{
case SlotsState.Empty:
case SlotsState.Ocupy:
base.EndDo();
break;
case SlotsState.Equal:
(EndSlot as UIStoreSlot_Countable).Append(StartSlot as UIStoreSlot_Countable);
break;
default:
Debug.LogError("移动空物体!!!");
break;
}
}
#region 事件区
public void OnMultiSelectDo()
{
UIStoreSlot_Countable selectStoreSlot = SelectShow.SelectSlot as UIStoreSlot_Countable;
selectStoreSlot.Split(SelectShow.GetSelectSlots<UIStoreSlot_Countable>());
}
#endregion
}
6. 可数型单元容器板
- 由于与普通的单元容器板的传入成员有所不同,并且在拖动单元相同的情况处理也不一样,所以没有重写
TransferTo
方法。 - 重写
TransferSelf
方法,以应对两个数据型拖动单元ID一样时的情况。
public class UISlotContainer_Countable : UISlotContainer
{
/// <summary>
/// 将物品转移给另一个容器
/// </summary>
/// <param name="otherContainer"> 要转移物品给的容器 </param>
public virtual void TransferTo(UISlotContainer_Countable 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]);
j = 0;
++i;
break;
case UISlotManager.SlotsState.Ocupy:
++j;
break;
case UISlotManager.SlotsState.Error:
++i;
break;
case UISlotManager.SlotsState.Equal:
(otherContainer.slots[j] as UIStoreSlot_Countable).Append(slots[i] as UIStoreSlot_Countable);
if (slots[i].IsEmpty) // 如果 slot 已经把物品都给光了
{
j = 0;
++i;
}
else // 如果没有给光,那一定是 otherSlot 满了
{
++j;
}
break;
default:
return;
}
}
}
public override 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:
(slots[i] as UIStoreSlot_Countable).Append(slots[j] as UIStoreSlot_Countable);
break;
default:
return;
}
}
}
}
/// <summary>
/// 添加物体
/// </summary>
/// <param name="dragSlots"></param>
public void Append(params UIDragSlot_Countable[] dragSlots)
{
for (int i = 0; i < dragSlots.Length; ++i)
Append(dragSlots[i]);
}
/// <summary>
/// 添加物体
/// </summary>
/// <param name="dragSlot"></param>
public void Append(UIDragSlot_Countable dragSlot)
{
for (int i = 0; i < slots.Length; ++i)
if (slots[i].IsEmpty) // 如果要放入的格子为空,则直接设置
{
slots[i].SetHold(dragSlot);
break;
}
else
{
UIStoreSlot_Countable countableSlot = slots[i] as UIStoreSlot_Countable;
if (countableSlot.IsSameID(dragSlot)) // 如果要放入的格子与 dragSlot.ID 一样,则叠加
{
int left = countableSlot.Append(dragSlot);
if (left == 0)
break;
else // 如果还剩一些,则寻找下一个格子
continue;
} // 如果要放入的格子与 dragSlot.ID 不一样,则寻找下一个格子
else
continue;
}
}
/// <summary>
/// 拆分所选物体(单元管理者选择的拖动单元)
/// ==安全== 会先检验空间是否足够和所选物体是否为 null
/// </summary>
public void SplitSelect()
{
if (IsFull || UISlotManager.manager.SelectSlot == null || UISlotManager.manager.SelectSlot.IsEmpty)
return;
for (int i = 0; i < slots.Length; ++i)
{
if (slots[i].IsEmpty)
{
(UISlotManager.manager.SelectSlot as UIStoreSlot_Countable).Split(slots[i] as UIStoreSlot_Countable);
break;
}
}
}
}
- 其中
SplitSelect
方法用在按钮事件(按钮点击触发这个方法)。
7. (关于工作台的在下一篇文章讲解)
二. 脚本附着
- 注意要把普通单元管理者换成可数型单元管理者,把普通的都换成可数型的,包括单元容器板、储存单元和拖动单元。
- 分堆按钮的 “构造” :
- 可数型拖动单元的 “构造” :
- 注意要把可数型拖动单元的数值显示文本(子物体)改成射线无法达到。
- 关于最大堆叠数的可编辑脚本(如果想每个物体最大堆叠数不一样就不用这一步,直接写在可数型拖动单元里面单做成员)。
- 演示视频: