Unity基础框架从0到1(五)延时任务调度模块

news2024/11/24 15:52:36

索引

这是Unity基础框架从0到1的第五篇文章,前面的文章和对应的视频我一起列到这里:

文章

Unity基础框架从0到1 开篇

Unity游戏框架从0到1 (二) 单例模块

Unity基础框架从0到1(三)高效的全局消息系统

Unity基础框架从0到1(四)资源加载与管理模块

视频

一 开篇

二 单例模块

三 消息模块

四 资源加载模块

正文

本来这个框架的第五篇内容是对象池模块的,但是在处理对象池模块时发现自动回收类的资源需要在未来某个时间点触发回收事件,未来某个时间点触发事件是一个很通用的操作,因此这里插入一章来阐述这个延时任务调度模块。

在游戏开发过程中我们经常会碰到延时任务,比如敌人还有3秒到达战场,红蓝Buff还有25刷新,大龙还有1分钟刷新,任务还有3分钟更新等等。如果在每个处理延时任务的地方都自己编写一段计时的代码,那将是会非常痛苦的,不仅加大了业务的复杂度,并且在规模较大时可能会导致出现性能问题,此外,在一些时间点挨得很近且强调调用先后顺序的地方甚至会出现调用顺序不一致的情况。因此,我们需要有一个东西来将这些延时任务统一管理调配,确保执行顺序没有问题,并简化系统逻辑,提升程序性能。

设计思路

不难看出,前面说的这些种种,抽象来看都是在某个时间点到了后做某件事,因此我们可以将这里的计时逻辑与触发逻辑抽离出来,业务这边只负责注册任务与任务执行的时间,注册时调度器为这些任务做好排序,并在时间更新的时候轮询判断是否要执行任务。由于在调度器这边做了排序,因此可以确保任务执行顺序是没毛病的。

再深入思考一下,如果时间T1的任务没有执行,那任意大于T1的时间T对应任务也不应该被执行,且不用去判断。我们前面已经得到了一个有序的列表,那我们时间更新时仅需判断有序列表的第一个位置上任务是否要执行,如果当前时间比第一个位置任务执行时间小,那后续的时间都不需要判断了。如果第一个位置任务需要执行,则继续此判断,直到没有任务或者当前时间比第一个位置任务执行时间小。

由于我们加入了一个排序操作,这使得我们在添加新任务以及移除现有任务时开销变得比之前大,并且在执行任务时需要将后续任务移动到列表前面来,如果使用链表,移动操作消耗减少了,但随之而来的是节点前后指针的内存占用问题。为了平衡新增任务、移除任务和执行任务的代价,可以加入优先队列(其实就是最小堆或者最大堆),这样三个操作的代价都降低了。同时,我们可以加入区间划分,将同一个时间区间内的任务放到一起,这样在排序时,可以对时间区间做排序,然后区间内自行排序,借此来降低排序对象的数量。在本文完成之际,从New Bing这边得知了一种更高效的时间轮算法,这种算法思路和区间划分有点类似,不过更为巧妙,不仅内存占用更少,并且三种操作执行代价也更小,感兴趣的童鞋可以自行查阅学习。

实现方案简述

下面就不多BB了,直接上本文的实现方案:

本文使用了优先队列+区间划分的思路来实现任务存储,优先队列是为了快速取到最接近当前时间的一个任务,在插入新任务时,调整的代价是O(logN),每次判断时代价是O(1),移除任务时找到移除的元素代价是O(N),实际移除后调整的代价是O(LogN),执行任务时调整的代价是Log(N)。本文区间划分单位是1,因此只是把同一个时间点的任务划分在了一起,借此来减少队列的元素规模。下图是一个简单的例子,当没有做区间划分时,队列中元素个数为8,做完区间划分后队列中元素个数为3。通过简单的区间划分,有效减少了队列的长度。(队列中的元素顺序可能不满足优先队列的标准,由于时间关系没有仔细构造数据了,希望没有对大家理解思路造成困扰)

image-20230412021008520

当然,在很多时候,我们的任务时间并不会这么密集,所以可以根据实际情况来修改区间划分长度与要划分的区间层数。如下图,可以增加了初始队列的区间划分长度,将[20-29]内的任务都放在了第一个元素中。在每个元素中,我们又对齐做了长度为1的区间划分,将AA放在一个元素,BBB放在一个元素。这样在每次插入和移除时,我们初始队列以及实际内部的队列调整代价规模都比较小。PS.多区间划分本文暂时没有处理,仅给出思路抛砖引玉。

image-20230412022730117

Show Code

该模块核心部分是调度类DelayedTaskModule、延时任务列表类DelayedTaskList和延时任务数据类DelayedTaskDataTimerUtil类提供两个获取时间的接口,Heap类用来实现优先队列提高检测的效率,避开了消息轮询判断。

调度类DelayedTaskModule负责对外提供相关的操作接口,比如注册延时任务的方法,回收延时任务的方法。并在内部提供一个更新当前时间的方法,在更新时间时去判断是否需要触发对应任务。

延时任务列表类DelayedTaskList存储了某个确定时间点注册的延时任务数据。

延时任务数据类DelayedTaskData存储了一个时间,一个到点触发的任务和一个被提前移除的任务。

具体设计

堆的代码比较长,这里就不贴了,感兴趣可以去这里https://github.com/tang-xiaolong/MapGridInUnity/blob/main/Assets/LMapModule/LDataStruct/Heap.cs看。堆内部根据外界传入的最小最大枚举值来确定比较方法,比较方法会调用堆内元素的CompareTo方法来比较,因此这也是DelayedTaskList需要实现IComparable的原因。

数据类比较简单,内部只有几个变量负责存储必要的数据。

DelayedTaskData.CS

public class DelayedTaskData
{
    public long Time;
    public Action Action;
    public Action EarlyRemoveCallback;
}

列表类也比较简单,里面存了一个列表,存储时间点注册的所有任务数据。类实现了IComparable是为了后续可以放入优先队列中对所有时间点对应的列表进行排序,避免每次更新时间都遍历所有列表。

DelayedTaskList.CS

public class DelayedTaskList : IComparable, IEnumerable<DelayedTaskData>, IDisposable
{
    private bool _disposed = false;
    public long Time;
    public List<DelayedTaskData> DelayedTaskDataList;

    public int CompareTo(object obj)
    {
        if (obj == null)
            return 1;
        return CompareTo((DelayedTaskList)obj);
    }

    public int CompareTo(DelayedTaskList obj)
    {
        return Time.CompareTo(obj.Time);
    }

    IEnumerator<DelayedTaskData> IEnumerable<DelayedTaskData>.GetEnumerator()
    {
        return DelayedTaskDataList.GetEnumerator();
    }

    public IEnumerator GetEnumerator()
    {
        return DelayedTaskDataList.GetEnumerator();
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (_disposed)
            return;
        if (disposing)
        {
            DelayedTaskDataList.Clear();
            DelayedTaskDataList = null;
        }
        _disposed = true;
    }

    ~DelayedTaskList()
    {
        Dispose(false);
    }
}

调度类内部有一个字典_delayedTaskDict,Key是注册过的时间点,Value是这个时间点对应的任务列表。_delayedTaskQueue是一个使用最小堆实现的优先队列,队列中存储了每个任务点注册的任务列表,即每个元素都是一个列表对象。

UpdateTime方法更新时间:

先更新当前的时间,并判断当前时间是否大于等于队列最前面的元素的时间,如果大于等于了,说明需要触发这个时间点对应列表里的所有任务了,先将这个元素从队列中移除并遍历任务列表依次触发注册的任务,处理完任务后继续判断,直到队列为空或者是队列头元素的时间大于当前时间

AddDelayedTask方法注册任务:

外界传入一个毫秒级别的时间戳,如果是一个过时的时间,直接结束。如果没注册过这个时间戳,则创建一个任务列表,并将任务列表加入到字典里。最后将新任务保存到一个新的任务对象中并加入到这个时间戳对应的任务列表,并返回新创建的这个任务对象给调用方,使得调用方可以持有任务对象做移除操作。

RemoveDelayedTask方法移除任务:

传入一个任务对象,如果判断存在这个任务,则将其从列表中移除。如果列表移除对象后数量为0,则将列表也从字典中移除。

PS. 需要注意的是,在不同的项目中,计算时间和更新时间的方法不尽相同,需要根据自己项目来修改。本项目是直接获取的UTC毫秒级别的时间戳,并在Update方法中调用更新时间的方法。

DelayedTaskScheduler.CS

/// <summary>
/// 延时任务调度器
/// </summary>
[DefaultExecutionOrder(1)]
public class DelayedTaskScheduler : MonoBehaviour, IDisposable
{
    private Dictionary<long, DelayedTaskList> _delayedTaskDict = new Dictionary<long, DelayedTaskList>();
    private Heap<DelayedTaskList> _delayedTaskQueue = new Heap<DelayedTaskList>(10, HeapType.MinHeap);
    private bool _disposed = false;
    [SerializeField] private long CurrentTime;
    public static DelayedTaskScheduler Instance { get; private set; }

    #region 时间事件管理

    /// <summary>
    /// 增加一个时间事件对象
    /// </summary>
    /// <param name="time">毫秒数</param>
    /// <param name="action"></param>
    public DelayedTaskData AddDelayedTask(long time, Action action, Action earlyRemoveCallback = null)
    {
        if (time < CurrentTime)
        {
            Debug.LogError($"The time is pass. Time is {time} CurrentTime is {CurrentTime}");
            return null;
        }

        if (!_delayedTaskDict.TryGetValue(time, out var delayedTaskList))
        {
            delayedTaskList = ObjectPoolFactory.Instance.GetItem<DelayedTaskList>();
            delayedTaskList.Time = time;
            delayedTaskList.DelayedTaskDataList = ObjectPoolFactory.Instance.GetItem<List<DelayedTaskData>>();
            delayedTaskList.DelayedTaskDataList.Clear();
            _delayedTaskQueue.Insert(delayedTaskList);
            _delayedTaskDict.Add(time, delayedTaskList);
        }

        var newEventData = ObjectPoolFactory.Instance.GetItem<DelayedTaskData>();
        newEventData.Time = time;
        newEventData.Action = action;
        newEventData.EarlyRemoveCallback = earlyRemoveCallback;
        delayedTaskList.DelayedTaskDataList.Add(newEventData);
        return newEventData;
    }

    /// <summary>
    /// 移除一个时间事件对象
    /// </summary>
    /// <param name="delayedTaskData"></param>
    /// <exception cref="Exception"></exception>
    public void RemoveDelayedTask(DelayedTaskData delayedTaskData)
    {
        if (delayedTaskData == null)
            return;
        if (_delayedTaskDict.TryGetValue(delayedTaskData.Time, out var delayedTaskList))
        {
            bool removeSuccess = delayedTaskList.DelayedTaskDataList.Remove(delayedTaskData);
            if (removeSuccess)
                delayedTaskData.EarlyRemoveCallback?.Invoke();
            if (delayedTaskList.DelayedTaskDataList.Count == 0)
            {
                _delayedTaskDict.Remove(delayedTaskData.Time);
                if (_delayedTaskQueue.Delete(delayedTaskList))
                {
                    ObjectPoolFactory.Instance.RecycleItem(delayedTaskList.DelayedTaskDataList);
                    ObjectPoolFactory.Instance.RecycleItem(delayedTaskList);
                    ObjectPoolFactory.Instance.RecycleItem(delayedTaskData);
                }
                else
                {
                    ObjectPoolFactory.Instance.RecycleItem(delayedTaskData);
                    throw new Exception("DelayedTaskScheduler RemoveDelayedTask Error");
                }
            }
        }
        else
        {
            ObjectPoolFactory.Instance.RecycleItem(delayedTaskData);
        }
    }

    /// <summary>
    /// TODO:根据自己游戏的逻辑调整调用时机
    /// </summary>
    /// <param name="time"></param>
    public void UpdateTime(long time)
    {
        CurrentTime = time;
        while (_delayedTaskQueue.Count > 0 && _delayedTaskQueue.GetHead().Time <= time)
        {
            long targetTime = _delayedTaskQueue.GetHead().Time;
            _delayedTaskDict.Remove(targetTime);
            var delayedTaskList = _delayedTaskQueue.DeleteHead();
            foreach (DelayedTaskData delayedTaskData in delayedTaskList)
            {
                delayedTaskData.Action?.Invoke();
                ObjectPoolFactory.Instance.RecycleItem(delayedTaskData);
            }

            //回收时记得把列表清空,防止下次使用时出现问题!!!!!不要问我为什么这么多感叹号 
            delayedTaskList.DelayedTaskDataList.Clear();
            ObjectPoolFactory.Instance.RecycleItem(delayedTaskList.DelayedTaskDataList);
            ObjectPoolFactory.Instance.RecycleItem(delayedTaskList);
        }
    }

    #endregion

    #region Mono方法与测试的设置时间代码

    private void Awake()
    {
        Instance = this;
        UpdateTime(TimerUtil.GetTimeStamp(true));
    }

    public void Update()
    {
        UpdateTime(TimerUtil.GetTimeStamp(true));
    }

    private void OnDestroy()
    {
        Dispose();
    }

    #endregion

    #region Dispose

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                _delayedTaskQueue?.Dispose();
                Instance = null;
            }

            _disposed = true;
        }
    }

    ~DelayedTaskScheduler()
    {
        Dispose(false);
    }

    #endregion
}

使用范例

在下面的例子中,我们在Start方法中注册了两个任务,让其在1.5秒后和4.5秒后执行打印的方法。并在Update中设置了通过按键来创建一个随机时间注册任务,以及通过按键移除随机创建的任务。

打开测试场景后,执行了Start方法里注册的任务,按下C键也执行了一个随机时间任务,再次按下C键并提前按下R键,将这个任务提前移除了,并且打印执行的方法没有再执行。

image-20230410135038434

TestDelayTask.CS

public class TestDelayTask : MonoBehaviour
{
    private void Start()
    {
        AddLaterExecuteFunc(1.5f);
        AddLaterExecuteFunc(1.5f);
        AddLaterExecuteFunc(1.5f);
        AddLaterExecuteFunc(4.5f);
        AddLaterExecuteFunc(4.5f);
        AddLaterExecuteFunc(4.5f);
    }
    
    public int forceTestCount = 100000;
    List<DelayedTaskData> futureEventDataList = new List<DelayedTaskData>(100000);
    List<long> testTimes = new List<long>(100000);
    
    [ContextMenu("暴力测试")]
    public void ForceTest()
    {
        testTimes.Clear();
        for (int i = 0; i < forceTestCount; i++)
        {
            testTimes.Add(TimerUtil.GetLaterMilliSecondsBySecond(UnityEngine.Random.Range(1, 15.0f)));
        }
        futureEventDataList.Clear();

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int i = 0; i < forceTestCount; i++)
        {
            futureEventDataList.Add(DelayedTaskScheduler.Instance.AddDelayedTask(testTimes[i], TestFunc));
        }

        for (int i = 0; i < forceTestCount; i++)
        {
            DelayedTaskScheduler.Instance.RemoveDelayedTask(futureEventDataList[i]);
        }
        
        stopwatch.Stop();
        Debug.Log($"暴力测试完成,共耗时{stopwatch.ElapsedMilliseconds / 1000.0f}秒");
    }

    void TestFunc()
    {
        Debug.Log("测试方法执行了");
    }

    private DelayedTaskData AddLaterExecuteFunc(float time, Action completeAction = null, Action earlyRemoveAction = null)
    {
        var pressTime = Time.time;
        Stopwatch stopwatch = ObjectPoolFactory.Instance.GetItem<Stopwatch>();
        stopwatch.Restart();
        return DelayedTaskScheduler.Instance.AddDelayedTask(TimerUtil.GetLaterMilliSecondsBySecond(time),
            () =>
            {
                stopwatch.Stop();
                ObjectPoolFactory.Instance.RecycleItem(stopwatch);
                // Debug.Log($"{time}秒后了,执行了对应方法。实际过去了{Time.time - pressTime}秒");
                Debug.Log($"{time}秒后了,执行了对应方法。实际过去了{stopwatch.ElapsedMilliseconds / 1000.0f}秒");
                completeAction?.Invoke();
            }, () =>
            {
                earlyRemoveAction?.Invoke();
                stopwatch.Stop();
                Debug.Log($"提前移除了,已经过去了{stopwatch.ElapsedMilliseconds / 1000.0f}秒");
                ObjectPoolFactory.Instance.RecycleItem(stopwatch);
            });
    }

    DelayedTaskData _delayedTaskData;

    void RecycleDelayedTask()
    {
        if (_delayedTaskData != null)
        {
            DelayedTaskScheduler.Instance.RemoveDelayedTask(_delayedTaskData);
            _delayedTaskData = null;
        }
    }

    private void Update()
    {
        //持续按下时不断创建和回收
        if (Input.GetKey(KeyCode.C))
        {
            RecycleDelayedTask();
            _delayedTaskData = AddLaterExecuteFunc(UnityEngine.Random.Range(1, 5.0f), () => _delayedTaskData = null);
        }

        if (Input.GetKeyDown(KeyCode.R))
        {
            RecycleDelayedTask();
        }
    }
}

总结

在本文中,我们使用C#语言实现了一个毫秒级别的延时任务调度器,为游戏中通用的延时行为提供管理调度,加入排序思想,来保证任务按顺序执行;使用优先队列快速索引对象,以及减少各个操作带来的消耗;使用区间划分来减少优先队列中元素数量。这个调度器在客户端已经基本够用了,但是如果想拓展到服务器那边使用,我们还需要继续优化算法来减少各个操作的时间以及内存占用,感兴趣的童鞋可以继续深入研究,后续有时间我也会将多轮区间划分和时间轮算法整合进这个延时任务调度模块中,欢迎大家与我交流。

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

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

相关文章

CentOS 7 DNS服务器架设

CentOS 7 DNS服务器部署 项目背景和要求 要保证即能够解析内网域名linuxidc.local的解析&#xff0c;又能解析互联网的域名。 主DNS服务器&#xff1a;ZZYH1.LINUXIDC.LOCAL 辅助DNS服务器&#xff1a;ZZYH2.LINUXIDC.LOCAL 包含以下域的信息&#xff1a; 1、linuxidc.lo…

mybatis多表联查(一对一、一对多/多对一、多对多)

mybatis多表联查(一对一、一对多/多对一、多对多) 在开发过程中单表查询往往不能满足需求分析的很多功能&#xff0c;对于比较复杂业务来说&#xff0c;关联的表有几个&#xff0c;甚至是几十个并且表与表之间的关联相当复杂。为了能够实现复杂的功能业务&#xff0c;就必须进…

Java 并发工具合集 JUC 大爆发!!!

并发工具类 通常我们所说的并发包也就是 java.util.concurrent (JUC)&#xff0c;集中了 Java 并发的各种工具类&#xff0c; 合理地使用它们能帮忙我们快速地完成功能 。 1. CountDownLatch CountDownLatch 是一个同步计数器&#xff0c;初始化的时候 传入需要计数的线程等待数…

Monaco Editor编辑器教程(二七):集成多个GitLab编辑器颜色样式主题

前言 在开发编辑器时,未来满足开发者的审美需求,有时需要设计多套颜色主题,最基础的是黑色主题和白色主题。如果用户愿意出钱的话,可以加上一些其他花里胡哨的颜色主题,在vscode中是可以搜到。今天就来教大家一些,如何借助现成的资源来为自己的monaco编辑器增加丰富的颜…

JavaScript控制语句及搭建前端服务器

目录 一、for in 二、for of 三、try catch 四、搭建前端服务器 &#xff08;1&#xff09;安装nvm &#xff08;2&#xff09;检查npm &#xff08;3&#xff09;搭建前端服务器 一、for in 主要用来遍历对象 let father {name:张三, age:18, study:function(){}};f…

Linux常用的网络命令有哪些?快速入门!

在Linux系统中&#xff0c;有许多常用的网络命令可以用来进行网络配置和故障排除。这些命令可以帮助我们了解网络的状态和性能&#xff0c;并且可以快速诊断和解决网络问题。在本文中&#xff0c;我们将介绍一些常用的Linux网络命令&#xff0c;并提供一些案例来帮助您更好地理…

高数基础2

目录 函数的极限&#xff1a; 自变量趋向于有限值时函数的极限 左右极限 需要分左右极限的三种问题&#xff1a; 例题&#xff1a; 极限性质&#xff1a; 函数的保号性&#xff1a; 函数的保号性&#xff1a; 例题&#xff1a; 极限值与无穷小的关系 极限存在准则&#…

FRRoute 路由信息采集 + MPLS VPN隧道信息采集与识别

FRRoute 路由信息采集 MPLS VPN隧道信息采集与识别FRRoute数据库-表路由信息采集命令输出字段 -> 映射到 -> 数据库字段数据结构 算法show int brroute -nshow interfaces loMPLS VPN隧道信息采集与识别step 1 : 采集mpls邻居表step 2 : MPLS 隧道识别FRRoute 数据库-…

RK3568平台开发系列讲解(Linux系统篇)线程 pthread 详解

🚀返回专栏总目录 文章目录 一、POSIX 线程模型二、pthread_create()创建线程三、线程属性3.1、初始化线程对象属性3.2、销毁一个线程属性对象3.3、线程的分离状态3.4、线程的调度策略3.5、线程的优先级3.6、线程栈四、线程退出五、回收已终止线程的资源沉淀、分享、成长,让…

Word处理控件Aspose.Words功能演示:使用C#创建MS Word文档

Aspose.Words是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。 Aspose API支持流行文件格式处理&#xff0c;并…

苹果6信号不好的快速解决方法

许多朋友反馈&#xff0c;苹果6的信号不佳&#xff0c;建议从以下方面查找&#xff1a; 方法一&#xff1a;开启飞行模式后再关闭 有时候手机由于周围环境网络比较差&#xff0c;会导致信号处于无服务状态&#xff0c;这时后我们开启飞行模式后再关闭飞行模式&#xff0c;系统就…

反序列化漏洞及PHP魔法函数

目录 1、漏洞原理 2、序列化&#xff08;以PHP语言为例&#xff09; 3、反序列化 4、PHP魔法函数 &#xff08;1&#xff09;__wakeup() &#xff08;2&#xff09;__destruct() &#xff08;3&#xff09;__construct() &#xff08;4&#xff09;__toString() &…

Arduino UNO驱动micro SD卡读写模块

目录一、简介二、使用前准备三、测试方法四、实验现象一、简介 Micro SD卡模块TF卡读写卡器板载电平转换电路&#xff0c;即接口电平可为5V或3.3V&#xff0c;支持支持Micro SD卡(≤2G)、Micro SDHC高速卡(≤32G)。供电电源为4.5V~5.5V&#xff0c;板载3.3V稳压电路&#xff0…

甘特图控件DHTMLX Gantt入门使用教程【引入】:dhtmlxGantt与ASP.NET Core(上)

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

minio 部署、迁移、使用

一、minio 部署 1、单节点部署 部署IP&#xff1a;192.168.206.10 1.1、下载minio的二进制文件 wget https://dl.min.io/server/minio/release/linux-amd64/minio 1.2、规划磁盘 minio 数据目录一定不能和 跟/ 在一个磁盘上&#xff0c;要单独挂载&#xff0c;比如规划将m…

一文看懂SpringBoot操纵数据库

1.前言 很多同学进入公司就开始参与项目开发&#xff0c;大多数情况是对某个项目进行维护或者需求迭代&#xff0c;能够从0到1参与到项目中的机会很少&#xff0c;因此并没有多少机会了解某些技术的运行机制。换句话说&#xff0c;有的面试官在面试的时候就会探讨深层的技术问题…

【产品设计】那些被迫妥协的产品设计背后的技术原因

刚入门的产品经理经常会听到前辈们说应该懂点技术&#xff0c;却不明白为什么。本文作者分享了几个被迫妥协的产品设计的例子&#xff0c;希望能让不是技术出身的产品经理了解到“产品经理应该懂点技术”在产品设计中有什么指导意义&#xff0c;一起来看一下吧。 刚入门的产品经…

学习C++:C++进阶(六)如何在C++代码中调用python类,实例化python中类的对象,如何将conda中的深度学习环境导入C++项目中

目录 1.应用场景 2.场景&#xff1a;利用maskrcnn深度学习网络实现语义分割 3.CMake配置python解释器 4.C中实现实例化python中的类的对象并调用类内方法 4.1 初始化python环境 4.2 实例化python类的对象 1.应用场景 我们在视觉SLAM以及目标检测和识别中&#xff0c;经常…

AMD GPU安装运行stable diffusion

本文操作环境为Windows10/11AMD AI绘画是一种利用人工智能技术进行绘画的方法。它可以通过机器学习算法来学习艺术家的风格&#xff0c;并生成类似于艺术家的作品。最近&#xff0c;AI绘画技术得到了很大的发展&#xff0c;许多公司和研究机构都在进行相关的研究和开发。例如&…

华为电脑录屏功能在哪?这样做,您可以快速找到

案例&#xff1a;如何在华为电脑上找到电脑录屏功能&#xff1f; “听说电脑有录屏功能&#xff0c;但我在我的华为电脑上找了很久&#xff0c;都没有找到。难道华为电脑没有录屏功能吗&#xff1f;有没有小伙伴可以告诉我华为电脑录屏功能在哪&#xff1f;孩子已经快绝望了&a…