Unity下的UI框架的推导和搭建 以及 消息通讯机制对于解耦的实际应用

27 篇文章 1 订阅
2 篇文章 0 订阅

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.

  1. UI通过管理器进行显示和隐藏,那么怎么样可以让管理器操作显示隐藏呐?

  2. 那么问题就来了,如果类似一个成功失败界面。成功和失败只是图片不一样,没有不要做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 神一般的狄狄

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值