[{"data":1,"prerenderedAt":2413},["ShallowReactive",2],{"blog-page-15":3,"blog-count":2412},[4,135,329,1597,2036],{"id":5,"title":6,"body":7,"date":113,"description":13,"extension":114,"meta":115,"navigation":118,"path":130,"seo":131,"stem":132,"tags":133,"__hash__":134},"blogs\u002F_legacy\u002F2017\u002F2017-12-31-at-the-end-of-2017.md","写在2017的最后",{"type":8,"value":9,"toc":105},"minimark",[10,14,17,21,24,27,30,33,36,39,42,45,48,51,54,57,60,63,66,69,72,75,78,81,84,87,90,93,96,99,102],[11,12,13],"p",{},"转眼马上2018年了，这个博客的历史也已经6年有余了。",[11,15,16],{},"很长一段时间没有写过关于自己的文章了，一来是没有什么时间，二来也不知道该写些什么。",[18,19,20],"h2",{"id":20},"沿革",[11,22,23],{},"还不到能说有历史的地步，只是不知为何博客就变成了技术博客的风格。",[11,25,26],{},"刚开始的时候总是会写一些非技术的文章，但总有些“为赋新词强说愁”的感觉，渐渐的也就不爱写了。",[11,28,29],{},"这个博客从某种程度上也算是记录了我的成长，没有想过会坚持那么久，中途也有数次想要不再更新，但最后还是写到了现在。",[11,31,32],{},"之所以会一直把博客写下去，有很大的因素是因为后来渐渐的有了稳定的访问量，间歇性的也收到不少留言的鼓励。每每产生存续性的疑惑时，都会想到当时看到的一篇文章，《为什么你应该（从现在开始就）写博客 》。",[11,34,35],{},"这篇文章网上转载颇多，说的也颇有道理，从博客的写作中，确实学到了很多东西。",[11,37,38],{},"有的知识，当想要写出来的时候，才会发现原来的理解上有不少的缺陷。因为写的时候会考虑到读者的存在，就会想要尽可能的将描述变得完备，而不是“感觉这样就可以了”。",[11,40,41],{},"最重要的是，通过博客确实认识了不少朋友，对于不擅长主动与人打交道的我而言也是不小的帮助。",[11,43,44],{},"总而言之，短期内，博客会一直写下去。",[18,46,47],{"id":47},"回望",[11,49,50],{},"2017年，生活上可谓是发生了巨大的变化。",[11,52,53],{},"最大的变化是现在已经是正经的社会人了，也因此自由支配的时间大规模变少，很多想做的事情都没有做成。例如今年的阅读计划就完全的落后了，连一半的进度都没有完成。",[11,55,56],{},"今年最后悔的事情莫过于在开始工作后由于没有时间，将手上的比特币出清了。否则现在已经是财富自由之身了，不过命运往往就是如此的无常，谁也说不清好坏。",[11,58,59],{},"离开学校4年有余，前后做过两个游戏项目，由于都是网上合起来做的项目，最终都无疾而终。现在在工作了这么久之后才发现，游戏这种大规模的项目，终归还是要有实体、承担起风险全力去做才有可能成功。期间始终只有一半的精力在游戏制作上，投过BP、做过外包，虽然收入上足以维生，却也没什么值得一书的成果。再看看这几年飙升的房价，想来自己还是输了。",[11,61,62],{},"日文有个俗语叫“不追二兔”，描述的就是这种情况呢。想要在梦想与现实之间把握一个平衡，最后却是一事无成。",[11,64,65],{},"而如今已经过了可以闯荡的年纪，为了安稳下来让父母放心，也为了让自己适应社会，便加入了正经人的行列。",[11,67,68],{},"一开始面试的时候总是不知道该如何向别人解释自己这4年的经历空白，好在对UE4本身比较熟悉，倒也没有遇到什么困难。",[11,70,71],{},"起初也考虑过去做人工智能方面，只是已经放下那些知识4年多了，说实话知识已经有些过时了，也没有什么时间去研究Tensorflow，更多的也不是很想放弃游戏这个梦想。",[11,73,74],{},"当年还是太过冲动，凭着满腔热血和几年的积蓄，就想着朝着梦想去拼搏。事实是，无数的前辈都建议在工作几年之后累计了经验之后再去考虑创业，现在想来自己实在是过于轻率。",[11,76,77],{},"人生并非游戏，确实不适合孤注一掷……",[18,79,80],{"id":80},"展望",[11,82,83],{},"最近才渐渐察觉一个事实：无论是独立游戏还是商业游戏，其本质都是商品，它们面向的对象都是“玩家”。而我过去都太过于关注技术本身了，甚至忘记了我为何会想要制作游戏的初心。",[11,85,86],{},"或许也与我的个性有关吧，比起不确定的世界，我更加喜欢确定的“逻辑”。",[11,88,89],{},"这是我的缺点，我总是无法与人保持稳定的联系。关系再亲密的朋友，只要拉开了物理上的距离，我也不知该如何交流，然后渐渐的就无话可谈了。",[11,91,92],{},"而且我不知如何与陌生人打交道，不知该如何把握交流的平衡，虽然不到社交恐惧症的程度，却也只是站在了正常人允许的范围的边缘。",[11,94,95],{},"所以2018年首先要改进的，就是这个缺点，因此现在在博客上完全的把联系方式给公开了。从前总是不知该如何回话，所以在没有主动询问的情况下，都不会告知自己的联系方式。",[11,97,98],{},"另外一点，就是要回归梦想的初心，去思考创作本身，而不是过多的关注技术细节。这点要感谢『ステラのまほう』，让我想起我也是为了让别人能够开心的玩我创作出来的东西才开始研究游戏创作的。",[11,100,101],{},"因此大概在把手头上的Note整理完之后就会降低技术文章的更新频率了呢，因为平时时间不多，如果一直研究技术的话，就没时间做游戏了。",[11,103,104],{},"还有就是，要认真的开始减肥了~",{"title":106,"searchDepth":107,"depth":108,"links":109},"",2,3,[110,111,112],{"id":20,"depth":107,"text":20},{"id":47,"depth":107,"text":47},{"id":80,"depth":107,"text":80},"2017-12-31","md",{"layout":116,"status":117,"published":118,"author":119,"author_login":121,"author_email":122,"wordpress_id":123,"wordpress_url":124,"date_gmt":125,"excerpt":126},"post","publish",true,{"display_name":120,"login":121,"email":122,"url":106},"风铃","flinkor","flinkor@foxmail.com",2175,"\u002F\u002F?p=2175","2017-12-31 15:59:58 +0000",{"type":8,"value":127},[128],[11,129,13],{},"\u002F2017-12-31-at-the-end-of-2017",{"title":6,"description":13},"_legacy\u002F2017\u002F2017-12-31-at-the-end-of-2017",[],"ODD4_6wB4AwrRDwkglPOMmaiyVdeAkuhaSs4cvqxBE8",{"id":136,"title":137,"body":138,"date":312,"description":142,"extension":114,"meta":313,"navigation":118,"path":322,"seo":323,"stem":324,"tags":325,"__hash__":328},"blogs\u002F_legacy\u002F2017\u002F2017-12-16-ue4-algo-note.md","UE4算法帮助类笔记",{"type":8,"value":139,"toc":300},[140,143,146,149,152,155,158,163,166,169,172,175,185,189,192,195,201,204,207,210,213,216,228,231,234,237,240,243,246,250,253,258,261,264,267,278,282,285,290,293,297],[11,141,142],{},"UE4有提供一套算法帮助类，命名空间为Algo，对于需要快速使用的情况可以参考。",[11,144,145],{},"当前使用的UE4版本为4.18.2。",[11,147,148],{},"整个Algo的定义分散在各个文件中，按照功能区分进行分布。",[11,150,151],{},"在定义上全部都是模板函数，通用性比较强。",[18,153,154],{"id":154},"数据整理",[11,156,157],{},"这一块是对内存数据块进行整理的通用模板。",[159,160,162],"h3",{"id":161},"copy","Copy",[11,164,165],{},"这是一个系列的拷贝模板函数，主要的工作就是对数据进行拷贝。",[11,167,168],{},"有一个CopyIf的变体，只会把符合条件的数据拷贝到目标中去。",[11,170,171],{},"通常需要拷贝的容器是Arrat，Map之类的，对于这个模板，只要保证源容器实现了range for和目标容器实现了Add函数就可以了。",[11,173,174],{},"当然，数据的类型必须至少是可转换的类型。",[176,177,182],"pre",{"className":178,"code":180,"language":181},[179],"language-text","TArray\u003Cint> TestArray;\nTArray\u003Cint> TestData;\nAlgo::Copy(TestData, TestArray);\n","text",[183,184,180],"code",{"__ignoreMap":106},[159,186,188],{"id":187},"transform","Transform",[11,190,191],{},"对数据进行某种操作，并填充到目标。",[11,193,194],{},"也有这Transfor_If的变体，要求传入的Transform操作必须返回要填充到目标的类型。",[176,196,199],{"className":197,"code":198,"language":181},[179],"Algo::Transform(TestData, TestArray, [](int i) { return FMath::DegreesToRadians(i); });\ncheck(TestArray.Num() == NUM_TEST_OBJECTS);\nfor (int i = 0; i \u003C TestArray.Num(); ++i)\n{\n   check(TestArray[i] == FMath::DegreesToRadians(TestData[i]));\n}\n",[183,200,198],{"__ignoreMap":106},[11,202,203],{},"这里由于源与目标的数据类型可以是不一致的，再加上有If变体，其实是一个非常好用的数据批量处理模板。",[11,205,206],{},"这里传入的if条件和转置条件即可以是成员的指针也可以是成员的函数，在使用的时候可以灵活的采用。",[18,208,209],{"id":209},"二分搜索",[11,211,212],{},"内部封装的二分搜索算法，不用每次想要进行搜索的时候临时到Google或者代码库中去翻找搜索算法的代码了。",[11,214,215],{},"提供一下三个基本的函数",[217,218,219,222,225],"blockquote",{},[11,220,221],{},"BinarySearch",[11,223,224],{},"LowerBound",[11,226,227],{},"UpperBound",[11,229,230],{},"功能就如同函数名称一样，是搜索，最小值和最大值的查找。",[18,232,233],{"id":233},"排序",[11,235,236],{},"封装了几个排序算法，不过由于排序算法早就还给课本了，在这里就不比较性能了。",[11,238,239],{},"毕竟一直使用std的sort也没怎么关心过其内部实现，虽然TArray也是有封装sort操作的，这里也就不去看其实现了。",[11,241,242],{},"整体的排序上，有一个Algo::IsSorted()函数可以用于检查数据是否已经经过了排序。",[11,244,245],{},"同时排序算法都有提供SortBy的变体以方便进行更加精致的排序。",[159,247,249],{"id":248},"heap","Heap",[11,251,252],{},"严格的来说也提供了不是排序的部分。这里引用一下Wiki",[217,254,255],{},[11,256,257],{},"a heap is a specialized tree-based data structure",[11,259,260],{},"给和我一样一下子想不起来Heap到底是什么的筒子。",[11,262,263],{},"可以Algo::Heapify(TestArray)来将一个数组转换为Heap，同时提供了IsHeap函数来检查一个数组是否是以Heap的形式保存的。",[11,265,266],{},"然后Algo::HeapSort()才是进行堆排序的，其结果满足Algo::IsHeap以及Algo::IsSorted。",[11,268,269,270,277],{},"另外，根据资料显示，[",[271,272,276],"a",{"href":273,"rel":274},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F19336881\u002Fwhy-isnt-heapsort-stable",[275],"nofollow","HeapSort并不是一个stable的排序算法","]，记得TArray是有提供StableSort的，果然其内部应该是有别的排序实现的。",[159,279,281],{"id":280},"introsort","IntroSort",[11,283,284],{},"这个可以明确的记得当初并没有学过呢，再次引用Wiki",[217,286,287],{},[11,288,289],{},"Introsort or introspective sort is a hybrid sorting algorithm that provides both fast average performance and (asymptotically) optimal worst-case performance.",[11,291,292],{},"似乎是叫内省排序总之是相当优秀的排序算法，只是不是Stable的。",[159,294,296],{"id":295},"sort","Sort",[11,298,299],{},"直接调用Sort就可以了，Unstable，并没有解释到底用的是什么算法。也没有去关注它的实现……",{"title":106,"searchDepth":107,"depth":108,"links":301},[302,306,307],{"id":154,"depth":107,"text":154,"children":303},[304,305],{"id":161,"depth":108,"text":162},{"id":187,"depth":108,"text":188},{"id":209,"depth":107,"text":209},{"id":233,"depth":107,"text":233,"children":308},[309,310,311],{"id":248,"depth":108,"text":249},{"id":280,"depth":108,"text":281},{"id":295,"depth":108,"text":296},"2017-12-16",{"layout":116,"status":117,"published":118,"author":314,"author_login":121,"author_email":122,"wordpress_id":315,"wordpress_url":316,"date_gmt":317,"excerpt":318},{"display_name":120,"login":121,"email":122,"url":106},2158,"\u002F\u002F?p=2158","2017-12-16 08:35:14 +0000",{"type":8,"value":319},[320],[11,321,142],{},"\u002F2017-12-16-ue4-algo-note",{"title":137,"description":142},"_legacy\u002F2017\u002F2017-12-16-ue4-algo-note",[326,327,296],"UE4","Algo","zFRtkSrzpzFpfSpjsqSMzc-9RQQLrc9SyeofTK2pIHw",{"id":330,"title":331,"body":332,"date":312,"description":336,"extension":114,"meta":1583,"navigation":118,"path":1592,"seo":1593,"stem":1594,"tags":1595,"__hash__":1596},"blogs\u002F_legacy\u002F2017\u002F2017-12-16-ue4-async-note.md","UE4异步操作总结",{"type":8,"value":333,"toc":1556},[334,337,339,342,345,348,351,354,357,361,364,367,370,373,377,380,386,389,393,396,399,405,408,411,414,418,421,435,438,444,448,451,457,460,466,469,473,476,479,482,486,489,492,496,499,503,506,512,515,518,521,524,530,533,538,541,544,547,550,555,558,563,566,571,574,577,580,584,587,593,596,599,603,606,609,612,618,621,624,628,631,636,868,871,943,946,950,953,1043,1046,1050,1053,1056,1171,1174,1226,1229,1233,1236,1239,1436,1439,1442,1453,1531,1534,1537,1540,1543,1546,1549,1552],[11,335,336],{},"虚幻本身有提供一些对异步操作的封装，这里是对这段时间接触到的“非同步”的操作进行的总结。",[11,338,145],{},[11,340,341],{},"在虚幻的游戏制作中，如果不是特殊情况一般不会有用到线程的时候。但是由于实际上虚幻内部是有着许多线程机制的。",[11,343,344],{},"例如通常的游戏引擎中游戏线程和渲染线程都是独立的，相互之间会存在一个同步的机制。",[11,346,347],{},"而物理线程与游戏线程之间的同步有时候也会导致游戏的表现与预期不一致。",[11,349,350],{},"通常会有线程同步需求的地方是网络相关的操作，但是实际上UE4已经对网络操作进行了封装，无需关心这个问题。",[11,352,353],{},"而游戏线程、渲染线程、物理线程内部也都已经有了封装，对游戏逻辑的构建基本是不可见的。",[11,355,356],{},"但是有时候还是会遇到需要使用线程相关逻辑的，这里就是这段时间内累计的“非同步”相关逻辑的总结。",[18,358,360],{"id":359},"tick","Tick",[11,362,363],{},"这个其实关于Tick的，虽然Actor是有默认的Tick函数的，Component与UMG也有对应的Tick机制。",[11,365,366],{},"但是如果是自定义的UObject或者Slate，要使用Tick机制的话就会有些麻烦。",[11,368,369],{},"例如，想要让自定义的Slate控件进行某种数据更新，而数据源本身并不提供通知机制的话就会有些麻烦。",[11,371,372],{},"虽然通过各种设计可以巧妙的绕过这个问题，但是有时候在类内部构建Tick机制才是最快速的解决方案。",[159,374,376],{"id":375},"timermanager","TimerManager",[11,378,379],{},"通过使用引擎提供的定时器机制，就可以进行自定义的Tick了：",[176,381,384],{"className":382,"code":383,"language":181},[179],"GetWord()->GetTimerManager().SetTimer(\n  m_hTimerHandle,\n  this,\n  &UNetPlayManager::TimerTick,\n  1.0,\n  true\n);\n",[183,385,383],{"__ignoreMap":106},[11,387,388],{},"这里需要能够获得UWorld的指针，如果是自定义的类型的话，就必须想办法提供有效的UWorld指针。",[159,390,392],{"id":391},"ftickablegameobject","FTickableGameObject",[11,394,395],{},"还有另一个方法，就是使用FTickableGameObject。",[11,397,398],{},"任何继承自FTickableGameObject的类型都会获得Tick的能力，就算不是虚幻原生的类型也可以使用，相当的便利。使用时继承自该类型，然后：",[176,400,403],{"className":401,"code":402,"language":181},[179],"public:\n\n\u002F** \u003CTick接口函数 *\u002F\nvirtual void Tick(float DeltaTime) override;\n\nvirtual bool IsTickable() const override\n{\n  return true;\n}\n\nvirtual bool IsTickableWhenPaused() const override\n{\n  return true;\n}\n\nvirtual TStatId GetStatId() const override\n{\n  RETURN_QUICK_DECLARE_CYCLE_STAT(USceneCapturer, STATGROUP_Tickables);\n}\n",[183,404,402],{"__ignoreMap":106},[11,406,407],{},"继承一下基本的函数就可以了。",[18,409,410],{"id":410},"线程同步",[11,412,413],{},"UE4对操作系统提供的线程同步相关接口进行了一定的封装。",[159,415,417],{"id":416},"atomics","Atomics",[11,419,420],{},"基本的接口可以在FPlatformAtomics找到，针对不同的平台，有不同的实现。",[217,422,423,426,429,432],{},[11,424,425],{},"InterlockedAdd",[11,427,428],{},"InterlockedCompareExchange (-Pointer)",[11,430,431],{},"InterlockedDecrement (-Increment)",[11,433,434],{},"InterlockedExchange (-Pointer)",[11,436,437],{},"详细的可以参看其源码。也可以参看引擎内部的使用方式：",[176,439,442],{"className":440,"code":441,"language":181},[179],"class FThreadSafeCounter\n{\npublic:\n  int32 Add( int32 Amount )\n  {\n    return FPlatformAtomics::InterlockedAdd(&Counter, Amount);\n  }\nprivate:\n  volatile int32 Counter;\n};\n",[183,443,441],{"__ignoreMap":106},[159,445,447],{"id":446},"fcriticalsection","FCriticalSection",[11,449,450],{},"用于对非线程安全的区域进行保护。",[176,452,455],{"className":453,"code":454,"language":181},[179],"FCriticalSection CriticalSection;\n",[183,456,454],{"__ignoreMap":106},[11,458,459],{},"声明之后在需要的地方进行锁操作即可，有提供作用域保护的封装：",[176,461,464],{"className":462,"code":463,"language":181},[179],"FScopeLock Lock(&CriticalSection);\n",[183,465,463],{"__ignoreMap":106},[11,467,468],{},"这样就不需要自己进行Lock和Unlock了，可以有效的防止误操作导致的Bug的出现。",[159,470,472],{"id":471},"fspinlock","FSpinLock",[11,474,475],{},"锁操作，提供Lock，Unlock以及BlockUntilUnlocked等便利的操作。",[11,477,478],{},"其实内部就是对FPlatformAtomics::InterlockedExchange的一个封装。",[11,480,481],{},"构造函数的InSpinTimeInSeconds就是默认的锁等待间隔，默认值为0.1。",[159,483,485],{"id":484},"fsemaphore","FSemaphore",[11,487,488],{},"这个是对信号量的封装，但是似乎不建议使用。",[11,490,491],{},"而且并不是对于所有的平台都有实现的，通常建议使用FEvent进行代替。",[159,493,495],{"id":494},"fevent","FEvent",[11,497,498],{},"这个相当于UE4封装的内部使用的互斥信号量机制，有基本的等待和唤醒操作。",[159,500,502],{"id":501},"fscopedevent","FScopedEvent",[11,504,505],{},"对FEvnet的封装，在注释上能够看到使用示例：",[176,507,510],{"className":508,"code":509,"language":181},[179],"{\n        FScopedEvent MyEvent;\n        SendReferenceOrPointerToSomeOtherThread(&MyEvent); \u002F\u002F Other thread calls MyEvent->Trigger();\n        \u002F\u002F MyEvent destructor is here, we wait here.\n}\n",[183,511,509],{"__ignoreMap":106},[11,513,514],{},"这个操作就是将MyEvent发送到其他线程，直到在其他的地方MyEvnet->Trigger()被调用为止，都不会离开这个作用域继续执行。",[159,516,517],{"id":517},"容器",[11,519,520],{},"包括TArray, TMap在内的几乎大部分的容器都不是线程安全的，需要自己对同步进行管理。",[11,522,523],{},"当然也能看到一些线程安全的封装，例如TArrayWithThreadsafeAdd。",[11,525,526],{},[527,528,529],"strong",{},"TLockFreePointerList",[11,531,532],{},"这个是一系列的类型，在Task Graph系统中被使用到。如其名称是LockFree的。",[11,534,535],{},[527,536,537],{},"TQueue",[11,539,540],{},"也是LockFree的，在初始化时可以指定线程同步的类型EQueueMode，分为Mpsc（多生产者单消费者）以及Spsc（单生产者单消费者）两种模式。",[11,542,543],{},"只有Spsc模式是contention free的。",[11,545,546],{},"仔细寻找的话UE4内部有实现很多便利的类型，例如TCircularQueue这种针对双线程，一个消费一个生产的线程安全类型。",[159,548,549],{"id":549},"工具类",[11,551,552],{},[527,553,554],{},"FThreadSafeCounter",[11,556,557],{},"就是前面例子中的线程安全的计数器。",[11,559,560],{},[527,561,562],{},"FThreadSingleton",[11,564,565],{},"为每一个线程创建一个实例。",[11,567,568],{},[527,569,570],{},"FThreadIdleStats",[11,572,573],{},"用于统计线程空闲状态。",[18,575,576],{"id":576},"异步执行",[11,578,579],{},"UE4中对基本的线程操作进行了一定程度的封装，使用相应的Helper就可以无需关心线程的创建这些问题。",[159,581,583],{"id":582},"asynctask","AsyncTask",[11,585,586],{},"这个函数可以将一些简单的任务扔到UE4的线程池中去进行，不必关心具体的线程同步问题。",[176,588,591],{"className":589,"code":590,"language":181},[179],"if(IsInGameThread())\n{\n  \u002F\u002F….一些操作\n}\nelse\n{\n  AsyncTask(ENamedThreads::GameThread, [=]()\n  {\n    \u002F\u002F….一些操作\n  });\n}\n",[183,592,590],{"__ignoreMap":106},[11,594,595],{},"其中第一个参数是发送到的线程的名称，通常一些工作线程是无法执行引擎中IsGameThread()保护或者其他隐形的游戏线程代码的，通过这个操作将其发送到游戏线程的话使用GameThread就可以了。",[11,597,598],{},"其实基本上的游戏逻辑中使用最多的就是这个函数了。",[159,600,602],{"id":601},"rhicmdlist","RHICmdList",[11,604,605],{},"这是一组独特的宏，用于将操作发送到渲染线程进行操作。",[11,607,608],{},"主要是对Texture之类的数据在GPU以及GPU相关的指令进行执行。",[11,610,611],{},"例如：",[176,613,616],{"className":614,"code":615,"language":181},[179],"if (IsInRenderingThread())\n{\n    \u002F\u002F Initialize the vertex factory's stream components.\n    FDataType NewData;\n    NewData.PositionComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Position, VET_Float3);\n    NewData.TangentBasisComponents[0] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentX, VET_PackedNormal);\n    NewData.TangentBasisComponents[1] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, TangentZ, VET_PackedNormal);\n    NewData.ColorComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(InVertexBuffer, FPaperSpriteVertex, Color, VET_Color);\n    NewData.TextureCoordinates.Add(FVertexStreamComponent(InVertexBuffer, STRUCT_OFFSET(FPaperSpriteVertex, TexCoords), sizeof(FPaperSpriteVertex), VET_Float2));\n    SetData(NewData);\n}\nelse\n{\n    ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(\n        InitPaperSpriteVertexFactory,\n        FPaperSpriteVertexFactory*, VertexFactory, this,\n        const FPaperSpriteVertexBuffer*, VB, InVertexBuffer,\n        {\n            VertexFactory->Init(VB);\n        });\n}\n",[183,617,615],{"__ignoreMap":106},[11,619,620],{},"这样就可以保证只能在渲染线程执行的代码不会被其他线程执行到。",[11,622,623],{},"渲染线程还有一些需要注意的是，UE4中有的代码的执行其实是在渲染线程中的，如果没有留意的话会造成隐形的线程同步问题。例如通常UMG的OnPaint。",[159,625,627],{"id":626},"fasynctask","FAsyncTask",[11,629,630],{},"这个是一组任务的封装类，是基本的任务单元，最简单的使用如下：",[632,633,635],"h4",{"id":634},"fautodeleteasynctask","FAutoDeleteAsyncTask",[176,637,641],{"className":638,"code":639,"language":640,"meta":106,"style":106},"language-cpp shiki shiki-themes github-light-high-contrast github-dark monokai","class ExampleAutoDeleteAsyncTask : public FNonAbandonableTask\n{\n    friend class FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>;\n\n    int32 ExampleData;\n\n    ExampleAutoDeleteAsyncTask(int32 InExampleData)\n        : ExampleData(InExampleData)\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[ExampleAutoDeleteAsyncTask] Construct()\"));\n    }\n\n    void DoWork()\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[ExampleAutoDeleteAsyncTask] DoWork()\"));\n    }\n\n    FORCEINLINE TStatId GetStatId() const\n    {\n        RETURN_QUICK_DECLARE_CYCLE_STAT(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);\n    }\n};\n","cpp",[183,642,643,666,671,692,698,707,712,732,744,750,771,777,782,794,799,815,820,825,843,848,857,862],{"__ignoreMap":106},[644,645,648,652,656,660,663],"span",{"class":646,"line":647},"line",1,[644,649,651],{"class":650},"sGZGq","class",[644,653,655],{"class":654},"sfUV7"," ExampleAutoDeleteAsyncTask",[644,657,659],{"class":658},"s0idv"," : ",[644,661,662],{"class":650},"public",[644,664,665],{"class":654}," FNonAbandonableTask\n",[644,667,668],{"class":646,"line":107},[644,669,670],{"class":658},"{\n",[644,672,673,677,680,683,686,689],{"class":646,"line":108},[644,674,676],{"class":675},"sLXdl","    friend",[644,678,679],{"class":650}," class",[644,681,682],{"class":654}," FAutoDeleteAsyncTask",[644,684,685],{"class":658},"\u003C",[644,687,688],{"class":654},"ExampleAutoDeleteAsyncTask",[644,690,691],{"class":658},">;\n",[644,693,695],{"class":646,"line":694},4,[644,696,697],{"emptyLinePlaceholder":118},"\n",[644,699,701,704],{"class":646,"line":700},5,[644,702,703],{"class":654},"    int32",[644,705,706],{"class":658}," ExampleData;\n",[644,708,710],{"class":646,"line":709},6,[644,711,697],{"emptyLinePlaceholder":118},[644,713,715,719,722,725,729],{"class":646,"line":714},7,[644,716,718],{"class":717},"scAT4","    ExampleAutoDeleteAsyncTask",[644,720,721],{"class":658},"(",[644,723,724],{"class":654},"int32",[644,726,728],{"class":727},"sbfHu"," InExampleData",[644,730,731],{"class":658},")\n",[644,733,735,738,741],{"class":646,"line":734},8,[644,736,737],{"class":658},"        : ",[644,739,740],{"class":717},"ExampleData",[644,742,743],{"class":658},"(InExampleData)\n",[644,745,747],{"class":646,"line":746},9,[644,748,749],{"class":658},"    {\n",[644,751,753,756,759,762,764,768],{"class":646,"line":752},10,[644,754,755],{"class":717},"        UE_LOG",[644,757,758],{"class":658},"(LogTemp, Log, ",[644,760,761],{"class":717},"TEXT",[644,763,721],{"class":658},[644,765,767],{"class":766},"sZnmU","\"[ExampleAutoDeleteAsyncTask] Construct()\"",[644,769,770],{"class":658},"));\n",[644,772,774],{"class":646,"line":773},11,[644,775,776],{"class":658},"    }\n",[644,778,780],{"class":646,"line":779},12,[644,781,697],{"emptyLinePlaceholder":118},[644,783,785,788,791],{"class":646,"line":784},13,[644,786,787],{"class":650},"    void",[644,789,790],{"class":717}," DoWork",[644,792,793],{"class":658},"()\n",[644,795,797],{"class":646,"line":796},14,[644,798,749],{"class":658},[644,800,802,804,806,808,810,813],{"class":646,"line":801},15,[644,803,755],{"class":717},[644,805,758],{"class":658},[644,807,761],{"class":717},[644,809,721],{"class":658},[644,811,812],{"class":766},"\"[ExampleAutoDeleteAsyncTask] DoWork()\"",[644,814,770],{"class":658},[644,816,818],{"class":646,"line":817},16,[644,819,776],{"class":658},[644,821,823],{"class":646,"line":822},17,[644,824,697],{"emptyLinePlaceholder":118},[644,826,828,831,834,837,840],{"class":646,"line":827},18,[644,829,830],{"class":658},"    FORCEINLINE ",[644,832,833],{"class":654},"TStatId",[644,835,836],{"class":717}," GetStatId",[644,838,839],{"class":658},"() ",[644,841,842],{"class":675},"const\n",[644,844,846],{"class":646,"line":845},19,[644,847,749],{"class":658},[644,849,851,854],{"class":646,"line":850},20,[644,852,853],{"class":717},"        RETURN_QUICK_DECLARE_CYCLE_STAT",[644,855,856],{"class":658},"(ExampleAutoDeleteAsyncTask, STATGROUP_ThreadPoolAsyncTasks);\n",[644,858,860],{"class":646,"line":859},21,[644,861,776],{"class":658},[644,863,865],{"class":646,"line":864},22,[644,866,867],{"class":658},"};\n",[11,869,870],{},"在完成定义后，可以有两种使用方式：",[176,872,874],{"className":638,"code":873,"language":640,"meta":106,"style":106},"\u002F\u002F 将任务扔到线程池中去执行\n(new FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>(5))->StartBackgroundTask();\n\n\u002F\u002F 直接在当前线程执行操作\n(new FAutoDeleteAsyncTask\u003CExampleAutoDeleteAsyncTask>(5))->StartSynchronousTask();\n",[183,875,876,882,911,915,920],{"__ignoreMap":106},[644,877,878],{"class":646,"line":647},[644,879,881],{"class":880},"sZdD5","\u002F\u002F 将任务扔到线程池中去执行\n",[644,883,884,886,889,891,893,895,898,902,905,908],{"class":646,"line":107},[644,885,721],{"class":658},[644,887,888],{"class":675},"new",[644,890,682],{"class":717},[644,892,685],{"class":658},[644,894,688],{"class":654},[644,896,897],{"class":658},">(",[644,899,901],{"class":900},"stP0Q","5",[644,903,904],{"class":658},"))->",[644,906,907],{"class":717},"StartBackgroundTask",[644,909,910],{"class":658},"();\n",[644,912,913],{"class":646,"line":108},[644,914,697],{"emptyLinePlaceholder":118},[644,916,917],{"class":646,"line":694},[644,918,919],{"class":880},"\u002F\u002F 直接在当前线程执行操作\n",[644,921,922,924,926,928,930,932,934,936,938,941],{"class":646,"line":700},[644,923,721],{"class":658},[644,925,888],{"class":675},[644,927,682],{"class":717},[644,929,685],{"class":658},[644,931,688],{"class":654},[644,933,897],{"class":658},[644,935,901],{"class":900},[644,937,904],{"class":658},[644,939,940],{"class":717},"StartSynchronousTask",[644,942,910],{"class":658},[11,944,945],{},"FAutoDeleteAsyncTask的一个优点是，在执行完成后会自动销毁，无需进行额外的关注。通常文件写入或者压缩数据之类的无须进行过程管理的操作可以交付给他执行。",[632,947,949],{"id":948},"fasync-task","FAsync Task",[11,951,952],{},"这个才是本尊，由于不会自动删除，有需要进行额外操作的情况。",[176,954,956],{"className":638,"code":955,"language":640,"meta":106,"style":106},"MyTask->StartSynchronousTask();\n\n\u002F\u002Fto just do it now on this thread\n\u002F\u002FCheck if the task is done :\n\nif (MyTask->IsDone())\n{\n}\n\n\u002F\u002FSpinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.\n\u002F\u002FEnsure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.\n\nMyTask->EnsureCompletion();\ndelete Task;\n",[183,957,958,967,971,976,981,985,999,1003,1008,1012,1017,1022,1026,1035],{"__ignoreMap":106},[644,959,960,963,965],{"class":646,"line":647},[644,961,962],{"class":658},"MyTask->",[644,964,940],{"class":717},[644,966,910],{"class":658},[644,968,969],{"class":646,"line":107},[644,970,697],{"emptyLinePlaceholder":118},[644,972,973],{"class":646,"line":108},[644,974,975],{"class":880},"\u002F\u002Fto just do it now on this thread\n",[644,977,978],{"class":646,"line":694},[644,979,980],{"class":880},"\u002F\u002FCheck if the task is done :\n",[644,982,983],{"class":646,"line":700},[644,984,697],{"emptyLinePlaceholder":118},[644,986,987,990,993,996],{"class":646,"line":709},[644,988,989],{"class":675},"if",[644,991,992],{"class":658}," (MyTask->",[644,994,995],{"class":717},"IsDone",[644,997,998],{"class":658},"())\n",[644,1000,1001],{"class":646,"line":714},[644,1002,670],{"class":658},[644,1004,1005],{"class":646,"line":734},[644,1006,1007],{"class":658},"}\n",[644,1009,1010],{"class":646,"line":746},[644,1011,697],{"emptyLinePlaceholder":118},[644,1013,1014],{"class":646,"line":752},[644,1015,1016],{"class":880},"\u002F\u002FSpinning on IsDone is not acceptable( see EnsureCompletion ), but it is ok to check once a frame.\n",[644,1018,1019],{"class":646,"line":773},[644,1020,1021],{"class":880},"\u002F\u002FEnsure the task is done, doing the task on the current thread if it has not been started, waiting until completion in all cases.\n",[644,1023,1024],{"class":646,"line":779},[644,1025,697],{"emptyLinePlaceholder":118},[644,1027,1028,1030,1033],{"class":646,"line":784},[644,1029,962],{"class":658},[644,1031,1032],{"class":717},"EnsureCompletion",[644,1034,910],{"class":658},[644,1036,1037,1040],{"class":646,"line":796},[644,1038,1039],{"class":675},"delete",[644,1041,1042],{"class":658}," Task;\n",[11,1044,1045],{},"但是如果是使用StartBackgroundTask()的话依然不需要自己进行管理。",[159,1047,1049],{"id":1048},"frunnable","FRunnable",[11,1051,1052],{},"这个是交付给线程的执行体封装，通常用于比AsyncTask更加复杂的操作。",[11,1054,1055],{},"分为Init(), Run(), Exit()三个操作，如果Init失败就不会执行Run()，Run()执行完成就会执行Exit()。",[176,1057,1059],{"className":638,"code":1058,"language":640,"meta":106,"style":106},"class FRunAbleTest : public FRunnable\n{\n    virtual uint32 Run() override\n    {\n        UE_LOG(LogTemp, Log, TEXT(\"[FRunAbleTest] Run()\"));\n        FPlatformProcess::Sleep(30);\n        UE_LOG(LogTemp, Log, TEXT(\"[FRunAbleTest] Run(): Comp\"));\n        return 0;\n    }\n\n};\n",[183,1060,1061,1075,1079,1095,1099,1114,1133,1148,1159,1163,1167],{"__ignoreMap":106},[644,1062,1063,1065,1068,1070,1072],{"class":646,"line":647},[644,1064,651],{"class":650},[644,1066,1067],{"class":654}," FRunAbleTest",[644,1069,659],{"class":658},[644,1071,662],{"class":650},[644,1073,1074],{"class":654}," FRunnable\n",[644,1076,1077],{"class":646,"line":107},[644,1078,670],{"class":658},[644,1080,1081,1084,1087,1090,1092],{"class":646,"line":108},[644,1082,1083],{"class":675},"    virtual",[644,1085,1086],{"class":654}," uint32",[644,1088,1089],{"class":717}," Run",[644,1091,839],{"class":658},[644,1093,1094],{"class":675},"override\n",[644,1096,1097],{"class":646,"line":694},[644,1098,749],{"class":658},[644,1100,1101,1103,1105,1107,1109,1112],{"class":646,"line":700},[644,1102,755],{"class":717},[644,1104,758],{"class":658},[644,1106,761],{"class":717},[644,1108,721],{"class":658},[644,1110,1111],{"class":766},"\"[FRunAbleTest] Run()\"",[644,1113,770],{"class":658},[644,1115,1116,1119,1122,1125,1127,1130],{"class":646,"line":709},[644,1117,1118],{"class":654},"        FPlatformProcess",[644,1120,1121],{"class":658},"::",[644,1123,1124],{"class":717},"Sleep",[644,1126,721],{"class":658},[644,1128,1129],{"class":900},"30",[644,1131,1132],{"class":658},");\n",[644,1134,1135,1137,1139,1141,1143,1146],{"class":646,"line":714},[644,1136,755],{"class":717},[644,1138,758],{"class":658},[644,1140,761],{"class":717},[644,1142,721],{"class":658},[644,1144,1145],{"class":766},"\"[FRunAbleTest] Run(): Comp\"",[644,1147,770],{"class":658},[644,1149,1150,1153,1156],{"class":646,"line":734},[644,1151,1152],{"class":675},"        return",[644,1154,1155],{"class":900}," 0",[644,1157,1158],{"class":658},";\n",[644,1160,1161],{"class":646,"line":746},[644,1162,776],{"class":658},[644,1164,1165],{"class":646,"line":752},[644,1166,697],{"emptyLinePlaceholder":118},[644,1168,1169],{"class":646,"line":773},[644,1170,867],{"class":658},[11,1172,1173],{},"通常也可以只指定Run()，然后交付给线程：",[176,1175,1177],{"className":638,"code":1176,"language":640,"meta":106,"style":106},"FRunnable* tp_Runable = new FRunAbleTest();\nmp_TestThread = FRunnableThread::Create(tp_Runable, TEXT(\"Test_01\"));\n",[183,1178,1179,1199],{"__ignoreMap":106},[644,1180,1181,1183,1186,1189,1192,1195,1197],{"class":646,"line":647},[644,1182,1049],{"class":658},[644,1184,1185],{"class":675},"*",[644,1187,1188],{"class":658}," tp_Runable ",[644,1190,1191],{"class":675},"=",[644,1193,1194],{"class":675}," new",[644,1196,1067],{"class":717},[644,1198,910],{"class":658},[644,1200,1201,1204,1206,1209,1211,1214,1217,1219,1221,1224],{"class":646,"line":107},[644,1202,1203],{"class":658},"mp_TestThread ",[644,1205,1191],{"class":675},[644,1207,1208],{"class":654}," FRunnableThread",[644,1210,1121],{"class":658},[644,1212,1213],{"class":717},"Create",[644,1215,1216],{"class":658},"(tp_Runable, ",[644,1218,761],{"class":717},[644,1220,721],{"class":658},[644,1222,1223],{"class":766},"\"Test_01\"",[644,1225,770],{"class":658},[11,1227,1228],{},"就可以了。",[159,1230,1232],{"id":1231},"async","Async",[11,1234,1235],{},"这是另一个异步执行的宏，与AsyncTask有少许不同。",[11,1237,1238],{},"Async的简单的使用方式在注释中有提到",[176,1240,1242],{"className":638,"code":1241,"language":640,"meta":106,"style":106},"    \u002F\u002F 使用全局函数\n    int TestFunc()\n    {\n        return 123;\n    }\n\n    TFunction\u003Cint()> Task = TestFunc();\n    auto Result = Async(EAsyncExecution::Thread, Task);\n\n    \u002F\u002F 使用lambda\n    TFunction\u003Cint()> Task = []()\n    {\n        return 123;\n    }\n\n    auto Result = Async(EAsyncExecution::Thread, Task);\n\n\n    \u002F\u002F 使用inline lambda\n    auto Result = Async\u003Cint>(EAsyncExecution::Thread, []() {\n        return 123;\n    }\n",[183,1243,1244,1249,1259,1263,1272,1276,1280,1305,1326,1330,1335,1354,1358,1366,1370,1374,1390,1394,1398,1403,1424,1432],{"__ignoreMap":106},[644,1245,1246],{"class":646,"line":647},[644,1247,1248],{"class":880},"    \u002F\u002F 使用全局函数\n",[644,1250,1251,1254,1257],{"class":646,"line":107},[644,1252,1253],{"class":650},"    int",[644,1255,1256],{"class":717}," TestFunc",[644,1258,793],{"class":658},[644,1260,1261],{"class":646,"line":108},[644,1262,749],{"class":658},[644,1264,1265,1267,1270],{"class":646,"line":694},[644,1266,1152],{"class":675},[644,1268,1269],{"class":900}," 123",[644,1271,1158],{"class":658},[644,1273,1274],{"class":646,"line":700},[644,1275,776],{"class":658},[644,1277,1278],{"class":646,"line":709},[644,1279,697],{"emptyLinePlaceholder":118},[644,1281,1282,1285,1287,1290,1293,1296,1299,1301,1303],{"class":646,"line":714},[644,1283,1284],{"class":658},"    TFunction",[644,1286,685],{"class":675},[644,1288,1289],{"class":650},"int",[644,1291,1292],{"class":658},"()",[644,1294,1295],{"class":675},">",[644,1297,1298],{"class":658}," Task ",[644,1300,1191],{"class":675},[644,1302,1256],{"class":717},[644,1304,910],{"class":658},[644,1306,1307,1310,1313,1315,1318,1320,1323],{"class":646,"line":734},[644,1308,1309],{"class":650},"    auto",[644,1311,1312],{"class":658}," Result ",[644,1314,1191],{"class":675},[644,1316,1317],{"class":717}," Async",[644,1319,721],{"class":658},[644,1321,1322],{"class":654},"EAsyncExecution",[644,1324,1325],{"class":658},"::Thread, Task);\n",[644,1327,1328],{"class":646,"line":746},[644,1329,697],{"emptyLinePlaceholder":118},[644,1331,1332],{"class":646,"line":752},[644,1333,1334],{"class":880},"    \u002F\u002F 使用lambda\n",[644,1336,1337,1339,1341,1343,1345,1347,1349,1351],{"class":646,"line":773},[644,1338,1284],{"class":658},[644,1340,685],{"class":675},[644,1342,1289],{"class":650},[644,1344,1292],{"class":658},[644,1346,1295],{"class":675},[644,1348,1298],{"class":658},[644,1350,1191],{"class":675},[644,1352,1353],{"class":658}," []()\n",[644,1355,1356],{"class":646,"line":779},[644,1357,749],{"class":658},[644,1359,1360,1362,1364],{"class":646,"line":784},[644,1361,1152],{"class":675},[644,1363,1269],{"class":900},[644,1365,1158],{"class":658},[644,1367,1368],{"class":646,"line":796},[644,1369,776],{"class":658},[644,1371,1372],{"class":646,"line":801},[644,1373,697],{"emptyLinePlaceholder":118},[644,1375,1376,1378,1380,1382,1384,1386,1388],{"class":646,"line":817},[644,1377,1309],{"class":650},[644,1379,1312],{"class":658},[644,1381,1191],{"class":675},[644,1383,1317],{"class":717},[644,1385,721],{"class":658},[644,1387,1322],{"class":654},[644,1389,1325],{"class":658},[644,1391,1392],{"class":646,"line":822},[644,1393,697],{"emptyLinePlaceholder":118},[644,1395,1396],{"class":646,"line":827},[644,1397,697],{"emptyLinePlaceholder":118},[644,1399,1400],{"class":646,"line":845},[644,1401,1402],{"class":880},"    \u002F\u002F 使用inline lambda\n",[644,1404,1405,1407,1409,1411,1413,1415,1417,1419,1421],{"class":646,"line":850},[644,1406,1309],{"class":650},[644,1408,1312],{"class":658},[644,1410,1191],{"class":675},[644,1412,1317],{"class":717},[644,1414,685],{"class":658},[644,1416,1289],{"class":650},[644,1418,897],{"class":658},[644,1420,1322],{"class":654},[644,1422,1423],{"class":658},"::Thread, []() {\n",[644,1425,1426,1428,1430],{"class":646,"line":859},[644,1427,1152],{"class":675},[644,1429,1269],{"class":900},[644,1431,1158],{"class":658},[644,1433,1434],{"class":646,"line":864},[644,1435,776],{"class":658},[11,1437,1438],{},"第一个参数为执行的类型，TaskGraph是将其放到任务图中去执行，Thread则是在单独的线程中执行，TreadPool则是放入线程池中去执行。",[11,1440,1441],{},"这里并不能像AsyncTask一样指定目标的线程。",[11,1443,1444,1445,1448,1449,1452],{},"同时Async会返回一个",[183,1446,1447],{},"TFuture\u003CResultType>","，而",[183,1450,1451],{},"ResultType","则是传入的执行函数的返回值。",[176,1454,1456],{"className":638,"code":1455,"language":640,"meta":106,"style":106},"TFunction\u003Cint()> My_Task= []() {\n    return 123;\n};\n\nauto Future = Async(EAsyncExecution::TaskGraph, My_Task);\nint Result = Future.Get();\n",[183,1457,1458,1479,1488,1492,1496,1515],{"__ignoreMap":106},[644,1459,1460,1463,1465,1467,1469,1471,1474,1476],{"class":646,"line":647},[644,1461,1462],{"class":658},"TFunction",[644,1464,685],{"class":675},[644,1466,1289],{"class":650},[644,1468,1292],{"class":658},[644,1470,1295],{"class":675},[644,1472,1473],{"class":658}," My_Task",[644,1475,1191],{"class":675},[644,1477,1478],{"class":658}," []() {\n",[644,1480,1481,1484,1486],{"class":646,"line":107},[644,1482,1483],{"class":675},"    return",[644,1485,1269],{"class":900},[644,1487,1158],{"class":658},[644,1489,1490],{"class":646,"line":108},[644,1491,867],{"class":658},[644,1493,1494],{"class":646,"line":694},[644,1495,697],{"emptyLinePlaceholder":118},[644,1497,1498,1501,1504,1506,1508,1510,1512],{"class":646,"line":700},[644,1499,1500],{"class":650},"auto",[644,1502,1503],{"class":658}," Future ",[644,1505,1191],{"class":675},[644,1507,1317],{"class":717},[644,1509,721],{"class":658},[644,1511,1322],{"class":654},[644,1513,1514],{"class":658},"::TaskGraph, My_Task);\n",[644,1516,1517,1519,1521,1523,1526,1529],{"class":646,"line":709},[644,1518,1289],{"class":650},[644,1520,1312],{"class":658},[644,1522,1191],{"class":675},[644,1524,1525],{"class":658}," Future.",[644,1527,1528],{"class":717},"Get",[644,1530,910],{"class":658},[11,1532,1533],{},"类似这样的调用即可。",[18,1535,1536],{"id":1536},"总结",[11,1538,1539],{},"UE4提供的异步操作大体上分为TaskGraph和TreadPool的管理方式，通常较简单的任务交付给TaskGraph，复杂的任务交付给Thread。",[11,1541,1542],{},"对于Task，引擎会有自己的管理，将其分配给空闲的Worker Thread。同时Task之间的依赖关系也会被管理，并按照需要的顺序被执行。",[11,1544,1545],{},"其实TaskGroup和ThreadPool都是可以自己进行申请和管理的，但是并没有实际的进行研究。",[11,1547,1548],{},"因为理论上，除非有需求，应当尽量的让游戏逻辑保持简洁。再加上线程同步是要支付额外的成本的，因此，要尽量避免对异步逻辑的使用，即使使用，也要尽量的保持逻辑单纯。而且这两个系统本身是虚幻为编辑器而设计的，虽然开放给用户使用，但是就像GamePlayAbility系统一样。本身每个程序员都有自己的实现思路，也没有必要一定要使用这套系统。",[11,1550,1551],{},"毕竟游戏最终是用户体验，没有用户在意屏幕背后的逻辑实现是否”Geek”。",[1553,1554,1555],"style",{},"html pre.shiki code .sGZGq, html code.shiki .sGZGq{--shiki-default:#A0111F;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sfUV7, html code.shiki .sfUV7{--shiki-default:#702C00;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .s0idv, html code.shiki .s0idv{--shiki-default:#0E1116;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sLXdl, html code.shiki .sLXdl{--shiki-default:#A0111F;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .scAT4, html code.shiki .scAT4{--shiki-default:#622CBC;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sbfHu, html code.shiki .sbfHu{--shiki-default:#702C00;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .sZnmU, html code.shiki .sZnmU{--shiki-default:#032563;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sZdD5, html code.shiki .sZdD5{--shiki-default:#66707B;--shiki-dark:#6A737D;--shiki-sepia:#88846F}html pre.shiki code .stP0Q, html code.shiki .stP0Q{--shiki-default:#023B95;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}",{"title":106,"searchDepth":107,"depth":108,"links":1557},[1558,1562,1572,1582],{"id":359,"depth":107,"text":360,"children":1559},[1560,1561],{"id":375,"depth":108,"text":376},{"id":391,"depth":108,"text":392},{"id":410,"depth":107,"text":410,"children":1563},[1564,1565,1566,1567,1568,1569,1570,1571],{"id":416,"depth":108,"text":417},{"id":446,"depth":108,"text":447},{"id":471,"depth":108,"text":472},{"id":484,"depth":108,"text":485},{"id":494,"depth":108,"text":495},{"id":501,"depth":108,"text":502},{"id":517,"depth":108,"text":517},{"id":549,"depth":108,"text":549},{"id":576,"depth":107,"text":576,"children":1573},[1574,1575,1576,1580,1581],{"id":582,"depth":108,"text":583},{"id":601,"depth":108,"text":602},{"id":626,"depth":108,"text":627,"children":1577},[1578,1579],{"id":634,"depth":694,"text":635},{"id":948,"depth":694,"text":949},{"id":1048,"depth":108,"text":1049},{"id":1231,"depth":108,"text":1232},{"id":1536,"depth":107,"text":1536},{"layout":116,"status":117,"published":118,"author":1584,"author_login":121,"author_email":122,"wordpress_id":1585,"wordpress_url":1586,"date_gmt":1587,"excerpt":1588},{"display_name":120,"login":121,"email":122,"url":106},2150,"\u002F\u002F?p=2150","2017-12-16 08:37:44 +0000",{"type":8,"value":1589},[1590],[11,1591,336],{},"\u002F2017-12-16-ue4-async-note",{"title":331,"description":336},"_legacy\u002F2017\u002F2017-12-16-ue4-async-note",[326,1232],"rkyu0u7S8Wi9q-cnfwEdHesRioNVI7qK3cet5qvrtlQ",{"id":1598,"title":1599,"body":1600,"date":2020,"description":1604,"extension":114,"meta":2021,"navigation":118,"path":2030,"seo":2031,"stem":2032,"tags":2033,"__hash__":2035},"blogs\u002F_legacy\u002F2017\u002F2017-11-18-ue4-render-flow-overview.md","UE4 Render Flow纵览",{"type":8,"value":1601,"toc":1999},[1602,1605,1608,1611,1614,1621,1624,1631,1635,1638,1641,1647,1650,1654,1657,1660,1668,1671,1675,1678,1681,1684,1687,1690,1693,1696,1699,1702,1706,1709,1712,1715,1722,1725,1729,1732,1735,1738,1744,1747,1753,1756,1759,1763,1766,1769,1772,1778,1781,1784,1788,1791,1794,1800,1803,1806,1811,1814,1817,1821,1824,1828,1831,1834,1840,1843,1846,1852,1855,1858,1861,1864,1867,1870,1874,1877,1880,1883,1887,1890,1893,1913,1916,1920,1923,1926,1929,1932,1935,1941,1944,1947,1950,1953,1956,1959,1963,1966,1971,1975,1978,1984,1987,1990],[11,1603,1604],{},"渲染优化时质量和效率的平衡，虽然按照官方的建议进行相应的调整即可，但是不稍微了解其内部的原理的话还是有些许让人困惑的。",[11,1606,1607],{},"本文基于CEDEC2016的一篇讲稿，目标UE4版本为4.13。",[11,1609,1610],{},"由于到目前的版本（4.18）引擎渲染已经有了很大的变动，所以有的内容只有参考作用。",[11,1612,1613],{},"从概览的角度来看，UE4的渲染可以划分成以下的阶段：",[11,1615,1616],{},[1617,1618],"img",{"alt":1619,"src":1620},"clip_image001","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image001_thumb-2.png",[11,1622,1623],{},"这个通路是针对延迟渲染的，与目前主要针对VR设备的前向渲染并不对应。",[11,1625,1626,1630],{},[271,1627,1629],{"href":1628},"\u002F2017-07-30-ue4-profiling-preview\u002F#i-4","针对渲染通道的优化","虽然之前有做过总结，但是并没有详细的研究过各个通道在整体渲染中的地位。",[18,1632,1634],{"id":1633},"base-pass","Base Pass",[11,1636,1637],{},"作为最重要的基础性通道，Base Pass运算的结果作为之后所有通道运算的基础。",[11,1639,1640],{},"基础通道里主要的可见操作是对Opaque\u002FMasked材质的物体进行的遮蔽运算并完成G-Buffer的生成，VS和PS也在这个阶段进行计算。",[11,1642,1643],{},[1617,1644],{"alt":1645,"src":1646},"image3","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage3_thumb.png",[11,1648,1649],{},"针对Base Pass的优化方向主要针对Vertex Shader和Pixel Shader两个阶段，另外在材质的Shader制作本身上也需要注意降低运算量。",[159,1651,1653],{"id":1652},"vertex-shader","Vertex Shader",[11,1655,1656],{},"顶点计算的优化主要就是一些通常的建议：对物体的Bound进行规划，不要让bound过大，避免使用覆盖视野前后的物体。以使得Culling能够在早期就剪除掉不需要的顶点计算。",[11,1658,1659],{},"在Console中可以使用一下命令辅助优化：",[217,1661,1662,1665],{},[11,1663,1664],{},"Stat InitViews可以查看裁剪计算的效果",[11,1666,1667],{},"FreezeRendering可以冻结裁剪，对裁剪结果进行可视化分析",[11,1669,1670],{},"更进一步的可以根据平台的GPU特性不同进行对应的优化。",[159,1672,1674],{"id":1673},"pixel-shader","Pixel Shader",[11,1676,1677],{},"由于Base Pass是后面所有通道的基础，所以会有较高的固有消耗。同时也是在场景中添加物品、Shader等产生性能消耗最直观的地方。",[11,1679,1680],{},"在理想的状态下，在没有Masked或者Translucent的情况，在PreZ阶段完成时就可以决定各个像素的深度并形成遮蔽计算了。在这种情况下，就可以极好的减小Piexel Shader阶段的运算量。但是实际上，为了场景中的特效质量，不能光依靠Opaque的材质，事情就没有那么简单了。",[11,1682,1683],{},"会导致PreZ完成时深度计算结果不完全的运算有，Masked材质的Alpha Test以及在Pixel Shader内部对深度数据的重写。因此需要有PostZ阶段对深度数据进行重新处理。",[11,1685,1686],{},"因此在这里容易形成两种造成性能影响的错误操作：在制作通用的材质时，明明有的不使用半透明蒙版通道情况却开启了半透明蒙版并往其上连接一个参数或者将参数连接到Piexel Depth Offset上。",[11,1688,1689],{},"由于PreZ和PostZ的决策是在GPU中完成的，无法在UE4中进行预览，因此在进行优化的时候要注意对上面的两种情况进行观察。",[11,1691,1692],{},"总体而言，作为G-Buffer的生成阶段，BassPass会直接的受到物体增加的影响，在添加物体时要注意检查以下两项：",[11,1694,1695],{},"Bounds的设置是否很好的完成了Culling。",[11,1697,1698],{},"Material的设置是否很好的避免了不必要的Pixel Shader计算。",[11,1700,1701],{},"另外，在项目设置中可以对G-Buffer的精度进行设定，对于需要高质量运算结果的情况或者想要降低性能消耗的情况，可以在这里进行调整。",[18,1703,1705],{"id":1704},"z-prepass","Z PrePass",[11,1707,1708],{},"这是在BasePass之前尝试进行深度计算。经过Z PrePass计算之后，可以减少到达Vertex Shader的顶点数量，以提高效率。",[11,1710,1711],{},"在项目设置中可以对Early Z-Pass相关的选型进行调整。",[11,1713,1714],{},"对于单个物体，这里的Use as Occluder默认是开启的，将其去掉就不会参与Early Z-Pass的计算。这个大部分时间应该保持默认，让引擎自行决定是否让物体参与深度计算。",[11,1716,1717,1718],{},"ll\n",[1617,1719],{"alt":1720,"src":1721},"clip_image0015","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0015_thumb-1.png",[11,1723,1724],{},"早期深度计算是为了减少Base Pass的运算负荷而存在的，但有时场景中物体布局可能会导致这个阶段形成瓶颈，可以在这里针对物体进行开启关闭来调整效果。",[18,1726,1728],{"id":1727},"custom-depthstencil","Custom Depth\u002FStencil",[11,1730,1731],{},"在BasePass之后有一个可以自行进行定义的阶段，就是自定义深度。",[11,1733,1734],{},"自定义深度可以使得用户为物体在渲染时额外的生成一张深度贴图，可以很好的对需要的物体进行裁剪。",[11,1736,1737],{},"在项目设置中开启自定义深度",[11,1739,1740],{},[1617,1741],{"alt":1742,"src":1743},"clip_image002","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image002_thumb-2.png",[11,1745,1746],{},"然后在需要自定义深度的物体上打开",[11,1748,1749],{},[1617,1750],{"alt":1751,"src":1752},"clip_image003","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image003_thumb-2.png",[11,1754,1755],{},"需要注意的是，由于是额外进行的深度计算，基本上等同于对Base Pass进行了一次重新计算，自定义深度在场景较大的开放世界或者物体较多的场景中尤其会造成大的性能损失。",[11,1757,1758],{},"因此虽然使用Custom Depth可以相对简单的实现一些效果，但是却是以性能为代价的，应当尽量避免使用这个思路，如果非用不可的话，需要进行更加严格的Profiling。",[18,1760,1762],{"id":1761},"pre-lighting","Pre-Lighting",[11,1764,1765],{},"这是光照计算之前的一个运算阶段，主要的作用是Decal和AO的计算。",[11,1767,1768],{},"在过去的版本中Decal经常与光照计算产生冲突，造成一些奇特的明显不符合预期的最终结果。",[11,1770,1771],{},"因此后来加入了",[11,1773,1774],{},[1617,1775],{"alt":1776,"src":1777},"clip_image0019","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0019_thumb.png",[11,1779,1780],{},"的选项，目前的引擎中是默认开启的，无需太多关心。由于之前的项目中Decal的使用似乎没有遇到过什么问题，想来目前的默认选项已经很好的解决了问题。",[11,1782,1783],{},"如果在Decal的使用过程中遇到了问题，可以针对性的进行搜索。",[18,1785,1787],{"id":1786},"lighting","Lighting",[11,1789,1790],{},"就是光照计算阶段，光照的优化其实能找到很多资料。光照在UE4的操作上分为三种，StaticLight是全静态光照，全部使用预计算的结果进行光照。而Movable的光照则是全动态的，所有的光照都在运行时进行计算。Stational的光照则介于两者之间，静态物体的阴影会在预计算阶段进行缓存。",[11,1792,1793],{},"另外Stationary Light有同一个区域只受5个光照作用的限制，多出来的范围最小的那个会变成红叉叉，变成动态光照，在使用时需要注意。在视图选项中可以使用",[11,1795,1796],{},[1617,1797],{"alt":1798,"src":1799},"image","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb.png",[11,1801,1802],{},"来对整体场景进行排查。",[11,1804,1805],{},"在动态光照的优化上，动态光照是重叠的越多性能消耗就越高的，相反的个数很多却相互不重叠的话光照复杂度的上升却不是很快。可以在编辑器中使用光照复杂度视图进行确认和优化。",[11,1807,1808],{},[1617,1809],{"alt":1798,"src":1810},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb-1.png",[11,1812,1813],{},"还有一点是，静态光照并不是在运行时完全没有性能消耗。静态光照在运行时InitDynamic Setup计算阶段是会造成CPU消耗的，因此并不是由于是预计算的就可以无计划的放置。另外，据说StaticLight在被移动等时会自动的被变更为Movable的，没有进行过测试所以并不是很确定呢。",[11,1815,1816],{},"总之在进行光照布局时，首先使用Stationary是比较合理的策略。",[18,1818,1820],{"id":1819},"reflect","Reflect",[11,1822,1823],{},"反射计算虽然在概念上算是光照的一部分，但是其实在运算中是一个额外的阶段。",[159,1825,1827],{"id":1826},"reflection-probe","Reflection Probe",[11,1829,1830],{},"反射捕获，是预计算的反射。在引擎中提供了球体反射捕获和盒体反射捕获两个选择，在使用反射捕获时，可以在拖入后对所在区域进行手动的重新捕获，也可以自己在其中指定CubeMap。",[11,1832,1833],{},"在项目设置中可以进行设置来调整反射捕获的精度。",[11,1835,1836],{},[1617,1837],{"alt":1838,"src":1839},"clip_image00111","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image00111_thumb.png",[11,1841,1842],{},"这部分的消耗是在Reflection Environment Compute Shader XXXX中反映的，与动态光照相同，个数对其性能消耗的影响不如区域重叠造成的影响。",[11,1844,1845],{},"官方的建议是，在场景全体放置一个总的反射捕捉，然后在一个单位房间内放一个整合性的捕捉，最后在反射性的物体上针对性的放置。",[11,1847,1848],{},[1617,1849],{"alt":1850,"src":1851},"clip_image0024","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0024_thumb-2.png",[11,1853,1854],{},"覆盖全体的捕捉",[11,1856,1857],{},"防止在场景内完全丢失反射信息的情况",[11,1859,1860],{},"房间单位的捕捉",[11,1862,1863],{},"在场景单元内形成详细的反射信息",[11,1865,1866],{},"物体单位的捕捉",[11,1868,1869],{},"反射要求较高的物体附近进行更加详细的捕捉",[159,1871,1873],{"id":1872},"screen-space-reflection","Screen Space Reflection",[11,1875,1876],{},"动态反射计算，没有深入看过其实现。",[11,1878,1879],{},"这里的主要问题是，由于是在屏幕空间内进行的计算，在屏幕外的反射无法正确的反映，同时有较多的噪点而且对Translucent的材质在计算时容易出现问题。",[11,1881,1882],{},"因此通常是与上面的反射捕获共同使用，作为其补充而存在的。因此如果出现了反射表面投影质量比较奇怪的问题，通常也可以检查一下是否该区域没有放置反射捕获，而不是一味的去加强动态光照和间接光照的次数，毕竟他们的性能消耗还是非常可观的。",[159,1884,1886],{"id":1885},"planar-reflection","Planar Reflection",[11,1888,1889],{},"效果很好的反射，全动态计算。",[11,1891,1892],{},"但是其负荷相当的高，如果场景中有两个以上的话，会有目视可见的性能消耗。",[217,1894,1895,1898,1901,1904,1907,1910],{},[11,1896,1897],{},"无法控制反射通道中启用的渲染功能。",[11,1899,1900],{},"反射通道中的动态阴影不正确。",[11,1902,1903],{},"为保证达到目标帧率，需计算资源是否足以使用平面反射。",[11,1905,1906],{},"只支持恒定的粗糙系数，其在平面反射组件上（而非在材质上）进行指定。",[11,1908,1909],{},"如可能，须尽量将世界场景中的平面反射 Actor 数量限制为 1 个，将其移动、旋转、缩放，和世界场景搭配。也可使用多个平面反射 Actor，但需多加注意，因为平面反射 Actor 不执行任何距离剔除，只进行视锥和遮蔽剔除。因此，如果画面中同时存在两个平面反射 Actor，项目的帧率将受到严重影响。",[11,1911,1912],{},"渲染平面反射 Actor 的开销直接来自当前关卡中渲染的内容。启用此功能后，由三角形组成、绘制调用较大的场景将遭受严重的性能影响，因为这些开销不会随屏幕百分比变化。",[11,1914,1915],{},"以上内容引用自官方文档，在使用时需要额外的进行留意。",[18,1917,1919],{"id":1918},"translucent","Translucent",[11,1921,1922],{},"由于深度计算的效率等问题，Translucent单独在另一条路径上进行处理。因此在半透明的计算在延迟渲染中，总是会有很多的问题。",[159,1924,1925],{"id":1925},"深度计算",[11,1927,1928],{},"将半透明的粒子投放到场景中时，可以看到并不会在深度数据中产生影响。这样在一些使用深度数据进行的效果如DOF中就会出现BUG。",[11,1930,1931],{},"因此UE4使用Separate Translucency来对半透明物体的深度进行处理",[11,1933,1934],{},"在项目设置中可以看到开关",[11,1936,1937],{},[1617,1938],{"alt":1939,"src":1940},"clip_image00113","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image00113_thumb.png",[11,1942,1943],{},"关于DepthOfField,在材质中有其运算结果的节点。",[159,1945,1946],{"id":1946},"成本问题",[11,1948,1949],{},"半透明物体会极大的加重场景的渲染负担，在着色器复杂度中能够看到，通常半透明的粒子会导致复杂度变为红色。",[11,1951,1952],{},"优化上可以考虑降低Separate Translucency的分辨率，使用r.SeparateTranslucencyScreenPercentage指令可以通过降低分辨率来减小其消耗。",[11,1954,1955],{},"另一个解决方案是使用Particle CutOut有效的减少半透明计算的区域。",[11,1957,1958],{},"似乎只要使用Create SubUV Animation就会自动应用，没有测试所以并不清楚。",[159,1960,1962],{"id":1961},"responsive-aa","Responsive AA",[11,1964,1965],{},"在半透明材质中可以看到这个选项，主要是针对使用了半透明材质的粒子的。",[11,1967,1968],{},[1617,1969],{"alt":1798,"src":1970},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fimage_thumb-2.png",[18,1972,1974],{"id":1973},"post-process","Post Process",[11,1976,1977],{},"pp是渲染的最后一个阶段，可以在这里对渲染结果进行进一步的加工。",[11,1979,1980],{},[1617,1981],{"alt":1982,"src":1983},"clip_image0026","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0026_thumb-1.png",[11,1985,1986],{},"PostProcess的成本消耗与添加的特效相关，每一个特效都会产生额外的消耗。如果自己使用了pp的材质来进行控制的话，其Shader复杂度也会对性能产生影响。",[11,1988,1989],{},"自带的PP特效可以通过指令来调整其效果，例如r.BloomQuality，其运算负荷是作为PostProcessWeightedSampleSum显示。",[11,1991,1992,1993,1998],{},"UE4的后期处理能在官方找到很多详细的[",[271,1994,1997],{"href":1995,"rel":1996},"https:\u002F\u002Fdocs.unrealengine.com\u002Flatest\u002FCHN\u002FEngine\u002FRendering\u002FPostProcessEffects\u002Findex.html",[275],"文档","]。",{"title":106,"searchDepth":107,"depth":108,"links":2000},[2001,2005,2006,2007,2008,2009,2014,2019],{"id":1633,"depth":107,"text":1634,"children":2002},[2003,2004],{"id":1652,"depth":108,"text":1653},{"id":1673,"depth":108,"text":1674},{"id":1704,"depth":107,"text":1705},{"id":1727,"depth":107,"text":1728},{"id":1761,"depth":107,"text":1762},{"id":1786,"depth":107,"text":1787},{"id":1819,"depth":107,"text":1820,"children":2010},[2011,2012,2013],{"id":1826,"depth":108,"text":1827},{"id":1872,"depth":108,"text":1873},{"id":1885,"depth":108,"text":1886},{"id":1918,"depth":107,"text":1919,"children":2015},[2016,2017,2018],{"id":1925,"depth":108,"text":1925},{"id":1946,"depth":108,"text":1946},{"id":1961,"depth":108,"text":1962},{"id":1973,"depth":107,"text":1974},"2017-11-18",{"layout":116,"status":117,"published":118,"author":2022,"author_login":121,"author_email":122,"wordpress_id":2023,"wordpress_url":2024,"date_gmt":2025,"excerpt":2026},{"display_name":120,"login":121,"email":122,"url":106},2126,"\u002F\u002F?p=2126","2017-11-18 09:45:59 +0000",{"type":8,"value":2027},[2028],[11,2029,1604],{},"\u002F2017-11-18-ue4-render-flow-overview",{"title":1599,"description":1604},"_legacy\u002F2017\u002F2017-11-18-ue4-render-flow-overview",[326,2034],"Rendering","FhVs1eAqGGNQqiSWGnuupyS0M44voIuEFC_oLNsA6WU",{"id":2037,"title":2038,"body":2039,"date":2396,"description":2043,"extension":114,"meta":2397,"navigation":118,"path":2406,"seo":2407,"stem":2408,"tags":2409,"__hash__":2411},"blogs\u002F_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-01.md","AnimatioinStarterPack的使用（上）",{"type":8,"value":2040,"toc":2378},[2041,2044,2047,2050,2053,2056,2059,2062,2065,2069,2072,2075,2080,2083,2086,2089,2092,2097,2100,2103,2108,2111,2114,2117,2120,2123,2129,2132,2135,2138,2141,2144,2147,2150,2153,2156,2159,2162,2166,2169,2175,2178,2181,2184,2187,2190,2193,2196,2199,2202,2205,2208,2211,2214,2217,2220,2225,2228,2231,2234,2237,2243,2246,2249,2252,2255,2258,2261,2264,2270,2273,2278,2281,2287,2290,2296,2299,2302,2305,2311,2314,2318,2321,2324,2327,2331,2334,2337,2343,2346,2352,2355,2358,2361,2364,2370,2372,2375],[11,2042,2043],{},"虚幻商城能看到官方提供的AnimationStarterPack，对于制作游戏原型非常的有用，毕竟如果一直用一个立方体来代替角色多少还是有些不足的。",[11,2045,2046],{},"当前使用的UE4版本为4.18.0。",[11,2048,2049],{},"4.18的升级中对角色动画相关的功能进行了改进，不过主要的改动是在PhysicAsset上的。刚好趁着这次开坑，对动画系统重新熟悉一下。",[11,2051,2052],{},"PS：这里只是操作笔记，并不会有很多细节上的说明哦~",[18,2054,2055],{"id":2055},"基础准备",[11,2057,2058],{},"首先当然是通过EpicLaucher添加AnimationStarterPack到项目中。",[11,2060,2061],{},"添加完成后能够看到动画包中有做好的基础角色蓝图，那么第一步就是参照着自己实现一遍。",[11,2063,2064],{},"动画蓝图的操作分别位于角色的蓝图和动画蓝图本身两个部分，这两者会互相交互来对状态进行更新。",[159,2066,2068],{"id":2067},"character","Character",[11,2070,2071],{},"角色蓝图直接新建一个Character即可，由于目标的系统是顶部的上帝视角的，所以会和官方的第一人称(?)的有些不同。",[11,2073,2074],{},"照例，先添加SpringArm和摄像",[11,2076,2077],{},[1617,2078],{"alt":1619,"src":2079},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image001_thumb.png",[11,2081,2082],{},"记得在SpringArm上设置Use Pawn Control Rotation。",[11,2084,2085],{},"之后，在Mesh中将Skeletal Mesh指定为SK_Mannequin，动画蓝图那里随便指定一个Asset，方便预览。",[11,2087,2088],{},"根据官方提供的Character将位置设为(0,0,-100)，同时将旋转设为(0,0,-90)以保持朝向与Arrow一致。",[11,2090,2091],{},"这里如果直接运行的话会发现角色穿到地面以下了，原因是胶囊体的大小不一致。由于不清楚官方设定的理由，这里暂且按照官方的来，修改为",[11,2093,2094],{},[1617,2095],{"alt":1742,"src":2096},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image002_thumb.png",[11,2098,2099],{},"这样就基本差不多了，然后将官方的角色蓝图中事件图表全部拷贝过来。这些蓝图都是些操作角色的输入处理，从Input拉到Character以及标志位的设置之类的，没有什么特别的地方呢。",[11,2101,2102],{},"没有的变量直接点右键生成，没有的输入直接到项目设置中添加。唯一要注意的是",[11,2104,2105],{},[1617,2106],{"alt":1751,"src":2107},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image003_thumb.png",[11,2109,2110],{},"Lookup的Scale是-1，要不然操作起来和通常的FPS是相反的。",[159,2112,2113],{"id":2113},"动画蓝图",[11,2115,2116],{},"然后新建一个C++类，继承自AnimInstance，这样做主要是考虑到之后动画蓝图的逻辑可能会变得复杂。",[11,2118,2119],{},"接下来在蓝图中新建一个动画蓝图，父类选择刚刚构建的CharaAnimate，骨架选择UE4_Mannequin_Skeleton。",[11,2121,2122],{},"然后在Character蓝图的Mesh里面将动画指定为刚刚新建的动画蓝图",[11,2124,2125],{},[1617,2126],{"alt":2127,"src":2128},"clip_image0014","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0014_thumb.png",[11,2130,2131],{},"指定完成后运行时就是个T字了，因为动画蓝图还是空的。",[11,2133,2134],{},"到达动画图表，添加一个简单的状态机。",[11,2136,2137],{},"状态机是动画蓝图的核心功能之一，可以通过设定的条件，根据变量的值等，跳转到不同的状态值，而不用自己根据众多的值对状态进行控制。",[11,2139,2140],{},"在动画图表中右键，新建一个状态机，名字就模仿官方的动画蓝图叫LocoMotion，并将它连到“最终动画姿势”上。",[11,2142,2143],{},"然后打开LocoMotion，右键新建一个状态，名为Idle。",[11,2145,2146],{},"打开Idle的状态，从右边的动画列表直接拖一个Idle_Rifle_Hip连接到Result上。",[11,2148,2149],{},"这样的话预览有些就能看到角色播放静止动画了。",[18,2151,2152],{"id":2152},"混合空间",[11,2154,2155],{},"混合空间是最基础的动画蓝图操作，大部分时候行走的动画都是靠其实现的。",[11,2157,2158],{},"新建一个混合空间，骨骼选择SK_Mannequin。",[11,2160,2161],{},"混合空间是2D的，通常的行走混合就是通过前进速度和前进方向来混合出八方向行走动画。",[159,2163,2165],{"id":2164},"blendspace","BlendSpace",[11,2167,2168],{},"打开混合空间，在左侧切换到Asset Detail标签，首先将混合用的两个坐标轴设置好。",[11,2170,2171],{},[1617,2172],{"alt":2173,"src":2174},"clip_image0016","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0016_thumb.png",[11,2176,2177],{},"方向是从-180~180之间，而速度则需要根据Character的设定来决定，由于这里是默认的，就保持和官方示例的一样就好了。",[11,2179,2180],{},"这里的坐标名称是随意的，之后会通过蓝图来设置当前的坐标值。",[159,2182,2183],{"id":2183},"状态机",[11,2185,2186],{},"在进一步操作混合空间前，先将动画蓝图设置好。",[11,2188,2189],{},"动画图表的状态机中新建一个状态，命名为Move。",[11,2191,2192],{},"新建两个变量Speed、Direction，",[11,2194,2195],{},"从Idle的边缘拉一根线到Move，就会自动生成一个Transition。然后在Transition中添加条件为Speed>10.0则执行状态迁移，也就是由Idle变为Move。",[11,2197,2198],{},"然后从Move拉一个Transition到Idle，条件设置为Speed\u003C=10.0。",[11,2200,2201],{},"打开Move的状态，直接拖入刚刚新建的混合空间，把Speed和Direction分别接到混合空间的两个坐标轴上。可以在右边的动画预览页调整两个值来查看效果，不过现在混合空间是空的，会变成摆T字。",[11,2203,2204],{},"这样状态机的部分就设置完了。",[159,2206,2207],{"id":2207},"关键帧",[11,2209,2210],{},"然后回到混合空间中，在坐标系的各个位置添加混合节点。",[11,2212,2213],{},"一般情况下在角度(-180, -90, 0, 90, 180)和(行走速度,跑步速度)上添加关键性的混合用节点就可以了。",[11,2215,2216],{},"但是AnimationStarterPack中似乎没有跑步动画，不过这个版本的引擎中可以在左侧对节点的速度缩放进行调节，某种程度上可以代替跑步，不过反正是用来做原型的，也不用太在意。",[11,2218,2219],{},"在Speed 270上拖放动画，按角度来区分的话-180和180都是向后采用BWD，0则采用FWD，-90是LT而90是RT，然后在Speed 0上放一样的动画，但是将Rate Scale调低一些，变成0.8。",[11,2221,2222],{},[1617,2223],{"alt":1850,"src":2224},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0024_thumb.png",[11,2226,2227],{},"然后将Target Weight Interpolation Speed Per Sec设定为2.0，这样混合空间的准备就完成了。",[159,2229,2230],{"id":2230},"状态绑定",[11,2232,2233],{},"此时在动画蓝图中调节预览的两个值，就可以看到效果了。",[11,2235,2236],{},"然后参照官方的动画蓝图中的节点，将速度和方向从Character那边读取过来。",[11,2238,2239],{},[1617,2240],{"alt":2241,"src":2242},"clip_image0034","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0034_thumb.png",[11,2244,2245],{},"直接运行游戏，就可以看到行走的动画切换了。",[18,2247,2248],{"id":2248},"额外动作",[11,2250,2251],{},"在基本的行走动画之外，还有一些额外的动作可以加入到动画蓝图的状态机中，通常的FPS中会有跳跃、蹲伏、趴下之类的动作。",[11,2253,2254],{},"AnimationStarterPack中虽然有提供趴下的动作，但是是没有移动动画的，所以是一个静止的状态，这里并没有做，其实动画蓝图这边只是新建一个孤立的状态机就可以。主要还是要在角色蓝图中添加静止移动的逻辑，由于并没有做这种功能的打算，这里就放弃了。",[159,2256,2257],{"id":2257},"静止跳跃",[11,2259,2260],{},"这里官方的动画蓝图有一个问题，由于在Idle->Jump的管道中有加上速度条件，而实际上这个速度是包含跳跃速度的，因此导致状态机沿着Idle->Jog->Run Jump的路径前进，Jump这个状态是永远都不可达的。",[11,2262,2263],{},"由于不知道官方的原始设计时什么样的，这里姑且在Speed计算时忽略掉Z轴的速度。",[11,2265,2266],{},[1617,2267],{"alt":2268,"src":2269},"clip_image0018","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0018_thumb.png",[11,2271,2272],{},"然后在检测到玩家按下跳跃键后，在动画蓝图中记录跳跃标记",[11,2274,2275],{},[1617,2276],{"alt":1982,"src":2277},"\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0026_thumb.png",[11,2279,2280],{},"基本可以将官方的蓝图中的Jump相关的部分直接抄过来，另外由于修改了Jump的逻辑，要为Jump添加状态进入事件",[11,2282,2283],{},[1617,2284],{"alt":2285,"src":2286},"clip_image0036","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image0036_thumb.png",[11,2288,2289],{},"并在事件蓝图中相应",[11,2291,2292],{},[1617,2293],{"alt":2294,"src":2295},"clip_image004","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image004_thumb.png",[11,2297,2298],{},"在这里的话就能在静止状态下跳起来了，但是如果在行走中按跳跃的话，就会有BUG：在停止运动后额外的播放了一次动画。",[11,2300,2301],{},"这个是因为没有加RunJump状态造成的，不过为了以后添加别的状态不出现这个Bug，需要对CanJump()的实现多加留意。不能跳跃的状态就不要将跳跃标记置为True。",[11,2303,2304],{},"另外一个方面就是，Jump这个动画有一个前摇的过程，但是实际上蓝图的实现是已经跳跃起来了。这里需要对设计进行调整，或者让动画从0.3秒开始播放",[11,2306,2307],{},[1617,2308],{"alt":2309,"src":2310},"clip_image005","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image005_thumb.png",[11,2312,2313],{},"这样调整之后会造成落地有些许的违和感，这个主要是官方没有准备浮空动画而是在跳跃动画播放完后直接切回非跳跃状态造成的。因此大概是为了防止这里的穿帮，官方给的默认跳跃高度还是比较低的。记得以前的某个示例中是有的，但是在AnimationStarterPack中并没有找到这个呢。由于是用来做游戏原型的，这个细节就不管了。",[159,2315,2317],{"id":2316},"runjump","RunJump",[11,2319,2320],{},"然后就是RunJump的添加。",[11,2322,2323],{},"这里大部分的逻辑可以按照Jump的一样的流程，也可以从官方的示例里面抄过来。不过从Move->Jump的状态其实没有必要对速度进行限制了。直接检测到跳跃标记就切换到RunJump状态就可以了。",[11,2325,2326],{},"RunJump的切换非常的流畅，所以很怀疑官方让Jump状态失效是故意而为的。",[159,2328,2330],{"id":2329},"crouching","Crouching",[11,2332,2333],{},"下蹲状态和跳跃状态有些类似，不过下蹲状态被当作了一个持续性状态来处理。",[11,2335,2336],{},"所以下蹲分为静止下蹲和下蹲移动两个状态，下蹲移动中也还是一个负责移动处理的BlendSpace，参照Move的BlendeSpace即可",[11,2338,2339],{},[1617,2340],{"alt":2341,"src":2342},"clip_image006","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image006_thumb.png",[11,2344,2345],{},"总体上而言下蹲的静止和移动与Stand->Run是一个对应的关系。",[11,2347,2348],{},[1617,2349],{"alt":2350,"src":2351},"clip_image007","\u002Fwp-content\u002Fuploads\u002F2017\u002F11\u002Fclip_image007_thumb.png",[11,2353,2354],{},"因此这里基本上没有什么新的东西。",[11,2356,2357],{},"唯一的不同是，由于前面修改过Speed运算方式。在Crouch的状态下按跳跃，只会有位置上升，而不会有动画的变更。",[11,2359,2360],{},"官方原始的类似于蹲下跳跃的效果，是由于Z轴速度大于10导致状态迁移到Crouch Move而产生的。",[11,2362,2363],{},"在CanJump()中添加CrouchingButtonDown时不允许设定Jump标志的逻辑，就可以防止在站起身之后进入跳跃的问题了。",[176,2365,2368],{"className":2366,"code":2367,"language":181},[179],"bool UCharaAnimate::CanJump(bool ShouldJump)\n{\n  return !EnableJump && ShouldJump && !Crouching;\n}\n",[183,2369,2367],{"__ignoreMap":106},[18,2371,1536],{"id":1536},[11,2373,2374],{},"至此官方的动画蓝图中实现的功能就基本完成了，虽然在实现上和官方少许有些不同，不过功能上已经没有什么缺陷了。",[11,2376,2377],{},"接下来，就是添加上一些额外的动画来与游戏模式相对应了。",{"title":106,"searchDepth":107,"depth":108,"links":2379},[2380,2384,2390,2395],{"id":2055,"depth":107,"text":2055,"children":2381},[2382,2383],{"id":2067,"depth":108,"text":2068},{"id":2113,"depth":108,"text":2113},{"id":2152,"depth":107,"text":2152,"children":2385},[2386,2387,2388,2389],{"id":2164,"depth":108,"text":2165},{"id":2183,"depth":108,"text":2183},{"id":2207,"depth":108,"text":2207},{"id":2230,"depth":108,"text":2230},{"id":2248,"depth":107,"text":2248,"children":2391},[2392,2393,2394],{"id":2257,"depth":108,"text":2257},{"id":2316,"depth":108,"text":2317},{"id":2329,"depth":108,"text":2330},{"id":1536,"depth":107,"text":1536},"2017-11-11",{"layout":116,"status":117,"published":118,"author":2398,"author_login":121,"author_email":122,"wordpress_id":2399,"wordpress_url":2400,"date_gmt":2401,"excerpt":2402},{"display_name":120,"login":121,"email":122,"url":106},2066,"\u002F\u002F?p=2066","2017-11-11 02:51:55 +0000",{"type":8,"value":2403},[2404],[11,2405,2043],{},"\u002F2017-11-11-animatioinstarterpack-note-01",{"title":2038,"description":2043},"_legacy\u002F2017\u002F2017-11-11-animatioinstarterpack-note-01",[326,2410],"Animation","pkfBqzdfLonxqYHExxcc5TCbq5IXzXFvboJ8DjRg40o",222,1788763183153]