一、界面简介
本次项目实现的是一个游戏的背包系统界面,用户在打开背包系统之后,就可以进行查看背包物品的操作。
二、实现功能
- 界面可以显示物品基本信息,详细信息
- 界面可以关闭
- 点击删除物品等按钮可以在控制台发出提示
三、项目效果
基于【unity UGUI】的游戏背包系统UI界面
四、项目设计
1、前端设计
正所谓,“一千个人眼中有一千个哈姆雷特”,前端设计往往体现的是人的审美观,每个人都有自己的想法。本文只会简要介绍前端设计,会介绍一些基本的设计步骤和方法。至于如何设计界面,则是需要每个人发挥想象力啦。
1.1 逻辑结构
场景结构如下
PackagePanel的结构如下
可以看到,整个PackagePanel是被分成几个部分的,接下来将具体介绍每个部分是怎样设计的。
1.2 具体设计
首先新建一个空对象,命名为GameManager,用于挂载脚本。然后创建一个Canvas,Canvas用于建立背包界面。
Background
新建UI -> Image,设置参数如下(颜色是475F7B)
需要注意的是,锚点的设置,选择完全伸展(按住Alt,选择右下角)。
锚点的设置虽然不是必须的,却可以简化我们的工作,将锚点设置在合适的位置,以后在该组件下添加子组件,位置就会更加好设置。
方位设置
每个方位都是先添加一个Empty对象,并将锚点设定为合适的位置(TopCenter设置如下,注意这里没有按Alt)
如果添加Empty对象时不是Rect Transform,就需要在Inspector窗口下的Add Component添加Rect Transform。
Button设置
以Left -> Button为例,在Source Image中拖入Icon,Icon是从素材包中引入的Texture 2D文件(也可以自己ps一个,保存为.png),最后一定一定记得,点Set Native Size,就可以自动显示为原来图片的大小。
图片的大小可以根据分辨率确定,本项目是1920*1080,在Game视图中选Full HD
Menus设置
在TopCenter中有一个Menus,是用于存放顶部的图标,Menus是空对象,里面添加了水平排序和自适应宽度的组件,用于图标自动排序。
同样在Center -> Scroll View -> Viewport -> Content中,也会添加这两个组件,用于自动排序物品。
Text设置
本项目采用的都是UI -> Legacy -> Text,因为可以直接使用中文,而UI -> Text - TextMeshPro使用中文的时候会出现乱码。
可以通过Font Style设置文字样式(粗体),Font Size设置文字大小,Horizontal Overflow如果设置成Overflow,突出的文字就会进入下一行。
滚动框设置
在Center中需要使用Scroll View,用于实现物品的展示功能,结构如下:
Viewport中是要显示的内容,新建空对象命名为Content,用于存储要显示的物品。
2、代码实现
2.1 类图
2.2 代码
BasePanel.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// BasePanel 类是所有 UI 面板的基类,提供了基本的面板管理功能
public class BasePanel : MonoBehaviour
{
// 是否已经标记为移除
protected bool isRemove = false;
// 面板的名称
protected new string name;
// 在面板被实例化时调用的虚方法
protected virtual void Awake()
{
// 这里可以添加初始化逻辑
}
// 设置面板的激活状态
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);
// 销毁面板的 GameObject
Destroy(gameObject);
// 从 UIManager 的面板字典中移除对应的记录
if (UIManager.Instance.panelDict.ContainsKey(name))
{
UIManager.Instance.panelDict.Remove(name);
}
}
}
PackagePanel.cs
该脚本需要挂载在PackagePanel对象上
其中RefreshScroll是最重要的,用于刷新滚动区域的数据,从而能每次重新打开游戏都能显示玩家现有物品数据。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// PackagePanel 类继承自 BasePanel,负责管理背包界面的显示和交互逻辑
public class PackagePanel : BasePanel
{
// UI元素的引用
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;
// PackageUIItemPrefab 是用于实例化背包物品项的预制体
public GameObject PackageUIItemPrefab;
// 在 Awake 方法中初始化 UI 元素
override protected void Awake()
{
base.Awake();
InitUI();
}
// 在 Start 方法中刷新 UI
private void Start()
{
RefreshUI();
}
// 初始化 UI 元素
private void InitUI()
{
InitUIName();
InitClick();
}
// 刷新整个 UI
private void RefreshUI()
{
RefreshScroll();
}
// 刷新滚动区域
private void RefreshScroll()
{
// 清理滚动容器中原本的物品
RectTransform scrollContent = UIScrollView.GetComponent<ScrollRect>().content;
for (int i = 0; i < scrollContent.childCount; i++)
{
Destroy(scrollContent.GetChild(i).gameObject);
}
// 遍历排序后的背包数据,实例化并刷新每个 PackageUIItem
foreach (PackageLocalItem localData in GameManager.Instance.GetSortPackageLocalData())
{
Transform PackageUIItem = Instantiate(PackageUIItemPrefab.transform, scrollContent) as Transform;
PackageCell packageCell = PackageUIItem.GetComponent<PackageCell>();
Debug.Log(packageCell);
packageCell.Refresh(localData, this);
}
}
// 初始化 UI 元素的名称
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);
}
// 初始化按钮的点击事件
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();
}
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");
}
}
GameManager.cs
GameManager需要挂载在GameManager上
这里定义了PackageLocalItem对象的排序规则,按星级、ID、等级排序。
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using UnityEngine;
// GameManager 类负责管理游戏中的全局数据和逻辑
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
private PackageTable packageTable;
// 在 Awake 方法中进行单例的初始化和对象的不销毁设置
private void Awake()
{
_instance = this;
DontDestroyOnLoad(gameObject);
}
// 获取 GameManager 的单例实例
public static GameManager Instance
{
get
{
return _instance;
}
}
// 游戏开始时调用,打开初始界面并输出背包本地数据和包裹表数据的数量
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();
}
// 根据物品ID获取包裹表中的物品数据
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;
}
}
// PackageItemComparer 类实现了 IComparer 接口,用于比较 PackageLocalItem 对象的排序规则
public class PackageItemComparer : IComparer<PackageLocalItem>
{
// 比较方法,首先按照包裹表中的星级、ID和等级进行排序
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)
{
// 如果 id 也相同,则按等级从大到小排序
return b.level.CompareTo(a.level);
}
return idComparison;
}
return starComparison;
}
}
UIManager.cs
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
// UIManager 类负责管理游戏中的 UI 界面
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;
// 获取 UIManager 的单例实例
public static UIManager Instance
{
get
{
if (_instance == null)
{
_instance = new UIManager();
}
return _instance;
}
}
// 获取或创建 UI 根节点
public Transform UIRoot
{
get
{
if (_uiRoot == null)
{
if (GameObject.Find("Canvas"))
{
_uiRoot = GameObject.Find("Canvas").transform;
}
else
{
_uiRoot = new GameObject("Canvas").transform;
}
};
return _uiRoot;
}
}
// 私有构造函数,初始化路径配置字典和预制件缓存字典
private UIManager()
{
InitDicts();
}
// 初始化路径配置字典和预制件缓存字典
private void InitDicts()
{
prefabDict = new Dictionary<string, GameObject>();
panelDict = new Dictionary<string, BasePanel>();
pathDict = new Dictionary<string, string>()
{
{UIConst.PackagePanel, "Package/PackagePanel Variant"},
};
}
// 根据界面名称获取已打开的界面实例,如果未打开返回 null
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 = "Prefab/Panel/" + path;
panelPrefab = Resources.Load<GameObject>(realPath) as GameObject;
prefabDict.Add(name, panelPrefab);
}
// 打开界面
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();
return true;
}
}
// UIConst 类用于存储界面名称的常量
public class UIConst
{
// PackagePanel 界面的名称常量
public const string PackagePanel = "PackagePanel Variant";
}
PackageCell.cs
PackageCell需要挂载在Package UI Item上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
// PackageCell 类用于管理背包界面中的每个物品项
public class PackageCell : MonoBehaviour
{
// UI元素的引用
private Transform UIIcon;
private Transform UIHead;
private Transform UINew;
private Transform UISelect;
private Transform UILevel;
private Transform UIStars;
private Transform UIDeleteSelect;
// 包裹本地数据、包裹表数据和 UI 父类的引用
private PackageLocalItem packageLocalData;
private PackageTableItem packageTableItem;
private PackagePanel uiParent;
// 在 Awake 方法中初始化 UI 元素
private void Awake()
{
InitUIName();
}
// 初始化 UI 元素的名称
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);
}
// 刷新 PackageCell 的显示内容
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);
}
}
}
}
PackageLocalData.cs和PackageTable.cs
这两个脚本是用于数据存储的,为了方便项目编写,本项目的数据都存放在本地中,PackageLocalData用于用户的本地数据(即一个用户有什么物品),用户的本地数据会存放到PlayerPref,PackageTable用于游戏的数据(即游戏有什么物品),会挂载在Resources -> TableData -> PackageTable.asset上(TableData需要新建,PackageTable.asset在脚本PackageTable.cs写好保存后,就可以在Create -> My -> PackageTable创建)
using UnityEngine;
using System.Collections.Generic;
// PackageLocalData 类用于管理本地背包数据的保存和加载
public class PackageLocalData
{
private static PackageLocalData _instance;
// 获取 PackageLocalData 的单例实例
public static PackageLocalData Instance
{
get
{
if (_instance == null)
{
_instance = new PackageLocalData();
}
return _instance;
}
}
// 背包中的物品列表
public List<PackageLocalItem> items;
// 将背包数据保存到 PlayerPrefs
public void SavePackage()
{
// 将 PackageLocalData 对象转换为 JSON 字符串
string inventoryJson = JsonUtility.ToJson(this);
// 保存 JSON 字符串到 PlayerPrefs
PlayerPrefs.SetString("PackageLocalData", inventoryJson);
PlayerPrefs.Save();
}
// 从 PlayerPrefs 加载背包数据
public List<PackageLocalItem> LoadPackage()
{
// 如果已经加载过数据,直接返回缓存的 items 列表
if (items != null)
{
return items;
}
// 如果 PlayerPrefs 中存在背包数据,解析 JSON 字符串并返回物品列表
if (PlayerPrefs.HasKey("PackageLocalData"))
{
string inventoryJson = PlayerPrefs.GetString("PackageLocalData");
Debug.Log(inventoryJson);
PackageLocalData packageLocalData = JsonUtility.FromJson<PackageLocalData>(inventoryJson);
items = packageLocalData.items;
return items;
}
else
{
// 如果 PlayerPrefs 中不存在背包数据,创建一个新的空列表并返回
items = new List<PackageLocalItem>();
return items;
}
}
}
// PackageLocalItem 类表示背包中的单个物品的数据结构
[System.Serializable]
public class PackageLocalItem
{
// 物品的唯一标识符
public string uid;
// 物品的ID
public int id;
// 物品的数量
public int num;
// 物品的等级
public int level;
// 物品是否为新物品
public bool isNew;
// 重写 ToString 方法,用于输出物品信息的字符串表示
public override string ToString()
{
return string.Format("[id]:{0} [num]:{1}", uid, num);
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// PackageTable 类是一个 ScriptableObject,用于存储游戏中的包裹信息
[CreateAssetMenu(menuName = "My/PackageTable", fileName = "PackageTable")]
public class PackageTable : ScriptableObject
{
// DataList 用于存储 PackageTableItem 对象的列表
public List<PackageTableItem> DataList = new List<PackageTableItem>();
}
// PackageTableItem 类是 PackageTable 中存储的每个包裹项的数据结构
[System.Serializable]
public class PackageTableItem
{
// 物品的唯一标识符
public int id;
// 物品的类型
public int type;
// 物品的等级
public int level;
// 物品的星级
public int star;
// 物品的名称
public string name;
// 物品的描述
public string description;
// 物品技能的描述
public string skillDescription;
// 物品的图片路径
public string imagePath;
}
3、存储数据
为了模拟游戏玩家拥有一定的游戏物品,本项目使用command创建新数据,代码如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
// GMCmd 类包含一些用于编辑器中的快捷命令的静态方法
public class GMCmd
{
// 用于在编辑器中创建一个 "读取表格" 的菜单项
[MenuItem("CMCmd/读取表格")]
public static void ReadTable()
{
// 从Resources加载PackageTable
PackageTable packageTable = Resources.Load<PackageTable>("TableData/PackageTable");
// 遍历PackageTable中的每个PackageTableItem并输出信息
foreach (PackageTableItem packageItem in packageTable.DataList)
{
Debug.Log(string.Format("【id】:{0}, 【name】:{1}", packageItem.id, packageItem.name));
}
}
// 用于在编辑器中创建一个 "创建背包测试数据" 的菜单项
[MenuItem("CMCmd/创建背包测试数据")]
public static void CreateLocalPackageData()
{
// 初始化 PackageLocalData 的实例并设置测试数据
PackageLocalData.Instance.items = new List<PackageLocalItem>();
for (int i = 1; i < 9; i++)
{
PackageLocalItem packageLocalItem = new PackageLocalItem()
{
uid = Guid.NewGuid().ToString(),
id = UnityEngine.Random.Range(1, 9),
num = UnityEngine.Random.Range(1, 11),
level = UnityEngine.Random.Range(1, 30),
isNew = i % 2 == 1
};
PackageLocalData.Instance.items.Add(packageLocalItem);
}
// 保存测试数据
PackageLocalData.Instance.SavePackage();
}
// 用于在编辑器中创建一个 "读取背包测试数据" 的菜单项
[MenuItem("CMCmd/读取背包测试数据")]
public static void ReadLocalPackageData()
{
// 读取 PackageLocalData 中的背包测试数据
List<PackageLocalItem> readItems = PackageLocalData.Instance.LoadPackage();
// 遍历读取到的数据并输出信息
foreach (PackageLocalItem item in readItems)
{
Debug.Log(item);
}
}
// 用于在编辑器中创建一个 "打开背包主界面" 的菜单项
[MenuItem("CMCmd/打开背包主界面")]
public static void OpenPackagePanel()
{
// 使用 UIManager 打开背包主界面
UIManager.Instance.OpenPanel(UIConst.PackagePanel);
}
}
添加并保存该脚本后,就可以看到如下图的指令操作。可以通过创建背包测试数据模拟玩家获得物品。
对于PackageTable,需要在Inspector窗口中添加游戏物品信息。在PackageTable.cs中有对子项目的描述,物品的图片路径要注意,本项目的图片是在Assets/Resources/Image/UI/Weapon/中的,但是图片路径只需要写(Image/UI/Weapon/图片名,不用加文件后缀!!!)。
五、项目地址
Ivan53471/Packcage-System: nothing
(github.com)
六、项目亮点
- 前端背包界面设计精美,实现了基本功能
- 每次重新启动游戏,玩家拥有的物品不会变化
- 文件结构组织清晰明确
- 后端代码实现简单易懂
参考资料