本质
对于背包系统,我们实际上是可以细分为 Item 和 Inventory 。
关于 Item :首先是有一个 id ,其应该有很多属性,比如 name , icon , description 这些固有信息,这种不变的属性就可以存储在数据库当中。而类似于 count , unlock 这种属性,其是会随着玩家的游玩而改变的,这些就可以存储在存档当中
关于 Inventory :其本质就一个是一个 Item List 或者 Item Array 。再在这个数据结构上面,对其支持一些操作。例如:Add , Remove 等等。
数据
首先我们需要 IBag 和 IBagItem 两个接口
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 
 | 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; }
 
 
 
 
 
 
 bool CheckLegal<T>(IBag<T> bag) where T : IBagItem;
 }
 
 | 
| 12
 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
 
 | public interface IBag<T> where T: IBagItem
 {
 
 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);
 
 
 
 
 
 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 此处使用一个泛型,是为了保证类型安全。
接着我们给出以上两个接口的一种实现:
| 12
 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
 
 | 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也同理,根据整个游戏,是否有升星系统之类的来判断到底需不需要检查。
| 12
 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
 
 | 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;
 
 foreach (var item in _bagItems) {
 List<T> list;
 var find = _bagItemMapById.TryGetValue(item.Id, out list);
 if (find) {
 
 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) {
 
 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;
 }
 
 
 if (!_bagItemMapById.ContainsKey(item.Id)) {
 RegisterBagItem(item);
 return;
 }
 
 
 if (!item.CheckLegal(this)) {
 Debug.Log("加入的物品不规范");
 return;
 }
 
 
 if (!item.Stackable) {
 RegisterBagItem(item);
 return;
 }
 
 
 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) {
 
 _bagItemMapById.Remove(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 之类的,再在这基础上二次封装一下即可。