学Unity的猫之背包系统(十二)
12.1 皮皮的梦想背包
皮皮:“太阳天空照,花儿对我笑,小鸟说早早早,你为什么背着小书包?”
我:“我去上ang班,天天不迟到,爱学习爱工作,要不哪里有钱买猫粮。”
皮皮:“包里都装什么东西呀?有吃的吗?”
我打开我的双肩包:“没有吃的,有雨伞、水杯、工牌、纸巾、移动电源、充电器、笔、本子、各种卡,还有水电煤的单子。”
皮皮:“我也想要一个背包,不过我想要里面装满了我的玩具和爱吃的食物。”
我:“现在就给你做一个吧。”模块设计如下
本工程使用的
Unity
版本为2020.1.14f1c1 (64-bit)
,工程已上传到GitHub
,感兴趣的同学可以下载下来学习。GitHub
地址:https://github.com/linxinfa/Unity-BackpackDemo
12.2 准备道具图片
皮皮:“你这道具图片也太没诚意了。” 我:“不要在意这些细节。” 我们需要把图片放到
Unity
工程中,如下,放在Assets/Atlas/Bag
文件夹中。皮皮:“为什么不放在
Resources
文件夹中呢,这样可以直接通过Resources.Load
加载图片。”
我:“因为后面我要将这些图片打成一个图集,放在Resources
中的图片是不会被打进图集中的。”
12.3 UGUI打图集
UGUI
中,我们可以使用Image
或RawImage
组件来显示图片。
皮皮:“RawImage
和Image
有什么区别呢?”
RawImage
和Image
都继承MaskableGraphic
,但Image
比RawImage
的封装重一些。RawImage
只为我们提供了修改UV
的方法,主要用来显示未加工的图片,比如背景图;而Image
提供了四种ImageType
:Simple
(普通)、Sliced
(切割)、Tiled
(平铺)、Filled
(填充),显示图片的方式丰富;另外,Image
支持精灵图的图集合批,可以减少DrawCall
。 道具种类繁多,道具图片可以打在一个图集中,这样显示多个道具图片可以合成一个批次显示,提升性能。
皮皮:“问题来了,怎么打图集呢?”
12.3.1 设置图集模式:Always Enabled(Legacy Sprite Packer)
点击菜单Edit - Project Settings...
,打开Project Settings
窗口。点击
Editor
标签页,将Sprite Packer
的Mode
选为Always Enabled(Legacy Sprite Packer)
。
12.3.2 设置图片为Sprite(2D and UI)类型
将图片的Texture Type
设置为为Sprite(2D and UI)
。设置成功后,会有个小箭头,点开可以看到对应的
Sprite
。
12.3.3 设置Packing Tag
想要把多张图片打在一个图集中,需要将这些图片的Packing Tag
设置为相同的值。 比如都设置成Bag
。不过这样每次都手动设置有点麻烦,写个
Editor
工具监听图片导入,自动修改图片格式,并设置Packing Tag
为所在文件夹名字。 代码如下,将其保存为PostTexture.cs
放在Editor
文件夹中。
using UnityEngine;
using UnityEditor;
using System.IO;
public class PostTexture : AssetPostprocessor
{
void OnPostprocessTexture(Texture2D texture)
{
var dirName = Path.GetDirectoryName(assetPath);
string atlasName = new DirectoryInfo(dirName).Name;
TextureImporter textureImporter = assetImporter as TextureImporter;
textureImporter.textureType = TextureImporterType.Sprite;
textureImporter.spritePackingTag = atlasName;
textureImporter.mipmapEnabled = false;
}
}
12.3.4 使用Sprite Packer打图集
点击菜单Window - 2D - Sprite Packer
。点击
Pack
按钮,即可将多张图片打成一个图集了。打图集成,即可看到对应的图集了。
如果有多张图集,可以在
View Atlas
下拉菜单中切换显示。
12.4 制作精灵预设,通过预设加载精灵
使用过NGUI
的同学应该知道,在NGUI
中图集会有一个对应的图集预设,要加载某个精灵图,需要先加载对应的图集。UGUI
则不同,直接加载对应的精灵即可。但是上面我们的精灵并不是放在Resources
文件夹中,是不会被打进包体中的。如果将精灵图拷贝到Resources
文件夹中,则会产生双份资源,造成资源冗余。 解决办法就是将精灵图包装成预设,把预设放在Resources
文件夹中,这样就可以通过Resources.Load
接口加载精灵图了。 预设中使用Sprite Renderer
组件,设置它的Sprite
属性为具体的精灵图。这样挨个制作精灵预设很麻烦,所以写个
Editor
工具批量执行。 将下面的代码保存为MakeSpritePrefabs.cs
放在Editor
文件夹中。
using UnityEngine;
using UnityEditor;
using System.IO;
public class MakeSpritePrefabs
{
[MenuItem("Tools/MakeSpritePrefabs")]
private static void Make()
{
var prefabRootDir = Path.Combine(Application.dataPath, "Resources/Sprite");
if (!Directory.Exists(prefabRootDir))
{
Directory.CreateDirectory(prefabRootDir);
}
var pngRootDir = Path.Combine(Application.dataPath, "Atlas");
var pngFils = Directory.GetFiles(pngRootDir, "*.png", SearchOption.AllDirectories);
foreach (string filePath in pngFils)
{
string assetPath = filePath.Substring(filePath.IndexOf("Assets"));
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
GameObject go = new GameObject(sprite.name);
go.AddComponent<SpriteRenderer>().sprite = sprite;
var prefabPath = Path.Combine(prefabRootDir, sprite.name + ".prefab");
prefabPath = prefabPath.Substring(prefabPath.IndexOf("Assets"));
PrefabUtility.SaveAsPrefabAsset(go, prefabPath);
GameObject.DestroyImmediate(go);
}
AssetDatabase.Refresh();
}
}
点击菜单Tools - MakeSpritePrefabs
即可自动将精灵图制作成预设了。 接着,我们就可以通过Resources.Load
接口加载精灵图了。
var obj = Resources.Load<GameObject>("Sprite/bj");
var sprite = .GetComponent<SpriteRenderer>().sprite;
12.5 json配置表
我们将道具以json
格式弄成配置表,如下
[
{
"id":1,
"name":"逗猫棒",
"sprite":"dmb",
"desc":"逗猫棒是一种深受猫咪喜爱的玩具"
},
{
"id":2,
"name":"猫布丁",
"sprite":"mbd",
"desc":"猫布丁是以香滑布丁粉为主要材料制作而成的一道甜品"
},
{
"id":3,
"name":"猫薄荷",
"sprite":"mbh",
"desc":"猫薄荷是指能让家猫等猫科动物产生幻觉的一类多年生草本植物"
},
{
"id":4,
"name":"鸡胸肉",
"sprite":"jxr",
"desc":"鸡胸肉,是鸡身上最大的两块肉,富含丰富的蛋白质"
},
{
"id":5,
"name":"猫麦草",
"sprite":"mmc",
"desc":"猫麦草可以刺激猫的肠胃蠕动,帮助猫吐出在胃中结成团的毛球"
},
{
"id":6,
"name":"猫罐头",
"sprite":"mgt",
"desc":"猫罐头是根据猫咪的特殊身体因素研制而成的食物,营养丰富"
},
{
"id":7,
"name":"猫条",
"sprite":"mt",
"desc":"猫条是一种猫的零食,口味多种多样"
}
]
12.6 配置表读取
将上面的配置表保存为PropCfg.json
放在Resources
目录中。
接着我们就可以通过Resources.load
加载配置文件,再通过LitJson
解析。
皮皮:“现在微信搜索公众号 [爱上游戏开发],回复 “资料”,免费领取 200G 学习资料!”
关于
LitJson
,第十一章已讲过,可以查阅第十一章:学Unity的猫》——第十一章:Unity猫咪救济管理系统,山岗的星光
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class PropCfg
{
public void LoadCfg()
{
var cfgJson = Resources.Load<TextAsset>("PropCfg");
var cfg = JsonMapper.ToObject<List<PropCfgItem>>(cfgJson.text);
foreach(var cfgItem in cfg)
{
m_cfgDic[cfgItem.id] = cfgItem;
}
}
public PropCfgItem GetCfg(int id)
{
if (m_cfgDic.ContainsKey(id))
return m_cfgDic[id];
return null;
}
private Dictionary<int, PropCfgItem> m_cfgDic = new Dictionary<int, PropCfgItem>();
private static PropCfg s_instance;
public static PropCfg Instance
{
get
{
if (null == s_instance)
s_instance = new PropCfg();
return s_instance;
}
}
}
public class PropCfgItem
{
public int id;
public string name;
public string sprite;
public string desc;
}
12.7 制作界面预设
大厅界面:PlazaPanel.prefab
。背包界面:
BackpackPanel.prefab
提示语预设:
TipsUi.prefab
12.8 资源管理器
编写一个资源管理器ResourceMgr
,用于加载资源,缓存资源。
using System.Collections.Generic;
using UnityEngine;
public class ResourceMgr
{
public Sprite LoadSprite(string name)
{
if (m_spritesDic.ContainsKey(name))
return m_spritesDic[name];
var obj = Resources.Load<GameObject>("Sprite/" + name);
var sprite = obj.GetComponent<SpriteRenderer>().sprite;
m_spritesDic[name] = sprite;
return sprite;
}
public GameObject LoadPanel(string name)
{
GameObject prefab = null;
if (m_panelsDic.ContainsKey(name))
prefab = m_panelsDic[name];
else
{
prefab = Resources.Load<GameObject>("Panel/" + name);
m_panelsDic[name] = prefab;
}
return prefab;
}
private Dictionary<string, Sprite> m_spritesDic = new Dictionary<string, Sprite>();
private Dictionary<string, GameObject> m_panelsDic = new Dictionary<string, GameObject>();
private static ResourceMgr s_instance;
public static ResourceMgr Instance
{
get
{
if (null == s_instance)
s_instance = new ResourceMgr();
return s_instance;
}
}
}
例:加载精灵图
var sprite = ResourceMgr.Instance.LoadSprite("mmc");
12.9 界面管理器
编写一个界面管理器PanelMgr
,用于显示界面。
using UnityEngine;
public class PanelMgr
{
public void Init(Canvas canvas)
{
m_canvasTrans = canvas.transform;
}
public GameObject ShowPanel(string panelName)
{
var prefab = ResourceMgr.Instance.LoadPanel(panelName);
var obj = Object.Instantiate(prefab);
obj.transform.SetParent(m_canvasTrans, false);
return obj;
}
private Transform m_canvasTrans;
private static PanelMgr s_instance;
public static PanelMgr Instance
{
get
{
if (null == s_instance)
s_instance = new PanelMgr();
return s_instance;
}
}
}
public class PanelName
{
public const string PlazaPanel = "PlazaPanel";
public const string BackpackPanel = "BackpackPanel";
public const string TipsUi = "TipsUi";
}
例:显示背包界面
var obj = PanelMgr.Instance.ShowPanel(PanelName.BackpackPanel);
12.10 背包数据读写
封装一个本地数据读写的类,模拟数据库,用到了PlayerPrefs
和LitJson
这两个类。 通过PlayerPrefs
可以方便地进行数据读写,通过LitJson
可以方便地进行json
数据格式转换。
using System.Collections.Generic;
using UnityEngine;
using LitJson;
public class PropsDatabase
{
public static Dictionary<int, int> LoadData()
{
PlayerPrefs.SetString("props", "{}");
var jsonStr = PlayerPrefs.GetString("props", "{}");
Debug.Log("LoadData: \n" + jsonStr);
var result = JsonMapper.ToObject<Dictionary<string, int>>(jsonStr);
Dictionary<int, int> data = new Dictionary<int, int>();
foreach (var item in result)
{
data[int.Parse(item.Key)] = item.Value;
}
return data;
}
public static void SaveData(Dictionary<int, int> data)
{
var jsonStr = JsonMapper.ToJson(data);
PlayerPrefs.SetString("props", jsonStr);
}
}
12.11 背包管理器
再封装一个背包管理器BackPackMgr
,提供读取道具数据和增减道具数量的方法。
using System.Collections.Generic;
public class BackPackMgr
{
public void Init()
{
m_propsDic = PropsDatabase.LoadData();
}
public void ChangePropCnt(int id, int deltaCnt)
{
if (m_propsDic.ContainsKey(id))
{
m_propsDic[id] += deltaCnt;
}
else
{
m_propsDic[id] = deltaCnt;
}
if (m_propsDic[id] < 0)
{
m_propsDic[id] = 0;
}
PropsDatabase.SaveData(m_propsDic);
EventDispatcher.instance.DispatchEvent(EventNameDef.EVENT_UPDATE_PROP_CNT, id, m_propsDic[id]);
}
private Dictionary<int, int> m_propsDic = new Dictionary<int, int>();
public Dictionary<int, int> propsDic
{
get { return m_propsDic; }
}
private static BackPackMgr s_instance;
public static BackPackMgr Instance
{
get
{
if (null == s_instance)
s_instance = new BackPackMgr();
return s_instance;
}
}
}
例:增加一个id
为1
的道具(逗猫棒)
BackPackMgr.Instance.ChangePropCnt(1, 1);
12.12 事件管理器
上面背包管理器中用到了事件管理器,当道具数量发生改变的时候,要通知界面ui
更新显示。使用观察者模式,通过事件订阅的方式实现道具数量更新时同步更新ui
的显示。 事件管理器如下
using UnityEngine;
using System.Collections.Generic;
public delegate void MyEventHandler(params object[] objs);
public class EventDispatcher
{
public void Regist(string type, MyEventHandler handler)
{
if (handler == null)
return;
if (!listeners.ContainsKey(type))
{
listeners.Add(type, new Dictionary<int, MyEventHandler>());
}
var handlerDic = listeners[type];
var handlerHash = handler.GetHashCode();
if (handlerDic.ContainsKey(handlerHash))
{
handlerDic.Remove(handlerHash);
}
listeners[type].Add(handler.GetHashCode(), handler);
}
public void UnRegist(string type, MyEventHandler handler)
{
if (handler == null)
return;
if (listeners.ContainsKey(type))
{
listeners[type].Remove(handler.GetHashCode());
if (null == listeners[type] || 0 == listeners[type].Count)
{
listeners.Remove(type);
}
}
}
public void DispatchEvent(string evt, params object[] objs)
{
if (listeners.ContainsKey(evt))
{
var handlerDic = listeners[evt];
if (handlerDic != null && 0 < handlerDic.Count)
{
var dic = new Dictionary<int, MyEventHandler>(handlerDic);
foreach (var f in dic.Values)
{
try
{
f(objs);
}
catch (System.Exception ex)
{
Debug.LogErrorFormat(szErrorMessage, evt, ex.Message, ex.StackTrace);
}
}
}
}
}
public void ClearEvents(string key)
{
if (listeners.ContainsKey(key))
{
listeners.Remove(key);
}
}
private Dictionary<string, Dictionary<int, MyEventHandler>> listeners = new Dictionary<string, Dictionary<int, MyEventHandler>>();
private readonly string szErrorMessage = "DispatchEvent Error, Event:{0}, Error:{1}, {2}";
private static EventDispatcher s_instance;
public static EventDispatcher Instance
{
get
{
if (null == s_instance)
s_instance = new EventDispatcher();
return s_instance;
}
}
}
例:事件订阅
EventDispatcher.Instance.Regist("EVENT_UPDATE_PROP_CNT", OnEventUpdatePropCnt);
EventDispatcher.Instance.UnRegist("EVENT_UPDATE_PROP_CNT", OnEventUpdatePropCnt);
抛出事件
EventDispatcher.Instance.DispatchEvent("EVENT_UPDATE_PROP_CNT", id, cnt);
响应函数
private void OnEventUpdatePropCnt(params object[] args)
{
int id = (int)args[0];
int cnt = (int)args[1];
}
12.13 界面代码
12.13.1 大厅界面
大厅界面主要是两个按钮,一个按钮点击了随机增加一个道具,另一个按钮点击了打开背包界面。
using UnityEngine;
using UnityEngine.UI;
public class PlazaPanel : MonoBehaviour
{
public Button packpackBtn;
public Button addPropBtn;
void Start()
{
packpackBtn.onClick.AddListener(() =>
{
PanelMgr.Instance.ShowPanel(PanelName.BackpackPanel);
});
addPropBtn.onClick.AddListener(() => {
var propId = Random.Range(1, 8);
BackPackMgr.Instance.ChangePropCnt(propId, 1);
var cfg = PropCfg.Instance.GetCfg(propId);
TipsUi.Show("恭喜获得" + cfg.name + "x1");
});
}
}
12.13.2 背包界面
背包界面稍复杂一点点,界面左边用一个Scroll View
用来显示道具列表,选中某个道具,界面右边显示道具信息,点击使用按钮,扣除一个道具。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class BackpackPanel : MonoBehaviour
{
private void Awake()
{
EventDispatcher.Instance.Regist(EventNameDef.EVENT_UPDATE_PROP_CNT, OnEventUpdatePropCnt);
}
private void OnDestroy()
{
EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_UPDATE_PROP_CNT, OnEventUpdatePropCnt);
}
private void Start()
{
propItemUi.SetActive(false);
closeBtn.onClick.AddListener(() =>
{
Destroy(gameObject);
});
usePropBtn.onClick.AddListener(() =>
{
if (-1 == m_selectPropId) return;
var cfg = PropCfg.Instance.GetCfg(m_selectPropId);
TipsUi.Show("使用了" + cfg.name);
BackPackMgr.Instance.ChangePropCnt(m_selectPropId, -1);
});
CreatePropList();
}
private void OnEventUpdatePropCnt(params object[] args)
{
int id = (int)args[0];
int cnt = (int)args[1];
if (cnt <= 0)
{
if (m_propUiDic.ContainsKey(id))
{
Destroy(m_propUiDic[id].obj);
m_propUiDic.Remove(id);
if (id == m_selectPropId)
AutoSelectOneProp();
}
}
else
{
if (m_propUiDic.ContainsKey(id))
{
m_propUiDic[id].cnt.text = "x" + cnt;
}
else
{
CreatePropItem(id, cnt);
}
}
}
private void CreatePropList()
{
foreach (var item in BackPackMgr.Instance.propsDic)
{
CreatePropItem(item.Key, item.Value);
}
AutoSelectOneProp();
}
private void AutoSelectOneProp()
{
var itor = m_propUiDic.GetEnumerator();
if (itor.MoveNext())
{
var item = itor.Current.Value;
item.tgl.isOn = true;
itor.Dispose();
OnPropItemSelected(item.propId);
}
else
{
OnPropItemSelected(-1);
}
}
private void CreatePropItem(int propId, int cnt)
{
if (m_propUiDic.ContainsKey(propId)) return;
if (cnt <= 0) return;
PropItemUI ui = new PropItemUI();
var obj = Instantiate(propItemUi);
obj.SetActive(true);
obj.transform.SetParent(propItemUi.transform.parent, false);
ui.obj = obj;
ui.propId = propId;
ui.icon = obj.transform.Find("Button/Icon").GetComponent<Image>();
ui.cnt = obj.transform.Find("Button/Cnt").GetComponent<Text>();
ui.tgl = obj.transform.Find("Button").GetComponent<Toggle>();
ui.tgl.onValueChanged.AddListener((v) =>
{
if (ui.tgl.isOn) OnPropItemSelected(propId);
});
var cfg = PropCfg.Instance.GetCfg(propId);
if (null != cfg)
{
var sprite = ResourceMgr.Instance.LoadSprite(cfg.sprite);
ui.icon.sprite = sprite;
}
ui.cnt.text = "x" + cnt;
m_propUiDic[propId] = ui;
}
private void OnPropItemSelected(int propId)
{
m_selectPropId = propId;
if (-1 == m_selectPropId)
{
rightInfoRoot.SetActive(false);
}
else
{
rightInfoRoot.SetActive(true);
var cfg = PropCfg.Instance.GetCfg(propId);
nameText.text = cfg.name;
descText.text = cfg.desc;
icon.sprite = ResourceMgr.Instance.LoadSprite(cfg.sprite);
}
}
public Button closeBtn;
public GameObject propItemUi;
public GameObject rightInfoRoot;
public Text nameText;
public Text descText;
public Image icon;
public Button usePropBtn;
private int m_selectPropId;
private Dictionary<int, PropItemUI> m_propUiDic = new Dictionary<int, PropItemUI>();
private class PropItemUI
{
public int propId;
public GameObject obj;
public Image icon;
public Toggle tgl;
public Text cnt;
}
}
12.13.3 提示语UI
制作一个提示语UI的动画,动画播放结束后通过帧事件调用OnAnimationEnd
方法销毁自己。
皮皮:“现在微信搜索公众号 [爱上游戏开发],回复 “资料”,免费领取 200G 学习资料!”TipsUi
组件挂在TipsUi.prefab
上。
using UnityEngine;
using UnityEngine.UI;
public class TipsUi : MonoBehaviour
{
public Text tipsText;
public static void Show(string text)
{
var obj = PanelMgr.Instance.ShowPanel(PanelName.TipsUi);
var bhv = obj.GetComponent<TipsUi>();
bhv.tipsText.text = text;
}
public void OnAnimationEnd()
{
Destroy(gameObject);
}
}
12.14 程序入口脚本
写一个程序入口脚本,挂到Canvas
上,并赋值Canvas
对象。
using UnityEngine;
public class UIMain : MonoBehaviour
{
public Canvas canvas;
private void Awake()
{
PropCfg.Instance.LoadCfg();
BackPackMgr.Instance.Init();
PanelMgr.Instance.Init(canvas);
}
void Start()
{
PanelMgr.Instance.ShowPanel(PanelName.PlazaPanel);
}
}
完成。 如果有什么疑问,欢迎留言或私信。
标题:学Unity的猫之背包系统(十二)
作者:shirlnGame
地址:https://www.mmzsblog.cn/articles/2021/01/12/1610456885137.html
-----------------------------
如未加特殊说明,此网站文章均为原创。
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!
