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()
    {
    }
}


目录
相关文章
|
1月前
|
弹性计算 运维 监控
阿里云云服务诊断工具:合作伙伴架构师的深度洞察与优化建议
作为阿里云的合作伙伴架构师,我深入体验了其云服务诊断工具,该工具通过实时监控与历史趋势分析,自动化检查并提供详细的诊断报告,极大提升了运维效率和系统稳定性,特别在处理ECS实例资源不可用等问题时表现突出。此外,它支持预防性维护,帮助识别潜在问题,减少业务中断。尽管如此,仍建议增强诊断效能、扩大云产品覆盖范围、提供自定义诊断选项、加强教育与培训资源、集成第三方工具,以进一步提升用户体验。
689 243
|
1月前
|
机器学习/深度学习 算法 数据可视化
基于深度混合架构的智能量化交易系统研究: 融合SSDA与LSTM自编码器的特征提取与决策优化方法
本文探讨了在量化交易中结合时序特征和静态特征的混合建模方法。通过整合堆叠稀疏降噪自编码器(SSDA)和基于LSTM的自编码器(LSTM-AE),构建了一个能够全面捕捉市场动态特性的交易系统。SSDA通过降噪技术提取股票数据的鲁棒表示,LSTM-AE则专注于捕捉市场的时序依赖关系。系统采用A2C算法进行强化学习,通过多维度的奖励计算机制,实现了在可接受的风险水平下最大化收益的目标。实验结果显示,该系统在不同波动特征的股票上表现出差异化的适应能力,特别是在存在明确市场趋势的情况下,决策准确性较高。
73 5
基于深度混合架构的智能量化交易系统研究: 融合SSDA与LSTM自编码器的特征提取与决策优化方法
|
11天前
|
消息中间件 监控 小程序
电竞陪玩系统架构优化设计,陪玩app如何提升系统稳定性,陪玩小程序平台的测试与监控
电竞陪玩系统架构涵盖前端(React/Vue)、后端(Spring Boot/php)、数据库(MySQL/MongoDB)、实时通信(WebSocket)及其他组件(Redis、RabbitMQ、Nginx)。通过模块化设计、微服务架构和云计算技术优化,提升系统性能与可靠性。同时,加强全面测试、实时监控及故障管理,确保系统稳定运行。
|
17天前
|
存储 弹性计算 架构师
老板点赞!技术人如何用架构优化打赢降本增效战?
大家好,我是小米,一个喜欢分享技术的小架构师。通过亲身经历,我将介绍如何通过架构优化帮助公司降本增效。两年前,我加入一家初创公司,面对成本高企的问题,通过弹性伸缩、微服务化和数据治理等手段,成功降低了40%的技术成本,提升了60%的系统响应速度。希望我的经验能给你启发!关注我的微信公众号“软件求生”,获取更多技术干货。
31 5
|
1月前
|
存储 机器学习/深度学习 人工智能
【AI系统】计算图优化架构
本文介绍了推理引擎转换中的图优化模块,涵盖算子融合、布局转换、算子替换及内存优化等技术,旨在提升模型推理效率。计算图优化技术通过减少计算冗余、提高计算效率和减少内存占用,显著改善模型在资源受限设备上的运行表现。文中详细探讨了离线优化模块面临的挑战及解决方案,包括结构冗余、精度冗余、算法冗余和读写冗余的处理方法。此外,文章还介绍了ONNX Runtime的图优化机制及其在实际应用中的实现,展示了如何通过图优化提高模型推理性能的具体示例。
63 4
【AI系统】计算图优化架构
|
27天前
|
人工智能 搜索推荐 算法
婚恋交友系统UI/UX设计优化 婚恋交友系统用户界面友好性提升 婚恋交友系统用户行为分析与优化 婚恋交友系统用户反馈收集与处理
针对婚恋交友系统的UI/UX设计优化,本文提出多项策略:简化用户界面、提升交互体验、个性化推荐算法;增强用户界面友好性,包括适应性、无障碍及情感化设计;通过数据收集与分析优化用户行为路径;建立多渠道反馈机制,分类处理并及时告知结果。这些措施旨在提高用户体验和满意度,促进平台健康发展。[点击查看完整演示和免费源码](https://gitee.com/duoke-official-open-source/hunlianjiaoyou)
69 4
|
1月前
|
机器学习/深度学习 前端开发 算法
婚恋交友系统平台 相亲交友平台系统 婚恋交友系统APP 婚恋系统源码 婚恋交友平台开发流程 婚恋交友系统架构设计 婚恋交友系统前端/后端开发 婚恋交友系统匹配推荐算法优化
婚恋交友系统平台通过线上互动帮助单身男女找到合适伴侣,提供用户注册、个人资料填写、匹配推荐、实时聊天、社区互动等功能。开发流程包括需求分析、技术选型、系统架构设计、功能实现、测试优化和上线运维。匹配推荐算法优化是核心,通过用户行为数据分析和机器学习提高匹配准确性。
99 3
|
2月前
|
监控 Serverless 云计算
探索Serverless架构:开发实践与优化策略
本文深入探讨了Serverless架构的核心概念、开发实践及优化策略。Serverless让开发者无需管理服务器即可运行代码,具有成本效益、高可扩展性和提升开发效率等优势。文章还详细介绍了函数设计、安全性、监控及性能和成本优化的最佳实践。
|
2月前
|
弹性计算 运维 开发者
后端架构优化:微服务与容器化的协同进化
在现代软件开发中,后端架构的优化是提高系统性能和可维护性的关键。本文探讨了微服务架构与容器化技术如何相辅相成,共同推动后端系统的高效运行。通过分析两者的优势和挑战,我们提出了一系列最佳实践策略,旨在帮助开发者构建更加灵活、可扩展的后端服务。
|
2月前
|
消息中间件 运维 Cloud Native
云原生架构下的微服务优化策略####
本文深入探讨了云原生环境下微服务架构的优化路径,针对服务拆分、通信效率、资源管理及自动化运维等核心环节提出了具体的优化策略。通过案例分析与最佳实践分享,旨在为开发者提供一套系统性的解决方案,以应对日益复杂的业务需求和快速变化的技术挑战,助力企业在云端实现更高效、更稳定的服务部署与运营。 ####