Unity设计模式—命令队列

news2025/1/12 5:52:08

Unity设计模式—命令队列

概要

本篇将介绍命令队列并用命令队列模式实现一个Unity里的WindowManager。

命令队列是一个按照FIFO顺序存储一系列通知或请求的队列。发出通知时系统会将请求置入队列并立即返回,请求处理器随后从命令队列中获取并处理这些请求。请求可由处理器直接处理或转发给对其感兴趣的模块。

命令队列模式对消息的发送者和受理者进行了解耦,是消息的处理变得动态且非实时。

原书这一章叫做EventQueue事件队列。当入队的时命令时,也就是命令队列。命令队列用的比较多。故本文直接写命令队列

需求&差代码

你在开发一个窗口管理器WindowManager。

任务的基本逻辑很简单,需要提供一个Push方法,使用者调用的时候弹出一个窗口。

并且要求弹出窗口的时候隐藏底部的窗口。

三下五除二你就开发好了代码。

public void PushWindow(UIBase view){
    foreach(var view in views){
        view.SetActive(False)
    }
    view.SetActive(True);
    views.Add(view);
}

测试报过来一系列bug:

  1. 如果已经有一个系统窗口置于最顶层,就不应该弹出一个普通窗口盖住系统窗口,无论调用时机的先后顺序。
  2. 被隐藏的界面的倒计时状态错误了,隐藏期间没有倒计时不会走

为了解决第一个问题,你决定在PushWindow里加入IsSystemWindow的检查,如果已经有系统窗口在最顶层,则把这个窗口插在系统窗口之下。

第二个问题你决定在界面弹出的时候再调用UpdateView,重新获取倒计时。

代码大致是这样的,这里以lua举例,确实是之前项目中经历数次迭代产生的代码……

local function PushWindow(view)
    local findIndex = 0
    for i = 1, #self.windows do
        if self.windows[i]:GetIsSystemWindow() then
            findIndex = i
        end
    end
    if #self.windows > 0 and view:GetIsSystemWindow() and findIndex > 0 then
        ---当前新入栈系统窗体,并且栈里面有其他系统窗体的时候,不能直接覆盖还在显示的系统窗体
        if self.windows[#self.windows].SetActive then
            self.windows[#self.windows]:SetActive(true)
        end
        if view.SetActive then
            view:SetActive(false)
        end
        table.insert(self.windows, findIndex, view)
    else
        if view.SetActive then
            view:SetActive(true)
            view:UpdateView()
        end
        table.insert(self.windows, view)
    end
    
local function PopWindow()
    if #self.windows > 0 then
        local topWindow = self.windows[#self.windows]
        topWindow.SetActive(True)
        topWindow.UpdateView()

到这里代码已经很难看了,PopWindow还要负责刷新底下的界面。

这样若能解决问题倒也罢了……结果是新问题又来了:

测试反馈有一些窗口会在调用UpdateView后发现倒计时已经结束,又触发关闭,策划看了很不满意。

于是你准备把SetActive放在UpdateView的回调里……

代码变成了这样

local function PushWindow(view)
    local findIndex = 0
    for i = 1, #self.windows do
        if self.windows[i]:GetIsSystemWindow() then
            findIndex = i
        end
    end
    if #self.windows > 0 and view:GetIsSystemWindow() and findIndex > 0 then
        ---当前新入栈系统窗体,并且栈里面有其他系统窗体的时候,不能直接覆盖还在显示的系统窗体
        if self.windows[#self.windows].SetActive then
            self.windows[#self.windows]:SetActive(true)
        end
        if view.SetActive then
            view:SetActive(false)
        end
        table.insert(self.windows, findIndex, view)
    else
        if view.SetActive then
            view:SetActive(true)
            view:UpdateView()
        end
        table.insert(self.windows, view)
    end
    
local function PopWindow()
    if #self.windows > 0 then
        local topWindow = self.windows[#self.windows]
        topWindow.UpdateView(function() topWindow.SetActive(True) end)

但你的领导也没有表扬你,他提了一个灵魂拷问:

底下的界面既然不需要显示,为什么要加载它再销毁它,嫌手机电量太多加载者玩是吧?

怎么办,很自然就想到了命令队列。

之所以上面的代码会那么难维护,就是消息的发送者与受理者没有解耦,每个消息(PushWindow)发出,受理者(WindowManager)都立即处理。

而如果我们引入命令队列,将每个PushWindow的消息视作存进一个命令队列,然后再在合适的时机执行队列的Dequeue,就能解决这个问题。

这个就是命令队列的原理。

引入命令队列实现

在本例中还实现了几个另外的功能

  1. 在部分场景中(如战斗),推送消息不弹出,直接舍弃
  2. 引入优先级,高优先级界面直接弹出并隐藏底下的界面,低优先级界面进入队列。

WindowManager类:

/// <summary>
/// 窗口管理器
/// </summary>
public class WindowManager : UIManager
{
    /// <summary>
    /// 界面所处的UILayer层级
    /// </summary>
    public override UILayer layer => UILayer.Window;

    // 界面命令队列
    private PriorityQueue<WindowCommand, int> _cmdQueue = new PriorityQueue<WindowCommand, int>();

    // UI栈
    public Stack<WindowBase> _uiStack = new Stack<WindowBase>();

    // 标准优先级,低于该优先级的界面不显示
    private int _horizonLinePriority = (int)PriorityStandardOffset.BothActiveAndPassive;

    private void Start()
    {
        Debug.Log("WindowManager Start");
        WindowCommand.parentTrf = this.transform;
    }

    /// <summary>
    /// 设置标准优先级,低于该值的命令将会被丢弃
    /// </summary>
    /// <param name="priority"></param>
    public void SetHorizonLinePriority(PriorityStandardOffset priority)
    {
        _horizonLinePriority = (int)priority;
    }

    /// <summary>
    /// 入栈界面
    /// </summary>
    /// <param name="uiTypeInfo">界面类型</param>
    /// <param name="windowPriority">界面优先级</param>
    /// <param name="timeout">超时时间</param>
    /// <param name="args">参数</param>
    public void Push(UITypeInfo uiTypeInfo, WindowPriority windowPriority = WindowPriority.ActiveWindow, float timeout = -1, params object[] args)
    {
        int priority = (int)windowPriority;
        // 优先级小于水平优先级,则直接丢弃
        if (priority < _horizonLinePriority)
        {
            return;
        }

        // 创建窗口命令
        var cmd = new WindowCommand(uiTypeInfo, priority, (WindowBase ui) =>
        {
            this._uiStack.Push(ui);
            ui.OnEnter();
        }, timeout, args);

        // 加入界面命令队列中
        _cmdQueue.Enqueue(cmd, priority);
        TryDequeue();
    }

    /// <summary>
    /// 出栈界面
    /// </summary>
    public void Pop()
    {
        if (_uiStack.Count > 0)
        {
            var ui = _uiStack.Pop();
            ui.OnExit();
            ui.Destroy();
        }
        TryDequeue();
    }

    // 尝试出队执行命令
    private void TryDequeue()
    {
        // 检查超时命令
        DequeueTimeoutCommands();
        // 比对优先级
        int topUIPriority = _uiStack.Count > 0 ? _uiStack.Peek().priority : -1;
        int topCmdPriority = _cmdQueue.Count > 0 ? _cmdQueue.Peek().priority : -1;
        if (topCmdPriority >= topUIPriority) //若命令队列里有优先级更高的界面,则显示该界面
        {
            if (topCmdPriority == topUIPriority && _uiStack.Count > 0)
            {
                // 相同优先级需要先隐藏当前界面
                _uiStack.Peek().Pause();
            }
            if (_cmdQueue.Count > 0)
            {
                var cmd = _cmdQueue.Dequeue();
                cmd.Execute();
            }
        }

        else // 若命令队列里无优先级更高的界面,则显示底下的界面(若有)
        {
            if (_uiStack.Count > 0)
            {
                _uiStack.Peek().Resume();
            }
        }
    }

    // 检查超时命令并移除
    private void DequeueTimeoutCommands()
    {
        while (_cmdQueue.Count > 0)
        {
            var command = _cmdQueue.Peek();
            // 若已超时,移除该命令
            if (DateTime.Now > command.timeoutDate)
            {
                _cmdQueue.Dequeue();
            }
            else
            {
                break;
            }
        }
    }
}

WindowCommand类

/// <summary>
/// 执行窗口UI的命令。
/// </summary>
public class WindowCommand : AbstractExecCommand<WindowBase>
{
    /// <summary>
    /// 父节点Transform。
    /// </summary>
    public static Transform parentTrf;

    /// <summary>
    /// 超时时间。
    /// </summary>
    public DateTime timeoutDate;

    /// <summary>
    /// 优先级。
    /// </summary>
    public int priority;

    /// <summary>
    /// UI类型。
    /// </summary>
    private UITypeInfo _uiTypeInfo;

    /// <summary>
    /// 命令参数。
    /// </summary>
    private object[] _arguments;

    /// <summary>
    /// 构造函数。
    /// </summary>
    /// <param name="uiTypeInfo">UI类型。</param>
    /// <param name="priority">优先级。</param>
    /// <param name="onExecuted">执行完回调函数。</param>
    /// <param name="timeout">超时时间。</param>
    /// <param name="args">命令参数。</param>
    public WindowCommand(UITypeInfo uiTypeInfo, int priority, Action<WindowBase> onExecuted, float timeout = -1, params object[] args)
    {
        this._uiTypeInfo = uiTypeInfo;
        this._arguments = args;
        this.priority = priority;
        this.onExecuted = onExecuted;
        if (timeout > 0)
        {
            timeoutDate = DateTime.Now.AddSeconds(timeout);
        }
        else
        {
            timeoutDate = DateTime.MaxValue;
        }
    }

    /// <summary>
    /// 执行命令。
    /// </summary>
    public override void Execute()
    {
        // 加载Prefab,并实例化
        GameObject prefab = Resources.Load<GameObject>(_uiTypeInfo.prefabPath);
        GameObject windowGo = GameObject.Instantiate(prefab, Vector3.zero, Quaternion.identity, parentTrf);

        // 创建Window并挂载到GameObject上
        WindowBase windowUI = (WindowBase)windowGo.AddComponent(_uiTypeInfo.scriptType);
        windowUI.priority = priority;
        // 初始化Window
        windowUI.Initialize(_arguments);
        onExecuted?.Invoke(windowUI);
    }
}

本例使用了优先队列来做命令缓冲区,是因为要实现优先级排序。

**若没有优先级排序需求,环形队列 | 问渠 (wenqu.site) **非常适合用来实现命令队列。

我见

命令队列的使用场景还有很多,如《游戏编程模式》中举的SoundManager的例子。

这里罗列几种适合使用命令队列的情况:

  • 对请求的处理会阻塞调用者。

    如播放音频,播放特效等需要同步加载的方法,使用命令队列可以更好地控制加载。

  • 处理者需要更灵活地受理调用者的请求。

    如音频并发的数量限制,若不解耦调用与受理,会导致并发超过临界值时随机的部分音效无法播放。

  • 处理者需要批量地处理请求

    命令队列下处理者可以批量地处理请求,如利用多线程

  • 我们想让处理者自定义何时执行命令,而非立即执行

总结:命令队列在接收消息与执行消息中定义了一个缓冲区,这个缓冲区解耦了命令的接收与受理,从而使得命令地处理变得动态且非实时。

源码

完整代码已上传至nickpansh/Unity-Design-Pattern | GitHub

其他设计模式

专题 | Unity3D游戏开发中的设计模式 | 问渠 (wenqu.site)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/457381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

集合专题·拔高·壹

文章目录 1 Collection单列集合、Map双列集合1.1 Collection单列集合1.1.1 Collection单列集合及其实现类1.1.1.1 list集合与Array数组1.1.1.1.1 ArrayList1.1.1.1.2 LinekdList1.1.1.1.2 Vector1.1.1.1.2.1 ArrayList、Vector &#xff08;线程安全&#xff09;的区别是什么1.…

【数据结构与算法】前缀和+哈希表算法

文章目录 一、引入二、前缀和与哈希表的结合三、例题3.1 和为 K 的子数组3.2 统计「优美子数组」3.3 路径总和III 四、总结 一、引入 关于前缀和和哈希这两个概念大家都不陌生&#xff0c;在之前的文章中也有过介绍&#xff1a;前缀和与差分算法详解 而哈希表最经典的一题莫过…

Kerberos设计和落地长常识

Kerberos 处理三类安全对象 票证 kerberos票证授予服务给每个客户发一张标记&#xff0c;该标记发送给一个特殊的服务器&#xff0c;证实kerberos最近已经认证了发送者&#xff0c;票证包括过期时间和新生成的会话密钥供客户和服务器使用。 认证 由客户构造的一个标记&#xff…

LVS负载均衡之NAT模式

实验准备四台虚拟机 192.168.255.128 做负载均衡器&#xff08;两张网卡&#xff0c;且都需要NAT模式&#xff09; 192.168.255.130 Nginx节点服务器1 192.168.255.131 Nginx节点服务器2 192.168.255.132 nfs共享服务器 现将四台虚拟机的防火墙关闭 一、nfs服务器配置 1…

使用Visual Studio 创建Windows服务,并安装部署

创建服务的工程 Program.cs是入口类&#xff0c;Service1.cs是写服务功能的组件&#xff0c;类似于winform项目中的Form1.cs。 打开Service1.cs&#xff0c;&#xff08;右键单击Service1.cs&#xff0c;选择查看源码&#xff09; OnStart&#xff1a;服务启动时&#xff0c;执…

产品说明书应该如何制作?

在当今互联网时代&#xff0c;产品说明书无疑是一种展示产品特性、引导用户使用和解决用户问题的重要方式。一份精心制作的产品说明书&#xff0c;能够让用户更快地了解和掌握产品的使用方法&#xff0c;让用户充分享受产品带来的便利。不过要做出一份高质量的产品说明书&#…

算法:在指定范围内生成随机不重复的位置

问题&#xff1a; 在游戏中&#xff0c;我们经常会遇到以下问题&#xff1a;在指定的范围内生成随机不重复的位置。 比如某次“神官赐福”活动中&#xff0c;需要在城门口生成n个不重复的宝箱。 针对这种问题&#xff0c;我们可以先将范围按照宝箱&#xff08;基本单元格&#…

塔望食研院丨百年益生菌,千亿市场正蓝海!

2022年12月塔望咨询开设塔望食品大健康消费研究院&#xff08;简称塔望食研院&#xff09;栏目&#xff0c;塔望食研院以“为食品行业品牌高质量发展赋能”为理念&#xff0c;将不定期发布食品大健康行业研究、消费研究报告。塔望食研院致力于结合外部数据、消费调研数据、企业…

目标追踪篇---yolov8_tracking复现

文章目录 目标追踪篇---yolov8_tracking复现1、下载源代码2、下载权重3、运行代码3.1、运行以下命令&#xff1a;3.2、结果如下3.3、视频结果 目标追踪篇—yolov8_tracking复现 本人的另一篇博客&#xff0c;本博客主要是源代码更新产生的新博客&#xff0c;比较过后两者还是有…

计算多列迭代次数的一种平均列近似方法

比较多列训练集的迭代次数&#xff0c;把多列训练集用单列近似&#xff0c;再通过计算单列的斥力比较迭代次数的大小顺序。 ( A, B )---3*30*2---( 1, 0 )( 0, 1 ) 让网络的输入有3个节点&#xff0c;AB各由5张二值化的图片组成&#xff0c;让A中有5个1&#xff0c;B中全是0&a…

魔兽服务端 MANGOS 数据库结构表中文解释

魔兽服务端 MANGOS 数据库结构表中文解释 MANGOS 数据库结构表 achievement_reward 巫妖王的奖励成就 areatrigger_involvedrelation 传送(区域触发)_包含的任务关系 areatrigger_scripts 区域触发脚本 areatrigger_tavern 进传送门…

优维低代码:个性化桌面配置

导语 优维低代码技术专栏&#xff0c;是一个全新的、技术为主的专栏&#xff0c;由优维技术委员会成员执笔&#xff0c;基于优维7年低代码技术研发及运维成果&#xff0c;主要介绍低代码相关的技术原理及架构逻辑&#xff0c;目的是给广大运维人提供一个技术交流与学习的平台。…

分布式系统概念和设计——安全模型中的设计和思考落地

分布式系统概念和设计 安全性攻击的形式——窃听&#xff0c;伪装&#xff0c;篡改&#xff0c;拒绝服务等。 可靠的分布式系统设计必须解决暴露的服务接口和不安全网络的问题&#xff0c;而攻击者可能了解其中所使用的算法并部署计算资源。 密码学为消息的私密性和完整性以及认…

Windows环境下的静态库和动态库的使用详解

文章目录 简介lib库的详细说明第一种是静态lib库第二种是lib导入库两种库的说明两种lib库的相同点和不同点 在visual studio下静态lib库的导出和使用导出过程演示使用过程演示使用方式一使用方式二使用方式三使用方式四使用方式五使用方式六使用方式七其他组合方式 在visual st…

2022-04-24:用go语言重写ffmpeg的muxing.c示例。

2022-04-24&#xff1a;用go语言重写ffmpeg的muxing.c示例。 答案2022-04-24&#xff1a; 本程序的大体过程如下&#xff1a; 打开输出文件并写入头部信息。 添加音频和视频流&#xff0c;并为每个流创建 AVCodecContext 对象&#xff0c;根据输入格式设置编码器参数&#x…

Volatile与ThreadLocal

一&#xff1a;Volatile 线程安全三方面 1 可见性&#xff1a;一个线程对共享变量修改&#xff0c;另一个线程可以看到最新结果 2 有序性&#xff1a; 一个线程内&#xff0c;代码编写按顺序执行 3 原子性&#xff1a; 一个线程内多行代码以一个整体运行&#xff0c;期间不能…

查询网站ip地址

IP地址是Internet Protocol&#xff08;互联网协议&#xff09;的一部分&#xff0c;是一个32位的数字&#xff0c;用于标识网络中的设备。它可以让不同的设备在网络上进行通信和交流&#xff0c;是网络通信的基础。IP地址的应用非常广泛&#xff0c;它可以用于识别和定位设备&…

最新数据, 芯片工程师平均月薪高达2.56W !

近日&#xff0c;据2023年一季度经济运行数据统计&#xff0c;其中提及全国居民人均可支配收入达到10870&#xff0c;同比增长3.8%。 2023年第一季度中&#xff0c;共有20个行业平均月薪超1W&#xff0c;除了大家所熟悉的金融行业薪资水平排名靠前&#xff0c;一些高技术制造业…

iOS SFSpeechRecognizer 语音识别

SFSpeechRecognizer 属于 Speech 框架&#xff0c;在 iOS 10 首次出现&#xff0c;并在 iOS13 中进行了比较重大的更新&#xff0c;在 iOS 13 上支持离线语音识别以及语音分析。WWDC2019 展示了其在 AI 领域的进步&#xff0c;其中 iOS 13 设备内置语音识别就是一项比较不错功能…

第二届中国抗衰老化妆品产业发展论坛暨国粹国妆品牌与文化交流会在京召开

消费日报网讯&#xff08;记者 王儒&#xff09;4月18日&#xff0c;以“国粹国妆 抗衰美丽“为主题的第二届中国抗衰老化妆品产业发展论坛暨国粹国妆品牌与文化交流会在北京召开。大会旨在挖掘国粹力量&#xff0c;搭建沟通交流合作的平台&#xff0c;推动中国高端化妆品品牌建…