UI应该是一个独立的模块
需要与其他模块之间低耦合,或者0耦合。
稍微简述一下解耦和利弊:
解耦通常的方案是用消息通讯机制来传递事件和数据。
比较好用的方案 可以搜索看看 CSharpMessenger
实现原理其实很简单,很多文章都有。
是用一个 唯一key对应delegate,保存在字典里。用的时候,找到key,Invoke对应的delegate就可以了。
解耦的弊端:
那带来的问题是 逻辑的断层,调试时候,会发现在调用delegate这一步断开了,调试器找不到之前或者之后的逻辑,
所以就需要人工的查找引用,找到对应的调用位置,或者逻辑执行位置。会些许麻烦。如果肉检代码,看起来会更累。
解耦的好处:
当你发现模块之间没有耦合了,那么对于某个单一模块的删除或者修改,就会变的极其方便。
1.因为模块之间没有直接调用,不需要批量去修改调用的接口,参数之类的。
2.当你需要屏蔽或者删除某个功能的时候。可以选择直接删除代码,或者其他操作,不影响其他模块的正常运行。
这也是多人开发时候,比较重要的一点(当然也需要其他的设计模式和多态来支持)。
3.对应热更类的项目会更友善,因为用于对应的只有一个key,key可以是任意类型。对设计和框架没有直接影响。
UI框架的设计:
首先先把框架的问题一步一步列出来,并解决。
UI是需要尽量解耦的,所以最好就是 有一个中间件,一个管理器。 在主体的逻辑流程里,任意地方只要调用这个管理器的接口,就能完成UI操作。
UI内部的事情,由UI自己解决。那么就能的出来。这个管理器应该长这样:
(对外部来说,只用打开和关闭界面就好了)
public class UIManager
{
public void Show()
{
}
public void Hide()
{
}
}
你做好了一个UI,那么UI本身是个Prefab或者Scene,大多数项目UI内容比较多,所以会打包AssetBundle.
-
UI通过管理器进行显示和隐藏,那么怎么样可以让管理器操作显示隐藏呐?
-
那么问题就来了,如果类似一个成功失败界面。成功和失败只是图片不一样,没有不要做2个UIPrefab。
那就是一个UI里,内部有内容不一样,内容需要根据某个变量切换。
那怎么把这个变量给传给UI。
设计模式和多态哇!
所有UI都有一个基类, 基类向管理器注册自己,绑定key对应UI实体,就可以了.
我们把管理器变成一个单例作为控制器,UI的主节点挂载一个继承自UI基类的脚本,就可以完成了。
就会是:
public class UIManager
{
private UIManager m_this = null;
public UIManager Instance
{
get
{
if (m_this == null)
{
m_this = new UIManager();
}
return m_this;
}
}
/// <summary>
/// 保存一一对应的关系,key to UI界面
/// </summary>
private Dictionary<string, UIBase> m_dic = new Dictionary<string, UIBase>();
public void Regsiter(UIBase uibase)
{
m_dic.Add(uibase.m_strKey, uibase);
}
public void Show()
{
}
public void Hide()
{
}
}
public class UIBase : MonoBehaviour
{
public string m_strKey = "";
void Awake()
{
UIManager.Instance.Regsiter(this);
}
}
这样,在运行后,UI主节点会走Awake, 会把自己保存进UIManager,
需要show的时候,只要在UIManager里的Dictionary里面 拿出UIBase,显示就可以了。
显示的话,Active和移动坐标 2个方案 都是可以的。之后有空再做一篇UI隐藏相关的性能分析吧。
那么再来解决第二个问题,怎么传参数。
现在已经可以通过管理器开关UI了。
那传参数就很简单了,把参数通过UIBase传给界面就好了。
但是每个界面的逻辑可能不一样。有的可能需要int参数,有个可能需要复合类型的参数。
那么每个界面的接口都会不一样,怎么样把他们弄成一样的,可以传递。
拆箱装箱呀!
全部转换成object,每个界面知道自己要什么类型的参数。自己对object参数做拆箱就好了。
那么代码就会是:
public class UIManager
{
private UIManager m_this = null;
public UIManager Instance
{
get
{
if (m_this == null)
{
m_this = new UIManager();
}
return m_this;
}
}
/// <summary>
/// 保存一一对应的关系,key to UI界面
/// </summary>
private Dictionary<string, UIBase> m_dic = new Dictionary<string, UIBase>();
public void Regsiter(UIBase uibase)
{
m_dic.Add(uibase.m_strKey, uibase);
}
public void Show(string strkey, object objParam)
{
UIBase ui = m_dic[strkey];
ui.Show(objParam);
}
public void Hide(string strkey)
{
}
}
public class UIBase : MonoBehaviour
{
public string m_strKey = "";
void Awake()
{
UIManager.Instance.Regsiter(this);
}
public virtual void Show(object objParam)
{
this.gameObject.SetActive(true);
}
public virtual void Hide()
{
this.gameObject.SetActive(false);
}
}
然后具体使用的时候:
public class TestLogic
{
public void DoLogic()
{
UIManager.Instance.Show("UILogin", 1);
}
}
/// <summary>
/// m_strKey 序列化面板上字符串 = UILogin
/// </summary>
public class UILogin : UIBase
{
public override void Show(object objParam)
{
int nParam = (int)objParam; // <- 这样就解出传入参数了.
base.Show(objParam);
}
public override void Hide()
{
base.Hide();
}
}
再想一下整个流程:
游戏过程中,通过动态加载, 加载UI的ab.然后把ab里的UI实例化出来。
UI需要显示内容,所以需要数据来显示UI上的内容,UI点击会有相应,需要对应操作。
那么刚才的内容,没有加载部分的。
因为需要动态加载,所以一开始也不会有注册这一步了,因为实体不存在。
再来改一下,设计上UIManager会变成:
(这部分是伪代码,只表明个意思。)
public class UIManager
{
public void Show(string strkey)
{
StartCoroutine(ItorShow(strkey)); // 这边简写
//在单例里是无法使用Coroutine的,因为协程是依赖于MonoBehaviour的,所以可以利用其它方法
//我之前帖子里的 SingleCoroutine可以解决这个问题。或者用其它的Mono对象的使用方法,这就随意了。
}
public void Hide()
{
}
/// <summary>
/// 当前已经加载的UI,因为某些是常驻UI,某些有互斥开关逻辑,这个就自己整理逻辑啦。
/// </summary>
private Dictionary<string, UIBase> m_dic = new Dictionary<string, UIBase>();
private IEnumerator ItorShow(string strkey)
{
string strPath = "路径" + strkey;
if (System.IO.File.Exists(strPath))
{
// 可以找到文件
c = AssetBundle.LoadFromFileAsync;
yield return c;
// 如果加载一切顺利;
m_dic.Add(c.uikey, c.uibase);
yield return c.uibase.show(); // UIBase里的Show,也可以改成异步了
//等待界面里面的内容加载和布局完成。再全部显示。
}
else
{
// LogError 报错,找不到ab
}
}
}
OK,然后,你会发现,UI框架相对其他模块是解耦的。除了调用显示隐藏外,基本没有耦合了。UI自身,可以通过传入参数来确定显示内容。
那如果界面打开的过程中,需要刷新,没有触发show怎么办?
我们就可以利用消息机制,文章开头提到的解耦方法
(CSharpMessenger之类的)
public class UILogin : UIBase
{
void Awake()
{
Messenger.AddListener("testmsg", OnCall);
Messenger<int>.AddListener("testmsg", OnCallInt);
}
public void OnCall()
{
// 刷新界面
}
public void OnCallInt(int intParam)
{
// 刷新界面
}
public override void Show(object objParam)
{
int nParam = (int)objParam; // <- 这样就解出传入参数了.
base.Show(objParam);
}
public override void Hide()
{
base.Hide();
}
}
public class TestLogic
{
public void DoLogic()
{
Messenger.Broadcast("testmsg");
Messenger<int>.Broadcast("testmsg", 1);
}
}
好了,这样就得到一个 耦合度非常低的一个UI的框架设计了!
这篇文章,写的比较粗浅,希望对一些能力还没有那么出色的,在设计方面有所欠缺的小朋友们,有所帮助。
程序学无止尽。
欢迎大家沟通,有啥不明确的,或者不对的,也可以和我私聊
我的QQ 334524067 神一般的狄狄