游戏UI系统框架设计

本文是基于Unity3D开发游戏写的,设计理念也适用于其他引擎。代码您可以直接使用,转载时希望您附上原文链接,金大爷先表示感谢~

看完您可以学到以下内容:

  1. 得到一个设计良好的、易用的UI框架,对自己项目中UI系统如何实现很清楚。(无论你开发王者荣耀还是COC的UI)
  2. 如何利用Adapter隐藏方法的访问权限,设计出良好的供他人使用的API
  3. 如果利用Assert,预防一些不需要在运行时发现的错误
  4. 单例设计模式的Unity上的使用

框架的2个类:

gfBaseWindow和gfWindowManager,gf是GameFramework的简写,是个人代码习惯。先上代码:

gfBaseWindow.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// @CopyRight 李金朋
/// 所有Window的基类,所有子类只需要实现其abstract方法和构造方法即可。
/// 设计原则:
/// 1.一个类型的Window在场景中只能new一个(否则会报错)。不同的Window显示的内容不一样,操作不一样,只要不同,就创建一个新的Window类型(无论Window功能多么简单)。
/// 如果除了显示内容,其它都一样,说明这些内容完全可以通过一个Window通过更新数据的方式完成。
/// 2.Window自身不提供控制打开、关闭等接口,全部操作都通过gfWindowManager完成。gfWindowManager会对Window的打开、关闭顺序等进行管理、控制,并保证Window操作的正确性。
/// 3.不关心Window Prefab用什么方案实现(NGUI或UGUI),但实现方案要统一。不同的实现方法,子窗口实现时对控件的操作会有所不同。
/// </summary>
public abstract class gfBaseWindow
{
    /// <summary>
    /// 初始化Window,从windowElements获取Window中需要操作(更新数据或状态)的组件。只会在Window创建时调用一次。
    /// </summary>
    /// <param name="windowElements">包含WindowPrefab的所有非重复名称节点</param>
    protected abstract void OnWindowInitialize(Dictionary<string, GameObject> windowElements);
    
    /// <summary>
    /// Window打开前调用。更新数据、组件状态等逻辑写在此方法中。比如:更新控件内容,Reset动画并播放。
    /// </summary>
    protected abstract void OnBeforeWindowOpen();

    /// <summary>
    /// Window打开后调用。统计事件、Window打开后延时执行的等逻辑写在此方法中。比如:Window打开2秒后开始播放背景音乐。
    /// </summary>
    protected abstract void OnAfterWindowOpen();

    /// <summary>
    ///  Window关闭前调用。统计事件等逻辑可以写在此方法中,一般不需要实现。
    /// </summary>
    protected abstract void OnBeforeWindowClose();

    /// <summary>
    ///  Window关闭后调用。窗口关闭后要执行的逻辑可以写在此方法中。比如:窗口关闭后自动弹出广告。
    /// </summary>
    protected abstract void OnAfterWindowClose();

    /// <summary>
    ///  Window销毁时调用。比如:清理Window中加载的资源。一般不需要实现,因为所有Window在场景切换时都会被销毁,并且Window不能主动调用Destroy。
    /// </summary>
    protected abstract void OnWindowDestroy();

    private GameObject m_WindowElementsRoot = null;
    private bool m_LogErrorIfWindowElementRepeated = true;

    /// <summary>
    /// 创建Window,创建成功后会添加windowManager中。
    /// </summary>
    /// <param name="windowElementsRoot">Window Prefab的GameObject,其所有节点(自已和所有非重复名称的Child)都将添加到一个Dictionary中,做为OnWindowInitialize方法的参数。</param>
    /// <param name="windowManager">场景中管理Window的Manager</param>
    /// <param name="logErrorIfWindowElementRepeated">将所有windowElementsRoot的节点添加到Dictionary时,名称重复的节点将不会添加到Dictionary中。
    /// 如果logErrorIfWindowElementRepeated值为true, 则编辑器运行会输出Error日志。</param>
    public gfBaseWindow(GameObject windowElementsRoot, gfWindowManager windowManager, bool logErrorIfWindowElementRepeated)
    {
        if (!gfDebug.Assert(windowManager != null, string.Format("创建Window前,必须在场景中创建gfWindowManager.")))
        {
            return;
        }

        m_WindowElementsRoot = windowElementsRoot;
        m_LogErrorIfWindowElementRepeated = logErrorIfWindowElementRepeated;

        if (m_WindowElementsRoot != null)
        {
            Transform windowTransform = m_WindowElementsRoot.transform;
            // 先Active,获取控件
            windowTransform.localPosition = Vector3.up * -100000;
            m_WindowElementsRoot.SetActive(true);
        }

        Dictionary<string, GameObject> windowElements = GetAllWindowElements(m_WindowElementsRoot);
        OnWindowInitialize(windowElements);
        windowElements.Clear();
        windowElements = null;

        if (m_WindowElementsRoot != null)
        {
            Transform windowTransform = m_WindowElementsRoot.transform;
            // 再Deactive
            m_WindowElementsRoot.SetActive(false);
            windowTransform = m_WindowElementsRoot.transform;
            windowTransform.SetParent(windowManager.windowsRoot);
            windowTransform.localPosition = Vector3.one;
            windowTransform.localScale = Vector3.one;
        }
        
        gfWindowManager.AddWindowAdapter addWindowAdapter = new gfWindowManager.AddWindowAdapter();
        addWindowAdapter.AddWindow(windowManager, this);
    }

    public bool IsVisable()
    {
        return m_WindowElementsRoot != null && m_WindowElementsRoot.activeSelf;
    }

    /// <summary>
    /// 控制Window打开、关闭,并执行其流程方法。
    /// </summary>
    /// <param name="visible"></param>
    private void SetVisible(bool visible)
    {
        if (visible)
        {
            OnBeforeWindowOpen();
            if (m_WindowElementsRoot != null)
            {
                m_WindowElementsRoot.SetActive(true);
            }
            OnAfterWindowOpen();
        }
        else
        {
            OnBeforeWindowClose();
            if (m_WindowElementsRoot != null)
            {
                m_WindowElementsRoot.SetActive(false);
            }
            OnAfterWindowClose();
        }
    }

    /// <summary>
    /// 将Window Prefab上的所有节点都添加到字典中。
    /// </summary>
    /// <param name="windowElementsRoot"></param>
    /// <returns></returns>
    private Dictionary<string, GameObject> GetAllWindowElements(GameObject windowElementsRoot)
    {
        Dictionary<string, GameObject> windowElementsContainer = new Dictionary<string, GameObject>();
        if (windowElementsRoot != null)
        {
            windowElementsContainer.Add(windowElementsRoot.name, windowElementsRoot);
            AddChildWindowElements(windowElementsContainer, windowElementsRoot);
        }
        return windowElementsContainer;
    }

    private void AddChildWindowElements(Dictionary<string, GameObject> windowElementsContainer, GameObject windowElementsRoot)
    {
        Transform windowRootTrans = windowElementsRoot.transform;
        int childCount = windowRootTrans.childCount;
        for (int i = 0; i < childCount; ++i)
        {
            Transform child = windowRootTrans.GetChild(i);
            if (!windowElementsContainer.ContainsKey(child.name))
            {
                windowElementsContainer.Add(child.name, child.gameObject);
            }
            else
            {
#if UNITY_EDITOR
                if (m_LogErrorIfWindowElementRepeated)
                {
                    Debug.LogError(string.Format("[此错误仅在Editor下输出]: Window节点({0})中存在重复名称的节点: {1},有可能导致Window初始化错误。", windowElementsRoot.name, child.name));
                }
#endif
                continue;
            }
            if (child.childCount > 0)
            {
                AddChildWindowElements(windowElementsContainer, child.gameObject);
            }
        }
    }

    private void Destroy()
    {
        OnWindowDestroy();
        Object.Destroy(m_WindowElementsRoot);
    }

    // 以前是为了实现方法隐藏的Adapter,只会在gfWindowManager中使用。

    public class SetWindowVisibleAdapter
    {
        public void SetVisible(gfBaseWindow window, bool visible)
        {
            window.SetVisible(visible);
        }
    }

    public class DestroyWindowAdapter
    {
        public void DestroyWindow(gfBaseWindow window)
        {
            window.Destroy();
        }
    }

}

gfWindowManager.cs

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// @CopyRight 李金朋
/// 管理场景中创建的所有Window。所有Window的打开或关闭都必须通过gfWindowManager完成。
/// 设计原则:
/// 1.一个场景中必须创建一个gfWindowManager,并设置windowsRoot,否则创建Window时会报错。gfWindowManager和所有的Window在场景切换时都会被Destroy。
/// 2.每个场景根据当前场景中需要的Window,各自创建Window。Window创建后会自动添加到gfWindowManager中。
/// 4.gfWindowManager不会对Window间的层级进行控制,所有Window间的层级的在设计Window Prefab时设计好,运行时不允许修改。
/// </summary>
public class gfWindowManager : MonoBehaviour
{
    public Transform windowsRoot;

    public static gfWindowManager Instance { get { return ms_Instance; } }
    
    private static gfWindowManager ms_Instance = null;

    private static Stack<gfBaseWindow> ms_VisibleWindowStack = null;

    private static Dictionary<Type, gfBaseWindow> ms_SearchCache = null;


    public T GetWindow<T>() where T : gfBaseWindow
    {
        gfBaseWindow window = null;
        if (ms_SearchCache.TryGetValue(typeof(T), out window))
        {
            return window as T;
        }
        return null;
    }

    /// <summary>
    /// 打开一个Window。
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="closeInFocusWindow">是否关闭当前拥有焦点的Window</param>
    public void OpenWindow<T>(bool closeInFocusWindow) where T : gfBaseWindow
    {
        T window = GetWindow<T>();
        if (window != null)
        {
            if (!ms_VisibleWindowStack.Contains(window))
            {
                if (closeInFocusWindow)
                {
                    CloseInFocusWindow();
                }
                ms_VisibleWindowStack.Push(window);
                SetWindowVisible(window, true);
            }
            else
            {
                Debug.LogError(string.Format("Window({0})已打开,不能重复打开。", typeof(T)));
            }
        }
        else
        {
            gfDebug.Assert(false, string.Format("Scene中没有创建类型为{0}的Window。", typeof(T)));
        }
    }
    
    /// <summary>
    /// 关闭当前拥有焦点的Window,也即显示在最前边的Window。
    /// </summary>
    public void CloseInFocusWindow()
    {
        if (ms_VisibleWindowStack.Count == 0)
            return;

        gfBaseWindow window = ms_VisibleWindowStack.Pop();
        SetWindowVisible(window, false);
    }

    /// <summary>
    /// 关闭所有打开的Window。
    /// </summary>
    public void CloseAllWindows()
    {
        while (ms_VisibleWindowStack.Count > 0)
        {
            gfBaseWindow window = ms_VisibleWindowStack.Pop();
            SetWindowVisible(window, false);
        }
    }

    /// <summary>
    /// 关闭所有比Window T后打开的Window。
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void CloseToWindow<T>() where T : gfBaseWindow
    {
        T targetWindow = GetWindow<T>();
        if (targetWindow != null && ms_VisibleWindowStack.Contains(targetWindow))
        {
            while (ms_VisibleWindowStack.Peek() != targetWindow)
            {
                gfBaseWindow currentWindow = ms_VisibleWindowStack.Pop();
                SetWindowVisible(currentWindow, false);
            }
        }
        else
        {
            Debug.LogError(string.Format("Window({0})没有被Open,不能执行Close操作。", typeof(T)));
        }
    }

    private void SetWindowVisible(gfBaseWindow window, bool visible)
    {
        gfBaseWindow.SetWindowVisibleAdapter setWindowVisibleAdapter = new gfBaseWindow.SetWindowVisibleAdapter();
        setWindowVisibleAdapter.SetVisible(window, visible);
    }

    private void Awake()
    {
        // 注意:更换场景后会销毁
        ms_SearchCache = new Dictionary<Type, gfBaseWindow>();
        ms_VisibleWindowStack = new Stack<gfBaseWindow>();
        ms_Instance = this; 

        gfDebug.Assert(windowsRoot != null, string.Format("gfWindowManager没有设置WindowsRoot"));
    }

    private void OnDestroy()
    {
        // 销毁所有Window
        ms_VisibleWindowStack.Clear();
        foreach (Type key in ms_SearchCache.Keys)
        {
            gfBaseWindow window = ms_SearchCache[key];
            gfBaseWindow.DestroyWindowAdapter destroyWindowAdapter = new gfBaseWindow.DestroyWindowAdapter();
            destroyWindowAdapter.DestroyWindow(window);
        }
        ms_SearchCache.Clear();
    }

    private void AddWindow(gfBaseWindow window)
    {
        Type windowType = window.GetType();
        //Debug.Log("Add Window Type: " + windowType);
        if (!ms_SearchCache.ContainsKey(windowType))
        {
            ms_SearchCache.Add(windowType, window);
        }
        else
        {
            gfDebug.Assert(false, string.Format("gfWindowManager中已存在类型为{0}的Window。每个Window类只允许New一次。", window.GetType()));
        }
    }

    public class AddWindowAdapter
    {
        public void AddWindow(gfWindowManager windowManager, gfBaseWindow window)
        {
            if (windowManager != null)
            {
                windowManager.AddWindow(window);
            }
        }
    }
}

本框架的设计原则如下:

  1. 一个类型的Window在场景中只能new一个(否则会报错)。不同的Window显示的内容不一样,操作不一样,只要不同,就创建一个新的Window类型(无论Window功能多么简单)。如果除了显示内容,其它都一样,说明这些内容完全可以通过一个Window通过更新数据的方式完成。
  2. Window自身不提供控制打开、关闭等接口,全部操作都通过gfWindowManager完成。gfWindowManager会对Window的打开、关闭顺序等进行管理、控制,并保证Window操作的正确性。
  3. 不关心Window Prefab用什么方案实现(NGUI或UGUI),但实现方案要统一。不同的实现方法,子窗口实现时对控件的操作会有所不同。
  4. 一个场景中必须创建一个gfWindowManager,并设置windowsRoot,否则创建Window时会报错。gfWindowManager和所有的Window在场景切换时都会被Destroy。
  5. 每个场景根据当前场景中需要的Window,各自创建Window。Window创建后会自动添加到gfWindowManager中。
  6. gfWindowManager不会对Window间的层级进行控制,所有Window间的层级的在设计Window Prefab时设计好,运行时不允许修改。(NGUI调整Panel的Depth,uGUI调整Canvas的SortingOrder)
  7. 一个场景要用的UI在场景加载时都提前加载好,Window打开和关闭只是显示和隐藏的过程,可避免界面卡顿。(如何比较优雅的处理加载,以后再说)
  8. Wnidow动画每个Window自己实现,如果有统一的动画,可自己创建gfBaseWindow实现。

建议开发中的命名:

Window Prefab命名:
UI+ PrefabName+Window,如:UIHomeWindow,UIMainMenuWindow
Window类的命名: 所有UI相关的代码必须在Assets/0_Scripts/UI目录下,尽量不再创建子目录。
项目代号+WindowName+Window,如:gpHomeWindow,gpMainMenuWindow
 

别外,想让自己开发的UI DrawCall少,执行效率高,请先看完NGUI和uGUI的源码,看完源码你就会理解,为啥运行时不修改NGUI Panel的Depth,等等。。。 别看网上那些什么NGUI Depth和z轴的关系测试,自动调整UI的层级关系之类的文章,他自己都没明白,在那瞎测试,看到结果对了就觉得可以了。人家代码都给你了,还瞎测试个毛啊。正确的方式是:看源码,理解,总结,验证,应用。

你金大爷人品太好,再给写个Demo:项目代号:ufd(UIFrameDemo),屏幕尺寸:1080x2080,包含NGUI、uGUI 2种实现。

百度网盘链接: https://pan.baidu.com/s/1guEHZwyPEv5R2wVM9pF9SQ 提取码: zmfc 

  

猜你喜欢

转载自www.cnblogs.com/goldpa/p/10408853.html