Unity设计模式—命令队列

这篇具有很好参考价值的文章主要介绍了Unity设计模式—命令队列。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Unity设计模式—命令队列

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后发现倒计时已经结束,又触发关闭,策划看了很不满意。

Unity设计模式—命令队列

于是你准备把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)文章来源地址https://www.toymoban.com/news/detail-433295.html

到了这里,关于Unity设计模式—命令队列的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 设计模式——命令模式

    将一个请求封装成一个对象,从而让你使用不同的请求吧客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。 命令模式是一个高内聚的模式。 优点 类间解耦。调用者与接收者之间没有任何依赖关系。调用者只需要调用execute()方法即可,不需要了

    2024年02月15日
    浏览(39)
  • 设计模式-命令模式

    接受者(Receiver) 请求的实际作用对象 抽象命令(Command) 声明了执行请求的execute方法 具体命令(ConcreteCommand) 调用实际操作对象,实现execute 调用者(Invoker) 调用命令请求发送者 功能键绑定 比如遥控器的功能键,现在想要实现为功能可以自定义绑定事件,请使用命令模

    2024年02月09日
    浏览(40)
  • 设计模式之命令模式

    定义: 命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。 目的: 将一个请求封装成一个对象,从而使您

    2024年02月20日
    浏览(43)
  • 设计模式:命令模式

    命令模式(Command Pattern)是一种行为设计模式,它将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。 定义 命令模式包括以下主要角色: Command :命令接口,声明执行操作的方法。 ConcreteCommand :具

    2024年04月16日
    浏览(41)
  • 设计模式(19)命令模式

    一、介绍: 1、定义:命令模式(Command Pattern)是一种行为设计模式,它将请求封装为一个对象,从而使你可以使用不同的请求对客户端进行参数化。命令模式还支持请求的排队、记录日志、撤销操作等功能。 2、组成结构: (1)命令接口(Command):定义执行命令的方法,可

    2024年02月07日
    浏览(37)
  • 设计模式—行为型模式之命令模式

    命令(Command)模式:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。 命令模式包含以下主要角色。 抽象命令类(Command)角色:声明执行命令的接口,

    2024年01月25日
    浏览(40)
  • 设计模式之命令模式【行为型模式】

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。各位小伙伴,如果您: 想系统/深入学习某技术知识点… 一个人摸索学习很难坚持,想组团高效学习… 想写博

    2024年02月01日
    浏览(40)
  • 设计模式-命令模式(Command)

    命令模式(Command Pattern)是一种行为型设计模式,也被称为动作模式或事务模式。它的核心思想是将一个请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化。对请求排队或记录,以及支持可撤销的操作。命令模式的主要目的是将发出请求的对象和执行请求的

    2024年04月27日
    浏览(33)
  • 设计模式行为型——命令模式

    目录 命令模式的定义      命令模式的实现 命令模式角色 命令模式类图 命令模式举例 命令模式代码实现 命令模式的特点 优点 缺点 使用场景 注意事项         命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。是对命令的封装,每一个命令都是

    2024年02月14日
    浏览(42)
  • Java设计模式-命令模式

    命令模式,将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。对请求排队或记录请求日志,以及支持可撤销的操作。 命令模式乍一看,有点懵懵的。即使这个定义看完,也是不明所以。但是结合例子来讲的话,就比较容易理解了。 其实它就是把一个

    2024年02月15日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包