Unity-UI 架构优化小技巧(一)

简介: Unity-UI 架构优化小技巧

前言


现在假设有这么一种情况:假设我们要做一个加载登录场景,其效果是:进入游戏时,显示一个加载的界面,界面上有进度条,此时会异步加载另一个场景,进度条随着加载而移动,场景加载完毕时,加载界面消失,同时在已加载的场景中显示注册登录界面。

那么我们该如何实现上述效果呢?

基本思路如下:


这里牵涉 3 个模块的交互,总管理模块负责初始化公共服务模块 (这里用到的是资源加载服务模块) 和单个业务系统模块 (这里是登录注册业务模块)。


随后从登录注册业务模块提供的方法来实现加载场景,加载的时候分三步:


一,展示加载界面。


二,异步加载场景并显示进度。


三,取消展示加载界面并展示登录注册界面。

注意,比较特殊的是某些窗口管理脚本并不归属于任何一个业务模块,比如加载界面管理,又如动态窗口展示界面管理,这两种界面是每个业务模块都有可能用到的,它其实类似于公共服务模块,但由于它们与某个 UI 界面关联,因此可以直接由总管理模块来控制。

那么现在问题来了:


显示加载界面的代码放哪里?


由于牵涉到模块的交互,因此显示加载界面的代码即可以放在登录注册业务模块中,也可以放在资源加载服务模块中,但由于其他业务模块也会用到加载服务,也要显示加载界面,因此,最好把显示加载界面的代码放在资源加载服务模块中。

注意,要显示加载界面,就要取到加载界面对应窗口脚本的引用,由于加载界面直接由总管理模块控制,因此不要在资源加载服务模块中持有此脚本的引用,而是要通过总管理模块间接访问加载界面,这样层次更清晰。

异步加载场景在资源加载服务模块中完成,怎么实时更新进度条呢?


基本思路是调用资源加载函数后,用 Update,在每一帧观测加载的进度 (加载函数的调用有个返回值,里面存着加载进度属性) 并进行 UI 的更新。

注意,这里很重要的一点是,我们怎么在 Update 里获取到加载函数调用的返回值,难道是以参数形式把加载函数调用的返回值传递给 Update 吗?


并不是。这就体现出委托的重要性了,我们可以在加载函数调用时设置一个委托方法,在委托方法里去查看这个返回值的加载进度,并进行 UI 更新,而 Update 里只进行委托的调用。

这里又有一个问题:登录界面怎么展示?


难道在资源加载服务模块中持有登录界面窗口管理脚本的引用吗?

当然不是,登录界面窗口是属于登录注册业务模块的,因此肯定要通过这个模块去访问此界面的脚本引用。


那是否可以把登录注册业务模块设置成单例的,在资源加载服务模块中去调用它,随后去进行登录界面展示呢?


也不是,因为这样的话,其他业务系统中要使用资源加载服务,到最后也会展示登录注册窗口界面,显然不行。怎么办?

答案也是使用委托,由于每个业务系统在使用加载服务完成后要显示的界面不同,因此可以在业务模块调用资源加载服务时,传入一个委托,该委托负责打开业务模块自己持有管理的一些界面,相应地,资源加载服务模块只要在加载完成后,调用此委托即可。

正文


代码架构展示


以上介绍了游戏开发的基本架构,下面以实现异步加载登录场景的例子进行说明,相关代码如下:

(为了方便说明,我们只展示代码中的和架构有关的部分)

//总管理脚本,它直接管理公共的加载进度界面,各个模块初始化完成后 (注意初始化顺序),通过登录注册业务模块提供的方法,开始加载场景。
using UnityEngine;
public class GameRoot : MonoBehaviour 
{
    //GameRoot可以被各系统用来访问公共界面,所以设置成单例
    public static GameRoot Instance = null;
    //加载进度界面和动态元素界面是公用的,由GameRoot持有
    public LoadingWnd loadingWnd;
    private void Start()
    {
        Instance = this;
        //切换场景时为了防止本场景物体被销毁,手动指定GameRoot不销毁,且把所有UI元素挂在其下保护起来
        DontDestroyOnLoad(this);
        Debug.Log("游戏启动...");
        Init();
    }
    //初始化各个模块
    private void Init()
    {
        //初始化资源加载服务模块
        ResSvc res = GetComponent<ResSvc>();
        res.InitSvc();
        //初始化登录注册业务系统模块
        LoginSys login = GetComponent<LoginSys>();
        login.InitSys();
        //进入登录场景并加载相应UI
        login.EnterLogin();
    }
}
//登录注册业务模块,它直接管理的 UI 窗口是登录注册界面,要加载场景时,它会去调用资源加载服务模块,因此资源加载服务模块必须是单例的,加载完成后通过匿名方法传递委托给资源加载服务模块,从而实现登录注册界面的显示。
using UnityEngine;
public class LoginSys : MonoBehaviour 
{
    //单例
    public static LoginSys Instance = null;
    //登录注册业务模块下有登录注册界面
    public LoginWnd loginWnd;
    //初始化模块
    public void InitSys()
    {
        Instance = this;
        Debug.Log("登录注册业务模块加载完毕...");
    }
    /// <summary>
    /// 进入登录场景
    /// </summary>
    public void EnterLogin()
    {
         //加载登录场景
         ResSvc.Instance.AsyncLoadScene(Constants.SceneLogin,()=>{
            //加载完成后打开登录注册界面
            loginWnd.gameObject.SetActive(true);
            //初始化登录注册界面
            loginWnd.InitWnd();
        });
    }
//资源加载服务模块,注意异步加载时它会通过委托实现每过一帧检测进度,设置进度条时需要用到总管理脚本下的公用的加载进度界面,因此需要通过 GameRoot 去访问,加载完成后取消对加载进度页面的展示,同时调用传入的回调委托,实现特定功能,比如登录注册业务模块为它传入的切换登录注册界面功能。
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
public class ResSvc : MonoBehaviour 
{
    //单例
    public static ResSvc Instance = null;
    //初始化模块
    public void InitSvc()
    {
        Instance = this;
        Debug.Log("资源加载服务模块加载完毕...");
    }
    Action prgCB = null;
    //异步加载场景方法
    public void AsyncLoadScene(string sceneName,Action loaded)
    {
        //显示加载界面
        GameRoot.Instance.loadingWnd.gameObject.SetActive(true);
        GameRoot.Instance.loadingWnd.InitWnd();
        //异步加载指定名字的场景
        AsyncOperation sceneAsync = SceneManager.LoadSceneAsync(sceneName);
        prgCB = ()=>
        {
            //获取当前进度
            float val = sceneAsync.progress;
            //在加载界面设置当前进度
            GameRoot.Instance.loadingWnd.SetProgress(val);
            //加载完成
            if(val == 1)
            {
                //加载完成后调用回调函数
                if(loaded != null)
                {
                    loaded();
                }
                //清空委托和中间结构
                sceneAsync = null;
                prgCB = null;
                //取消对加载界面的展示
                GameRoot.Instance.loadingWnd.gameObject.SetActive(false);
            }
        };
    }
    //加载开始后,每一帧检测进度
    private void Update()
    {
        if(prgCB != null)
        {
            prgCB();
        }
    }
}
//加载进度界面的管理脚本,由于它和 Unity 中的一些 UI 关联了,这里我们重点介绍架构,因此无需理解它里面做了什么,只需要知道它有一个自己的初始化方法和设置进度方法就行了。
using UnityEngine;
using UnityEngine.UI;
public class LoadingWnd : MonoBehaviour
{
    //Tips文字
    public Text txtTips;
    //进度条图片
    public Image loadingFg;
    //进度条滑点
    public Image imgPoint;
    //进度条文字
    public Text txtPrg;
    //初始化加载进度界面
    protected override void InitWnd()
    {
        //初始化Tips文字
        txtTips.text = "这是一条游戏Tips";
        //进度条归零
        loadingFg.fillAmount = 0;
        //初始化进度条文字
        txtPrg.text = "0%";
        //初始化进度条点位置
        imgPoint.transform.localPosition = new Vector3(-508f, 0, 0);
    }
    //设置进度
    public void SetProgress(float prg)
    {
        //设置进度条
        loadingFg.fillAmount = prg;
        //设置进度条文字(转换成百分比显示,且忽略小数)
        txtPrg.text = (int)(prg * 100) + "%";
        //设置进度条点位置(教程里设置的是recttransform里的anchoredPosition)
        imgPoint.transform.localPosition = 
            new Vector3(loadingFg.rectTransform.sizeDelta.x * prg - 508f, 0,0);
    }
}
//登录注册界面管理脚本,同样的,由于它和 Unity 中的一些 UI 关联了,这里我们重点介绍架构,因此无需理解它里面做了什么,只需要知道它有一个自己的初始化方法就行了。
using UnityEngine;
using UnityEngine.UI;
public class LoginWnd : MonoBehaviour 
{
    public InputField iptAcct;
    public InputField iptPass;
    public Button btnNotice;
    public Button btnEnter;
    //初始化登录注册界面
    public void InitWnd()
    {
        //获取本地存储的账号与密码
        if(PlayerPrefs.HasKey("Acct") && PlayerPrefs.HasKey("Pass"))
        {
            iptAcct.text = PlayerPrefs.GetString("Acct");
            iptPass.text = PlayerPrefs.GetString("Pass");
        }
        else
        {
            iptAcct.text = "";
            iptPass.text = "";
        }
    }
}

注意,这篇文章的重点是讲述架构的优化技巧,因此我们不详细描述异步加载进度条的具体实现,先熟悉下基本架构就够了。

那么重点来了,上面这个架构怎么进行优化呢?又在哪里优化呢?


要知道,每个界面窗口,比如上面的加载界面和登录注册界面,都有自己的窗口管理脚本,而当我们控制某个界面进行展示的时候,是通过其脚本获取对此界面的引用,从而将它展示的。


但是,每个界面要做的初始化工作是不一样的,比如加载界面要将进度条置 0,比如登录注册界面要获取本地的账号密码,这些初始化工作,都是由该界面的窗口管理脚本提供的,而我们在每个地方调用并将界面显示出来时 (比如上面的例子就是在资源加载服务模块中显示加载界面和登录注册界面),都要手动地去调用管理脚本提供的初始化方法.


仔细想想,其实这不大合理,毕竟我只要用到这个界面的展示,或者这个界面脚本提供的一些功能,我不应该还要负责界面的初始化工作的调用。

怎么解决这个问题?


答案就是抽取出一个窗口基类,所有界面管理脚本都继承自这个窗口基类,将界面的初始化方法与设置界面是否激活绑定在一起,并向外部提供这个设置显示状态的方法,这样,当外部 (比如资源加载服务模块) 调用显示某个界面时 (比如加载界面),只要调用这个设置显示状态方法就行.


那如何让不同的界面管理脚本有特定的初始化方法呢?

办法就是利用多态,将窗口基类中的初始化方法设为虚函数,在每个界面管理脚本中去覆写。而初始化方法的调用因为与设置显示状态的方法被绑定在一起,因此是在窗口基类中的设置显示状态的方法中被调用的,这样的话这些初始化方法权限设置为 protected 就行了,只给子类调用,外部访问不到它,也有保障安全性的作用。

优化后窗口基类代码如下:

using UnityEngine;
public class WindowRoot : MonoBehaviour 
{
    //设置界面显示状态
    public void SetWndState(bool isActive = true)
    {
        //只有界面当前状态和要显示的状态不同才要改变状态
        if(gameObject.activeSelf != isActive)
        {
            gameObject.SetActive(isActive);
        }
        //显示后需要初始化
        if(isActive)
        {
            InitWnd();
        }
        //关闭显示后需要清理
        else
        {
            ClearWnd();
        }
    }
    //初始化方法,子类中覆盖实现
    protected virtual void InitWnd()
    {
    }
    //清理方法,子类中覆盖实现
    protected virtual void ClearWnd()
    {
    }
}


目录
相关文章
|
12天前
|
消息中间件 存储 缓存
十万订单每秒热点数据架构优化实践深度解析
【11月更文挑战第20天】随着互联网技术的飞速发展,电子商务平台在高峰时段需要处理海量订单,这对系统的性能、稳定性和扩展性提出了极高的要求。尤其是在“双十一”、“618”等大型促销活动中,每秒需要处理数万甚至数十万笔订单,这对系统的热点数据处理能力构成了严峻挑战。本文将深入探讨如何优化架构以应对每秒十万订单级别的热点数据处理,从历史背景、功能点、业务场景、底层原理以及使用Java模拟示例等多个维度进行剖析。
34 8
|
18天前
|
监控
SMoA: 基于稀疏混合架构的大语言模型协同优化框架
通过引入稀疏化和角色多样性,SMoA为大语言模型多代理系统的发展开辟了新的方向。
30 6
SMoA: 基于稀疏混合架构的大语言模型协同优化框架
|
4天前
|
消息中间件 运维 Cloud Native
云原生架构下的微服务优化策略####
本文深入探讨了云原生环境下微服务架构的优化路径,针对服务拆分、通信效率、资源管理及自动化运维等核心环节提出了具体的优化策略。通过案例分析与最佳实践分享,旨在为开发者提供一套系统性的解决方案,以应对日益复杂的业务需求和快速变化的技术挑战,助力企业在云端实现更高效、更稳定的服务部署与运营。 ####
|
16天前
|
存储 NoSQL 分布式数据库
微服务架构下的数据库设计与优化策略####
本文深入探讨了在微服务架构下,如何进行高效的数据库设计与优化,以确保系统的可扩展性、低延迟与高并发处理能力。不同于传统单一数据库模式,微服务架构要求更细粒度的服务划分,这对数据库设计提出了新的挑战。本文将从数据库分片、复制、事务管理及性能调优等方面阐述最佳实践,旨在为开发者提供一套系统性的解决方案框架。 ####
|
25天前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
69 1
|
2月前
|
监控 API 开发者
后端开发中的微服务架构实践与优化
【10月更文挑战第17天】 本文深入探讨了微服务架构在后端开发中的应用及其优化策略。通过分析微服务的核心理念、设计原则及实际案例,揭示了如何构建高效、可扩展的微服务系统。文章强调了微服务架构对于提升系统灵活性、降低耦合度的重要性,并提供了实用的优化建议,帮助开发者更好地应对复杂业务场景下的挑战。
22 7
|
2月前
|
运维 监控 Serverless
利用Serverless架构优化成本和可伸缩性
【10月更文挑战第13天】Serverless架构让开发者无需管理服务器即可构建和运行应用,实现成本优化与自动扩展。本文介绍其工作原理、核心优势及实施步骤,探讨在Web应用后端、数据处理等领域的应用,并分享实战技巧。
|
2月前
|
Cloud Native API 持续交付
利用云原生技术优化微服务架构
【10月更文挑战第13天】云原生技术通过容器化、动态编排、服务网格和声明式API,优化了微服务架构的可伸缩性、可靠性和灵活性。本文介绍了云原生技术的核心概念、优势及实施步骤,探讨了其在自动扩展、CI/CD、服务发现和弹性设计等方面的应用,并提供了实战技巧。
|
2月前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
59 3
|
2月前
|
存储 Kubernetes 监控
深度解析Kubernetes在微服务架构中的应用与优化
【10月更文挑战第18天】深度解析Kubernetes在微服务架构中的应用与优化
114 0
下一篇
无影云桌面