unreal4 协程 - CSDN
精华内容
参与话题
  • 为什么Unreal 4引擎能轻易实时渲染出vray要花半天才能渲染出的场景? 这不是真的!This is Unreal! 看了这个文章,大为感慨。如果有人以unreal 4为基础开发渲染软件,和rhino、su、revit等常用建筑软件对接...
    
    
    这不是真的!This is Unreal!

    看了这个文章,大为感慨。如果有人以unreal 4为基础开发渲染软件,和rhino、su、revit等常用建筑软件对接,画面太美不敢想。
    更多回答
    7

    Peng DU游戏技术 Game Technologies

    不用做光线跟踪的快都是耍流氓。屏幕空间反射还好吧。屏幕空间折射很容易穿帮。
    来补充些内容,让这个事情说得更明白一些:

    先上视频:
    UE4场景练习—在线播放—优酷网,视频高清在线观看
    http://v.youku.com/v_show/id_XMTM0OTg3NDIyMA==.html


    我和余德杰都是建筑学的,爱好CG,做这件事情,就是想学一下UE4,视频什么的都是学习过程的副产品,学了半天总得做个小成果展示一下啊。

    楼下说软文的先生 @haisenberg ,你确实是误会了,不过也不怪你,因为这个问题看起来确实很像软文!(笑)为啥咧?因为这是建筑系的同学问的,搞引擎和游戏的专业同学不会有这样的误解。

    要知道建筑学这边的学生一般都是拿Vray之类的渲效果图的,也没人教,都要很苦逼的自己摸索,突然看见这么好的实时效果,自然会冒出这样的困惑。

    所以,要认认真真的的回答这个问题的话,还要按知乎的规矩来:先问是不是,再问为什么。

    不存在题主所说的情况:UE4并不能轻易实时渲染出写实场景。

    想要一个好的UE场景,前期需要大量的优化,模型的优化,贴图的优化,然后还需要光影烘培“Build”的时间,而这个时间,就类似于Vray在渲图的时间。参考这个问题下的几个专业人士的回答,拿Vray渲染器和UE比其实是不公平的。

    但是UE的间接光线不像Vray渲染的精度那么高,在能容忍的粗糙范围内,小场景的Build的时间也不需要太长。

    以我们这个场景为例,因为模型用的是现成的Evermotion高精度模型,未经优化,最终整个小沙盒的Build时间是2小时左右。配置是:i7 5820k。如果模型优化得好,Build时间会大大降低,但是优化的时间要数倍于为Build节省的时间,但是别人运行起我们这个场景来就会更流畅。我们要是把周边的墙断开成四个小面的话,墙的光影贴图面积就不用那么大了,墙上的光影精度会大大提升,Build时间也会下降。

    你发现了吗?就像等价交换一样,一切效果都要代价,效果好则时间肯定相应增加,无论你是离线还是实时渲染。处处要取舍。

    我个人一直主张在学习设计的过程中把建筑的可视化技术前置,结合到方案推敲过程中,而不是方案做完了画张效果图交差,那样对建筑学习没啥意义。我们看中的是实时渲染沙盒的自由度,以及模型搭建出来以后,推敲材料的方便性。

    所以这里提醒各位建筑系同学,好工具要用对了地方,不要看视频效果好就拿UE做效果图,最后很可能吃力不讨好。这事情不是这么个玩法的。


    当然了,如果是专业做效果图的公司,自己优化好大量的模型库,那做起效果图来效率也是非常高的。

    Lumion就是想做这方面的尝试,我之前搞错了,我之前以为是ue3,经评论提醒,Lumion是基于quest3d引擎开发的。

    但是之前的Lumion效果还是有点假,不知这次的新Lumion怎么样,有没有突“假与真”的临界点。

    现在UE4效果更上一层楼了,如果有人用UE4做一个供建筑设计专业使用的软件,那确实会很有助于空间材质效果的推敲。

    下面简单说说光影烘培技术的思路。

    实时引擎的光影烘焙利用的一个事实就是漫反射物体的光照和光线反弹是“摄像机无关的”。

    比如下面这个简单的场景,立方体、球体、光源的空间位置一旦敲定,每个点的着色就不会随着观察者位置的移动而改变了。说白了就是,我摄影师扛着相机围着你乱跑,模特脸上的颜色不会跟着变来变去的。

    如果场景光照条件固定,那么这些光影信息就没必要参与实时计算了,只需要预先计算出来,在UE4里叫“Build”。然后把这些光线的明暗着色“烘焙”到物体表面就好了。

    所以物体表面的贴图着色就变成这样了:

    如是制造了光影着色的错觉。

    所以虽然叫实时渲染,但是很多时候并不是“实时”计算的,而是预先计算的。

    UE4里的lightmass还不简单的是一个光影贴图,而是一个立体的阴影区域,Build好以后,只要物体挪到了影子里,也会变黑。很棒的技术。

    刚才说了光照是“摄像机无关”,反射效果就是“摄像机相关”的:

    要补一句,这里的反射是狭义的像金属之类物体的光亮的反射,不包括漫反射,昨天有位同学问我漫反射也是反射啊,把我提醒了。

    一旦涉及反射折射等效果的着色,那么就和摄像机的位置关系很大了(不同角度反射到的内容自然是一直在变),这种东西就要靠实时的显卡计算,但是在实时渲染中,这里面trick很多,能满足视觉效果,但是不科学。

    科学的还是maxwell那种物理模拟计算,需要离线渲染,需要很多时间。

    知乎图形学大神太多,我这个压力还是很大的。
    展开全文
  • lua协程的使用列子分析

    千次阅读 2015-06-30 16:33:41
    handle = coroutine.create(function (arg1,arge2) local start = 0 print(arg1,arg2) while true do if( start == 0 or start == 20) then print("yield arg is :",coroutine.yield(arg .
    
    

    例子一

    handle = coroutine.create(function (arg1,arge2)
      local start = 0
    
      print(arg1,arg2)
    
      while true do
        if( start == 0 or start == 20) then
          print("yield arg is :",coroutine.yield(arg .. "me",arg2+1))
          print("back to",start)
        end
    
        if start == 30 then
          break
        end
    
        start = start + 1
        print("it is first coroutine istance")
      end
    
      return "coroutine is over"
    
    end)
    
    print(coroutine.resume(handle,"test",1999))
    print(coroutine.resume(handle,"ooo",33))
    print(coroutine.resume(handle,"111",77))
    print(coroutine.resume(handle,"jjj",55))
    
    


    输出结果:


    分析

    第一次调用resume,此时没有对应的yield,它的参数时传递给匿名函数的

    第二次调用resume,此时有对应的yield, 此时resume的作用是从函数yield返回,resume的参数正好传递给yield作为返回值(yield的第一个返回值是固定的,返回调用状态true or false)


    第一次调用yield,并传递了参数,此时的yield参数是作为:让本次yield调用返回的(匿名函数继续执行,而不是卡在yield函数调用处),resume 调用的返回值。


    例子二(生产者,消费者)

    function send(x)
      coroutine.yield(x)
    end
    
    pd_handle = coroutine.create(function ()
      local x = 0
      while true do
        x = io.read()
        send(x)
    end)
    
    fuction receive()
      local status,value = coroutine.resume(pd_handle)
      return value
    end
    
    function consumer()
      local y = 0
      while true do
        y = receive()
        print("receive value is :",y)
      end
    end
    
    consumer()
    
    




     

    展开全文
  • UE4套件-多线程库开发

    2019-08-13 21:34:00
    哈喽大家好,我叫人宅,这一期我们给大家带来一套工具类教程,如何开发UE4多线程商业库。 本套教程将融合了UE4代理,UE4多线程,UE4智能指针,插件开发等综合知识进阶的技巧,是一套综 合实战课程。, 虽然UE4为...

     


    哈喽大家好,我叫人宅,这一期我们给大家带来一套工具类教程,如何开发UE4多线程商业库。

    本套教程将融合了UE4代理,UE4多线程,UE4智能指针,插件开发等综合知识进阶的技巧,是一套综 合实战课程。,

    虽然UE4为我们游戏开发提供了三套多线程解决方案,但是在我们日常开发项目中,依然需要封装和整合UE4线程,比如做服务器开发 or 是引擎开发。如果对多线程使用上只是熟悉或者不太了解的,那么在使用中会出现很多奇奇怪怪的问题,而束手无策。那有没有什么库可以解决这个问题,当然有,非常多,比如std::thread,Boost库等,都提供了丰富的多线程的封装,但是有一点,它们的风格不是UE4C++风格,维护上也是一件很麻烦的事情,因为这些库的源码不可能为你公开的,及时公开, 源码也不一定看得懂。

     

    这一期我们来封装一套基于UE4C++商业版本的线程库,直接可以用于项目中,最重要是使用方便,轻巧便捷,维护性强。


    我们简单介绍一下这套教程会给您带来哪些技能的知识点;

    1. 掌握UE4多线使用,

    怎么加锁,如何挂起线程,如何唤醒,如何避免死锁,如何让线程同步,如何异步,如何开启线程,如何安全的销毁线程,如何高效的使用UE4线程,如何创建任务队列,如何搭建和维护自己的线程池。

    2 封装技巧

    我们主要讲解的是商业版本的多线程库的封装,你可以在该教程中学习到很多高级的封装技巧,如何封装管理类,如何使用模板的高级封装,这些封装最终是让使用者更加便捷,我们会用最少的代码来描述一套商业线程是如何高质量开发出来的,学完这些技巧后您可以运用在自己的项目中,使代码令人眼前一亮。

    3 代理的高级运用

    我们会讲解代理的高级运用,这套教程您将对代理有了更深刻的理解,这些技巧您将会在日后的项目开发中,让代码更有分量感。

    4 协程

    我们会讲解什么是协程,怎么使用以及,如何高级的封装UE4协程。

    5. 原生多线程

    我们会讲解最最原生的多线程案例,UE4的多线程都是基于此API架构的,知彼知己方能百战不殆。

    6 UE4异步资源加载系统

    我们会封装一套基于UE4的异步加载资源系统,并且会讲解所涉及到UE4异步资源加载类,比如FGCObject,FStreamableManager,FStreamableHandle等类的讲解

    7 其他知识点

    通过本套教程,您将对UE4多线程的使用有一个非常深刻的认识,不论是做服务器还是客户端,还是热更新,这将是一个非常好的基础。


    说了这么多,我们介绍一下每个章节讲解的内容:

    第一章 搭建线程插件

    这一节我们会讲解如何封装一套属于自己的Runnable多线程,这一节为后面封装线程管理莫定基础。

    第二章 封装线程池的同步和异步

    这一章我们通过对信号量的设置来实现线程的同步和异步,这一章同样我们会封装我们的代理线程,并且解决线程安全等问题

    第三章 封装线程任务管理

    这一章我们开始封装线程池任务,我们也称为任务队列,我们可以不断的向任务线程发送任务请求,一旦有闲置线程便可以执行我们的任务,通过这种方式,我们便可以高效的利用我们线程池里面的线程,防止线程资源的浪费,这一节为我们后面搭建服务器有巨大的帮助。

    当然我们还会分析该插件跨平台问题等。

    第四章 通过模板高效封装UE4线程

    我们已经封装代理线程和任务线程,UE4Abandonable线程,这一章我们开始通过模板整合这三种线程通用的函数,使其结构更加精炼,使用上风格更加统一,最后我们再加入协程,并且讲解什么是协程,最终我们会将协程封装到我们的插件中。

    第五章 前置任务线程封装

    这一节我们开始封装UE4使用频率最高的线程Graph线程,我们也会讲解如何封装我们的渲染线程,这对我们以后学习渲染有很大的帮助哦~

    当然我们还会讲解如何封装我们的原生Windows线程,理解并且使用我们的它。

    第六章 封装异步资源管理

    这节我们开始讲解异步资源管理,并且将它进行高级封装,最终效果是方便又实用,而且我们还会讲解里面涉及到的异步管理,以及GCObject类讲解等。

    第七章 如何使用SimpleThread

    这一章我们主要是以测试线程为主,通过前面的各种封装,这一章是真正检验我们线程插件商业价值的地方.这一章我们会讲解如何使用代理线程,任务线程,同步异步线程,资源管理线程,图表线程,协程,以及各种宏线程。

     

    宣传视频:

    视频封面

    上传视频封面

    UE4套件-多线程库开发

     

    以下是本套教程的目录

     

    第一章 搭建线程插件

    1-1 创建线程插件

    1-2 创建线程接口

    1-3 封装Runnable线程

    1-4 封装线程池管理

     

    第二章 封装线程池同步异步

    2-1 建立线程绑定执行机制

    2-2 如何正确删除线程

    2-3线程安全与线程同步

    2-4 Bind匹配代理模板

    2-5 线程异步问题

     

    第三章 封装任务管理

    3-1 自动化任务队列设计

    3-2 封装信号量

    3-3 高级封装线程管理接口

    3-4 高级封封装代理线程管理

    3-5 高级封装任务线程管理和总管理

    3-6 插件的跨平台问题

     

    第四章 通过模板高效的封装UE4线程

    4-1 利用宏编程实现UE4线程池同步和异步

    4-2 封装线程同步和异步到管理

    4-3 通过模板匹配同意代理接口

    4-4 什么是协程

    4-5 封装协程对象

    4-6 封装协程管理

     

    第五章 前置任务线程封装

    5-1 封装前置任务UE4线程管理

    5-2充分发挥Graph线程特性

    5-3 CreateThreadWindowsAPI封装

    5-4 渲染线程讲解

     

    第六章 封装异步资源读取

    6-1 封装资源异步加载

    6-2 StreamableManager讲解

    6-3FGCObject讲解

    6-4FStreamableHandle讲解

     

    第七章 如何使用SimpleThread

    7-1测试Proxy线程解决线程绑定和销毁问题

    7-2测试Task线程解决传值的乱码问题

    7-3测试Abandonable线程的同步异步

    7-4 测试协程

    7-5测试Graph线程和Windows线程

    7-6测试异步资源加载使用

     

     

    通过这套教程您可以学到哪些内容?

    1. 如何使用线程,理解同步和异步的封装以及使用,理解并且运用线程挂起和唤醒技巧,锁的使用,如何规避死锁等

    2. 如何优雅的封装多线程,如何设计出令人眼前一亮的代码。

    3. 理解资源异步读取的使用,这一块我们会对着UE4文件讲解,并且讲解里面涉及到的函数使用方法

    4. 协程的封装和使用,理解协程,并且封装协程

    5. 代理的高级运用等

    如果遇到不会的问题可以及时问我。

     

     

     

    本套教程地址

    Unreal Engine 4套件开发系列教程:多线程库

    作者的其他文章地址

    人宅系列

     

    展开全文
  • 0. 关于这个专题 游戏要给用户良好的体验,都会尽可能的保证60帧或者更...因此UE4提供了一套较为完备的异步机制,来应对这个挑战。这个专题将深入浅出分析UE4中的解决方案,并分析其中的关键代码。 1. 同步和异步...

    0. 关于这个专题

    游戏要给用户良好的体验,都会尽可能的保证60帧或者更高的fps。一帧留给引擎的时间也不过16ms的时长,再除去渲染时间,留给引擎时间连10ms都不到,能做的事情是极其有限的。同步模式执行耗时的任务,时长不可控,在很多场景下是不能够接受的。因此UE4提供了一套较为完备的异步机制,来应对这个挑战。这个专题将深入浅出分析UE4中的解决方案,并分析其中的关键代码。

    1. 同步和异步

    异步的概念在wiki和教科书上有很权威的解释,这里就拿一些例子来打个比方吧。

    每天下午2点,公司有一个咖啡小分队去买咖啡喝。在小蓝杯出来之前,我们都是去全家喝咖啡。一行人约好之后,就去全家排个小队,向小哥点了几杯大杯拿铁后,就在一旁唠嗑,等待咖啡制作完成。这是同步模式,我们向店员点了咖啡后就一直在等待咖啡制作完成。

     

    同步买咖啡

    去年小蓝杯出来了,算不上精品咖啡,价格还不错,而更重要的是我们可以异步了。在App上下单完成后,继续做自己的事情,等到咖啡制作好的短信来了之后,再跟着咖啡小队愉快地去拿咖啡。

     

    异步买咖啡

    2. 命令模式

    在上一节提及的场景中,咖啡小队买咖啡的行为,实际上是发出了一个制作咖啡的请求。咖啡小队在全家买咖啡的时候,也就是同步模型下,咖啡小队买咖啡会等待制作咖啡的过程,这里隐含了一层执行依赖的关系。但在向小蓝杯买咖啡的时候,异步模型,买咖啡和制作咖啡的依赖关系消失了。虽然多一个响应咖啡制作完成,去拿咖啡的流程;但是这一层解耦,可以让咖啡小队剩下了等待咖啡制作的时间,提高了工作效率。当然,有时候咖啡小队也想在外面多聊聊,而选择去全家买咖啡(:逃

    如果选择使用异步模型,就必须要使用到命令模式来实现了。因为异步模型必须要将命令的请求者和实际的执行者分离开。咖啡小队申请制作咖啡的请求,而咖啡制作的流程,调度及制作完成的通知,都是由小蓝杯来决定的。这与在全家直接与店员要求制作咖啡有很大的不同。

    命令模式两个关键点:命令调度。命令是提供给请求者使用的外观,而调度则是执行者从收到命令请求到执行完成的策略,可以是简单的单线程延迟执行,也可以是多线程的并发执行。这个系列会花第一篇的整个篇幅,来介绍与命令请求外观相关的内容。对于调度方面的内容,会在后续的文章详细探讨。

    3. 泛化仿函数

    Modern Cpp Design,这本书介绍了泛化仿函数, generic functor. 泛化仿函数使用了类似函数式的编程风格,用于取代C++老旧的命令模式的实现,为命令请求的使用者提供了一个接口更友好,并且功能更强大的外观。当然,这篇文章并不是为了布道函数式编程的优越性,并且泛化仿函数只是借鉴了函数式编程的风格,并不完全是函数式编程。鉴于其他语言中,函数作为第一类值类型已经广泛被认可,并且C++11标准也补完了λ表达式,并提供了std::function基础设施,我觉得这里还是很有必要讨论一下,为什么从传统的命令模式到现在的设计实现,是一种更好的设计思路。让我们首先来回顾一下纯C和面向对象的命令模式的外观。

    纯C的命令外观大概如下列代码所示:

    struct command_pure_c
    {
        int command_type;
        uint32_t struct_size;
        char data[0];
    };
    

    也有大部分类库会固定执行函数的签名:

    typedef int (*call_back_func)(void* data);
    
    struct command_pure_c
    {
        int command_type;
        uint32_t struct_size;
        call_back_func call_back;
        char data[0];
    };
    

    Command会携带不同的状态参数,在C语言的实现里面就不得不使用动态结构体来精确管理内存。执行者可以通过command_type或者call_back的函数指针来分派的正确的执行函数上。到了C++中,进入面向对象的时代,就有了如下面向对象的设计:

    class ICommand
    {
    public:
        virtual int execute() = 0;
    };
    
    class MyCommand : public ICommand
    {
    public:
        MyCommand(int param) : param_(param) {}
        int execute() override final;
    private:
        int param_;
    };
    

    到了OOD,实现变得简单了不少。类型可以携带参数,利用C++多态实现分派,也能利用C++类型的布局结构来精确控制内存。

    上一个时代的设计,首先无形中引入了框架性的设计。例如OOD中,执行代码要实现ICommand接口,执行函数体只能写在execute中,或者说必须以execute为入口。

    其次老旧的设计,只能在面对简单的场景才能够胜任的。简单的场景,是指的命令执行完成后,只是简单地收到成功与失败的通知,没有回调链的场景。因为这种设计最大的缺点,就是执行函数的实现与发起请求这两个部分代码的位置,并不是按照人类线性逻辑的习惯来组织的。也就是说,它需要我们的理解现有系统的运作机制,并让我们推算出它们逻辑关系。当回调链是一个冗长而复杂的过程,它会给我们带来巨大的心智负担。

    泛化仿函数优雅地解决了第一个问题,它可以携带状态,并能够统一不同的调用语义。文章后面的篇幅会提及,这实际上是一种类型擦除方法。从而使得执行的函数实现从框架性的设计中解放出来。

    但是第二个问题,直到C++11标准引入λ表达式,才得以完全解决。通过匿名函数,我们可以直接把请求执行的函数体,内联地(就地代码而非inline关键字)写在请求命令的位置,如下所示:

    std::string file_name = "a.mesh";
    request([file_name = std::move(file_name)]()
    {
        // ... file io
        // callback hell 在后续的文章中讨论
    });
    

    得益于C++11标准的完善,我们在C++中可以把函数对象当做第一类值对象来使用了,而且为我们的设计和抽象提供了强有力的基础设施。

    4. 泛化仿函数的实现原理

    上一节我曾提到过,我们在C++中可以把函数对象当做第一类值来使用,但是C++也有沉重的历史包袱,所以相比其他语言,在C++中使用函数对象有着C++特色的问题。

    我们知道在C++中,有调用语义的类型有:

    1. 函数(包括静态成员函数)指针(引用)
    2. 指向成员函数的指针,pointer to member function
    3. 仿函数
    4. λ表达式

    值得提及的是,曾经的C++是把指向成员变量的指针,pointer to member data(PMD), 也当做具有调用语义的对象。因为PMD可以绑定成一个以类型作为形参,成员变量类型作为返回值的函数,并且std::result_of曾经一度也接受PMD类型作为输入。

    虽然这些具有调用语义的类型,都可以当做函数来使用,但是他们之间有着语义上的巨大差异,我们主要从两个维度:是否带状态和是否需要调用者,来分析并列举出了下表:

    可以想象AA大神,当时看到C++此番情景的表情:

    泛化仿函数的第一目标,就是抹平这些语义上的鸿沟,抽象出一个语义统一的callable的概念。先给出早期实现外观代码: (为了简单起见,我们假定已经有了C++11的语法标准,因为C++98时代为了可变模板参数而使用的type_list会引入相当复杂的问题)

    // 为避免引入function_traits,我们选择较为直白的实现方式
    template <typename Ret, typename ... Args>
    class function_impl_base 
    {
    public:
      virtual ~function_impl_base() {}
      virtual Ret operator() (Args...) = 0;
      // TODO ... Copy & Move
    };
    
    template<typename FuncType>
    class function;
    
    template <typename Ret, typename ... Args>
    class function<Ret(Args...)>
    {
      // ...
    private:
      function_impl_base<Ret, Args...>* impl_;
    };
    

    为了抹平这些语义上的鸿沟,一个比较简单的思路,就是逐个击破。

    4.1 处理函数指针,函数指针和λ表达式

    为什么把这三个放在一起处理,因为他们有相同的调用语意。而函数指针无法携带状态,也可以很好的解决。

    仿函数和lambda实际上是同一个东西。lambda实际上也是一个class,只不过是编译期会给它分配一个类型名称。lambda绝大部分场景是出现在function scope当中,而成为一个local class. 这也是处理仿函数,会比处理普通函数指针略微复杂的地方,因为不同类型的仿函数会有相同的函数签名。

    template <typename Functor, typename Ret, typename ... Args>
    class function_impl_functor final : public function_impl_base<Ret, Args...>
    {
    public:
      using value_type = Functor;
    
      // constructors
      function_impl_functor(value_type const& f)
        : func_(f) {}
      function_iimpl_functor(value_type&& f)
        : func_(std::move(f)) {}
    
      // override operator call
      Ret operator()(Args... args) override
      {
        return func_(std::forward<Args>(args)...);
      }
    
    private:
      value_type func_;
    };
    

    值得提及的是,这个实现隐藏了一个编译器已经帮我们解决的问题。仿函数中可能会有non-trivially destructible的对象,所以编译器会在必要时帮我们合成正确析构functor的代码,这也包含λ表达式中捕获的变量(通常是值捕获的)。

    4.2 处理指向成员函数的指针

    指向成员函数的指针,与前面三位同僚有着不同的调用语义。参考MCD中的实现,大概如下:

    template <typename Caller, typename CallerIndeed, typename Ret, typename ... Args>
    class function_impl_pmf final : public function_impl_base<Ret, Args...>
    {
    public:
      using value_type = Ret(Caller::*)(Args...);
    
      // constructor
      function_impl_pmf(CallerIndeed caller, value_type pmf) 
        : caller_(caller), pmf_(pmf) 
      {
        // TODO... do some static check for CallerIndeed type here
      }
    
      // override operator call
      Ret operator()(Args... args) override
      {
        return (caller_->*pmf_)(std::forward<Args>(args)...);
      }
    
    private:
      CallerIndeed caller_;
      value_type   pmf_;
    };
    

    这样的实现方案,是为了考虑继承的情况,例如我们传递了基类的成员函数指针和派生类的指针,当然还有智能指针的情况。然而标准库并没有采取这种实现方式,而是需要我们使用std::bind或者套一层λ表达式来让使用者显式地确定caller的生命周期,才能够绑定到一个std::function的对象中。

    而笔者,更喜欢把一个指向成员函数的指针,扁平化成一个λ表达式,并多引入caller类型作为第一个参数:

    /* 
    Ret(Caller::*)(Args...) => [pmf](Caller* caller, Args ... args) -> Ret 
    { 
      return (caller->*pmf)(std::forward<Args>(args)...);
    }
    */
    

    4.3 集成

    function作为外观,就通过构造函数的重载来分派到创建三种不同语义的具体实现的创建中,只保存一个基类指针:

    template <typename Ret, typename ... Args>
    class function<Ret(Args...)>
    {
    public:
      template <typename Functor, typename = std::enable_if_t<
        std::is_invocable_r_v<Ret, Functor, Args...>>>
      function(Functor&& functor)
        : impl_(new function_impl_functor<
            std::remove_cv_t<std::remove_reference_t<Functor>>, 
            Ret, Args...>{ std::forward<Functor>(functor) })
      {}
    
      template <typename Caller, typename CallerIndeed>
      function(Ret(Caller::*pmf)(Args...), CallerIndeed caller)
        : impl_(new function_impl_pmf<Caller, CallerIndeed, Ret, Args...>{ pmf, caller })
      {}
    
      // TODO ... Copy and Move
    
      ~function()
      {
        if(impl_)
        {
          delete impl_;
          impl_ = nullptr;
        }
      }
    
    private:
      function_impl_base<Ret, Args...>* impl_ = nullptr; 
    };
    

    4.4 优化

    这个实现简单粗暴,有两个很明显的缺点。

    1. 调用operator call的时候,是一个虚函数调用,有不必要的运行期开销;
    2. 对很小的函数对象,例如函数指针,使用了堆分配。

    因此,某同x交友社区上出现了不少fast_function的实现。问题1的解决思路,就是进一步抹平语义的鸿沟,把caller和指向成员函数的指针先包成一个functor,再传递给function. 实现就不用考虑这种特殊情况了。问题2,如同std::string内部的预分配内存块的思路一样,当下的标准库std::function,folly::Function,当然还有UE4的TFunction都有一个针对小函数对象的内联内存块,来尽可能的减少不必要的堆分配。具体的优化实践,让我们进入下一节,看看UE4是如何处理的。大家如果有兴趣也可以去看看folly::Function的实现,它内部使用了一个小的状态机,并对函数的const有更强的约束。

    5. TFunction in UE4

    UE4中有实现比较完备的的泛化仿函数,TFunction. 但是UE4并没有选择使用标准库的std::function,通过阅读源码我总结了以下三个原因:

    1. 有定制TFunction内存分配策略的需求,并且实现了小函数对象的内联优化;
    2. UE4有复杂的编译选项,并希望在不同的编译选项中对abi有完全的把控,使用标准库无法做到;
    3. UE4对TFunction有携带Debug信息的需求。

    首先TFunction的实现几乎全部在,UnrealEngine/Engine/Source/Runtime/Core/Public/Templates/Funciton.h中。

    template <typename FuncType>
    class TFunction final : public //.....
    {};
    

    TFunction仅仅只是一个外观模板,真正的实现都在基类模板UE4Function_Private::TFunctionRefBase当中。外观只定义了构造函数,移动及拷贝语义和operator boolean. 值得一提的是TFunction的带模板参数的构造函数:

    /**
     * Constructor which binds a TFunction to any function object.
     */
    template <
      typename FunctorType,
      typename = typename TEnableIf<
        TAnd<
          TNot<TIsTFunction<typename TDecay<FunctorType>::Type>>,
          UE4Function_Private::TFuncCanBindToFunctor<FuncType, FunctorType>
        >::Value
      >::Type
    >
    TFunction(FunctorType&& InFunc);
    

    这个函数的存在是对FunctorTypes做了一个参数约束,与std::is_invocable_r是同样的功能。首先FuncTypes不能是一个TFunction的实例化类型,因为可能会跟移动构造函数或者拷贝构造函数有语义冲突,导致编译错误;并且不同类型的TFunction实例化类型之间的转换也是不支持的。其次UE4还检查了绑定的函数对象的签名是否跟TFunction定义的签名兼容。兼容检查是较为松弛的,并不是签名形参和返回值类型的一一对应。传参支持隐式类型转换和类型退化,返回值也支持隐式类型转换,满足这两个条件就可以将函数对象绑定到TFunction上。这样做的好处就是可以让类型不匹配的编译错误,尽早地发生在构造函数这里,而不是在更深层次的实现中。编译器碰到此类错误会dump整个实例化过程,会出现井喷灾难。

    接下来是UE4Function_Private::TFunctionRefBase模板类:

    template <typename StorageType, typename FuncType>
    struct TFunctionRefBase;
    
    template <typename StorageType, typename Ret, typename... ParamTypes>
    struct TFunctionRefBase<StorageType, Ret (ParamTypes...)>
    {
      // ...
    private:
      Ret (*Callable)(void*, ParamTypes&...);
      StorageType Storage;
      // ...
    };
    

    模板泛型没有定义,只是一个前向申明,只有当FuncType是一个函数类型时的特化实现。这告诉我们TFunction只接受函数类型的参数。并且TFunctionRefBase是遵循基于策略的模板设计技巧,Policy based designed,把分配策略的细节从该模板类的实现中剥离开。

    再来看看TFunction向基类传递的所有模板参数的情况:

    template <typename FuncType>
    class TFunction final : public UE4Function_Private::TFunctionRefBase<
      UE4Function_Private::FFunctionStorage, FuncType
    > // ....
    

    UE4Function_Private::FFunctionStorage是作为TFunction的内存分配策略,它把控着TFunction的小对象内联优化和堆分配策略的选择。与之相关的代码如下:

    // In Windows x64
    typedef TAlignedBytes<16, 16> FAlignedInlineFunctionType;
    
    typedef TInlineAllocator<2> FFunctionAllocatorType;
    
    struct FFunctionStorage : public FUniqueFunctionStorage 
    { 
        //... 
    };
    
    struct FUniqueFunctionStorage
    {
        // ...
    private:
        FunctionAllocatorType::ForElementType<FAlignedInlineFunctionType> Allocator;
    };
    

    FFunctionStroage继承自FUniqueFunctionStorage,主要是为了复用基类的设施,并覆盖和实现了带有拷贝语义的Storage策略。而它的基类,顾名思义,是没有拷贝语义,唯一独占的Storage策略。最开头的两个类型定义,是UE4在win平台64位下开启小对象内联优化的两个关键类型定义。

    需要注意的是,本文提及的小对象内联优化与UE4的USE_SMALL_TFUNCTIONS宏的意义是相反的。它所指明的Small Function是指的sizeof(TFunction<...>)较小的,也就是没有内联内存块函数。开启这个宏的时候只有堆分配的模式。

    • FAlignedInlineFunctionType定义了大小为16bytes,16bytes对齐的一个内存单元
    • FFunctionAllocatorType定义了2个内存单元

    由此可以推断FUniqueFunctionStorage的成员变量就定义了2个大小为16bytes并以16bytes对齐的存储内存块, 也就是说在此编译选项下可以存储的小函数对象的大小,不能超过32bytes. 举个例子:

    void foo()
    {
        int temp = 0;
        TFunction<int()> func_with_inline_memory = [temp]() { return 1; };
        std::array<int, 9> temp_array = { 0 };
        TFunction<int()> func_with_heap_allocation = [temp_array]() { 
            return static_cast<int>(sizeof(temp_array)); };
    }
    

    func_with_inline_memory绑定的lambda函数,仅捕获了一个int大小的变量,所以它会使用TFunction中内联的小对象内存块。而func_with_heap_allocation,捕获了一个元素个数为9的int数组,大小为36,所以它绑定在TFunction中,被分配在了堆上。

    最后需要注意的是,UE4触发分配行为的代码,略不太直观。它使用了user-defined placement new, 参看cppreference的第11至14条。对应的代码如下:

    struct FFunctionStorage
    {
      template <typename FunctorType>
      typename TDecay<FunctorType>::Type* Bind(FunctorType&& InFunc)
      {
        // ...
        // call to user-defined placement new
        OwnedType* NewObj = new (*this) OwnedType(Forward<FunctorType>(InFunc));
        // ...
      }
    };
    
    // definition of user-defined placement new operator
    inline void* operator new(size_t Size, 
      UE4Function_Private::FUniqueFunctionStorage& Storage)
    {
      // ...
    }
    

    简单提及一下TFunctionRefBase的Callable成员,是在绑定的时候赋予TFunctionRefCaller<>::Call,而其内部实现就是类似std::invoke的实现,利用std::index_sequence展开形参tuple的套路。

    那么UE4的TFunction的关键实现点,都已经介绍完毕了。UE4除了TFunciton还有TFunctionRef和TUniqueFunciton,都有着不同的应用场景。但本质上的不同就是Storage的策略,大家感兴趣可以阅读以下代码和Test Cases.

    6. 小结

    本文是介绍UE4异步编程的第一篇。异步模型本质上是一个命令模式的实现。异步模型最重要的两个关键点就是命令和调度。所以本文以第一个要点为线索,从旧时代的设计到现代编程语言设计变迁,讨论了其中设计思路和实现细节。并以UE4的TFunction作为一个详细的案例,对其源码做了简析。

    命令的实现部分比较简单易懂,但对于异步模型而言,更重要的是执行命令的调度策略。这个系列后续的篇幅,将会着重讨论UE4在其中的取舍和实现细节。

    展开全文
  • 协程 # -*-coding=utf-8-*- import gevent from gevent.queue import Queue, Empty import time import requests from gevent import monkey # 把下面有可能有IO操作的单独做上标记 """ 多协程爬虫coroutine,慢...
  • 视频链接:游戏开发入门(四)逻辑与脚本开发(8节课 时常:约2小时30分钟 第4节看不了) 笔记与总结(请先学习视频内容): 1.逻辑系统目的 实现策划的设计文档 构建游戏玩法 实现程序与玩家交互 2.逻辑...
  • UnLua是Unreal Engine 4下特性丰富且高度优化的Lua脚本插件。它遵循Unreal Engine 4的编程模式,简单易上手,UE4程序员更是可以零学习成本...
  • 一面:(视频面试) Unity性能优化 实时渲染和延迟渲染 描边实现 阴影实现 如何申请的指针在堆上的地址是64的倍数 ...Unity协程原理 一道关于排行榜的数据结构体 一道2个数组做加法的算法题 ...
  • 当你学习编程时,最先被困扰在哪一步?是不是很容易陷入在语法之类的细节而忽视基础概念?解决当前任务的最佳方法是什么?在多种编程语言之间来回切换,却感觉不到效率的提高?0 基础学习编程,最先...
  • 在游戏开发过程中,常常需要Loading界面。一方面是为了等待后台加载场景;一方面也是为了美观,可以在Loading界面放上游戏里有趣的截图,战斗场面,小提示等。自己最近做的毕业设计中使用了场景的异步加载来使游戏变...
  • UE4热更新:基于UnLua的Lua编程指南 https://imzlp.me/posts/36659/ https://imzlp.me/posts/36659/ Z's Blog 首页 归档 分类 标签 笔记 微言 资源 简历 关于 待办事项 站点日志 搜索 UE4热更新:...
  • 在知乎上看到一篇不错的性能优化文章,进行了比较系统的概括,下面分享一下,原文地址: ... 1.渲染 利用reflect probe代替反射、折射,尽量不用RTT、GrabPass、RenderWithShader、CommandBuffer.Blit ...
  • 《游戏引擎架构》中英词汇索引表

    千次阅读 2015-05-05 15:53:50
    简介 此词汇索引表源自《游戏引擎架构》的中英索引,支持搜寻及排序,以方便读者查阅。遇到游戏相关的术语也可利用本表查找其中英翻译。欢迎提供意见反馈。 中英双语索引表 Search: 英文 ...5
  • unity3d杂记

    2019-07-11 13:44:22
     由于公司用unity3d开发客户端部分,今天去参加了下unity3d成都开发者大会。...相对于unreal与crysis这些大牌引擎商,unity3d无论从上手难度,使用成本,开发便捷都要更胜一筹。虽然它在次世代效果上...
  • UE3中的时间

    2019-07-13 12:25:06
    为了管理时间,Unreal将游戏运行时间片分隔为"Ticks"。一个Tick是关卡中所有Actors更新的最小时间单位。一个tick一般是10ms-100ms(CPU性能越好,游戏逻辑越简单,tick的时间越短) UE3使用的是游戏循环模型为:可...
  • lua内存机制分析

    千次阅读 2017-03-02 21:58:55
    Lua在运行代码之前,会先把源码预编译成一种内部编码,这种编码由一连串的虚拟机能够识别的指令构成,与CPU的机器码很相似。接下来由C代码中的一个while循环负责解释这些内部编码,这个while循环中有一个很大的...
  • 使用Golang开发手游服务器的感想

    万次阅读 2016-01-11 11:30:01
    从最初决定使用Golang开发游戏服务器(参考这里),到服务器基本成型,经过了两个多礼拜的时间。这里记录一下感想和心得。具体Golang的使用经验将来有时间会再开篇文章来写。  两个礼拜的时间,完成了Golang的入门...
  • Unity优化百科(UWA 博客目录)持续更新中...
  • 网站导出20190118

    千次阅读 2019-01-18 11:07:02
    Bookmarks   书签栏   重要网站   资源商店 Artlist.io - Music Licensing For Video, Film &amp; YouTube ...unity3d插件免费下载 商业资源代购 团购 unity外包 Unity时空 ...unity3...
  • C++ 面试刷题

    千次阅读 2019-10-10 00:56:41
    问的比较随意 1.M个不重复字符随机组合问是否在一个字符串中会出现,要求O(N) 2.M个重复字符(包括中文)随机组合,问会不会在一个...4.超时重传 5.拥塞控制 先通俗解释下什么是拥塞控制,在讲出四个算法 当发...
1 2 3
收藏数 59
精华内容 23
热门标签
关键字:

unreal4 协程