需求
- 对于程序来说:我们要实现用最少的代码,实现最强大的效果。
- 对于美术来说:Prefab的随意调整,不需要程序的参与。
实现上述两个要求的方法就是:UI组件化。
和C#的组件化类似,我们只需要在代码中,注册组件(一行代码),那么组件的功能就全部集成。
组件的功能:差异化显示,交互,数据更新。
差异化显示:同一功能模块(比如:人物属性)出现在不同的地方做不同的显示。
交互:组件也能点击,拖拽等等。
数据更新:功能模块(比如:人物属性)在数据改变的时候,能够自动更新。
有了以上的基础对于前端程序来说,写系统就成了拼组件的人员了,这也极大的节约了时间和出错的机会,对于后期维护和升级来说异常的方便。
刚开始项目并没有采取这样的方式,比如人物属性,很多UI都要显示,但是你还不能直接复制,因为美术做了不同的表现,节点的位置都不一样了,所以每次都要重新写:三个地方需要显示人物属性就要写三次类似代码,后期的维护更加难受,因为一旦要改,就是所有地方都改。
真正可怕的地方是图标的显示,几乎所有界面都有,你想想,如果这个要改,你会怎么办?你肯定是这么办的:这个改动太大,需要时间。
现在我们知道了组件是实现上述功能的核心,接下来我们一步步的建立起组件化的流程。
差异化显示
先看一种表现:
对于上图有四个Label需要程序通过数据去显示,我们一般是这么做的:
是不是感觉到了很熟悉味道,我敢说你绝对这么搞过,但是这并不是一个好的方法,如果当前UI需要控制的控件有100个,你想想你的代码会是什么样子的?绝对是崩溃的!
上面都是Label那为何不用循环?不需要写成这么难看吧,于是就变成了下面的这样:
是简洁了很多,这里有两个问题:
- 因为都是Label组件所以你可以这样做,如果里面还有其他组件(UISprite,UIPanel),如果还想这样循环,就得加一个组件类型的列表了。
- 访问起来非常的蛋疼,你会看到self.labs[1],self.labs[2],这样的代码充满你的眼球,你不去看对应的列表根本就不知道这个代表什么,到时候你能分清self.labs[99]是啥?数都数不来。这样的方法只适用于需要控制少量UI控件的地方。
那应该如果优化?
我们只要通过路径去找节点,通过节点去找控件,就避免不了上面的问题。这里通过路径去找节点,还有一个非常坑的地方,如果Prefab的制作是美术人员,美术并不知道你用了哪个节点,在调整的时候,非常容易改变节点的路径,一旦改变,你就找不到了,你必须和美术同步修改,这太蛋疼了。
节点寻找的另外一种方式:就是引用。
我们可以绑定一个脚本,然后将节点都拖上去,像下面这样:
通过引用的方式好处:
- 不在关心美术的制作Prefab的路径,所以美术可以自由大胆的飞翔了;
- 像前面的OnCreate里面的代码可以全部删除,那种写法太恶心了;
- 可以直接就找到对应节点的控件(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增加一个点击事件,我们连代码都不用改,还是以前的代码(写法二):
这样交互就有了。
接下来我们将组件的更新也加进去,让其自成一体,不依赖使用者数据。
数据更新
我们加入了玩家数据更新,我们的使用者代码还是不用改,还是下面的(写法二):
总结
通过以上的修改,对于使用来说,只需要显示人物属性这个组件就行了,显示,交互,更新全部集成。这样你想想组件的功能越强大,给我们带来的好处就越大:
- 使用者使用简单,就一行代码;
- 美术想修改也不需要和程序说,直接在Prefab上根据当前系统修改即可,不同部门之间的工作尽量分开;
- 后期调整功能异常方便,就像使用的代码一直只有一行,但是我们却慢慢的加入了交互和数据更新。
知识点:
- 控件绑定(UICore.cs):给Lua提供直接访问节点控件的能力;
- 组件脚本(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系统来说,很多地方都是类似的,只是表现不太一样,表现的部分让美术去自由发挥,我们只关心逻辑部分,既然逻辑相似那么就应该抽象出来。越是到后期,越是觉得这个方式很棒。很多项目后期很难做出大的调整,因为功能涉及的点太多,如果采取组件的方式,只需要修改组件内部即可,使用者不用修改,这样就不会出现大的问题。