Unity MVC设计模式与UI背包界面制作

Unity MVC设计模式与UI背包界面制作

MVC设计模式非常适合UI的架构,UI界面相当于View,UI转换控制相当于Controller,UI上面的数据变换相当于Model。MVC设计模式在软件设计中无处不在,结合其他设计模式或设计思想,同一设计方案中,对于更好的MVC模式的追求几乎是没有尽头的。SO,作为Unity行业新人,本文旨在讨论关于MVC设计模式在UI背包界面制作过程中,相关的思路及应用,顺便附上本次实际应用中的代码及思路。

写在前边:

1.本文使用Unity版本为Unity 2019.1.14f1 (64-bit),VS版本为Microsoft Visual Basic 2015
2.UI使用Unity默认UGUI
3.涉及服务器数据与静态数据,使用JSON进行存取 ,并在C#中使用LitJson工具进行编译
4.文章内知识综合各类文章、读物学习总结而来,并非完全原创,以下为创作过程中使用知识点较多的文章:
站内优秀文章:
Unity (C#) 使用 LitJson 处理 JSON 数据
unity基于MVC的ui框架(一)
MVC的理解和优缺点的总结
Unity拖动背包物品/技能图标位置互换

正式开始

一、图示MVC结构

Model–view–controller (usually known as MVC) is a software design pattern commonly used for developing user interfaces that divides the related program logic into three interconnected elements. This is done to separate internal representations of information from the ways information is presented to and accepted from the user.——Wikipedia

机翻:模型-视图-控制器(通常称为MVC)是一种软件设计模式,通常用于开发用户界面,它将相关的程序逻辑划分为三个相互关联的元素。这样做是为了将信息的内部表示与向用户显示信息和从用户接受信息的方式分开。

下图是来自维基百科对于MVC基本交互模型的图示
Unity MVC设计模式与UI背包界面制作_第1张图片

二、UI背包界面效果构想图

具体到本次UI背包界面制作,将会围绕下图进行展开
Unity MVC设计模式与UI背包界面制作_第2张图片

三、正式开始

  • 新建Canvas搭建UI视图

根据构想图搭建UI视图,具体Hierachy面板层级部署如下:
(对比上文构想图)
Unity MVC设计模式与UI背包界面制作_第3张图片
————————————————————————————————————————————————————

  • 新建Canvas搭建UI视图

  • 根据上图五个部分拆分需要实现的功能

  • 第一部分包括:两个button组件(V),控制BackPack层视图的开启和关闭(C)

  • 第二部分(search)包括:一个图片,一个InputFiled组件(V),根据道具名称(M),搜索道具(C)

  • 第三部分(Types)包括:一个Toggle组件(V),根据道具类型(M),筛选道具(C)

  • 第四部分(Items)包括:一个Scrollbar组件,一个固定物品框图片(V),一个可变图标图片(M)

  • 第五部分包括:一个button组件(V),实现物品栏的简单整理(C)

————————————————————————————————————————————————————

  • 新建Canvas搭建UI视图
  • 根据上图五个部分拆分需要实现的功能
  • 根据需求拆分MVC

总体而言,我们需要通过传入Model到Controller相应去修改View

综合上述五部分:

  • M部分——包括道具栏中的道具的数据,通过JSON来进行读写,实现数据的CURD操作,即
    Create增加数据,Update修改数据,Read读取数据 以及 Delete删除数据
  • V部分——图片,字符,按键等各个UI组件的显示
  • C部分——理论上需要实现View的部分,都需要传入Model到Controller部分,去修改View

同时需要注意:

  • 使用MVC的目的是将M和V实现代码分离(理想情况),从而使同一个程序可以使用不同的表现形式。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。更好的调节M和V的搭配。

  • View里会包含Model信息,不可避免的还要包括一些业务逻辑。
    在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即View
    Unity MVC设计模式与UI背包界面制作_第4张图片

————————————————————————————————————————————————————

  • 新建Canvas搭建UI视图
  • 根据上图五个部分拆分需要实现的功能
  • 根据需求拆分MVC
  • 编写代码

1.ItemStaticDataManager.cs 和 ItemDynamicDataManger.cs

建立两个类来装载道具的表数据和服务器数据,对应Model部分

下图为本次示例中的部分JSON格式表数据截图(放在Resources/JSON/Items目录下),根据表数据表头内容来编写静态数据脚本ItemStaticDataManager.cs
Unity MVC设计模式与UI背包界面制作_第5张图片

静态类数据代码 ItemStaticDataManager.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LitJson;

/// 
/// 静态数据管理类
/// 键值对Dictionart类型读取JSON数据中的道具表,定义一个类来获取值Value
/// 
public class ItemStaticData
{
    public int ItemID;
    public string Name;
    public string Description;
    public string Icon;
    public int Type;
    public int Quality;
    public int CanSell;
    public int CellGold;
    public int UseEffect;
    public int UseNumerical;
}

public class ItemStaticDataManager
{
    private static ItemStaticDataManager instance;
    public static ItemStaticDataManager Instance
    {
        get
        {
            if(instance == null)
            {
                instance = new ItemStaticDataManager();
            }
            return instance;
        }
    }

    //定义Dictionary类型变量itemStaticDataDic
    private Dictionary<int, ItemStaticData> itemStaticDataDic;
    
    //读取JSON表中数据,并用list接收ItemStaticData
    //LitJson反序列化:JsonMapper.ToObject()
    public ItemStaticDataManager()
    {
        itemStaticDataDic = new Dictionary<int, ItemStaticData>();

        TextAsset temp = Resources.Load<TextAsset>("JSON/Items");
        List<ItemStaticData> list = JsonMapper.ToObject<List<ItemStaticData>>(temp.text);
        
        //遍历list,获取ItemStaticData,itemStaticDataDic获取键值
        for (int i = 0; i < list.Count; i++)
        {
            itemStaticDataDic.Add(list[i].ItemID, list[i]);
            
        }
    }
    /// 
    /// 根据id访问ItemStaticData类的ItemStaticData方法,获取反序列化后的数据(Dictionary类型)
    /// 
    /// 
    /// 
    public ItemStaticData GetItemByID(int id)
    {
        if (itemStaticDataDic.ContainsKey(id))
        {
            return itemStaticDataDic[id];            
        }
        else
        {
            Debug.LogError("没有这个Item:" + id);
            return null;
        }
    }

}
服务器类数据代码 ItemDynamicDataManger.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using LitJson;

/// 
///封装的服务器数据类,包括道具ID和数量,即ItemID和Count 
/// 
public class ItemServerData
{
    public int id;
    public int count;
    public ItemServerData() { }
    public ItemServerData(int id, int count)
    {
        this.id = id;
        this.count = count;
    }
}
/// 
/// 数据:服务器数据+表数据
/// 
public class ItemDynamicDataManger
{
    private static ItemDynamicDataManger instance;
    public static ItemDynamicDataManger Instance
    {
       get
        {
            if (instance == null)
            {
                instance = new ItemDynamicDataManger();
            }
            return instance;
        }
    }

    //服务器数据类list
    public List<ItemServerData> list = null;
    private string bagDataPath = "D:/Unity4.2/bagData.json";

    //伪服务器数据,手动修改背包初始物品
    private ItemDynamicDataManger()     
    {
        //1.读本地玩家数据表---》服务器数据
        //2.服务器数据和表数据通过id进行组合,生成完整数据      

        if (!File.Exists(bagDataPath))
        {
            list = new List<ItemServerData>();
            //前是十个道具是武器,各一个
            ItemServerData[] data = new ItemServerData[10];
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = new ItemServerData(i + 1, 1);
                list.Add(data[i]);
            }
            //11-14为药水,各99个
            ItemServerData data11 = new ItemServerData(11, 99);
            list.Add(data11);
            ItemServerData data12 = new ItemServerData(12,99);
            list.Add(data12);
            ItemServerData data13 = new ItemServerData(13, 99);
            list.Add(data13);
            ItemServerData data14 = new ItemServerData(14, 99);
            list.Add(data14);

            //将list序列化
            string json = JsonMapper.ToJson(list);
            File.WriteAllText(bagDataPath, json, System.Text.Encoding.UTF8);
        }
        else
        {
            //读取JSON文档,并反序列化
            string json = File.ReadAllText(bagDataPath);
            list = JsonMapper.ToObject<List<ItemServerData>>(json);
        }           
    }
    // 获取背包中所有的装备数据
    public List<ItemServerData> GetAllData()
    {
        return list;
    }
}

2.BackPackPanel.cs

  • 根据上边的思路,为整个BackPack的Panel新建一个脚本BackPackPanel.cs,用来显示整个BackPack中的各个动态的子级视图。即View部分:
  • 首先通过服务器类数据确认有哪些道具,以及道具的数量,加载出预制体中的ItemCell和ItemCell_Empty(V)
  • 然后再根据静态类数据加载出每个道具的图标(C)
背包Panel视图 BackPackPanel.cs代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//挂载到 BackPack
public class BackPackPanel : MonoBehaviour
{   
    public Transform itemContent;
    private List<ItemServerData> bagDataList;

    public static BackPackPanel Instance;
    void Awake()
    {
        Instance = this;
    }

    void Start()
    {
        bagDataList = ItemDynamicDataManger.Instance.GetAllData();
        ShowItems(bagDataList);
    }

    /// 
    /// 根据ServerData显示所有图标
    /// 
    /// 

    private GameObject itemCellMod;
    private GameObject Cell_Empty;

    public void ShowItems(List<ItemServerData> dataList)
    {
        //每次Show先清除原有列表
        for (int i = 0; i < itemContent.childCount; i++)
        {
            Destroy(itemContent.GetChild(i).gameObject);
        }
		//加载出放在预制体目录的ItemCell,原层级图中ItemCell和ItemCell_Empty提前手动删除
        if (itemCellMod == null)
        {
            itemCellMod = Resources.Load<GameObject>("Prefabs/UI/ItemCell");
        }
        //遍历获取服务器数据列表List
        for (int i = 0; i < dataList.Count; i++)
        {
            GameObject item = Instantiate(itemCellMod, itemContent);
            item.transform.localScale = Vector3.one;
            item.transform.localPosition = Vector3.zero;

            item.name = itemCellMod.name;
            //显示每一行也就是每一个物品的图标
            ItemCellController ic = item.GetComponent<ItemCellController>();
            ic.Show(dataList[i]);
        }
		//定义格子数量为16,没有图标的格子显示ItemCell_Empty
        for (int i = 0; i < 16 - dataList.Count; i++)
        {
            if (Cell_Empty == null)
            {
                Cell_Empty = Resources.Load<GameObject>("Prefabs/UI/ItemCell_Empty");
            }

            GameObject Item_Empty = Instantiate(Cell_Empty, itemContent);
            Item_Empty.transform.localScale = Vector3.one;
            Item_Empty.transform.localPosition = Vector3.zero;
            Item_Empty.name = Cell_Empty.name;
        }
    }
}

3. ItemCellController.cs

上边提到的根据静态类数据加载出每个道具的图标(C),这里注意图集名称和数据列表中一一对应
Unity MVC设计模式与UI背包界面制作_第6张图片

图标控制器 ItemCellController.cs代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.U2D;

public class ItemCellController : MonoBehaviour
{
    public Image icon;  //表内数据取得Icon
    public Text countText;  //服务器数据取得数量

    private ItemServerData mServerData;
    //加载图集,加载图集中的图标 
    //icon :  UI/SpriteAtlas/Prop#0001 

    public void Show(ItemServerData data)
    {
        mServerData = data;

        ItemStaticData itemStaticData = ItemStaticDataManager.Instance.GetItemByID(data.id);
        if (itemStaticData == null)
        {
            Debug.LogError("没有数据:" + data.id);
            return;
        }
        //加载图集,#分隔 图集#图标
        string[] icons = itemStaticData.Icon.Split('#');
        SpriteAtlas prop = Resources.Load<SpriteAtlas>("UI/SpriteAtlas/"+ icons[0]);
        icon.sprite = prop.GetSprite(icons[1]);
        //加载道具数量
        countText.text = data.count + "";
    }
}

————————————————————————————————————————————————————

  • 新建Canvas搭建UI视图
  • 根据上图五个部分拆分需要实现的功能
  • 根据需求拆分MVC
  • 编写代码
  • 查看效果

进行到这里就可以先运行项目查看一下图标的显示效果了(当然控制物品栏打开的操作这里依然没有讨论,需要的朋友还请自己添加)
Unity MVC设计模式与UI背包界面制作_第7张图片

四、体验MVC模式开发UI背包的优点

如果只看上边完成的这部分UI,似乎展示背包中物品数据这样一个简单的功能,只要用一个类就能完成所有代码的书写,为什么还要如此大费周折,分析各个部分来进行MVC模式的设计呢?下边我们通过为上边完成的这个简单背包添加新的功能,来体验一下MVC模式下,开发UI背包的优点。

1.为背包添加搜索功能

  • 当玩家在搜索栏中输入文字后,立马展示背包中名称包含该文字的道具

上文已分析过,第二部分(search)包括:一个图片,一个InputFiled组件(V),根据道具名称(M),搜索道具(C)

第一步:分析新增需求,在动态数据类ItemDynamicDataManger.cs中,新增根据玩家输入,查找道具栏道具的方法

 //新增至 ItemDynamicDataManger.cs
 //根据名称筛选道具
    public List<ItemServerData> GetItemByName(string name)
    {
        if (name == string.Empty)
        {
            return list;
        }
        List<ItemServerData> nameList = new List<ItemServerData>();
        for (int i = 0; i < list.Count; i++)
        {
            ItemStaticData data = ItemStaticDataManager.Instance.GetItemByID(list[i].id);
            if (data.Name.Contains(name))
            {
                nameList.Add(list[i]);
            }
        }
        return nameList;
    }

第二步:在层级图中,找到对应第二部分的UI效果图中,search部分的游戏物体,新增SearchController.cs
search

search部分 searchController.cs代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//挂载到 search
public class SearchController : MonoBehaviour
{
    public void SearchEquip(string name)    
    {
        List<ItemServerData> list = ItemDynamicDataManger.Instance.GetItemByName(name);
        BackPackPanel.Instance.ShowItems(list);     
    }
}

搜索功能完成效果:
Unity MVC设计模式与UI背包界面制作_第8张图片

2.为背包添加按类型筛选道具功能

  • 当玩家在道具栏上方点击道具类型后,展示背包中该类型下的所有道具

上文已分析过,第三部分(Types)包括:一个Toggle组件(V),根据道具类型(M),筛选道具(C)

第一步:分析新增需求,在动态数据类ItemDynamicDataManger.cs中,新增根据玩家点击,筛选道具栏道具的方法

 //新增至 ItemDynamicDataManger.cs
 //根据Types筛选道具
    public List<ItemServerData> GetItemByType(int type)
    {
        if (type == 0)
        {
            return list;
        }
        else
        {
            List<ItemServerData> typeList = new List<ItemServerData>();
            for (int i = 0; i < list.Count; i++)
            {
                if (ItemStaticDataManager.Instance.GetItemByID(list[i].id).Type == type)
                {
                    typeList.Add(list[i]);
                }
            }
            return typeList;
        }
    }

第二步:在层级图中,找到对应第三部分的UI效果图中,Types部分的游戏物体,新增TypesController.cs
Unity MVC设计模式与UI背包界面制作_第9张图片

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

//挂载到 Types
public class TypesController : MonoBehaviour
{
    public ToggleGroup group;
    int selectType = 0;
    public void SelectType(bool isOn)
    {
        if (!isOn)
        {
            return;
        }

        foreach (Toggle t in group.ActiveToggles())
        {
            switch (t.gameObject.name)
            {
                case "All":
                    selectType = 0;
                    break;
                case "consumables":
                    selectType = 1;
                    break;
                case "equip":
                    selectType = 2;
                    break;

                default:
                    break;
            }
        }

        //Debug.Log("selectType:" + selectType);
        List<ItemServerData> list = ItemDynamicDataManger.Instance.GetItemByType(selectType);
        BackPackPanel.Instance.ShowItems(list);
    }
}

第三步:Content中新增ToggleGroup组件,并拖入All/equip/consumables中,三个ToggleGroup成员的OnValueChanged组件记得添加新增的事件
Unity MVC设计模式与UI背包界面制作_第10张图片
筛选道具功能完成效果:
Unity MVC设计模式与UI背包界面制作_第11张图片

3.为背包添加整理功能

  • 当玩家在道具栏下方点击”整理“按钮后,整理道具栏中道具间的空位

上文已分析过,第五部分包括:一个button组件(V),实现物品栏的简单整理(C)

在添加整理功能前,为了方便展示功能,先加入一个可以通过鼠标拖动来改变道具位置的方法DragImage.cs,详细方法请移步站内优秀文章: Unity拖动背包物品/技能图标位置互换
DragImage

第一步:分析新增需求,因为道具栏中显示的道具和道具的数量并未发生改变,所以和在动态数据类ItemDynamicDataManger.cs中,无需新增方法

第二步:在层级图中,找到对应第五部分的UI效果图中,Clear部分的游戏物体,新增ClearCellsController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//挂载到 Clear
public class ClearCellsController : MonoBehaviour
{
    public Transform Content;
    private GameObject Cell_Empty;

    //整理
    //删除背包空显示
    public void ClearCells()
    {
        int deleteNum = 0;
        for (int i = 0; i < Content.childCount; i++)
        {
            Transform t = Content.GetChild(i);
            if (t.childCount == 0)
            {
                Destroy(t.gameObject);
                deleteNum++;
            }
        }
        //用空格填补移走的位置
        for (int i = 0; i < deleteNum; i++)
        {
            if (Cell_Empty == null)
            {
                Cell_Empty = Resources.Load<GameObject>("Prefabs/UI/ItemCell_Empty");
            }

            GameObject Item_Empty = Instantiate(Cell_Empty, Content);
            Item_Empty.transform.localScale = Vector3.one;
            Item_Empty.transform.localPosition = Vector3.zero;
            Item_Empty.name = Cell_Empty.name;
        }
    }
}

完成代码后记得给按钮添加事件

整理功能完成效果:
Unity MVC设计模式与UI背包界面制作_第12张图片

写在最后:

通过第四部分为我们搭建好的UI背包来新增的三个功能,不难看出,在MVC设计模式的基础下写好的脚本中,每次新增功能,我们只需要考虑:

  • Model部分的数据是否发生了改变(也就是是否需要新增或改变方法),View部分的内容是否需要跟着进行改变

  • 在View部分并无新增显示时(这里指已经搭建好的UI模版未发生新增游戏物体),如何通过对Controller部分类的改进或者新增,实现View部分的实时跟进,而无需对View层进行直接的改动,实现了M-V的代码分离

  • 在View部分有新增显示时,也就是UI层中新增了游戏物体,比如本次案例中,新增点击道具图标或鼠标悬停显示道具的描述信息,那么就需要我们首先在层级图中搭建新的UI组件,并且去View部分的代码中新增这部分的显示,再根据上边的步骤来进行Model部分和Controller部分的改进或新增

所以可以总结,在MVC设计模式下的UI背包系统,具有:

  • 结构清晰,耦合性低
  • 如果MVC三部分由不同的开发人员完成,View部分人员在拿到需求后对UI框架和View部分代码进行新增,Model部分和Controller部分的人员只需根据新增的游戏物体去代码中实现对应的新增即可,使得开发人员分工明确,提高开发的效率
  • 后期维护更加方便,降低了维护成本,如果出现BUG,只需到对应游戏物体的MVC部分分别进行检查。

你可能感兴趣的:(Unity实战,游戏开发,unity,mvc,ugui)