小眼游戏架构:UI篇:组件化流程设计

需求

  1. 对于程序来说:我们要实现用最少的代码,实现最强大的效果。
  2. 对于美术来说:Prefab的随意调整,不需要程序的参与。

实现上述两个要求的方法就是:UI组件化
C#组件化类似,我们只需要在代码中,注册组件(一行代码),那么组件的功能就全部集成。
组件的功能:差异化显示交互数据更新
差异化显示:同一功能模块(比如:人物属性)出现在不同的地方做不同的显示。
交互:组件也能点击,拖拽等等。
数据更新:功能模块(比如:人物属性)在数据改变的时候,能够自动更新。
有了以上的基础对于前端程序来说,写系统就成了拼组件的人员了,这也极大的节约了时间和出错的机会,对于后期维护和升级来说异常的方便。

刚开始项目并没有采取这样的方式,比如人物属性,很多UI都要显示,但是你还不能直接复制,因为美术做了不同的表现,节点的位置都不一样了,所以每次都要重新写:三个地方需要显示人物属性就要写三次类似代码,后期的维护更加难受,因为一旦要改,就是所有地方都改。
真正可怕的地方是图标的显示,几乎所有界面都有,你想想,如果这个要改,你会怎么办?你肯定是这么办的:这个改动太大,需要时间。

现在我们知道了组件是实现上述功能的核心,接下来我们一步步的建立起组件化的流程。

差异化显示

先看一种表现:
在这里插入图片描述
对于上图有四个Label需要程序通过数据去显示,我们一般是这么做的:
在这里插入图片描述
是不是感觉到了很熟悉味道,我敢说你绝对这么搞过,但是这并不是一个好的方法,如果当前UI需要控制的控件有100个,你想想你的代码会是什么样子的?绝对是崩溃的!
上面都是Label那为何不用循环?不需要写成这么难看吧,于是就变成了下面的这样:
在这里插入图片描述
是简洁了很多,这里有两个问题:

  1. 因为都是Label组件所以你可以这样做,如果里面还有其他组件(UISprite,UIPanel),如果还想这样循环,就得加一个组件类型的列表了。
  2. 访问起来非常的蛋疼,你会看到self.labs[1],self.labs[2],这样的代码充满你的眼球,你不去看对应的列表根本就不知道这个代表什么,到时候你能分清self.labs[99]是啥?数都数不来。这样的方法只适用于需要控制少量UI控件的地方。

那应该如果优化?
我们只要通过路径去找节点,通过节点去找控件,就避免不了上面的问题。这里通过路径去找节点,还有一个非常坑的地方,如果Prefab的制作是美术人员,美术并不知道你用了哪个节点,在调整的时候,非常容易改变节点的路径,一旦改变,你就找不到了,你必须和美术同步修改,这太蛋疼了。

节点寻找的另外一种方式:就是引用
我们可以绑定一个脚本,然后将节点都拖上去,像下面这样:
在这里插入图片描述
通过引用的方式好处:

  1. 不在关心美术的制作Prefab的路径,所以美术可以自由大胆的飞翔了;
  2. 像前面的OnCreate里面的代码可以全部删除,那种写法太恶心了;
  3. 可以直接就找到对应节点的控件(UILabel、UIPanel、UISprite…),不必再GetComponent了;
上面的好处这么多,我们现在就来实现吧。

我们的框架使用的是Lua脚本,所以我们寻找的控件之后,最终是给Lua使用的。
在前面的教程中有说过Prefab加载完成之后就开始绑定获取控件脚本,当时没有说明具体原因,用处就在这个地方。
在这里插入图片描述
具体实现:绑定UI控件,并且注册到Lua脚本中。
在这里插入图片描述
代码并不复杂(文章最后面有完整代码),就是将Lua脚本传到C#中,然后赋值,这样Lua脚本就可以访问了。
这样我们就实现了,绑定UI控件,再也不用担心美术随便调整了(当然了不能删除控件)。
我们的代码也变得异常的简洁,因为去除了代码中通过路径去找节点的方式。
我们的写法就变成了:
在这里插入图片描述
这样可还满意?

上面说了控件是怎么获取的,现在说说控件的表现:

一般控件表现如下:
在这里插入图片描述
如果你有多个地方,那么就得多个地方赋值:
在这里插入图片描述
你有两个UI,数据是类似的,虽然通过控件绑定的方式去除了寻找控件的那一步,但是在我们编写代码的时候,还是在做重复的事情。
我们往上抽象出这两个显示:定义一个组件脚本。
在这里插入图片描述
Prefab的配置:在这里插入图片描述
调用方式改变从写法一变成写法二:
在这里插入图片描述
写法二就明显的简单了很多,self.playerCore是在节点Core上绑定的。
这样我们原本需要几十行的代码,这里我们只需要一行,我们不必关心实现的细节,以后就算有再多的地方,我们也根本不关心,不在意,随它去。
也许你并没有觉得提升很大,但是如果这个组件实现的需要绑定的UI控件特别的多,几百行,你就不会这样觉得了。
如果你还是觉得用处不大,那么接下来为组件增加功能:交互。

交互

在这里插入图片描述
给lvLab增加一个点击事件:
在这里插入图片描述
原先我们绑定事件的方式是:UIEventListener.Get(go).onClick的方式写在代码里面,这里因为有了UICore的绑定方式,所以可以直接绑定给Lua使用:
在这里插入图片描述
这样我们连注册事件的步骤都省略了。
策划说:要给战力Label增加一个点击事件,我们连代码都不用改,还是以前的代码(写法二):
在这里插入图片描述
这样交互就有了。
接下来我们将组件的更新也加进去,让其自成一体,不依赖使用者数据。

数据更新

在这里插入图片描述
我们加入了玩家数据更新,我们的使用者代码还是不用改,还是下面的(写法二):
在这里插入图片描述

总结

通过以上的修改,对于使用来说,只需要显示人物属性这个组件就行了,显示,交互,更新全部集成。这样你想想组件的功能越强大,给我们带来的好处就越大:

  1. 使用者使用简单,就一行代码;
  2. 美术想修改也不需要和程序说,直接在Prefab上根据当前系统修改即可,不同部门之间的工作尽量分开;
  3. 后期调整功能异常方便,就像使用的代码一直只有一行,但是我们却慢慢的加入了交互和数据更新。
知识点:
  1. 控件绑定(UICore.cs):给Lua提供直接访问节点控件的能力;
  2. 组件脚本(PlayerDisPlayPlugin.lua):将显示抽象出来,不关心表现;
完整PlayerDisPlayPlugin.lua:
---
--- 人物显示组件
---

PlayerDisplayPlugin = Class("PlayerDisplayPlugin");

-- 创建组件
function PlayerDisplayPlugin.Create(uiCore)
	-- 实例化组件
	local plugin = PlayerDisplayPlugin.New();
	-- 给组件绑定UI控件
	uiCore:Init(plugin);
	return plugin;
end

-- 构造函数
function PlayerDisplayPlugin:ctor()
	-- 注册更新消息
	RegisterMessage("UpdatePlayerData",self.UpdatePlayerData,self);
end

-- 玩家信息更新
function PlayerDisplayPlugin:UpdatePlayerData(msg)
	self.lvLab.text 	= "等级:2";
    self.bloodLab.text 	= "血量:2";
    self.powerLab.text 	= "战力:2";
    self.speedLab.text 	= "速度:2";
end

-- 显示
function PlayerDisplayPlugin:Show()
	self.lvLab.text 	= "等级:1";
    self.bloodLab.text 	= "血量:1";
    self.powerLab.text 	= "战力:1";
    self.speedLab.text 	= "速度:1";
end

-- 点击事件
function PlayerDisplayPlugin:ClickLvLab()
	
end

-- 创建并且显示
-- uiCore 绑定在prefab上的脚本
function PlayerDisplayPlugin.ShowDirect(uiCore)
	PlayerDisplayPlugin.Create(uiCore):Show();
end
完整UICore,cs:
using UnityEngine;
using System.Collections.Generic;
using LuaInterface;

public class UICore : MonoBehaviour {

    #region enum
    // 控件类型
    public enum ComponentType
    {
        Transform,
        GameObject,
        Panel,
        Label,
        Input,
        Button,
        Texture,
        Sprite,
        Progressbar,
        Toggle,
        BoxCollider,
        ScrollView,
        UICore,
        UIGrid,
        UIWidget,
        UIPlayTween,
        TweenScale,
        UITable,
        UISlider,
    }

    public enum EventType
    {
        Null,
        Click,
        Press,
    }
    #endregion

    #region class
    [System.Serializable]
    public class ParamEvent
    {
        public string EventCallBack;
        public EventType eventType = EventType.Null;
    }

    [System.Serializable]
    public class Param
    {
        public string name;
        public Transform transform;
        public ComponentType componentType = ComponentType.Transform;
        public List<ParamEvent> events = new List<ParamEvent>();

        public Param Clone()
        {
            return (Param)MemberwiseClone();
        }
    }

    [System.Serializable]
    public class ParamArray
    {
        public Param parent;
        public ParamArrayEle first;
    }

    [System.Serializable]
    public class ParamArrayEle
    {
        public Param root;
        public List<Param> childs;
    }

    #endregion

    #region menber
    public List<Param> param = new List<Param>();
    public List<ParamArray> paramArray = new List<ParamArray>();

    public List<Param> cacheParam = new List<Param>();
    #endregion

    #region method
    public void Init(LuaTable t)
    {
        if (t == null)
        {
            Debug.LogError("InitCore is error");
            return;
        }

        for(int i =0;i< cacheParam.Count; i++)
        {
            BindingWidget(t,cacheParam[i]);
        }
    }
    public void UnInit()
    {

    }

    public void BindingWidget(LuaTable t,Param param)
    {
        string name = param.name;
        Transform trans = param.transform;
        if (trans == null)
        {
            Debug.LogError("BindingWidget transform is null : ");
            return;
        }
        GameObject go = trans.gameObject;
        ComponentType componentType = param.componentType;
        // 引用绑定
        if (componentType == ComponentType.Transform)
        {
            t[name] = trans;
        }
        else if (componentType == ComponentType.Input)
        {
            t[name] = go.GetComponent<UIInput>();
        }
        else if (componentType == ComponentType.Label)
        {
            t[name] = go.GetComponent<UILabel>();
        }

        // 事件绑定
        List<ParamEvent> events = param.events;
        for(int i = 0;i < events.Count; i++)
        {
            RegisterCallBack(t, events[i].EventCallBack, param.transform.gameObject, events[i].eventType);
        }
    }

    public void RegisterCallBack(LuaTable t ,string luaFactionName, GameObject go ,EventType eventType)
    {
        if (luaFactionName == "")
            return;
        LuaFunction luaFunction = t.GetLuaFunction(luaFactionName);
        if (luaFunction == null)
        {
            Debug.LogError("RegisterCallBack is error " + luaFactionName);
            return;
        }

        if (eventType == EventType.Click)
        {
            UIEventListener.Get(go).onClick = (GameObject sender) => { luaFunction.Call(t,sender); };
        }
        else {
            Debug.LogError("please add new a EventType");
        }
    }

    public void BindAllWidgets()
    {
        cacheParam.Clear();

        foreach(Param v in param)
        {
            cacheParam.Add(v);
        }

        foreach(ParamArray v in paramArray)
        {
            Param frist = v.first.root;
            Transform parent = v.parent.transform;
            if (parent  != null)
            {
                cacheParam.Add(v.parent);
            }
            int count = parent.childCount;
            for(int i = 0; i < count; i++)
            {
                string rootName = frist.name + (i + 1);
                string index = (i + 1).ToString();
                Transform rooTrans = parent.FindChild(index);
                cacheParam.Add(BindAllWidgetsHelper(frist, rootName, rooTrans));
                for(int j = 0; j < v.first.childs.Count; j++)
                {
                    Param param = v.first.childs[j];
                    string childName = rootName + "_" + param.name;
                    Transform childTrans = rooTrans.FindChild(param.transform.name);
                    cacheParam.Add(BindAllWidgetsHelper(param, childName, childTrans));
                }
            }
        }
    }

    public Param BindAllWidgetsHelper(Param clone,string name,Transform trans,bool isClone = true)
    {
        Param param = null;
        if (isClone)
        {
            param = clone.Clone();
        }
        else
        {
            param = clone;
        }
        param.name = name;
        param.transform = trans;
        return param;
    }
    #endregion
}

组件的相关就全部说完了,使用组件的方式,你的思维慢慢的就一样了,你会觉得对于UI系统来说,很多地方都是类似的,只是表现不太一样,表现的部分让美术去自由发挥,我们只关心逻辑部分,既然逻辑相似那么就应该抽象出来。越是到后期,越是觉得这个方式很棒。很多项目后期很难做出大的调整,因为功能涉及的点太多,如果采取组件的方式,只需要修改组件内部即可,使用者不用修改,这样就不会出现大的问题。

项目地址:https://github.com/xiaoyanxiansheng/SmallEyeGame

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值