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拖动背包物品/技能图标位置互换
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)是一种软件设计模式,通常用于开发用户界面,它将相关的程序逻辑划分为三个相互关联的元素。这样做是为了将信息的内部表示与向用户显示信息和从用户接受信息的方式分开。
根据构想图搭建UI视图,具体Hierachy面板层级部署如下:
(对比上文构想图)
————————————————————————————————————————————————————
新建Canvas搭建UI视图
根据上图五个部分拆分需要实现的功能
第一部分包括:两个button组件(V),控制BackPack层视图的开启和关闭(C)
第二部分(search)包括:一个图片,一个InputFiled组件(V),根据道具名称(M),搜索道具(C)
第三部分(Types)包括:一个Toggle组件(V),根据道具类型(M),筛选道具(C)
第四部分(Items)包括:一个Scrollbar组件,一个固定物品框图片(V),一个可变图标图片(M)
第五部分包括:一个button组件(V),实现物品栏的简单整理(C)
————————————————————————————————————————————————————
总体而言,我们需要通过传入Model到Controller相应去修改View
综合上述五部分:
同时需要注意:
使用MVC的目的是将M和V实现代码分离(理想情况),从而使同一个程序可以使用不同的表现形式。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。更好的调节M和V的搭配。
View里会包含Model信息,不可避免的还要包括一些业务逻辑。
在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,即View
————————————————————————————————————————————————————
建立两个类来装载道具的表数据和服务器数据,对应Model部分
下图为本次示例中的部分JSON格式表数据截图(放在Resources/JSON/Items目录下),根据表数据表头内容来编写静态数据脚本ItemStaticDataManager.cs
静态类数据代码 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;
}
}
背包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;
}
}
}
上边提到的根据静态类数据加载出每个道具的图标(C),这里注意图集名称和数据列表中一一对应
图标控制器 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 + "";
}
}
————————————————————————————————————————————————————
进行到这里就可以先运行项目查看一下图标的显示效果了(当然控制物品栏打开的操作这里依然没有讨论,需要的朋友还请自己添加)
如果只看上边完成的这部分UI,似乎展示背包中物品数据这样一个简单的功能,只要用一个类就能完成所有代码的书写,为什么还要如此大费周折,分析各个部分来进行MVC模式的设计呢?下边我们通过为上边完成的这个简单背包添加新的功能,来体验一下MVC模式下,开发UI背包的优点。
上文已分析过,第二部分(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部分 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);
}
}
上文已分析过,第三部分(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
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组件记得添加新增的事件
筛选道具功能完成效果:
上文已分析过,第五部分包括:一个button组件(V),实现物品栏的简单整理(C)
在添加整理功能前,为了方便展示功能,先加入一个可以通过鼠标拖动来改变道具位置的方法DragImage.cs,详细方法请移步站内优秀文章: Unity拖动背包物品/技能图标位置互换
第一步:分析新增需求,因为道具栏中显示的道具和道具的数量并未发生改变,所以和在动态数据类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;
}
}
}
完成代码后记得给按钮添加事件
通过第四部分为我们搭建好的UI背包来新增的三个功能,不难看出,在MVC设计模式的基础下写好的脚本中,每次新增功能,我们只需要考虑:
Model部分的数据是否发生了改变(也就是是否需要新增或改变方法),View部分的内容是否需要跟着进行改变
在View部分并无新增显示时(这里指已经搭建好的UI模版未发生新增游戏物体),如何通过对Controller部分类的改进或者新增,实现View部分的实时跟进,而无需对View层进行直接的改动,实现了M-V的代码分离
在View部分有新增显示时,也就是UI层中新增了游戏物体,比如本次案例中,新增点击道具图标或鼠标悬停显示道具的描述信息,那么就需要我们首先在层级图中搭建新的UI组件,并且去View部分的代码中新增这部分的显示,再根据上边的步骤来进行Model部分和Controller部分的改进或新增
所以可以总结,在MVC设计模式下的UI背包系统,具有: