欢迎来到我的博客小站。  交流请加我微信好友: studyjava。  也欢迎关注同名公众号:Java学习之道

学Unity的猫之背包系统(十二)

  |   0 评论   |   0 浏览

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中,我们可以使用ImageRawImage组件来显示图片。

皮皮:“RawImageImage有什么区别呢?”

RawImageImage都继承MaskableGraphic,但ImageRawImage的封装重一些。RawImage只为我们提供了修改UV的方法,主要用来显示未加工的图片,比如背景图;而Image提供了四种ImageTypeSimple(普通)、Sliced(切割)、Tiled(平铺)、Filled(填充),显示图片的方式丰富;另外,Image支持精灵图的图集合批,可以减少DrawCall。 道具种类繁多,道具图片可以打在一个图集中,这样显示多个道具图片可以合成一个批次显示,提升性能。

皮皮:“问题来了,怎么打图集呢?”

12.3.1 设置图集模式:Always Enabled(Legacy Sprite Packer)

点击菜单Edit - Project Settings...,打开Project Settings窗口。点击Editor标签页,将Sprite PackerMode选为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 背包数据读写

封装一个本地数据读写的类,模拟数据库,用到了PlayerPrefsLitJson这两个类。 通过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;
        }
    }
}

例:增加一个id1的道具(逗猫棒)

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
-----------------------------
如未加特殊说明,此网站文章均为原创。
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!

个人微信公众号 ↓↓↓                 

微信搜一搜爱上游戏开发