前言

花了几天时间去磨这个背包系统,发现有很多可学习的点

在背包系统有以下知识点,包括但不限于

对于UI的综合应用,对于设计模式的综合应用,对于数据处理的综合应用,对于存储方式的应用,编辑器扩展的应用。

建立背包UI

这一部分大量使用了UGUI的组件,是对于组件的灵活应用

UGUI框架

  • 限定分辨率

在Canvas下

Canvas Scaler组件 -> UI Scale Mode -> Scale Whith Sceeen Size

Reference Resolution -> X 1920 Y 1080

确保界面在任意分辨率下有着正确的表现方式

  • 创建背包空物体

创建空对象PackagePanel

在Rect Transform组件下的锚点点击Alt选中右下角的跟随父物体伸展宽高

  • 顶部菜单

    在顶部创建一个空对象Menus挂载

    Horizontal Layout Group:

    Content Size Fitter: 将Horizontal Fit 设置为Preferred Size

  • 滚动容器

    创建一个Scroll View

    在Scroll Rect中可以选择Horizontal和Vertical来选择是否水平或者垂直滚动

    取消Horizontal选项

    在Scroll View ->Viewport下创建一个子空对象Content

    添加组件

    Grid Layout Group :调整Padding

    Content Size Fitter:将Verrical Fit 设置为Preferred Size

  • 小结

    寥寥几行,花了一天时间,在这个背包下对于UI的整理以及UGUI的配置是很大耐心挑战

存储数据

对于unity的存储有PlayerPrefs,ScriptableObject,json,xml,csv(excle)等方式

静态数据与动态数据

对于静态数据和动态数据来说

类别 描述 存储时长 示例
静态数据 一些永久性的数据,一般存储在硬盘中。硬盘的存储空间一般都比较大,现在普通计算机的硬盘都有500G左右,因此硬盘中可以存放一些比较大的文件。 计算机关闭之后再开启,这些数据依旧还在,只要你不主动删除或者硬盘没坏,这些数据永远都在。 静态数据一般是以文件的形式存储在硬盘上,比如:文档,照片,视频。
动态数据 动态数据指在程序运行过程中,动态产生的临时数据,一般存储在内存中,内存的存储空间一般都比较小,现在普通计算机的内存只有8G左右,因此要谨慎使用内存,不要占用太多的内存空间。 计算机关闭之后,这些临时数据就会被清除。 当运行某个程序(软件)时,整个程序就会被加载到内存中,在程序运行过程中,会产生各种各样的临时数据。当程序停止运行或者计算机被强制关闭时,这个程序产生的多有的临时数据都会被清除。

为什么不把所有的应用程序加载到硬盘中执行?因为内存的访问速度比硬盘快N倍。

静态数据的配置

PackageTable.cs

PackageTableItem为每个物品的属性,PackageTable为背包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "XiaoQi/PackageTable", fileName = "PackageTable")]
public class PackageTable : ScriptableObject
{
public List<PackageTableItem> DataList = new List<PackageTableItem>();
}

[System.Serializable]
public class PackageTableItem
{
public int id; //物品id
public int type; //物品类型(食物?武器?)
public int star; // 物品星级(稀有度)
public string name; //物品名字
public string description; //物品简单描述
public string skillDescription; //物品详细描述
public string imagePath; //物品图片路径
}

调试

在Editor文件夹下

GMCmd.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using Unity.VisualScripting;

public class GMCmd : EditorWindow
{
[MenuItem("GMCmd/ReadItem")]
public static void ReadItem()
{
PackageTable packageTable = Resources.Load<PackageTable>("Table/Inventory");

foreach (PackageTableItem packageTableItem in packageTable.DataList)
{
Debug.Log(string.Format("[id]:{0},[name]:{1}", packageTableItem.id, packageTableItem.name));
}
}
//打开背包界面,这是在后面测试用的
[MenuItem("GMCmd/OpenBag")]
public static void OpenBag()
{
UIManager.Instance.OpenPanel(UIConst.PackagePanel);
}

}

存储本地数据

PackageLocalData.cs

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
using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// 这个类使用了对象转为json后利用PlayerPrefs存储
/// </summary>
public class PackageLocalData
{
private static PackageLocalData _instance;

// 单例模式
public static PackageLocalData Instance
{
get
{
if (_instance == null)
{
_instance = new PackageLocalData();
}
return _instance;
}
}


public List<PackageLocalItem> items;

/// <summary>
/// 保存信息
/// </summary>
public void SavePackage()
{
// 将对象序列化为json文件
string inventoryJson = JsonUtility.ToJson(this);
// PlayerPrefs本质是一个哈希表
PlayerPrefs.SetString("PackageLocalData", inventoryJson);
PlayerPrefs.Save();
}

/// <summary>
/// 读取信息
/// </summary>
public List<PackageLocalItem> LoadPackage()
{
if (items != null)
{
return items;
}

// PlayerPrefs哈希表有PackageLocalData的key的话
if (PlayerPrefs.HasKey("PackageLocalData"))
{
string inventoryJson = PlayerPrefs.GetString("PackageLocalData");
// 将JSON字符串反序列化为PackageLocalData对象
PackageLocalData packageLocalData = JsonUtility.FromJson<PackageLocalData>(inventoryJson);
items = packageLocalData.items;
return items;
}
else
{
items = new List<PackageLocalItem>();
return items;
}
}
}


[System.Serializable]
public class PackageLocalItem
{
public string uid;
public int id;
public int num;
public int level;
public bool isNew;
/// <summary>
/// 这边重载了Tostring方法来方便debug
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("[id]:{0} [num]:{1}", id, num);
}
}

这一部分是MVC框架中Model模型部分。

通过数据处理和数据存储来进行逻辑书写

界面逻辑

对于UI开关的框架

BasePanel.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasePanel : MonoBehaviour
{
protected bool isRemove = false;
protected new string name;

protected virtual void Awake()
{
}

/// <summary>
/// 关闭视图函数
/// </summary>
public virtual void SetActive(bool active)
{
gameObject.SetActive(active);
}

public virtual void OpenPanel(string name)
{
this.name = name;
SetActive(true);
}

public virtual void ClosePanel()
{
isRemove = true;
SetActive(false);
Destroy(gameObject);
// 从字典中寻找name 如果有则删除改键值对
if (UIManager.Instance.panelDict.ContainsKey(name))
{
UIManager.Instance.panelDict.Remove(name);
}
}
}

这边使用了this函数是个this函数的经典应用

[C++中this指针的理解与作用详解 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/95735331#:~:text=其作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用,this 来代表指向该函数作用的对象的指针。)

UIManager.cs

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
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
public class UIManager
{
private static UIManager _instance;
private Transform _uiRoot;
// 路径配置字典
private Dictionary<string, string> pathDict;

//这个字典使用了观察者模式
// 预制件缓存字典
private Dictionary<string, GameObject> prefabDict;
// 已打开界面的缓存字典
public Dictionary<string, BasePanel> panelDict;

/// <summary>
/// 单例模式
/// </summary>
/// <value></value>
public static UIManager Instance
{
get
{
if (_instance == null)
{
_instance = new UIManager();
}
return _instance;
}
}

public Transform UIRoot
{
get
{
if (_uiRoot == null)
{
if (GameObject.Find(FindCanvasPath()))
{
_uiRoot = GameObject.Find(FindCanvasPath()).transform;
}
else
{
_uiRoot = new GameObject(FindCanvasPath()).transform;
}
};
return _uiRoot;
}
}

private UIManager()
{
InitDicts();
}
/// <summary>
/// 配置字典
/// </summary>
private void InitDicts()
{
prefabDict = new Dictionary<string, GameObject>();
panelDict = new Dictionary<string, BasePanel>();

// 配置
pathDict = new Dictionary<string, string>()
{
{UIConst.PackagePanel, "Package/PackagePanel"},
};
}

public BasePanel GetPanel(string name)
{
BasePanel panel = null;
// 检查是否已打开
if (panelDict.TryGetValue(name, out panel))
{
return panel;
}
return null;
}

public BasePanel OpenPanel(string name)
{
BasePanel panel = null;
// 检查是否已打开
if (panelDict.TryGetValue(name, out panel))
{
Debug.Log("界面已打开: " + name);
return null;
}

// 检查路径是否配置
string path = "";
if (!pathDict.TryGetValue(name, out path))
{
Debug.Log("界面名称错误,或未配置路径: " + name);
return null;
}

// 使用缓存预制件
GameObject panelPrefab = null;
if (!prefabDict.TryGetValue(name, out panelPrefab))
{
string realPath = FindPrefabsPanelPath(path);

panelPrefab = Resources.Load<GameObject>(realPath) as GameObject;
prefabDict.Add(name, panelPrefab);

}

// 打开界面
// 创建UI实例
if (panelPrefab == null)
{
Debug.Log("未找到相应路径的prefabs" + FindPrefabsPanelPath(path));
return panel;
}
GameObject panelObject = GameObject.Instantiate(panelPrefab, UIRoot, false);

// 并记录到字典中
panel = panelObject.GetComponent<BasePanel>();
panelDict.Add(name, panel);

panel.OpenPanel(name);

return panel;
}

public bool ClosePanel(string name)
{
BasePanel panel = null;
if (!panelDict.TryGetValue(name, out panel))
{
Debug.Log("界面未打开: " + name);
return false;
}

panel.ClosePanel();
// panelDict.Remove(name);
return true;
}

string FindCanvasPath()
{
return "Canvas";
}
/// <summary>
/// 获取P预制件的路径
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
string FindPrefabsPanelPath(string path)
{
return "Prefabs/Panel/" + path;
}
}



public class UIConst
{
// menu panels
//配置
public const string PackagePanel = "PackagePanel";
}

UIManager.cs 才用了单例模式和观察者模式(字典)

对于Open和Close操作进行了封装,

在封装类中加了多重判断来保证对于页面的单一开启

对于页面的实例

PackagePanel.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PackagePanel : BasePanel
{
private Transform UIMenu;
private Transform UIMenuWeapon;
private Transform UIMenuFood;
private Transform UITabName;
private Transform UICloseBtn;
private Transform UICenter;
private Transform UIScrollView;
private Transform UIDetailPanel;
private Transform UILeftBtn;
private Transform UIRightBtn;
private Transform UIDeletePanel;
private Transform UIDeleteBackBtn;
private Transform UIDeleteInfoText;
private Transform UIDeleteConfirmBtn;
private Transform UIBottomMenus;
private Transform UIDeleteBtn;
private Transform UIDetailBtn;

public GameObject PackageUIItemPrefab;

override protected void Awake()
{
base.Awake();
InitUI();
}

private void Start()
{
RefreshUI();
}

private void InitUI()
{
InitUIName();
InitClick();
}

private void RefreshUI()
{
RefreshScroll();
}


private void RefreshScroll()
{
// 清理滚动容器中原本的物品
//NGUI
RectTransform scrollContent = UIScrollView.GetComponent<ScrollRect>().content;
for (int i = 0; i < scrollContent.childCount; i++)
{
Destroy(scrollContent.GetChild(i).gameObject);
}
foreach (PackageLocalItem localData in GameManager.Instance.GetSortPackageLocalData())
{
Transform PackageUIItem = Instantiate(PackageUIItemPrefab.transform, scrollContent) as Transform;
PackageCell packageCell = PackageUIItem.GetComponent<PackageCell>();
packageCell.Refresh(localData, this);
}

}

private void InitUIName()
{
UIMenu = transform.Find("TopCenter/Menu");
UIMenuWeapon = transform.Find("TopCenter/Menus/Weapon");
UIMenuFood = transform.Find("TopCenter/Menus/Food");
UITabName = transform.Find("LeftTop/TabName");
UICloseBtn = transform.Find("RightTop/Close");
UICenter = transform.Find("Center");
UIScrollView = transform.Find("Center/Scroll View");
UIDetailPanel = transform.Find("Center/DetailPanel");
UILeftBtn = transform.Find("Left/Button");
UIRightBtn = transform.Find("Right/Button");

UIDeletePanel = transform.Find("Bottom/DeletePanel");
UIDeleteBackBtn = transform.Find("Bottom/DeletePanel/Back");
UIDeleteInfoText = transform.Find("Bottom/DeletePanel/InfoText");
UIDeleteConfirmBtn = transform.Find("Bottom/DeletePanel/ConfirmBtn");
UIBottomMenus = transform.Find("Bottom/BottomMenus");
UIDeleteBtn = transform.Find("Bottom/BottomMenus/DeleteBtn");
UIDetailBtn = transform.Find("Bottom/BottomMenus/DetailBtn");

UIDeletePanel.gameObject.SetActive(false);
UIBottomMenus.gameObject.SetActive(true);
}
/// <summary>
/// 注册点击事件
/// </summary>
private void InitClick()
{
UIMenuWeapon.GetComponent<Button>().onClick.AddListener(OnClickWeapon);
UIMenuFood.GetComponent<Button>().onClick.AddListener(OnClickFood);
UICloseBtn.GetComponent<Button>().onClick.AddListener(OnClickClose);
UILeftBtn.GetComponent<Button>().onClick.AddListener(OnClickLeft);
UIRightBtn.GetComponent<Button>().onClick.AddListener(OnClickRight);

UIDeleteBackBtn.GetComponent<Button>().onClick.AddListener(OnDeleteBack);
UIDeleteConfirmBtn.GetComponent<Button>().onClick.AddListener(OnDeleteConfirm);
UIDeleteBtn.GetComponent<Button>().onClick.AddListener(OnDelete);
UIDetailBtn.GetComponent<Button>().onClick.AddListener(OnDetail);

}
private void OnClickWeapon()
{
print(">>>>> OnClickWeapon");
}

private void OnClickFood()
{
print(">>>>> OnClickFood");
}

private void OnClickClose()
{
print(">>>>> OnClickClose");
ClosePanel();
// UIManager.Instance.ClosePanel(UIConst.PackagePanel);
}

private void OnClickLeft()
{
print(">>>>> OnClickLeft");
}

private void OnClickRight()
{
print(">>>>> OnClickRight");
}

private void OnDeleteBack()
{
print(">>>>> onDeleteBack");
}

private void OnDeleteConfirm()
{
print(">>>>> OnDeleteConfirm");
}

private void OnDelete()
{
print(">>>>> OnDelete");
}

private void OnDetail()
{
print(">>>>> OnDetail");
}
}

PackagePanel.cs中

继承与BasePanel .cs使用了 原型模式

初始化了滚动容器,在滚动容器中清理并添加新的数据

命名并初始化了UI的Transform

在有Btn的下来注册点击事件(目前是空)

PackageCell.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PackageCell : MonoBehaviour
{
private Transform UIIcon;
private Transform UIHead;
private Transform UINew;
private Transform UISelect;
private Transform UILevel;
private Transform UIStars;
private Transform UIDeleteSelect;

private PackageLocalItem packageLocalData;
private PackageTableItem packageTableItem;
private PackagePanel uiParent;

private void Awake()
{
InitUIName();
}
private void InitUIName()
{
UIIcon = transform.Find("Top/Icon");
UIHead = transform.Find("Top/Head");
UINew = transform.Find("Top/New");
UILevel = transform.Find("Bottom/LevelText");
UIStars = transform.Find("Bottom/Stars");
UISelect = transform.Find("Select");
UIDeleteSelect = transform.Find("DeleteSelect");

UIDeleteSelect.gameObject.SetActive(false);
}

public void Refresh(PackageLocalItem packageLocalData, PackagePanel uiParent)
{
// 数据初始化
this.packageLocalData = packageLocalData;
this.packageTableItem = GameManager.Instance.GetPackageItemById(packageLocalData.id);
this.uiParent = uiParent;
// 等级信息
UILevel.GetComponent<Text>().text = "Lv." + this.packageLocalData.level.ToString();
// 是否是新获得?
UINew.gameObject.SetActive(this.packageLocalData.isNew);
// 物品的图片
Texture2D t = (Texture2D)Resources.Load(this.packageTableItem.imagePath);
Sprite temp = Sprite.Create(t, new Rect(0, 0, t.width, t.height), new Vector2(0, 0));
UIIcon.GetComponent<Image>().sprite = temp;
// 刷新星级
RefreshStars();
}
public void RefreshStars()
{
for (int i = 0; i < UIStars.childCount; i++)
{
Transform star = UIStars.GetChild(i);
if (this.packageTableItem.star > i)
{
star.gameObject.SetActive(true);
}
else
{
star.gameObject.SetActive(false);
}
}
}

}

PackageCell.cs 中

和PackagePanel.cs相同,命名并初始化了UI的Transform

并且对于特殊的星级判断独立写了一个显示函数

GameManager.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{
private static GameManager _instance;
private PackageTable packageTable;

// 单例模式
private void Awake()
{
_instance = this;
DontDestroyOnLoad(gameObject);
}

public static GameManager Instance
{
get
{
return _instance;
}
}

// Start is called before the first frame update
void Start()
{
UIManager.Instance.OpenPanel(UIConst.PackagePanel);
// print(GetPackageLocalData().Count);
// print(GetPackageTable().DataList.Count);
}

// 逻辑:对于有动态数据的表来说直接引用动态数据--内存处理快
// 对于只有静态数据没有加载的表来说,加载静态数据--硬盘处理慢
public PackageTable GetPackageTable()
{
if (packageTable == null)
{
packageTable = Resources.Load<PackageTable>("TableData/PackageTable");
}
return packageTable;
}

public List<PackageLocalItem> GetPackageLocalData()
{
return PackageLocalData.Instance.LoadPackage();
}

public PackageTableItem GetPackageItemById(int id)
{
List<PackageTableItem> packageDataList = GetPackageTable().DataList;
foreach (PackageTableItem item in packageDataList)
{
if (item.id == id)
{
return item;
}
}
return null;
}

public PackageLocalItem GetPackageLocalItemByUId(string uid)
{
List<PackageLocalItem> packageDataList = GetPackageLocalData();
foreach (PackageLocalItem item in packageDataList)
{
if (item.uid == uid)
{
return item;
}
}
return null;
}


public List<PackageLocalItem> GetSortPackageLocalData()
{
List<PackageLocalItem> localItems = PackageLocalData.Instance.LoadPackage();
localItems.Sort(new PackageItemComparer());
return localItems;
}


}


public class PackageItemComparer : IComparer<PackageLocalItem>
{
public int Compare(PackageLocalItem a, PackageLocalItem b)
{
PackageTableItem x = GameManager.Instance.GetPackageItemById(a.id);
PackageTableItem y = GameManager.Instance.GetPackageItemById(b.id);
// 首先按star从大到小排序
int starComparison = y.star.CompareTo(x.star);

// 如果star相同,则按id从大到小排序
if (starComparison == 0)
{
int idComparison = y.id.CompareTo(x.id);
if (idComparison == 0)
{
return b.level.CompareTo(a.level);
}
return idComparison;
}

return starComparison;
}
}

GameManager.cs 使用了命令模式在Manager下统一对于数据的管理

结尾

本篇作为背包系统的落地实现

有着多个设计模式:观察者模式(Dictionary),命令模式(Manager),单例模式,原型模式(简直就是一个设计模式的大集合)

而且对于UI的综合应用有着很大提升

对于存储方式也有很大理解(具体可能会在写一篇新的文章)

最全面的游戏背包系统讲解 | 技术原理分析 | 框架逻辑设计| 背包+抽卡+整理一网打尽 | 小棋出品,必属精品_哔哩哔哩_bilibili

Unity 简单背包系统(ScriptableObject)_unity官方背包系统代码-CSDN博客

UI框架bug修复版 - 哔哩哔哩 (bilibili.com)

背包系统源码及工程(截止第三节课) - 哔哩哔哩 (bilibili.com)