Siki学院换装项目的MVC实现

项目文件已上传github,如果有需要可以查看和下载:github链接

引子

最近找工作,闲来无事翻了翻siki教程,其中有一份针对初学者的换装系统教学,大致看完以后发现其中存在两个问题——当然这些问题的存在并不会影响功能的实现,毕竟这个视频教程针对的对象是初学者,简单的脚本对于初学者更容易学习,大概讲课老师也是这样的想法。

我突然想到最近大火的《太吾绘卷》,半路出家的语文老师做游戏,上万个if分支最后搭成了一整个世界,如今听说已经招聘了程序对代码进行重构,希望它早日成功吧。

对它进行MVC实现的想法是突然涌上来的,最近找工作并不算太顺利,发现自己这一年来的技能树点得有点歪,加之已经有半月没写过Unity代码,便决定花一天时间完成这件事,顺便作为第一篇技术分享博客。

关于MVC:MVC框架参考链接

该项目存在的两个问题

1.代码结构不清晰,非对象本身的数据和对其他对象的操作函数全部挤在同一个类中,例如角色对象上的脚本“AvatarSys.cs”,其中既包含角色数据,又包含角色修改方法,同时还对UI面板进行了操作,并且在一个功能十分简单的模块中使用了单例——个人实在是对单例模式没多大好感,这种强耦合对于模块独立性的友好度极差,非必要情况尽量不要使用单例模式;

6777308-84a42728e480081d

操作对象太多

6777308-2ebf8d92f01580b9

数据和功能混杂

2.UI实现的重复度过高,角色身上五个位置的操作其实没有什么区别,但在绘制UI上却花掉了大量时间。

6777308-6689d6f7b3a5fac1

UI重复度高

6777308-26e749ba1579ab02

UI重复度高

3.UI直接引用数据内容,导致扩展操作不便。因为大量的功能重复的UI图片都是直接手动配置,在实际项目中,一旦出现需要替换图片、增加资源或者遇到其他需求,那么必然导致工作量巨大。

6777308-f5907f9524099e06

UI面板功能繁复

MVC框架思路

原项目的相关脚本只有四个,几乎所有的逻辑功能都挤在了AvatarSys这个类中,虽然编写思路清晰,但难免臃肿,假设这个换装系统再加几个身体部位,甚至角色信息或者不同的职业,这个脚本就会更加膨胀,对UI的修改量也会变得更加巨大,因此针对这两个问题,解决办法分别如下:

1.将数据和表现分离,这与MVC框架的理念不谋而合——Controller只是一个操作器,起粘连作用,如果是不同的Controller可以产生不同的表现,这是一种策略模式的变体。
2.将UI功能独立出来,另写一个简单的框架,减少面板数量,动态载入图标并且绑定事件。

首先对这个系统进行分析,以MVC为框架,我们可以知道需要三个基本构件:数据实体(Model)、视图(View)、控制器(Controller),玩家通过使用控制器对数据实体进行修改,接下来由数据实体的修改对视图形成修改,这样就完成了三个层的独立性,玩家不会直接控制View界面的显示,而是由Model来决定如何显示。

画出这个换装系统的构件图:

6777308-5514b00515d67801

换装系统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类图如下:

6777308-2a919bde8927830b

image

RoleModel类对外提供两个方法供调用,ChangeSex用于切换当前角色性别,调用此方法时会同时修改curData的引用,若传入的性别为男性则指向male,反之指向femaleModelDressChange用于切换当前角色服装。


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视图,在这个项目中我并没有将它合成一个整体,在必要的情况下,可以将所有的视图统一放入一个服务管理类中。

  1. 角色模型视图
    我们的角色模型是场景中的特定物体,因此我们需要一个继承自MonoBehaviour的脚本来对模型进行操作。
    在拥有对模型的操作能力后,我们需要一个视图来进行展示,外部可以对它的参数进行修改,在这个项目中,人物模型有性别部位具体服装 三个不同的参数;此外,作为一个视图,它还需要有一个用于更新显示的方法。
    画出UML类图:

    6777308-405f066125729285.png

    角色模型视图类图

进入代码环节,我们建立一个角色操作类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才能进行操作。

  1. UI视图
    UI视图和角色视图存在一定区别,在这个换的项目中,玩家的操作是针对于UI的,因此UI视图既包含了数据的输出,也存在对UI的显示刷新。
    首先分析一下UI的结构,在原项目中, 每个部位对应了一整页的UI,所有的连接关系都通过手拖动来调整,这个项目中一共有两种性别,每个性别分为五个部位,除了眼睛是三种外,每种部位都有六种不同的变装,这意味着必须手动调整2*(4*6+1*3)=54个UI框,这个量是非常巨大的,再算上相互之间的拖拉连接操作,一旦连错一点,修改会非常麻烦;万一项目需要扩展,在此基础上加上六个职业,那么这个操作更加费时费力,UI也会变得非常臃肿,查找麻烦。因此,将UI划分为三个区域,全部动态生成,这样只需要通过外部的配置表进行修改即可。
    具体分布方式可以根据项目需要进行排版,由于与框架设计无关,就不细细记录了。

    6777308-ee73eca69ac494ba.png

    区域划分图

     

    6777308-1f5624d79a0232ad.png

    区域划分图

接下来根据面向对象的迪米特原则,我们要将这三个区域尽可能地设计成为相对独立的模块。
1.对部位区域的功能进行分析。可以注意到,这一块在新的思路中不再是多个固定不同的信息,而变成了一块专用的显示面板,这一块的信息是自从初始化后就不会再改变的,它的功能是通过点击向右边服装区域发送信息,使其显示对应的内容;同时,虽然是自从初始化后就不再改变,但它实际上是只刷新了一次。由于是多选一,所以我们可以使用ToggleGroup组件和Toggle组件配合,使用一个Toggle数组存储Toggle对象 。在初始化时传入一个Action<string>作为被选中时返回的回调,这样就实现了它的功能,当然同时传入的还有每个toggle的数据信息。
2.对服装区域的功能进行分解,与部位区域类似,它也是一块显示区域,但它会根据左边部位区域和右边下方性别区域的选择不同而显示不同的信息,所以我们需要使用一个List来存储toggle;另一个与左边部位区域不相同的是,它的显示内容是图片而不是文字,具体的图片不方便定位,所以我们建立另一个类来辅助这个功能,它起一个连接作用,同时对每个toggle的数据和事件绑定也可以放到单个方法里来做。
3.对右边下方性别区域的功能进行分解,可以知道,它的显示内容格式和右边服装区域的块是基本一致的,但它和左边部位区域类似的地方是只刷新一次并且只有数据向外并没有其他向内的操作,而与右边服装区域类似的地方是显示单元相同,所以我们可以使用与部位区域相同的类来实现这一区域的功能,只需要将刷新方法独立出来,不再调用即可。

根据以上分析,我们可以画出下面的类图:
UIView直接对外进行沟通,而内部使用UIToggle类操作左边部位区域,使用ChooseArea类操作右边的两块区域。为了实现图片定位,我们建立AssistantToggle类来进行辅助。此处将AssistantToggle组件制成Prefab,需要时直接加载即可。

6777308-a85b3f42d2910fdc.png

UIView类图

以下是四个类的代码。
注意:

  1. 由于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视图中对应的类,扩展性非常强。

6777308-070efae21bf12bdf.png

整体类图

下面我们来实现我们的Controller
在这个项目中,一共有两个View、一个Model,初始化时将它们传入Controller中,由Controller完成各个功能的连接。
Controller中有五个方法,OnUIViewSetPartOnUIViewSetChooseOnUIViewSetSex分别监听UIView中玩家的操作,OnRoleDataChangeOnRoleSexChange分别监听RoleModel中数据的变化,并且在方法中根据需要操作roleModelroleviewUIview三个对象,经过合理连接,就实现了想要的功能。

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,在需要的时候只要将它载入,即可运行。

6777308-4ef1050ad3896f3a.png

Launcher

 

以下是实现代码,其中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之间是否存在直接的交互,同样地,这个交互并不涉及到依赖关系。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值