基于框架制作的Unity背包系统
你需要大概了解的东西
框架:peterwu3156/Star-FrameWork (github.com)
UGUI的一些应用
数据序列化 持久化 反序列化
本文将基于json和UGUI来做逻辑处理
包含背包的加载/拖拽/切换
对于数据的处理
我们知道 Unity有很多种数据存储的方式
有储存在存储表里的PlayerPrefs
有直接内置成游戏对象的ScriptableObject
也可以通过工具库来存储为json或者xml
那么如何选择数据存储的方式呢
首先我们要对数据进行分类
把数据分为:
- 固定之后不会再修改
- 需要频繁修改的部分
对于背包来说 什么是固定之后不会再修改呢
是道具本身的属性 例如名字 描述 图标的id 等等自身的东西
我们是不会在游戏中进行修改的
那么什么是会频繁修改的呢? 就是玩家背包里存储的道具
我们只会用id和数量去形容背包里的道具 需要的信息再从固定的部分读取
所以到这里我们就明了了我们需要对数据进行怎么样的处理
注意:对于固定之后不再修改的部分 你当然可以用ScriptableObject
但是对于需要在游戏中修改的部分 就不能再使用它了
因为它的修改只能在runtime模式下进行
我们这里统一使用的是JsonUtility对json进行操作
配表转json
如果你手头上有可用的工具自然很好
当然没有的话就老老实实打开bejson吧
现在我们要配两张表
一张是道具信息表 这张表我们将通过这个方式来制作
在exccel里面写好你的表
然后复制到bejson里面进行转换
转换好了记得要在前面加上这个实体类的名称
大概就像这样 然后序列化检验一下就好了
读取道具信息
思路:声明这个实体类
然后把它序列化 通过jsonutility读取对应的信息
存储到这个实体类里进行使用
这里要注意文件的编码格式utf-8
其他的地方基本都是api 就不打算详细讲了
using System.Collections; using System.Collections.Generic; using UnityEngine; /// <summary> /// 背包管理器 是玩家背包 商人背包 装备系统公用的基类 /// </summary> public class InventoryManager : BaseManager<InventoryManager> { private Dictionary<int, Item> inventory = new Dictionary<int, Item>(); /// <summary> /// 读取json数据 初始化背包 /// </summary> public void InitInventory() { if(inventory.Count > 0) { return; } string itemInfo = ResourceManager.GetInstance().Load<TextAsset>("Json/itemInfo").text; ItemData itemData = JsonUtility.FromJson<ItemData>(itemInfo); for(int i = 0; i < itemData.info.Count; i++) { inventory.Add(itemData.info[i].id, itemData.info[i]); } } /// <summary> /// 根据ID获取道具 /// </summary> /// <param name="id"></param> /// <returns></returns> public Item GetItemInfo(int id) { if(inventory.ContainsKey(id)) { return inventory[id]; } return null; } } /// <summary> /// 中转的List容器 暂时存放配表信息 /// </summary> public class ItemData { public List<Item> info; } /// <summary> /// 道具实例化信息 /// </summary> [System.Serializable] public class Item { public int id; public string name; public string icon; public int type; public int price; public string tips; }
搭UI
这一步是比较自由的 完全取决于你想要你的背包看起来是什么样子
所以我打算在这里介绍一下我搭的UI的思路
装备 素材 重要 这三个其实都是toggle
它们用一个toggle group包起来 这样就能有标签切换的效果了
底下则是一个scorll view加上grid layout
我取消掉了横向的滑动条 让它只能跟随grid layout在竖直方向上延伸
然后就是关闭按钮和金钱数量的显示了
不算太复杂
格子的话就是你所看到的 图标 背景 数目
同时还有一个我截图中没显示出来的提示窗口 相比格子多了描述的部分
你可以自己拼一个符合你要求的UI
玩家背包的信息读取和写入
玩家的实体类有很多不相关的功能
所以这里我只截取一些代码作为说明
可以看到 这里声明了三个List
分别代表刚刚UI里提到的三个部分 装备 素材 重要
他们存储的都是id+数目这样的信息
/// <summary> /// 配置文件实例化玩家类信息 /// </summary> [System.Serializable] public class Player { public string name; public float hp; public float mp; public float ep; public int hpMax; public int mpMax; public int epMax; public int money; public List<ItemInfo> equip; public List<ItemInfo> prop; public List<ItemInfo> mission; public Player() { name = "海星"; this.hp = 100; this.mp = 100; this.ep = 100; this.hpMax = 100; this.mpMax = 100; this.epMax = 100; this.money = 100; this.equip = new List<ItemInfo> { new ItemInfo { id = 0, number = 1 }, new ItemInfo { id = 1, number = 1 }, new ItemInfo { id = 2, number = 1 }, new ItemInfo { id = 3, number = 20 }, new ItemInfo { id = 6, number = 20 }, new ItemInfo { id = 10, number = 1 }}; this.prop = new List<ItemInfo> { new ItemInfo { id = 11, number = 10 } }; this.mission = new List<ItemInfo> {}; } } /// <summary> /// 单个物品的信息 /// </summary> [System.Serializable] public class ItemInfo { public int id; public int number; }
保存数据 就是将数据重新赋值一遍
然后再写回文件里
/// <summary> /// 将玩家数据写入配置文件 /// </summary> public void SavePlayerData() { if (hp <= 0) { UIManager.GetInstance().ShowPanel<ResurrectionPanel>("Resurrection", E_UI_Layer.System); Time.timeScale = 0; AudioManager.GetInstance().BGMPause(); } playerInfo.name = name; playerInfo.hp = hp; playerInfo.mp = mp; playerInfo.ep = ep; playerInfo.hpMax = hpMax; playerInfo.mpMax = mpMax; playerInfo.epMax = epMax; playerInfo.money = money; playerInfo.equip = equip; playerInfo.prop = prop; playerInfo.mission = mission; WriteDataToFile(); } /// <summary> /// 写入文件 /// </summary> public void WriteDataToFile() { string json = JsonUtility.ToJson(playerInfo); File.WriteAllBytes(playerInfo_Url, Encoding.UTF8.GetBytes(json)); }
然后我们要进行读取的操作
读取的时候要先判断这个文件是否存在
如果不存在 就按照构造函数里的初始化部分来赋值
然后在指定路径下新建这个文件再读取
用字节数组把信息读取进来 一样用json序列化后赋值
/// <summary> /// 构造函数 读取数据 /// </summary> public PlayerData() { InitPlayerData(); MonoManager.GetInstance().StartCoroutine(RecoverEp()); } /// <summary> /// 初始化玩家数据 /// </summary> public void InitPlayerData() { if (!File.Exists(playerInfo_Url)) { playerInfo = new Player(); Debug.Log(playerInfo_Url); WriteDataToFile(); LoadPlayerData(); } else { LoadPlayerData(); } } /// <summary> /// 从配置文件读取玩家数据 /// </summary> public void LoadPlayerData() { byte[] bytes = File.ReadAllBytes(playerInfo_Url); string json = Encoding.UTF8.GetString(bytes); playerInfo = JsonUtility.FromJson<Player>(json); name = playerInfo.name; hp = playerInfo.hp; mp = playerInfo.mp; ep = playerInfo.ep; hpMax = playerInfo.hpMax; mpMax = playerInfo.mpMax; epMax = playerInfo.epMax; money = playerInfo.money; equip = playerInfo.equip; prop = playerInfo.prop; mission = playerInfo.mission; }
这样就算玩家的背包信息处理好了
UI上的生成
用一个枚举来代表三种类型
然后找到对应的组件后进行更新
哪一个激活就把List替换成哪一个
然后删除之前的格子生成新的格子
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public enum E_InventoryType { Equip, Prop, Mission } public class InventoryUI : BasePanel { public Transform Content; private List<InventoryGrid> grids = new List<InventoryGrid>(); public override void UIComponentOn() { GetUIComponent<Button>("Close").onClick.AddListener(() => { UIManager.GetInstance().HidePanel("InventoryUI"); UIManager.GetInstance().ShowPanel<PlayerDataShow>("GUI", E_UI_Layer.Bottom); }); UIManager.GetInstance().HidePanel("GUI"); GetUIComponent<Text>("MoneyNumber").text = PlayerData.GetInstance().money.ToString(); GetUIComponent<Toggle>("Equip").onValueChanged.AddListener(ToggleValueChange); GetUIComponent<Toggle>("Prop").onValueChanged.AddListener(ToggleValueChange); GetUIComponent<Toggle>("Mission").onValueChanged.AddListener(ToggleValueChange); ChangeType(E_InventoryType.Equip); Time.timeScale = 0; } public override void UIComponentOff() { Time.timeScale = 1; } private void ToggleValueChange(bool value) { if(GetUIComponent<Toggle>("Equip").isOn) { ChangeType(E_InventoryType.Equip); } else if(GetUIComponent<Toggle>("Prop").isOn) { ChangeType(E_InventoryType.Prop); } else if(GetUIComponent<Toggle>("Mission").isOn) { ChangeType(E_InventoryType.Mission); } } private void ChangeType(E_InventoryType type) { List<ItemInfo> items = PlayerData.GetInstance().equip; switch(type) { case E_InventoryType.Equip: break; case E_InventoryType.Prop: items = PlayerData.GetInstance().prop; break; case E_InventoryType.Mission: items = PlayerData.GetInstance().mission; break; } UpdateInventoryUI(items); } /// <summary> /// 背包数据更新 /// </summary> public void DragUpdate(E_InventoryType type) { ChangeType(type); } private void UpdateInventoryUI(List<ItemInfo> items) { //清空格子 for (int i = 0; i < grids.Count; i++) { Destroy(grids[i].gameObject); } grids.Clear(); //更新数据 for (int i = 0; i < items.Count; i++) { InventoryGrid grid = ResourceManager.GetInstance().Load<GameObject>("UI/Grid").GetComponent<InventoryGrid>(); grid.transform.SetParent(Content, false); grid.InitGrid(items[i]); grids.Add(grid); } } }
格子的初始化与拖拽
格子自身的初始化与UI上生成格子的部分是分开的
这里要对格子的各个信息进行赋值
同时这里也可以做拖拽事件的触发
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// <summary> /// 格子 /// </summary> public class InventoryGrid : BasePanel { public ItemInfo itemInfo; public Image imageIcon; void Start() { UIManager.AddCustomEventListener(GetUIComponent<Image>("GridIcon"), EventTriggerType.PointerEnter, (data) => { EventCenter.GetInstance().EventTrigger<InventoryGrid>("GridIconIn", this); }); UIManager.AddCustomEventListener(GetUIComponent<Image>("GridIcon"), EventTriggerType.PointerExit, (data) => { EventCenter.GetInstance().EventTrigger<InventoryGrid>("GridIconOut", this); }); } /// <summary> /// 拖拽事件 /// </summary> public void DragEvent() { UIManager.AddCustomEventListener(GetUIComponent<Image>("GridIcon"), EventTriggerType.BeginDrag, (data) => { EventCenter.GetInstance().EventTrigger<InventoryGrid>("GridBeginDrag", this); }); UIManager.AddCustomEventListener(GetUIComponent<Image>("GridIcon"), EventTriggerType.Drag, (data) => { EventCenter.GetInstance().EventTrigger<BaseEventData>("GridDrag", data); }); UIManager.AddCustomEventListener(GetUIComponent<Image>("GridIcon"), EventTriggerType.EndDrag, (data) => { EventCenter.GetInstance().EventTrigger<InventoryGrid>("GridEndDrag", this); }); } /// <summary> /// 根据道具信息初始化格子信息 /// </summary> public void InitGrid(ItemInfo itemInfo) { this.itemInfo = itemInfo; Item itemData = InventoryManager.GetInstance().GetItemInfo(itemInfo.id); GetUIComponent<Image>("GridIcon").sprite = ResourceManager.GetInstance().Load<Sprite>("Icon/" + itemData.icon); imageIcon = GetUIComponent<Image>("GridIcon"); GetUIComponent<Text>("ItemNumber").text = itemInfo.number.ToString(); //只有装备类型才会启用拖拽 if(itemData.type == (int)E_InventoryType.Equip) { DragEvent(); } } }
拖拽的处理
以前我们肯定会一下子想到三个接口
IBeginDragHandle
IDragHandle
IEndDragHandle
这里实际上是差不多的
只不过是自己写了事件
我们都会把拖拽的处理分为三个部分
开始拖拽 拖拽中 拖拽后
这里的思路也不例外 我们在开始拖拽的时候
动态加载一张图片 让这张图片跟着我们的鼠标光标走
然后检测到进入的格子的位置后 更新列表 再更新UI即可
要注意的就是异步加载并不是立刻加载完成的
要注意判断是否为空 否则就会空引用异常
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; /// <summary> /// 背包拖动事件处理模块 /// </summary> public class InventoryDrag : BaseManager<InventoryDrag> { private InventoryGrid nowDragGrid; private InventoryGrid nowInGrid; private Image nowDragIcon; private bool isDraging = false; public void InitDrag() { EventCenter.GetInstance().AddEventListener<InventoryGrid>("GridBeginDrag", BeginDrag); EventCenter.GetInstance().AddEventListener<BaseEventData>("GridDrag", OnDrag); EventCenter.GetInstance().AddEventListener<InventoryGrid>("GridEndDrag", EndDrag); EventCenter.GetInstance().AddEventListener<InventoryGrid>("GridIconIn", GridIconIn); EventCenter.GetInstance().AddEventListener<InventoryGrid>("GridIconOut", GridIconOut); } private void BeginDrag(InventoryGrid item) { UIManager.GetInstance().HidePanel("GridTips"); isDraging = true; nowDragGrid = item; PoolManager.GetInstance().GetObj("UI/GridIcon", (obj) => { nowDragIcon = obj.GetComponent<Image>(); nowDragIcon.sprite = item.imageIcon.sprite; nowDragIcon.transform.SetParent(UIManager.GetInstance().canvas.transform); nowDragIcon.transform.localScale = Vector3.one; //如果加载完拖动太快没反应过来 就不显示了 if(!isDraging) { PoolManager.GetInstance().PushObj(nowDragIcon.name, nowDragIcon.gameObject); } }); } private void OnDrag(BaseEventData data) { if (nowDragIcon != null) { //坐标转换 Vector2 localPos; RectTransformUtility.ScreenPointToLocalPointInRectangle (UIManager.GetInstance().canvas, (data as PointerEventData).position, (data as PointerEventData).pressEventCamera, out localPos); nowDragIcon.transform.localPosition = localPos; } } private void EndDrag(InventoryGrid item) { ChangeGrid(); isDraging = false; nowDragGrid = null; if (nowDragIcon != null) { PoolManager.GetInstance().PushObj(nowDragIcon.name, nowDragIcon.gameObject); } nowDragIcon = null; } private void GridIconIn(InventoryGrid item) { if(isDraging) { nowInGrid = item; return; } if(item.itemInfo == null) { return; } UIManager.GetInstance().ShowPanel<TipsPanel>("GridTips", E_UI_Layer.Top, (panel) => { panel.InitTips(item.itemInfo); panel.transform.position = item.imageIcon.transform.position; //异步加载结束后发现开始拖动了 就不显示了 if(isDraging) { nowInGrid = null; UIManager.GetInstance().HidePanel("GridTips"); } }); } private void GridIconOut(InventoryGrid item) { if (isDraging) { return ; } if (item.itemInfo == null) { return; } UIManager.GetInstance().HidePanel("GridTips"); } public void ChangeGrid() { Debug.Log("交换格子"); if(nowDragGrid != null && nowInGrid != null) { PlayerData.GetInstance().ChangeEquip(nowDragGrid.itemInfo, nowInGrid.itemInfo); UIManager.GetInstance().GetPanel<InventoryUI>("InventoryUI").DragUpdate(E_InventoryType.Equip); if(UIManager.GetInstance().GetPanel<ToolPanel>("Tool") != null) { UIManager.GetInstance().GetPanel<ToolPanel>("Tool").InitTool(); } } } }
到这里背包的逻辑才算基本写完
如果想看看实际效果的话可以浏览我B站的demo视频
https://www.bilibili.com/video/BV1434y1j73W?share_source=copy_web