Siki学院换装项目的MVC实现
引子
最近找工作,闲来无事翻了翻siki教程,其中有一份针对初学者的换装系统教学,大致看完以后发现其中存在两个问题——当然这些问题的存在并不会影响功能的实现,毕竟这个视频教程针对的对象是初学者,简单的脚本对于初学者更容易学习,大概讲课老师也是这样的想法。
我突然想到最近大火的《太吾绘卷》,半路出家的语文老师做游戏,上万个if分支最后搭成了一整个世界,如今听说已经招聘了程序对代码进行重构,希望它早日成功吧。
对它进行MVC实现的想法是突然涌上来的,最近找工作并不算太顺利,发现自己这一年来的技能树点得有点歪,加之已经有半月没写过Unity代码,便决定花一天时间完成这件事,顺便作为第一篇技术分享博客。
关于MVC:MVC框架参考链接
该项目存在的两个问题
1.代码结构不清晰,非对象本身的数据和对其他对象的操作函数全部挤在同一个类中,例如角色对象上的脚本“AvatarSys.cs”,其中既包含角色数据,又包含角色修改方法,同时还对UI面板进行了操作,并且在一个功能十分简单的模块中使用了单例——个人实在是对单例模式没多大好感,这种强耦合对于模块独立性的友好度极差,非必要情况尽量不要使用单例模式;
操作对象太多 数据和功能混杂2.UI实现的重复度过高,角色身上五个位置的操作其实没有什么区别,但在绘制UI上却花掉了大量时间。
UI重复度高 UI重复度高3.UI直接引用数据内容,导致扩展操作不便。因为大量的功能重复的UI图片都是直接手动配置,在实际项目中,一旦出现需要替换图片、增加资源或者遇到其他需求,那么必然导致工作量巨大。
UI面板功能繁复MVC框架思路
原项目的相关脚本只有四个,几乎所有的逻辑功能都挤在了AvatarSys
这个类中,虽然编写思路清晰,但难免臃肿,假设这个换装系统再加几个身体部位,甚至角色信息或者不同的职业,这个脚本就会更加膨胀,对UI的修改量也会变得更加巨大,因此针对这两个问题,解决办法分别如下:
1.将数据和表现分离,这与MVC框架的理念不谋而合——Controller只是一个操作器,起粘连作用,如果是不同的Controller可以产生不同的表现,这是一种策略模式的变体。
2.将UI功能独立出来,另写一个简单的框架,减少面板数量,动态载入图标并且绑定事件。
首先对这个系统进行分析,以MVC为框架,我们可以知道需要三个基本构件:数据实体(Model)、视图(View)、控制器(Controller),玩家通过使用控制器对数据实体进行修改,接下来由数据实体的修改对视图形成修改,这样就完成了三个层的独立性,玩家不会直接控制View界面的显示,而是由Model来决定如何显示。
画出这个换装系统的构件图:
换装系统MVC框架结构图如上图所示,View组件和Model组件之前不存在直接调用关系,全部通过Controller进行操作,Controller同时依赖于View组件和Model组件。View视图分为两部分,一部分是角色模型的显示,另一部分是UI显示,UIView既是显示入口,也是Controller的操作入口。
代码实现思路
我们对每个构件单独进行实现,首先从相互解耦的Model和View开始。
方便起见我们先定义几个常量
public class RoleDefine
{
//性别
public const string female = "female";
public const string male = "male";
//部位
public const string hair = "hair";
public const string eyes = "eyes";
public const string top = "top";
public const string pants = "pants";
public const string shoes = "shoes";
}
1.角色实体(Model)
Model组件对外提供两个方法,一个方法用于修改性别,另一个方法用于修改服装,对应地提供两个事件用于外界系统监听,当调用修改性别和修改服装的方法时对应地触发这两个事件,而Model本身不关心外界做了什么。
进入到具体实现,我们建立RoleModel
类用于对外提供数据的存储和监听服务,再建立RoleData
类用于存放数据。
Model组件UML类图如下:
imageRoleModel
类对外提供两个方法供调用,ChangeSex
用于切换当前角色性别,调用此方法时会同时修改curData
的引用,若传入的性别为男性则指向male
,反之指向female
,ModelDressChange
用于切换当前角色服装。
public class RoleModel
{
public RoleModel()
{
this.male = new RoleData(RoleDefine.male);
this.female = new RoleData(RoleDefine.female);
this.curData = female;
}
private RoleData male;
private RoleData female;
private RoleData curData;
public RoleData CurData { get { return curData; } }
public System.Action<string, string> onModeDataChange;
public System.Action<string> onSexChange;
public void ModelDressChange(string bodyPart, string assistantName)
{
curData.SetData(bodyPart, assistantName);
if (onModeDataChange != null) onModeDataChange(bodyPart, assistantName);
}
public void ChangeSex(string sex)
{
if (sex == "male")
curData = male;
else
curData = female;
if (onSexChange != null) onSexChange(sex);
}
}
当然还有我们的RoleData
类,RoleData
是个普通的数据类,提供数据的存储和提取。
这里我继承了IEnumerable
接口,方便对所有的服装进行遍历,sex
参数是可以在外部直接调用的,由于项目中只有男女两名角色,所以初始化时只有一个参数,如果项目较大,例如还有多种职业、多个种族等,只要增加对应的参数,就都可以在初始化时进行设定,不仅限于基础的参数,也可以使用更复杂的结构体或者类等。
public class RoleData : IEnumerable
{
public RoleData(string sex)
{
this.sex = sex;
hair = "1";
eyes = "1";
top = "1";
pants = "1";
shoes = "1";
}
public string sex { get; private set; }
private string hair;
private string eyes;
private string top;
private string pants;
private string shoes;
public void SetData(string bodyPart, string assistantName)
{
switch (bodyPart)
{
case RoleDefine.hair: hair = assistantName; break;
case RoleDefine.eyes: eyes = assistantName; break;
case RoleDefine.top: top = assistantName; break;
case RoleDefine.pants: pants = assistantName; break;
case RoleDefine.shoes: shoes = assistantName; break;
}
}
public string GetData(string part)
{
switch (part)
{
case RoleDefine.hair: return hair;
case RoleDefine.eyes: return eyes;
case RoleDefine.top: return top;
case RoleDefine.pants: return pants;
case RoleDefine.shoes: return shoes;
}
return "";
}
public IEnumerator GetEnumerator()
{
Dictionary<string, string> dir = new Dictionary<string, string>();
dir.Add(RoleDefine.hair, hair);
dir.Add(RoleDefine.eyes, eyes);
dir.Add(RoleDefine.top, top);
dir.Add(RoleDefine.pants, pants);
dir.Add(RoleDefine.shoes, shoes);
return dir.GetEnumerator();
}
}
角色实体定义完毕,现在我们只要调用对应的方法就可以对RoleModel
进行角色存储和相关方法了。注意这里的两个事件是完全对外公开的,外部有全部的操作权限,在必要的情况下可以再对其进行private
处理,增加两个方法分别用于清理和添加监听。
2.视图(View)
我们的视图分为两部分,一部分是角色模型视图,另一部分是UI视图,在这个项目中我并没有将它合成一个整体,在必要的情况下,可以将所有的视图统一放入一个服务管理类中。
-
角色模型视图
我们的角色模型是场景中的特定物体,因此我们需要一个继承自MonoBehaviour
的脚本来对模型进行操作。
在拥有对模型的操作能力后,我们需要一个视图来进行展示,外部可以对它的参数进行修改,在这个项目中,人物模型有性别、部位和具体服装 三个不同的参数;此外,作为一个视图,它还需要有一个用于更新显示的方法。
画出UML类图:
角色模型视图类图
进入代码环节,我们建立一个角色操作类Character
,它负责直接对模型进行操作,对外部有两个方法,InitCharacterParts()
是初始化方法,通过遍历角色身上的SkinnedMeshRenderer
来初始化角色,这个项目中此方法没有参数,在更复杂的项目中也可以根据模型的具体情况传入参数进行初始化。
SetCharacter(string partName, string assistantName)
是外部操作模型改动的唯一方法,传入角色部位和具体编号,对模型进行改动。
实现代码如下:
//对模型直接操作的角色类
public class Character : MonoBehaviour
{
private Dictionary<string, Dictionary<string, GameObject>> bodyParts;
private Animation anim;
private void Awake()
{
anim = GetComponent<Animation>();
}
private void PlayAct(string partName)
{
switch (partName)
{
case RoleDefine.top:
anim.CrossFade("item_shirt");
anim.CrossFadeQueued("idle1");
break;
case RoleDefine.pants:
anim.CrossFade("item_pants");
anim.CrossFadeQueued("idle1");
break;
case RoleDefine.shoes:
anim.CrossFade("item_boots");
anim.CrossFadeQueued("idle1");
break;
}
}
//外部操作唯一接口
public void SetCharacter(string partName, string assistantName)
{
Dictionary<string, GameObject> part;
if (bodyParts.TryGetValue(partName, out part))
{
foreach (var partDic in part)
{
partDic.Value.SetActive(partDic.Key == assistantName);
}
}
PlayAct(partName);
}
//初始化角色模型
public void InitCharacterParts()
{
bodyParts = new Dictionary<string, Dictionary<string, GameObject>>();
SkinnedMeshRenderer[] meshs = GetComponentsInChildren<SkinnedMeshRenderer>();
foreach (var mesh in meshs)
{
string[] nameSplits = mesh.name.Split('-');
if (nameSplits.Length < 2) continue;
Dictionary<string, GameObject> innerParts;
if (!bodyParts.TryGetValue(nameSplits[0], out innerParts))
{
innerParts = new Dictionary<string, GameObject>();
bodyParts.Add(nameSplits[0], innerParts);
}
innerParts.Add(nameSplits[1], mesh.gameObject);
}
foreach (var part in bodyParts)
{
int index = 0;
foreach (var item in part.Value)
{
item.Value.SetActive(index == 0);
index++;
}
}
}
}
实现对模型的操作后,我们开始编写更上层的代码,通过RoleView
来控制Character
对模型进行操作。
建立RoleView
类,初始化时传入一个Transform
,这是用于载入模型的定位,这里我已经制作好了角色模型,直接通过Resources.Load
方法载入,在其他项目中可以将这个Transform
参数换成其他,比如直接传入两个GameObject
或者传入一个地址列表等。
为了保证显示,初始化完成后立即调用UpdateView
更新视图。
由于RoleView
身上没有其他操作,所以仅有入口,没有数据出口,与角色实体RoleModel
对应,这个类也有两个方法,一个用于修改性别,另一个用于修改部位和具体服装 ,由于Character
对模型的修改是实时的,而我们需要更新和监听分开,因此对于这三个参数我们分别使用一个值来对它进行缓存,在调用UpdateView
时才对视图进行更新。
public class RoleView
{
public RoleView(Transform parent)
{
LoadModels(parent);
UpdateView();
}
private string curSex = RoleDefine.female;
private string curPartName = RoleDefine.hair;
private string curAssistantName = "1";
private Character male;
private Character female;
private void LoadModels(Transform parent)
{
GameObject go = GameObject.Instantiate(Resources.Load<GameObject>("FemaleModel"), parent);
female = go.AddComponent<Character>();
go = GameObject.Instantiate(Resources.Load<GameObject>("MaleModel"), parent);
male = go.AddComponent<Character>();
female.InitCharacterParts();
male.InitCharacterParts();
}
//只做数据缓存,不立即更新
public void SetCharacter(string partName, string assistantName)
{
curPartName = partName;
curAssistantName = assistantName;
}
//只做数据缓存,不立即更新
public void SetSex(string sex)
{
curSex = sex;
}
//只在此方法中进行更新
public void UpdateView()
{
Character choosed;
if (curSex == "male")
{
choosed = male;
male.gameObject.SetActive(true);
female.gameObject.SetActive(false);
}
else
{
choosed = female;
male.gameObject.SetActive(false);
female.gameObject.SetActive(true);
}
choosed.SetCharacter(curPartName, curAssistantName);
}
}
使用上面的两个类,我们实现了模型视图的操作,外部操作和具体的模型无关,模型被封闭了起来,只有通过RoleView
才能进行操作。
-
UI视图
UI视图和角色视图存在一定区别,在这个换的项目中,玩家的操作是针对于UI的,因此UI视图既包含了数据的输出,也存在对UI的显示刷新。
首先分析一下UI的结构,在原项目中, 每个部位对应了一整页的UI,所有的连接关系都通过手拖动来调整,这个项目中一共有两种性别,每个性别分为五个部位,除了眼睛是三种外,每种部位都有六种不同的变装,这意味着必须手动调整2*(4*6+1*3)=54
个UI框,这个量是非常巨大的,再算上相互之间的拖拉连接操作,一旦连错一点,修改会非常麻烦;万一项目需要扩展,在此基础上加上六个职业,那么这个操作更加费时费力,UI也会变得非常臃肿,查找麻烦。因此,将UI划分为三个区域,全部动态生成,这样只需要通过外部的配置表进行修改即可。
具体分布方式可以根据项目需要进行排版,由于与框架设计无关,就不细细记录了。
区域划分图
区域划分图
接下来根据面向对象的迪米特原则,我们要将这三个区域尽可能地设计成为相对独立的模块。
1.对部位区域的功能进行分析。可以注意到,这一块在新的思路中不再是多个固定不同的信息,而变成了一块专用的显示面板,这一块的信息是自从初始化后就不会再改变的,它的功能是通过点击向右边服装区域发送信息,使其显示对应的内容;同时,虽然是自从初始化后就不再改变,但它实际上是只刷新了一次。由于是多选一,所以我们可以使用ToggleGroup
组件和Toggle
组件配合,使用一个Toggle
数组存储Toggle对象 。在初始化时传入一个Action<string>
作为被选中时返回的回调,这样就实现了它的功能,当然同时传入的还有每个toggle的数据信息。
2.对服装区域的功能进行分解,与部位区域类似,它也是一块显示区域,但它会根据左边部位区域和右边下方性别区域的选择不同而显示不同的信息,所以我们需要使用一个List来存储toggle;另一个与左边部位区域不相同的是,它的显示内容是图片而不是文字,具体的图片不方便定位,所以我们建立另一个类来辅助这个功能,它起一个连接作用,同时对每个toggle的数据和事件绑定也可以放到单个方法里来做。
3.对右边下方性别区域的功能进行分解,可以知道,它的显示内容格式和右边服装区域的块是基本一致的,但它和左边部位区域类似的地方是只刷新一次并且只有数据向外并没有其他向内的操作,而与右边服装区域类似的地方是显示单元相同,所以我们可以使用与部位区域相同的类来实现这一区域的功能,只需要将刷新方法独立出来,不再调用即可。
根据以上分析,我们可以画出下面的类图:
UIView
直接对外进行沟通,而内部使用UIToggle
类操作左边部位区域,使用ChooseArea
类操作右边的两块区域。为了实现图片定位,我们建立AssistantToggle
类来进行辅助。此处将AssistantToggle
组件制成Prefab,需要时直接加载即可。
以下是四个类的代码。
注意:
- 由于
UIView
既负责了一部分输出,又需要负责一部分显示内容,为了分离更新操作,我们采用与RoleView
同样的方法,使用三对string参数 - SetXXX(string) 函数
来缓冲数据,单独使用一个UpdateView()
方法进行调用更新。
2.对外输出数据的部分与RoleModel
类似,使用事件对外提供监听接口,不考虑外部所做的具体操作;由于三个区域独立运行,所以使用三个Action<string>
事件分别表示触发性别、部位和具体服装 的点击。
UIView类,直接与外界沟通,封闭几个组件的操作
public class UIView
{
private Dictionary<string, Dictionary<string, Sprite>>[] imageDatas;
private ChooseArea chooseArea;
private ChooseArea sexArea;
private UIToggle uiToggle;
public UIView(UIToggle uiToggle, ChooseArea chooseArea, ChooseArea sexArea, Dictionary<string, string> partKeys, Dictionary<string, Dictionary<string, Sprite>>[] imageDatas, Dictionary<string, Sprite> sexDatas)
{
this.imageDatas = imageDatas;
this.uiToggle = uiToggle;
this.chooseArea = chooseArea;
this.sexArea = sexArea;
sexArea.InitToggles(sexDatas, curSex, ActiveSex);
uiToggle.InitToggles(partKeys, AcitvePart);
foreach (var part in partKeys)
{
curPart = part.Value;
chooseArea.InitToggles(imageDatas[0][part.Value], curAssitant, ActiveAssitant);
break;
}
}
#region 缓冲数据
private string curPart;
public void SetPart(string part)
{
curPart = part;
}
private string curSex = RoleDefine.female;
public void SetSex(string sex)
{
curSex = sex;
}
private string curAssitant = "1";
public void SetAssitant(string assistant)
{
curAssitant = assistant;
}
#endregion
#region 外部监听事件
public System.Action<string> onSetPart;
public System.Action<string> onSetChoose;
public System.Action<string> onSetSex;
private void ActiveAssitant(string assistant)
{
if (onSetChoose != null) onSetChoose(assistant);
}
private void AcitvePart(string part)
{
if (onSetPart != null) onSetPart(part);
}
private void ActiveSex(string sex)
{
if (onSetSex != null) onSetSex(sex);
}
#endregion
//刷新方法
public void UpdateView()
{
Dictionary<string, Sprite> datas;
if (curSex == RoleDefine.female)
{
datas = imageDatas[0][curPart];
}
else
{
datas = imageDatas[1][curPart];
}
chooseArea.InitToggles(datas, curAssitant, ActiveAssitant);
}
}
UIToggle类,由UIView调用
public class UIToggle : MonoBehaviour
{
private ToggleGroup tg;
private Toggle[] toggles;
private void Awake()
{
tg = GetComponent<ToggleGroup>();
}
public void InitToggles(Dictionary<string, string> keys, System.Action<string> onToggleActive)
{
if (keys.Count == 0 || onToggleActive == null) return;
Toggle toggle = Resources.Load<Toggle>("ChooseToggle");
toggles = new Toggle[keys.Count];
int i = 0;
foreach (var key in keys)
{
toggles[i] = Instantiate(toggle, transform);
toggles[i].isOn = false;
toggles[i].onValueChanged.AddListener((x) => { if (x) onToggleActive(key.Value); });
toggles[i].group = tg;
Text text = toggles[i].GetComponentInChildren<Text>();
text.text = key.Key;
i++;
}
toggles[0].isOn = true;
}
public void ReleaseToggles()
{
for (int i = 0; i < toggles.Length; i++)
{
toggles[i].onValueChanged.RemoveAllListeners();
}
}
}
ChooseArea类,供UIView调用
public class ChooseArea : MonoBehaviour
{
private AssistantToggle toggleSample;
private ToggleGroup tg;
private List<AssistantToggle> toggles;
private void Awake()
{
toggleSample = Resources.Load<AssistantToggle>("AssistantToggle");
tg = GetComponent<ToggleGroup>();
toggles = new List<AssistantToggle>();
}
public void InitToggles(Dictionary<string, Sprite> datas, string activeKey, System.Action<string> onToggleActive)
{
int index = 0;
foreach (var data in datas)
{
if (index >= toggles.Count)
{
AssistantToggle toggle = Instantiate(toggleSample, transform);
toggles.Add(toggle);
}
toggles[index].gameObject.SetActive(true);
toggles[index].InitToggle(tg, data.Value, data.Key == activeKey, (x) => { if (x) onToggleActive(data.Key); });
index++;
}
for (; index < toggles.Count; index++)
{
toggles[index].gameObject.SetActive(false);
}
}
}
public class AssistantToggle : MonoBehaviour
{
[SerializeField]
private Toggle toggle;
[SerializeField]
private Image map;
public void InitToggle(ToggleGroup tg, Sprite sprite, bool state, UnityAction<bool> onToggleActive)
{
toggle.onValueChanged.RemoveAllListeners();
toggle.group = tg;
toggle.isOn = state;
map.sprite = sprite;
toggle.onValueChanged.AddListener(onToggleActive);
}
}
至此,我们完成了两个视图的搭建,接下来要做的就是使用Controller
将它们结合起来了。
3.控制器(Controller)
MVC框架本身是由几种设计模式有机组合起来的,例如在视图和模型层中都通过事件进行监听来进行数据传输其本质是观察者模式,而控制器层的本质是一种策略模式。
通过上面两部分的代码分析和编写我们可以很清楚地知道,视图和模型这两层之间没有任何耦合关系,它们本身都各自是独立的模块,所以在这个项目中,决定项目功能如何实现的就是这个Controller
类,假设我们还有一个Controller_2
类,它和Controller
都派生自同一个接口IController
,那么只要我们能调用这个接口的实例,就能随意更换不同的项目实现——而这正与策略模式的使用不谋而合,只要模型和视图层功能能满足,当我们需要一些特定功能时,只需要调整Controller
类就好了,不需要对模型和视图层做任何修改。
结合View和Model层,画出我们的类图,这时可以在类图上明显地看到,Model和View层之间没有任何的耦合,我们不仅可以随意更换Controller,甚至可以随意更换Model和View视图中对应的类,扩展性非常强。
下面我们来实现我们的Controller
类
在这个项目中,一共有两个View、一个Model,初始化时将它们传入Controller中
,由Controller
完成各个功能的连接。
Controller中有五个方法,OnUIViewSetPart
、OnUIViewSetChoose
和OnUIViewSetSex
分别监听UIView中玩家的操作,OnRoleDataChange
和OnRoleSexChange
分别监听RoleModel中数据的变化,并且在方法中根据需要操作roleModel
、roleview
和UIview
三个对象,经过合理连接,就实现了想要的功能。
public class Controller
{
private RoleModel roleModel;
private RoleView roleview;
private UIView UIview;
public Controller(RoleModel roleModel, RoleView roleview, UIView UIview)
{
this.roleModel = roleModel;
this.roleview = roleview;
this.UIview = UIview;
curPart = RoleDefine.hair;
//设定监听内容
this.roleModel.onModeDataChange = OnRoleDataChange;
this.roleModel.onSexChange = OnRoleSexChange;
this.UIview.onSetSex = OnUIViewSetSex;
this.UIview.onSetPart = OnUIViewSetPart;
this.UIview.onSetChoose = OnUIViewSetChoose;
}
#region 视图(View)数据监听
private string curPart;
//监听UI点击part操作
private void OnUIViewSetPart(string part)
{
//由于没有进行具体服装选择,所以此时缓存部位参数
curPart = part;
//UI切换部位选择,仅有UI服装部位视图发生改变和更新
UIview.SetPart(part);
//从模型中获取数据,获取当前模型对应部位的数据
UIview.SetAssitant(roleModel.CurData.GetData(part));
//刷新UI视图
UIview.UpdateView();
}
//监听UI点击assistant操作
private void OnUIViewSetChoose(string assistant)
{
//UI切换具体服装选择时,对模型数据进行修改
roleModel.ModelDressChange(curPart, assistant);
}
//监听UI点击sex操作
private void OnUIViewSetSex(string sex)
{
//UI切换性别选项时,对模型数据进行修改
roleModel.ChangeSex(sex);
}
#endregion
#region 模型(Model)数据监听
//监听模型数据改动
private void OnRoleDataChange(string part, string assistant)
{
//模型数据中Part和assistant发生修改时,需要对UI视图中part和assitant数据进行修改
UIview.SetPart(part);
UIview.SetAssitant(assistant);
//与UI视图类似,要对RoleView数据也进行修改
roleview.SetCharacter(part, assistant);
//数据都修改完成后进行更新
roleview.UpdateView();
UIview.UpdateView();
}
//监听模型数据改动
private void OnRoleSexChange(string sex)
{
//模型数据中sex发生修改,需要对UI视图中sex进行修改
UIview.SetSex(sex);
//Sex发生变化后,part还保持原来显示,assistant保持与数据同步
UIview.SetAssitant(roleModel.CurData.GetData(curPart));
//同样地对角色视图进行更新
roleview.SetSex(sex);
//数据都修改完成后进行更新
roleview.UpdateView();
UIview.UpdateView();
}
#endregion
}
功能入口
至此,我们完成了这个框架的全部组件,但是只是这样,它还跑不起来,我们还需要调用这个方法。
建立Launcher
类,由它来负责启动我们的框架,我们的UIToggle等数据也可以存放在它下面存在于场景中,手动将需要的内容绑定在其上,也可以将它作为一个Prefab,在需要的时候只要将它载入,即可运行。
以下是实现代码,其中LoadSprite()
、LoadPartKeys()
和LoadSexSprites()
数据内容在实际项目中可以通过数据库、XML、Json等多种方式导入,此处直接手写了,也没有仔细对照角色衣物是否对应。
public class Launcher : MonoBehaviour
{
private Controller controller;
public Transform roleParent;
public ChooseArea chooseArea;
public ChooseArea sexArea;
public UIToggle uiToggle;
void Start()
{
RoleView rv = new RoleView(roleParent);
RoleModel rm = new RoleModel();
UIView UIview = new UIView(uiToggle, chooseArea, sexArea, LoadPartKeys(), LoadSprite(), LoadSexSprites());
controller = new Controller(rm, rv, UIview);
}
//载入UIView需要的数据信息,实际使用时可以通过数据库、XML、json等方式载入
private Dictionary<string, Dictionary<string, Sprite>>[] LoadSprite()
{
//载入女性角色UI图
Dictionary<string, Dictionary<string, Sprite>> female = new Dictionary<string, Dictionary<string, Sprite>>();
female.Add("hair", new Dictionary<string, Sprite>());
female["hair"].Add("1", Resources.Load<Sprite>("UISprites/girl/hair/hair_brown"));
female["hair"].Add("2", Resources.Load<Sprite>("UISprites/girl/hair/hair_cyan"));
female["hair"].Add("3", Resources.Load<Sprite>("UISprites/girl/hair/hair_dark"));
female["hair"].Add("4", Resources.Load<Sprite>("UISprites/girl/hair/hair_pink"));
female["hair"].Add("5", Resources.Load<Sprite>("UISprites/girl/hair/hair_red"));
female["hair"].Add("6", Resources.Load<Sprite>("UISprites/girl/hair/hair_yellow"));
female.Add("eyes", new Dictionary<string, Sprite>());
female["eyes"].Add("1", Resources.Load<Sprite>("UISprites/girl/eyes/eyes_blue"));
female["eyes"].Add("2", Resources.Load<Sprite>("UISprites/girl/eyes/eyes_brown"));
female["eyes"].Add("3", Resources.Load<Sprite>("UISprites/girl/eyes/eyes_green"));
female.Add("top", new Dictionary<string, Sprite>());
female["top"].Add("1", Resources.Load<Sprite>("UISprites/girl/top/top_blue"));
female["top"].Add("2", Resources.Load<Sprite>("UISprites/girl/top/top_green"));
female["top"].Add("3", Resources.Load<Sprite>("UISprites/girl/top/top_green2"));
female["top"].Add("4", Resources.Load<Sprite>("UISprites/girl/top/top_orange"));
female["top"].Add("5", Resources.Load<Sprite>("UISprites/girl/top/top_pink"));
female["top"].Add("6", Resources.Load<Sprite>("UISprites/girl/top/top_purple"));
female.Add("pants", new Dictionary<string, Sprite>());
female["pants"].Add("1", Resources.Load<Sprite>("UISprites/girl/pants/pants_black"));
female["pants"].Add("2", Resources.Load<Sprite>("UISprites/girl/pants/pants_blue"));
female["pants"].Add("3", Resources.Load<Sprite>("UISprites/girl/pants/pants_blue1"));
female["pants"].Add("4", Resources.Load<Sprite>("UISprites/girl/pants/pants_dark"));
female["pants"].Add("5", Resources.Load<Sprite>("UISprites/girl/pants/pants_green"));
female["pants"].Add("6", Resources.Load<Sprite>("UISprites/girl/pants/pants_orange"));
female.Add("shoes", new Dictionary<string, Sprite>());
female["shoes"].Add("1", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_blue"));
female["shoes"].Add("2", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_blue1"));
female["shoes"].Add("3", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_green"));
female["shoes"].Add("4", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_red"));
female["shoes"].Add("5", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_yellow"));
female["shoes"].Add("6", Resources.Load<Sprite>("UISprites/girl/shoes/shoes_yellow1"));
//载入男性角色UI图
Dictionary<string, Dictionary<string, Sprite>> male = new Dictionary<string, Dictionary<string, Sprite>>();
male.Add("hair", new Dictionary<string, Sprite>());
male["hair"].Add("1", Resources.Load<Sprite>("UISprites/boy/hair/hair_blond"));
male["hair"].Add("2", Resources.Load<Sprite>("UISprites/boy/hair/hair_blond1"));
male["hair"].Add("3", Resources.Load<Sprite>("UISprites/boy/hair/hair_brown"));
male["hair"].Add("4", Resources.Load<Sprite>("UISprites/boy/hair/hair_brown1"));
male["hair"].Add("5", Resources.Load<Sprite>("UISprites/boy/hair/hair_orange"));
male["hair"].Add("6", Resources.Load<Sprite>("UISprites/boy/hair/hair_orange1"));
male.Add("eyes", new Dictionary<string, Sprite>());
male["eyes"].Add("1", Resources.Load<Sprite>("UISprites/boy/eyes/eyes_blue"));
male["eyes"].Add("2", Resources.Load<Sprite>("UISprites/boy/eyes/eyes_brown"));
male["eyes"].Add("3", Resources.Load<Sprite>("UISprites/boy/eyes/eyes_green"));
male.Add("top", new Dictionary<string, Sprite>());
male["top"].Add("1", Resources.Load<Sprite>("UISprites/boy/top/top_blue"));
male["top"].Add("2", Resources.Load<Sprite>("UISprites/boy/top/top_gray"));
male["top"].Add("3", Resources.Load<Sprite>("UISprites/boy/top/top_green"));
male["top"].Add("4", Resources.Load<Sprite>("UISprites/boy/top/top_orange"));
male["top"].Add("5", Resources.Load<Sprite>("UISprites/boy/top/top_pink"));
male["top"].Add("6", Resources.Load<Sprite>("UISprites/boy/top/top_yellow"));
male.Add("pants", new Dictionary<string, Sprite>());
male["pants"].Add("1", Resources.Load<Sprite>("UISprites/boy/pants/pants_blue"));
male["pants"].Add("2", Resources.Load<Sprite>("UISprites/boy/pants/pants_blue1"));
male["pants"].Add("3", Resources.Load<Sprite>("UISprites/boy/pants/pants_dark"));
male["pants"].Add("4", Resources.Load<Sprite>("UISprites/boy/pants/pants_green"));
male["pants"].Add("5", Resources.Load<Sprite>("UISprites/boy/pants/pants_lillac"));
male["pants"].Add("6", Resources.Load<Sprite>("UISprites/boy/pants/pants_purple"));
male.Add("shoes", new Dictionary<string, Sprite>());
male["shoes"].Add("1", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_black"));
male["shoes"].Add("2", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_brown"));
male["shoes"].Add("3", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_dark"));
male["shoes"].Add("4", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_green"));
male["shoes"].Add("5", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_red1"));
male["shoes"].Add("6", Resources.Load<Sprite>("UISprites/boy/shoes/shoes_red2"));
return new Dictionary<string, Dictionary<string, Sprite>>[] { female, male };
}
private Dictionary<string, string> LoadPartKeys()
{
Dictionary<string, string> partKeys = new Dictionary<string, string>();
partKeys.Add("头发", RoleDefine.hair);
partKeys.Add("眼睛", RoleDefine.eyes);
partKeys.Add("上衣", RoleDefine.top);
partKeys.Add("裤子", RoleDefine.pants);
partKeys.Add("鞋子", RoleDefine.shoes);
return partKeys;
}
private Dictionary<string, Sprite> LoadSexSprites()
{
Dictionary<string, Sprite> res = new Dictionary<string, Sprite>();
res.Add(RoleDefine.female, Resources.Load<Sprite>("UISprites/sexSelect/girl"));
res.Add(RoleDefine.male, Resources.Load<Sprite>("UISprites/sexSelect/boy"));
return res;
}
}
结语
相比原代码一共两百多行,使用框架改写后代码行数大大增加,但换来的是更佳的扩展性和更好的独立性,将数据和视图独立出来,大大降低了有新需求时对项目的修改难度,也增加了操作性。
但是话再说回来,搭建框架本身的复杂程度也大大增加了,在非必要的地方没有必要强行设计,开发时应该选择最合适的方法进行。
PS:这里实现的MVC框架由于比较简单,Model和View之间的交互完全由Controller控制,事实上已经可以算是MVC的优化版——MVP框架了,而这两种框架之间的区别也只在于View和Model之间是否存在直接的交互,同样地,这个交互并不涉及到依赖关系。