本质

对于背包系统,我们实际上是可以细分为 ItemInventory

关于 Item :首先是有一个 id ,其应该有很多属性,比如 name , icon , description 这些固有信息,这种不变的属性就可以存储在数据库当中。而类似于 count , unlock 这种属性,其是会随着玩家的游玩而改变的,这些就可以存储在存档当中

关于 Inventory :其本质就一个是一个 Item List 或者 Item Array 。再在这个数据结构上面,对其支持一些操作。例如:Add , Remove 等等。

数据

首先我们需要 IBagIBagItem 两个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// IBagItem.cs
public interface IBagItem : IEquatable<IBagItem>
{
string Id { get; set; }
string Name { get; set; }
int Index { get; set; }
bool Stackable { get; set; }
int StackLimit { get; set; }
int StackCount { get; set; }
/// <summary>
/// 根据自身属性检查是否符合规范
/// </summary>
/// <param name="bag"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
bool CheckLegal<T>(IBag<T> bag) where T : IBagItem;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// IBag.cs
public interface IBag<T> where T: IBagItem
{
// Action
event Action OnItemAdd;
event Action OnItemRemove;
event Action OnItemTakeOut;
// ...
int Count { get; }
int MaxCapacity { get; set; }
void Add(T item);
void TakeOut(int index, int takeoutCount);
void RemoveById(string id);
void RemoveByIndex(int index);
void Clear();
T GetItemByIndex(int index);
List<T> GetItemsById(string id);
/// <summary>
/// 返回第一个 相同 ID 未满 StackLimit 的可堆叠物品,如果都满则任意返回
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
T GetCanStackItemById(string id);
List<T> GetAllItems();
void Sort(Func<T, object> sortByProperty);
Bag<T> Select(Func<T, bool> filter);
bool CheckItemPropertyLegal(IBagItem item, Func<IBagItem, IComparable> checkByProperty);
}

此时,我们就基本上抽象出了背包和背包物品的属性和行为。对于 IBag 此处使用一个泛型,是为了保证类型安全。

接着我们给出以上两个接口的一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// BagItem.cs
public enum BagItemType
{
Weapon,
Coin,
Sword,
Shield,
Bow,
Gun,
Pao,
Dao
}

public class BagItem : IBagItem
{
public string Id { get; set; }
public string Name { get; set; }
public int Index { get; set; }
public bool Stackable { get; set; }
public int StackLimit { get; set; }
public int StackCount { get; set; }
public BagItemType BagItemType { get; set; }
public int Grade { get; set; }
public int Star { get; set; }

public BagItem(string id, string name, BagItemType itemType, int stackCount, int grade, int star) {
Id = id;
Name = name;
BagItemType = itemType;
Grade = grade;
Star = star;
if (stackCount > 0) {
Stackable = true;
StackLimit = 99;
StackCount = stackCount;
}
}

public BagItem(BagItem item) {
Id = item.Id;
Name = item.Name;
BagItemType = item.BagItemType;
Stackable = item.Stackable;
StackLimit = item.StackLimit;
StackCount = item.StackCount;
Grade = item.Grade;
Star = item.Star;
}


public bool CheckLegal<T>(IBag<T> bag) where T : IBagItem {
bool result = true;
// 检查可堆叠属性是否相同
result = bag.CheckItemPropertyLegal(this, item => item.Stackable) && result;
// 检查其他属性是否相同
result = bag.CheckItemPropertyLegal(this, item => item.Name) && result;
result = bag.CheckItemPropertyLegal(this, item => ((BagItem)item).BagItemType) && result;
result = bag.CheckItemPropertyLegal(this, item => ((BagItem)item).Grade) && result;
result = bag.CheckItemPropertyLegal(this, item => ((BagItem)item).Star) && result;
return result;
}

public bool Equals(IBagItem other) {
return other != null && Id == other.Id;
}
}

关于背包物品接口实现,添加了一些类型,等级,星级之类的属性。关于 CheckLegal 这个函数的实现,主要功能是判断添加进的同 Id 物品(一类的物品),其是否符合规范(规范由首次添加进的物品决定)。

  • 比如添加进了一把四星的xx剑,第二次添加相同物品的时候,即 Id 相同,我们就得开始检查两把武器的属性,Name 得一样吧,Type 得一样吧,其实这里 Grade 应该是不用检查的,毕竟同一类武器也有等级不一样的情况。Star 也同理,根据整个游戏,是否有升星系统之类的来判断到底需不需要检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
// Bag.cs
public class Bag<T> : IBag<T> where T : IBagItem
{
private List<T> _bagItems;
private readonly Dictionary<string, List<T>> _bagItemMapById;
private Bag<T> _selectedItems;
public event Action OnItemAdd;
public event Action OnItemRemove;
public event Action OnItemTakeOut;
public int Count => _bagItems.Count;
public int MaxCapacity { get; set; }

public Bag(int maxCapacity) {
_bagItemMapById = new Dictionary<string, List<T>>();
_bagItems = new List<T>();
MaxCapacity = maxCapacity;
}

public Bag(List<T> bagItems) {
_bagItemMapById = new Dictionary<string, List<T>>();
_bagItems = bagItems;
// 将每一个物品都加入 id 索引的字典
foreach (var item in _bagItems) {
List<T> list;
var find = _bagItemMapById.TryGetValue(item.Id, out list);
if (find) {
// 如果有相同物品直接存入拿到的 list
list.Add(item);
} else {
// 没有相同物品就新建插入
_bagItemMapById.Add(item.Id, new List<T> { item });
}
}

MaxCapacity = _bagItems.Count;
}

#region CRUD

public void Add(T item) {
if (Count >= MaxCapacity) {
// 如果添加进的有相同 ID 且符合规范且最终没有超过 item.StackLimit
var sameIdItem = GetCanStackItemById(item.Id);
if (sameIdItem != null && item.CheckLegal(this) &&
sameIdItem.StackCount + item.StackCount <= item.StackLimit) {
sameIdItem.StackCount += item.StackCount;
OnItemAdd?.Invoke();
} else {
Debug.LogError("背包容量不足");
}

return;
}

// 对应此 ID 的物品第一次加入背包
if (!_bagItemMapById.ContainsKey(item.Id)) {
RegisterBagItem(item);
return;
}

// 检查新加入背包的物体是否符合规范,规范由第一批加入的物体决定
if (!item.CheckLegal(this)) {
Debug.Log("加入的物品不规范");
return;
}

// 不可堆叠物品的添加
if (!item.Stackable) {
RegisterBagItem(item);
return;
}

// 后面的物品在背包里面肯定有一个已有的同 Id 物品且可堆叠
var canStackableItem = GetCanStackItemById(item.Id);
var mergeCount = item.StackCount + canStackableItem.StackCount;
var endCount = mergeCount - item.StackLimit > 0 ? mergeCount - item.StackLimit : mergeCount;
if (endCount == mergeCount) {
canStackableItem.StackCount = endCount;
} else {
canStackableItem.StackCount = canStackableItem.StackLimit;
item.StackCount = endCount;
RegisterBagItem(item);
}
}

public void TakeOut(int index, int takeoutCount) {
var item = GetItemByIndex(index);
if (item.StackCount < takeoutCount) {
Debug.LogError("此格子物品数量不足");
} else if (item.StackCount == takeoutCount) {
_bagItems.Remove(item);
var items = GetItemsById(item.Id);
items.Remove(item);
} else {
item.StackCount -= takeoutCount;
}

OnItemTakeOut?.Invoke();
}

public void RemoveById(string id) {
// 清除字典里面的 Id
_bagItemMapById.Remove(id);
// 清除列表里面的 Id
_bagItems.RemoveAll(item => item.Id == id);

OnItemRemove?.Invoke();
}

public void RemoveByIndex(int index) {
if (index < 0 || index >= Count) {
return;
}

var item = _bagItems[index];
_bagItemMapById[item.Id].Remove(item);
_bagItems.Remove(item);

OnItemRemove?.Invoke();
}

public void Clear() {
_bagItems.Clear();
}

public T GetItemByIndex(int index) {
if (index < 0 || index >= Count) {
Debug.LogError($"Index: {index} 数组越界");
}

return _bagItems[index];
}

public List<T> GetItemsById(string id) {
List<T> list;
_bagItemMapById.TryGetValue(id, out list);
return list;
}

public T GetCanStackItemById(string id) {
var list = GetItemsById(id);
if (list == null) {
return default(T);
}

var findItem = list.Find(item => item.Stackable && item.StackCount < item.StackLimit);
if (findItem != null) {
return findItem;
}

return list.Find(item => item.Stackable && item.StackCount == item.StackLimit);
}

public List<T> GetAllItems() {
return _bagItems;
}

#endregion

public void Sort(Func<T, object> sortByProperty) {
_bagItems = _bagItems.OrderBy(sortByProperty).ToList();
RefreshItemIndex();
}

public List<T> SelectAndOutList(Func<T, bool> filter) {
return _bagItems.Where(filter).ToList();
}

public Bag<T> Select(Func<T, bool> filter) {
return new Bag<T>(SelectAndOutList(filter));
}

public bool CheckItemPropertyLegal(IBagItem item, Func<IBagItem, IComparable> checkByProperty) {
var property = checkByProperty(item);
var item2 = GetItemsById(item.Id)[0];
var property2 = checkByProperty(item2);
return property.Equals(property2);
}

private void RegisterBagItem(T item) {
item.Index = _bagItems.Count;
_bagItems.Add(item);
if (_bagItemMapById.ContainsKey(item.Id)) {
_bagItemMapById[item.Id].Add(item);
} else {
_bagItemMapById.Add(item.Id, new List<T> { item });
}

OnItemAdd?.Invoke();
}

public T this[int index] {
get => GetItemByIndex(index);
set {
if (index < 0 || index >= Count) {
Debug.LogError($"Index: {index} 数组越界");
} else {
_bagItems[index] = value;
}
}
}

private void RefreshItemIndex() {
for (int i = 0; i < _bagItems.Count; i++) {
_bagItems[i].Index = i;
}
}

public override string ToString() {
string info = "BagInfo: ";
for (int i = 0; i < _bagItems.Count; i++) {
var item = _bagItems[i];
info += $"Index: {i}, Id: {item.Id}, Name: {item.Name}, Count: {item.StackCount}\n";
}

return info;
}
}

以上就是一个背包数据操作基本的封装。

  • 使用 List<T> 来作为保存物品背包的数据结构
  • 使用 Dictionary<string, List<T>> 来做一个根据 Id 的快速索引一类物品
  • 关于 CheckItemPropertyLegal ,在调用其之前我们其实是保证了背包已有同 Id 物品,因此可以直接在 Id 缓存里面拿。
  • default(T) 是返回一个类型的默认值,而对于我们的 BagItem 来说,其是一个引用类型,因此默认值就为 null

基本的背包 CRUD 逻辑基本上完成,其余如展示出来背包 ui 之类的,再在这基础上二次封装一下即可。