前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UE4的执行流程和CPU优化

UE4的执行流程和CPU优化

作者头像
quabqi
发布2021-11-04 10:54:19
1.8K0
发布2021-11-04 10:54:19
举报
文章被收录于专栏:Dissecting UnrealDissecting Unreal

UE4是一个非常庞大的游戏引擎,说是游戏引擎,但其实内部实现的已经和一个小型操作系统差不多了,源码更是海量级别的。在这样海量的源码面前想要搞清楚是怎样运行的本身就不是一件容易的事情,除此外引擎本身是基于多线程设计的,不同线程之间错综复杂的关系更加深了理解引擎的难度。平时在写代码时候,我们也可能更多的只是关注Actor,Component,Level,World以及游戏逻辑怎么写,但很少去研究他们都是怎样运行的,但是如果不了解这些Actor,Component,Level,World,在游戏线程和渲染线程之间是怎样执行的,不清楚内部的运行机制是怎样的,就很容易写出性能糟糕或有各种问题的代码。为了解决这个问题,我整个梳理了一下UE4的大流程,画了一张图,关键点都用颜色标记了出来,让各个环节能够一目了然,这样就可以围绕着这个执行流程,来介绍一些常见的问题和性能优化手段,避免大家写出糟糕的代码。

我们可以看到,引擎启动的时候,会先初始化各个模块,然后就进入了Tick,在Tick中会先执行游戏逻辑,调用World的Tick,然后Tick所有注册需要Tick的Actor和Component,这里会根据注册的阶段分别在不同时期Tick。结束之后会进入绘制视口,会先画场景,在画场景时才相当于是渲染线程这帧真正开始了,然后画UI。然后中间很多地方都穿插着多线程调度。最终我们看到引擎执行一帧大概是像下图所示

这里具体每一步的细节看最上面那张很大的时序图就好了,我觉得非常清楚就不细说了,下面来具体讲解一些关键点和可能的问题以及优化手段:

初始化阶段

可以看到分为了PreInit和Init两步,这里没什么特别的,需要注意一些组件的初始化顺序。

可以看到引擎的Shader是在游戏启动非常早的时候就会准备好,可能都比业务的下载更新流程还要早,这里如果遇到热更要特别小心的处理,处理不好很有可能出现崩溃。在Init中比较关键的一步是创建Viewport,这个就是游戏最终画到的地方,最终会调用到GameInstance的StartGameInstance函数,这里也是业务逻辑的入口。

Tick阶段

在Tick的一开始,会先把场景数据的各种信息比如Transform在渲染线程上刷一遍,因为很多东西是会动的。之后引擎会开始Tick World。这里比较重要的一点是,我们可以看到Tick的对象有很多阶段,平常用的比较多的是PrePhysics(如果没关注过,打开Tick默认会是这里),During Physics, Post Physics这3个地方。为什么要区分这些阶段呢?这是因为UE4是个多线程的引擎,物理是每帧一个很重要的计算流程,物理的计算发生在一个单独的线程上,因此将Tick拆分成这些阶段,就可以让业务代码选择在什么时期执行。因为大部分的组件都是需要先准备好数据,交给物理线程来执行,所以UE4把Tick默认都放在了Pre Physics上,这样当所有组件Tick完,物理线程得到的数据就是最新的。但是考虑到假如你的组件或Actor和物理没任何关系,那么物理线程就会等待逻辑执行,在物理线程开始执行后,由于Durning Physics基本没事情做,又反过来等待物理线程,这样游戏线程的总耗时就会被拉长。因此可以把一些不需要依赖物理的组件放在其他阶段,说不定能起到很好的优化效果。

如上图所示,这样修改后,总的耗时就会减少。同样的道理,只要涉及到多线程或可以改成多线程的地方,比如动画组件,移动组件等,都可以用类似方式来优化,目标就是要尽量把多线程串行执行变成多线程并行执行。

这里额外提一点,Actor和ActorComponent的Tick是分别注册且互相独立的,互相不存在依赖关系,当你关掉Actor的Tick时,ActorComponent的Tick如果是打开状态,是不会受到影响的,所以当你发现不需要Tick的组件时,关掉Actor是不够的,Component也要单独关闭。另外在我们的招聘面试中,这个问题很多人都回答错了。

绘制阶段

可以看到,引擎的绘制是等待业务Tick全部完成后才开始的,绘制发生在渲染线程上,渲染线程做完相关流程后又可能再单独开一个RHI线程(iOS不开RHI单独的线程,安卓会开单独的RHI线程),他们3个线程之间是在不同时间点执行的。渲染线程和RHI线程和游戏线程不一样,游戏线程会把任务提交到渲染的命令队列里,而渲染线程会依次从队列里取任务执行,当没有新任务的时候,就会等待,而当任务特别多的时候,因为游戏线程会在很多阶段触发Flush操作强行等待渲染线程执行到某个位置,就会导致游戏线程等待,一个比较优秀的游戏肯定更希望把所有的线程都跑满,所以在性能优化时,通过观察stat是哪个线程在等待,就可以知道瓶颈是卡在了他等待对应的那个线程上,只要去优化对应的地方就好。

当场景绘制完成之后,才会开始绘制UI,这里也是UE4比较坑的一个地方,假如UI遮挡住了大部分场景,被遮挡住的部分就白画了。所以如果能修改引擎代码的话,可以考虑在绘制开始阶段,先在场景的RT上UI对应的位置写上深度(需要额外处理半透明)或者建一些对应轮廓面片放在镜头近平面上挡住场景对应区域,这样就可以跳过这些像素的绘制。

我们知道绘制这里游戏线程做的事情很少,基本上会阻塞在最后FrameSync,当你有一些很重的工作,但是又和渲染无关,比如网络游戏的解包或其他比较重的逻辑,就可以考虑在绘制这一阶段期间开启一个单独的线程,让子线程去做这些工作,而不是放在前面的Tick阶段。

程序的入口

我们知道所有的C++程序都是从main函数开始的,UE4也不例外,所以只要找到入口,你就可以一步一步跟着上面那张图,调试跟踪到底UE4是怎样执行的。可以在引擎的Launch模块内看到这部分代码。

windows的入口是WinMain,内部会调用到GuardedMain

mac的入口是INT32_MAIN_INT32_ARGC_TCHAR_ARGV,其实展开就是main,内部会调用到objc的NSApp,这个就是系统提供的App对象,具体应用能实现的就只有后面的Delegate,所以UE4实现了UE4AppDelegate

如果有稍微了解过苹果开发,都会知道真正做初始化都会写在applicationDidFinishLaunching这里

这里根据宏决定是否开一个新线程

最后也调用到GuardMain

iOS就是main,内部会调用到objc的UIApplicationMain,这是iOS提供的app入口,具体业务实现的是后面的IOSAppDelegate,跟Mac大同小异。

安卓会在Java的Activity调用回来,具体流程类似不单独截图了。

因此可以看到,UE4的游戏线程基本上是单独启动了一个子线程作为GameThread,并不是App的主线程,所以GameThread卡死或者耗时非常久,也不会导致应用无响应。当你在手机上接任何第三方SDK,如各种安卓或iOS的SDK一般都会从app的主线程调用回调,这时如果直接调用UE4,做一些操作就很有可能发生崩溃或不可预知的问题,所以要通过TaskGraph或其他方式抛到GameThread上再执行对应逻辑。这里是要特别注意的一点。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 初始化阶段
  • Tick阶段
  • 绘制阶段
  • 程序的入口
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档