找回密码
 立即注册
查看: 196|回复: 5

Unreal Engine 的启动流程

[复制链接]
发表于 2023-3-13 09:57 | 显示全部楼层 |阅读模式
如果你是Unity开发者,可以先跳转看该篇文章获得快速入门秘籍。普通开发者可以直接从本篇开始,不影响阅读体验。写给Unity开发者的Unreal Engine开发指南 (扫盲) 本文章对应的引擎版本为5.1。

做知识的粘合剂。

Unreal 的基础学习有很多种方式,文档,代码,Demo等都是比较好的入门形式。很多博主都会先从蓝图入手,完成一个小的Demo或者功能。也有直接从引擎的模块实现开始的,比如像大钊这样,系统性的讲解Unreal的重要架构 InsideUE5 。 我这个系列也会从一些代码和设计入手,但流程和视角会不一样,我们先从最底层的、Unreal 引擎的启动开始聊起吧。

应用程序的启动入口



所有的应用程序都有一个启动函数。写过C语言的都知道叫Main。它在Window上是 WinMain winMain 函数 (winbase.h)  - Win32 apps,在安卓平台上是android_main NativeActivity开发APP原理_android_main_大雄_RE的博客-CSDN博客 在IOS上是main函数 iOS的App启动详细过程,看这篇就够了 。这里可以简单总结一下:


  • Windows系统,当你双击一个exe启动的时候,最后会由内核调起一个应用程序的WinMain函数。这个函数在Unreal Engine的实现如下



  • 安卓系统下,当你启动一个APP的时候,会走到一个ANativeActivity类中。这个类定义了所有APP的流程和回调函数,我们只需要关注 android_app_create 这个启动创建函数。它会创建一个线程来调用 android_app_entry 函数。而android_app_entry中就调用了android_main。



这个 android_main 函数是一个C++的函数,需要自己通过外部来实现,而在Unreal 引擎里,它的实现如下:



  • IOS系统的实现就更简单了。由于XCode可以直接编译C++(也可以混编OC代码),而IOS的入口函数就是main,所以直接在LaunchIOS.cpp文件中定义main函数即可。


main 函数的最后一行是用OC语言调起了IOS的UI初始化逻辑 UIApplicationMain。它会创建一个代理来完成APP的主循环。



这些代理函数定义的地方是 IOSAppDelegate.h (Engine\Source\Runtime\ApplicationCore\Public\IOS),实现的地方是 LaunchIOS.cpp(Engine\Source\Runtime\Launch\Private\IOS)






  • 其他系统,MAC上的的也很简单,通过主函数进入。



其他Linux 和 Unix 其实并没有完整实现,不展开说了。



总结一下就是,各个平台在启动应用程序的时候,会由内核调用应用程序的main函数,而这它们都会在Engine 里进行实现,自然而然,引擎的整个逻辑就可控了。

引擎初始化流程



如上图所示,所有不同平台的main函数只是一个系统调用程序的入口。这个入口会拉起引擎,并进入到引擎的循环中,就像普通的应用程序一样。
所以一个对于一个可正常工作的引擎而言,它需要实现三个基本的流程:初始化,循环,和结束。对应的Unreal的引擎流程就是:

  • Init
  • Tick
  • Exit
Unreal 启动流程的定义 是在 Launch.cpp中。引擎的启动流程以各个平台的main函数作为入口,最终会进入到GuardedMain函数中。
该函数会优先解析随程序启动一起传进来的命令行参数。比如判定是否需要等待调试器准备就绪。




其他更多的参数解析会下发到引擎层面去解析,这个步骤对应的是 EnginePreInit 。



然后会根据当前运行环境是Editor还是Game来决定初始化编辑器引擎还是直接初始化游戏引擎。



之后进到Unreal的主循环中。



循环结束之后进行退出清理工作。




所以一个简易的流程可以表述为如下:

  • GuardedMain

    • EnginePreInit
    • EditorInit || EngineInit
    • EngineTick
    • EditorExit

引擎初始化实现


GuardedMain 函数定义了引擎的启动流程和主循环。但这些函数只是一个壳,核心的实现都是FEngineLoop 这个类来实现的。










  • PreInit 的实现。主要包含了2个部分,一部分是不需要依赖其他组件,并且是必须要先初始化(PreStartup)的组件,然后是其他需要依赖其他组件的组件初始化(PostStartup)。




  PreInitPreStartupScreen 是一个非常长的函数,大约1500行。里面对命令行提供的关键字和项目宏做出各种辅助模块的初始化工作。比如:设置字体编码格式,是否需要等待调试器,是否需要手动设置游戏名称,是否初始化LLM内存分析器UE4 Low Level Memory Tracker 使用,是否需要创建控制台输出,是否需要创建log线程,内存分配的分析器,GPU分析器,自检自身是GIsClient、GIsServer、还是GIsEditor,指定随机种子的生成形式,初始化平台相关的文件系统和路径,是否直接启动指定项目的加载,初始化Shader文件路径,线程池管理,平台相关的初始化,引擎配置相关的初始化,物理引擎初始化,Slate初始化,RHIInit初始化等等。其中跟开发关联比较大的模块初始化是 LoadCoreModules,LoadPreInitModules。

  • LoadCoreModules。很简单,只是初始化了Unreal Engine最基础最核心的组件:CoreUObject。它包含了含虚幻引擎的对象系统(UObject)和类型系统(UClass)。

    • UObject。它是引擎所有对象的基类,提供了对象的反射、序列化、GC等功能。
    • UClass。它是UObject对象的反射对象,记录了大量UObject的对象数据;这些记录的对象数据帮助UObject实现反射、序列化、GC等功能。

  • LoadPreInitModules。初始化引擎本身所需要的核心模块。比如:Engine,Renderer,AnimGraphRuntime,SlateRHIRenderer,Landscape,RenderCore,TextureCompressor,Virtualization,AudioEditor,AnimationModifiers等。






2. Init的实现。preinit阶段创建的其实是引擎公共的组件部分,接下来在Init阶段就会根据当前的运行环境来创建Editor或者是Game特有的部分了。



然后初始化进行引擎加载的屏幕显示并开始计算百分比,初始化引擎的定时器逻辑,GameEngine || EditorEngine自身的Init动作,然后加载所有标记为PostEngineInit阶段的引擎组件和插件组件。




执行引擎的开始逻辑:GEngine->Start();
最后就是根据情况初始化AutomationWorker,AutomationController,ProfilerClient,SequenceRecorder,SequenceRecorderSections等组件。有兴趣可以查阅这些组件,不展开介绍。

3. Tick的实现。Tick就像是一个心跳,它驱动引擎按帧执行各种各样的任务。比如最开始就让LLM更新每帧的统计数据。




  • 驱动心跳线程执行自身的帧开始逻辑:FGameThreadHitchHeartBeat::Get().FrameStart();
  • 检测热修复逻辑:FPlatformMisc::TickHotfixables();
  • 驱动渲染的tick更新 : TickRenderingTickables();
  • 如果有开启Profiler功能,执行帧数据:ActiveProfiler->FrameSync();
  • CsvProfiler的数据获取:FCsvProfiler::Get()->IsCapturing();CSV分析器
  • 核心代理事件,分发帧开始事件:FCoreDelegates::OnBeginFrame.Broadcast();
  • 更新场景信息



  • 开始渲染线程的工作 BeginFrameRenderThread 并调用Scene的StartFrame();
  • 各种调试和分析工具的数据统计
  • 处理消息循环 FPlatformApplicationMisc::PumpMessages(true);
  • 处理输入 FCoreDelegates::OnSamplingInput.Broadcast()
  • 进入GEngine的Tick逻辑(在此之前是GEngineLoop.Tick(),也就是引擎本体的tick,现在是进入到Editor或者Game的tick)。
……
tick作为引擎的核心驱动逻辑,负责循环的模块太多了:在GEngineLoop层会处理各种Profiler的数据统计,渲染线程的驱动逻辑,消息输入,Slate等,而后会进入到GEngine层的tick中,处理网络,无缝世界,导航,物理,相机,风场,特效粒子,GC,渲染,后处理,UI,视频,线程管理等等,详细代码不列举,可以参考文章后面的引用或者自行查看代码。

4. Exit()的实现。
做各种收尾工作,比如是否结束动画,是否是服务器需要进行关闭和存储,释放音频设备的占用,销毁运行期间创建的线程,正确处理缓存,保存运行时更改的引擎配置,unload各种组件等。

全部的初始化流程可以参考以下文章:
UE4引擎主流程框架 - 可可西 - 博客园
UE4的执行流程和CPU优化
剖析虚幻渲染体系(01)- 综述和基础 - 0向往0 - 博客园

额外的初始化工作



上面其实已经介绍完UE引擎的基本启动流程,但相对于Windows平台而言,Android 和 IOS 在权限管控上更加严格,因此需要多做一些额外的初始化工作来辅助完成整个环境的初始化。


  • Android
   首先UE只是一个开发引擎,它没有办法直接获取和管理APP的状态。所以它需要定义一系列的APP代理函数,通过注册为对应的APP事件,来进行处理。


其次,对于APP系统来说,它提供的是Java层面的系统接口,而UE使用C++进行开发,这就需要定制常用的Java接口来完成双向的逻辑调用(UE调安卓,安卓调UE)。
安卓调UE通过上面的APPEvent可以完成,而UE调安卓就需要通过反射来实现。比如保持屏幕常亮,就可以通过反射拿到安卓Native层写的函数来实现。


这里拿到了Java的函数之后,还不能直接使用,需要在它外层再包装一个C++的实现


最后,由正常的业务逻辑调用这个CPP的函数就可以完成安卓Native函数的调用。


除了上述的交互之外,Android 在初始化的时候还有一些额外的内容,比如通过读取安卓指定目录下的UECommandLine.txt文件来完成引擎的命令行初始化,音频的初始化和管理等等。

  • IOS
IOS的情况和Android大体类似。也需要解决后台音频的问题,以及APP层和引擎层之间的通信关系。不过IOS比Android要简单很多,因为OC和CPP是可以混写的,甚至直接在CPP里写OC代码。剩下的就是解决APP在流程上调用的问题,这在第一节,程序启动入口的部分已经介绍过了。
APP启动完成一系列常规初始化之后就会调用应用层的接口 didFinishLaunchingWithOptions。这个函数在 IOSAppDelegate.cpp中实现。函数很长,主要区分了IOS,TVOS等平台做一些特性的初始化。




关于引擎的启动流程就先聊到这里,下一篇聊一下GamePlay的初始化过程和一些新手需要掌握的关键概念。
已更新: 放牛的星星:Unreal Engine的Gameplay框架和重点

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-3-13 09:57 | 显示全部楼层
催更[害羞]
发表于 2023-3-13 10:06 | 显示全部楼层
可以,可以,必须点赞!
发表于 2023-3-13 10:12 | 显示全部楼层
浩哥,看的不过瘾,催更催更
发表于 2023-3-13 10:18 | 显示全部楼层
写的真好,催更催更!!
发表于 2023-3-13 10:22 | 显示全部楼层
牛哇大佬
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-3 07:28 , Processed in 0.129274 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表