用Unity做个游戏(四) - 基于UGUI的MVP框架
前言
UI在任何游戏里都是个重要的东西,作为一个程序员我们暂时先不考虑如何设计UI才好看,优先还是考虑怎么高效地实现功能。
在很多重度UI的游戏中,UI占的比重经常超过核心玩法,UI变多的时候我们需要用编辑器来设计UI,用代码生成工具来生成相关的代码,之后我们只关心如何实现相关的逻辑就行了,Unity的场景文件完全不用修改,因为所有的UI都是代码控制动态添加移除的。
这里使用MVP的架构来实现,Model为主要以单例形式存在的类,用来存储相关的数据;View为根据UI的Prefab生成出来的代码,人工不修改,纯自动生成;Presenter就是我们需要写代码逻辑的地方了。
设想的工作流
这样子最大的好处在于,当需求有变化时,我们只需要在编辑器里调整各个UI控件的布局,然后生成相关代码就行了,并不用修改View相关的代码,只需要考虑游戏逻辑方面的修改就够了。
不管UI怎样多怎样复杂,Unity的Scene文件永远不用修改,多人合作时,最大化地分隔开了不同人的工作,最大程度上地避免了冲突的发生。
UI事件
这是个重点,UGUI的UI事件都是基于Unity的EventSystem的,不过我们之前已经自己实现了一套适合我们的事件系统了,这里就通过动态增加组件的方式把Unity.EventSystem的事件转换为我们自己的事件,这样就能统一处理了
以按钮为例,我们需要监听它的点按事件,为了将这个事件以我们SFEvent
的形式发布,我继承UnityEngine.EventSystems.EventTrigger
写了一个自定义组件SFUIEventListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class SFUIEventListener : EventTrigger { public SFEventDispatcher dispatcher = null ; public static SFEventDispatcher getDispatcherWithGo(GameObject go) { // 静态方法,自动创建UI控件的事件派发器 var listener = go.GetComponent(); if (listener == null ) { listener = go.AddComponent(); } if (listener.dispatcher == null ) { listener.dispatcher = new SFEventDispatcher(go); } return listener.dispatcher; } public override void OnPointerClick(PointerEventData) { // 这里是Unity原生的事件 if (dispatcher != null ) { dispatcher.dispatchEvent(SFEvent.EVENT_UI_CLICK); } } // more... }; |
MVP
当然这里只有V和P
View
首先需要用编辑器创建并设计UI,最后保存成Prefab
Prefab的结构是这样的:
蓝色的部分就是我们保存成Prefab的了,以vwXXX命名,其父层级Canvas - UICamera - UIRoot
每个场景只有一个。为了方便,我规定我们游戏的UI分辨率以1280*720为基准,UI切图素材均参照这个分辨率进行制作。
其中UI绘制的话UGUI支持3中:直接叠加,UI摄像机,作为3D物体被主摄像机渲染。我们使用单独的UI摄像机来实现,Canvas - Render Mode
选择Screen Space - Camera
,然后再Canvas下面创建一个摄像机子节点,命名为UICamera,并将其设置到Canvas中。
现在我们发现Game窗口现在显示的是UICamera拍摄到的东西,所以要设置一下UICamera的属性,首先Clear Flags
改成Depth only
,默认的选项是要重新绘制天空盒,这样一来比他层级低的摄像机拍摄到的画面就完全被挡住了。Culling Mask
选择UI
,作用是只让这个摄像机拍摄UI层的内容(Canvas节点下所有的子节点默认都是UI层,可以在Transform组件上面的位置看到Tag和Layer,就能发现Layer都已经是UI了)。
另外一点,截图中没有截到:Camera的Projection一项,要改成正交投影Orthographic
,默认的透视投影不能保证UI完全占满UICamera的镜头
其中UICamera的Transform有个偏移1280,这个不重要,只是我不想在编辑UI的时候看到场景上的东西,也就是在Scene窗口中把UI和场景物件分开
接下来是UIRoot这个节点,这其实是一个全屏的透明Panel,只不过它的Pos Z
值为500,作用是让UI摄像机妥当地看到UI内容,500这个值是随便填的,只是因为如果PosZ为默认值0的话UICamera会拍不到UI内容,大于一个值(好像是20多)才会正常显示。
好了废话说完,就可以导出了,导出的工具涉及到编辑器的扩展,暂时还不会弄= =先留个坑,之后再填(不是
最终导出的结果大概是这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class SFTestView : SFBaseView { public Text lblTitle{ get { return m_lblTitle; } } public Button btnOk { get { return m_btnOk; } } private Text m_lblTitle; private Button m_btnOk; private SFTestPresenter m_presenter; void Start() { GameObject lblTitleGO = SFUtils.findChildWithParent(gameObject, "lblTitle" ); if (lblTitleGO != null ) { m_lblTitle = lblTitleGO.GetComponent(); } // other widgets m_presenter = new SFTestPresenter(); m_presenter.initWithView( this ); } } |
其中SFBaseView
继承自MonoBehavior,提供了一个添加事件监听的公共方法:
1 2 3 4 5 | public void addEventListener(Component widget, string eventType, SFListenerSelector sel) { var dispatcher = SFUIEventListener.getDispatcherWithGo(widget.gameObject); dispatcher.addEventListener(eventType, sel); } |
最后别忘记把View脚本挂载在Prefab上
Presenter
Presenter文件会在第一次导出UI View时候一并创建出来,初始默认的内容非常简单:
1 2 3 4 5 6 7 8 | public class SFTestPresenter { SFTestView m_view; public void initWithView(SFTestView view) { m_view = view; } } |
之后我们就可以开始写逻辑了,比如我们想给这个按钮加一个点击事件,当点击时改变lblTitle的文本。非常方便,首先在Presenter类中的initWithView()
方法中添加点击事件监听:
m_view.addEventListener(m_view.btnOk, SFEvent.EVENT_UI_CLICK, onButtonClicked);
然后实现回调函数
1 2 3 4 | void onBtnClicked(SFEvent e) { m_view.lblTitle.text = "Button Clicked!" ; } |
最后实现添加UI View到场景的操作,这里我用了一个静态变量来存储当前场景的UIRoot节点,这个静态变量属于类SFSceneManager
,这个类挂载在每个场景的一个空节点(Empty GO)中,在场景加载时找到该场景的UIRoot节点并保存到静态变量中,并在场景卸载时清空静态变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class SFSceneManager : MonoBehaviour { static public GameObject uiRoot = null ; void Start() { var uiRootGO = GameObject.Find( "UIRoot" ); if (uiRootGO == null ) { SFUtils.logWarning( "当前场景没有找到UIRoot节点" ); } uiRoot = uiRootGO; } } |
于是我们就可以在任何一个地方添加UI了,只需要编写如下代码:
GameObject.Instantiate(vwPrefab, SFSceneManager.uiRoot.transform);
最后就可以看效果啦
点击按钮之后就可以发现上面的文字变化了