MMORPG游戏核心技术-UI基本框架(四)
序言:
一款游戏最先展现给玩家的是UI,UI对于一款游戏的外观来说非常重要,在谈到游戏UI时,可能会从几个方面去看:一个是界面风格,一套好的界面风格融合了游戏的特有元素可以让玩家看着舒服同时更加融入游戏世界;另一个是聊得比较多的UI优化,一个游戏大部分内存都在图片上而UI是占用最多的,另外在GPU渲染上也不容忽视;还有一点比较少看到,就是游戏的UI框架,这一章我们主要就是讲它了。
正文:
由于我们使用的是UnityEngine但是我并不想要求每个写逻辑的都要熟记Unity的API,所以我将UI系统主要分为如下三大块:
1、底层接口层,这一层和引擎息息相关,通过引擎提供的API直接控制UI上的每个元素的变化,并根据需求给外部提供接口;
2、逻辑控制层,这一层可以淡化引擎的概念(甚至根本不用了解引擎的API),通过使用UI的底层接口控制UI的显示和状态;
3、资源管理层,负责提供资源下载、销毁的方法以及获取资源数据的接口;
函数介绍:
LayoutMgr类:UI逻辑层管理类,游戏其他逻辑通过它来控制UI界面显示状态和更新UI显示内容。
Widget类:逻辑控制层基类,该类包含对应界面的UI对象引用和具体逻辑处理。
UI类:底层接口层基类,它继承自MonoBehaviour,绑定在一个界面(如背包界面、坐骑界面等)的根节点上,为逻辑类提供接口。
UILoad类:继承自MonoBehaviour,是UI图片或者文字节点上的绑定组件,通过它可以方便调用下载、查看、修改当前所用资源;
UIDownloadMgr类:管理UI资源的下载、销毁以及全局数据管理;
UIAtlasDataMgr类,UIFontDataMgr类:管理UI图集资源和UI字库资源;
整套类架构:
流程:
LayoutMgr类
initGame函数:登录游戏后将首先下载UI主资源,这个资源仅包含UI层级和节点信息。下载完成后实例化得到ms_GameUI,在此函数中设置它的父节点为UI根节点(ms_UIRoot)。初始化所有的Widget,并切换到主界面显示状态,一些常用资源开始预先下载。
1 2 3 4 5 6 7 8 9 10 11 12 | public void InitGame() { RectTransform rect = ms_GameUI.GetComponent(); rect.SetParent(ms_UIRoot.transform); foreach (KeyValuePair< string , widget= "" > key in m_dicGameAllPanel) { key.Value.Init(key.Key); m_indexGetNameDic.Add(key.Value.GetMouduleIdx(), key.Key); } SwitchMainUI( true ); UIDownloadMgr.PreloadGameUIResources(); } |
SwitchState函数:通过游戏的状态控制当然UI的切换,比如:登陆状态,显示登陆的主UI,选择角色状态,切换为选择角色的UI,当然进入游戏状态将显示游戏的主UI。函数中m_dicGameAllPanel和m_dicLoginAllPanel为登陆界面与游戏界面的所以界面列表。
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 32 33 34 35 36 37 38 39 40 41 42 43 | public void SwitchState(GameState state) { List lstStates = m_dicStates[state]; if (lstStates == null ) { Debug.LogError( "无效的游戏模式:" + state.ToString()); return ; } LogicGlobleConfig.m_uCurGameState = state; //隐藏所有界面 foreach (KeyValuePair< string , widget= "" > key in m_dicLoginAllPanel) { key.Value.SetActive( false ); } foreach (KeyValuePair< string , widget= "" > key in m_dicGameAllPanel) { if (key.Key.Equals(LayoutName.S_MainChat)) { continue ; } key.Value.SetActive( false ); } if (state == GameState.eLogin || state == GameState.eSelectLogin) { foreach (OneLayOutState widget in lstStates) { if (widget.m_isVisible) { m_dicLoginAllPanel[widget.m_name].SetActive( true ); } } } else { foreach (OneLayOutState widget in lstStates) { if (widget.m_isVisible) { m_dicGameAllPanel[widget.m_name].SetActive( true ); } } } } |
SingletonUpdate函数:所有界面的逻辑心跳在这里统计处理,需要有心跳功能的逻辑UI只需要继承基类的UpdateUi就OK了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 模块心跳 public override void SingletonUpdate( float fTime, float fDTime) { if (LogicGlobleConfig.m_uCurGameState == GameState.eLogin || LogicGlobleConfig.m_uCurGameState == GameState.eSelectLogin) { var e = m_dicLoginAllPanel.GetEnumerator(); while (e.MoveNext()) { e.Current.Value.UpdateUI(fTime, fDTime); } UIComManager.Inst.Update(); } else if (LogicGlobleConfig.m_uCurGameState == GameState.eGame) { var e = m_dicGameAllPanel.GetEnumerator(); while (e.MoveNext()) { e.Current.Value.UpdateUI(fTime, fDTime); } } UIComManager.Inst.Update(); } |
这是个有趣的管理类,里面可以扩展互斥界面的管理、同时需要打开的界面管理、还有玩家退出的时候清除资料的管理等等。
Widget类
Init函数:界面初始化函数,通过调用引擎API找到当前界面上的底层接口对象,之后将通过它控制界面显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public virtual void Init( string strRootName) { m_strRootName = strRootName; GameObject m_objRoot = GameObject.Find(strRootName); if (m_objRoot == null ) { Debug.LogError( "ui object is null:" + strRootName); return ; } m_uiComp = m_objRoot.GetComponent(); if (m_uiComp == null ) { Debug.LogError( "ui component is null:" + strRootName); return ; } m_uiComp.m_loaded = Loaded; } |
SetVisible函数:虚函数,所有的UI都会继承这个接口,在界面打开或者关闭的时候独立的逻辑都将在各自界面处理。下面的函数是统一的逻辑处理,needAnima表示以动画方式打开或关闭,bShow即需要将界面打开还是关闭。由于首次打开界面的时候,界面中的资源可能还没有下载,需要先去下载资源,直到资源下载完成才打开界面。代码中的IsUIDownload()和DownloadUI()都使用了m_uiComp的接口。
1 2 3 4 5 6 7 8 9 10 | public virtual bool SetVisible( bool needAnima, bool bShow) { if ( null == m_uiComp) return false ; if (!IsUIDownloaded()) { DownloadUI(bShow); return false ; } bool bResult = m_uiComp.OpenPanel(bShow, needAnima); } |
UI类
Init函数:第一次需要显示时的初始化函数,如果有资源没有下载完成则继续下载,否则通过回调函数告诉对应Widget类实例。OnLoadUIResource是一个资源下载完成的回调,在这个回调中刷新loading进度同时更新m_resToLoadList。
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 | public void Init() { if (m_isInit) return ; m_isInit = true ; if (m_resToLoadList == null || m_resToLoadList.Count == 0) { Loaded(); } else { m_curDownloadList = new List< uint >(m_resToLoadList); if (m_needShowLoading) UIComResLoading.Get().SetVisible( true ); foreach ( uint toLoadId in m_resToLoadList) { if (UIDownloadMgr.HadLoadReource(toLoadId)) { OnLoadUIResource(toLoadId); } else { UIDownloadMgr.DownloadUIRes(toLoadId); UIDownloadMgr.RegOnDownloadRes(toLoadId, OnLoadUIResource); } } } } |
OpenPanel函数:通过GameObject.SetActive(bool)来控制界面的根节点的显示隐藏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public virtual bool OpenPanel( bool bOpen, bool bNeedAnim) { if (!IsPanelLoaded()) { return false ; }; if (bNeedAnim) { return OpenAnimaPanel(bOpen); } else { return OpenPanel(bOpen); } } |
UILoad类
根据引擎特性,这个类继承自MonoBehaviour,作为界面中任何一个带有UI资源的对象的组件存在。
Load函数:用于设置使用的资源类型和显示值,resId为资源id,val为要显示的值,handleEnd是资源下载后处理完成时的回调。如果现在有个文本对象,我需要让它显示文字”Hello”,某静态字库的resId为1100,则需调用Load(1100, “Hello”, callback, null);如果资源已经下载好了,在子类中重写Handle函数执行特定功能,如果资源还没有下载,调用UIDownloadMgr类的静态方法下载所需资源并注册下载完成时的回调方法。
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 | public virtual bool Load( uint resId, string val, UIResHandleEnd handleEnd) { if ( null == m_gameObject) return false ; m_resId = resId; m_val = val; m_onHandleEnd = handleEnd; bool bHadLoad = false ; if (m_preResId == m_resId) { bHadLoad = true ; } else { bHadLoad = UIDownloadMgr.ContainsResource(m_resId, GetUIType()); } if (!bHadLoad) { UIDownloadMgr.DelOnDownloadRes(m_preResId, OnDownloadEnd); UIDownloadMgr.DownloadUIRes(m_resId, GetUIType()); UIDownloadMgr.RegOnDownloadRes(m_resId, GetUIType(), OnDownloadEnd); return false ; } Handle(); return true ; } |
UIDownloadMgr类
DownloadUIRes函数:此函数用来下载UI资源,代码中的CreateGameObjectResource是游戏资源管理器底层封装的下载接口。m_downloadWaitingDict 存储的是需要下载的资源列表。eUIResType为资源类型,包括图集、字库等。DownLoadGroup是将同一个资源下载的所有请求作为一组,下载完成时用于分发回调。
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 | public static void DownloadUIRes( uint resId, eUIResType resType) { if (ContainsResource(resId, resType)) { return ; } DownLoadGroup downItem; if (m_downloadWaitingDict.TryGetValue(resId, out downItem)) { if (resType != downItem.m_resType) { downItem.m_resType = resType; } if (downItem.m_downloadState) return ; downItem.m_downloadState = true ; } else { downItem = new DownLoadGroup(resId, true ); downItem.m_resType = resType; m_downloadWaitingDict.Add(resId, downItem); } uint resHandle = LogicSystem.Inst.CreateGameObjectResource(resId, true , OnUIDownloaded); AddResHandle(resId, resHandle); } |
OnUIDownloaded函数:资源下载完成时的回调,在这里将资源数据存入相应数据管理器,并派发回调。
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 | private static void OnUIDownloaded(Resource uiResource) { uint nResID = uiResource.GetResInfo().nResID; uint nResHandle = GetResHandle(nResID); GameobjectResource gor = LogicSystem.Inst.GetGameObjectResource(nResHandle); if ( null != gor) { Object[] resData = gor.GetData(); eUIResType eResType = m_downloadWaitingDict[nResID].m_resType; switch (eResType) { case eUIResType.eAtlas: UIAtlasDataMgr.OnLoadNewRes(nResID, resData); break ; case eUIResType.eFont: UIFontDataMgr.OnLoadNewRes(nResID, resData); break ; } m_allUIResource.Add(nResID, new sUIResource(nResID, nResHandle, eResType)); if ( null != m_downloadWaitingDict[nResID].m_callBack) { m_downloadWaitingDict[nResID].m_callBack(nResID); } m_downloadWaitingDict.Remove(nResID); } } |
总结
UI的架构是一套比较具备扩张性的架构,里面具体的内容都比较多。本来想分几篇文章来介绍。后面想想,主要是思路其实在上面已经做好了很好的表示。大家如果存在问题可以留言交流。