本文章只是我个人在学习虚幻引擎过程中的一些理解,不一定正确,若有说的不对的地方,欢迎指正。
在《UE4 运行流程源码浅析(1)》中我们简略讲了一下虚幻引擎运行流程,相信大家已经对整体框架有了初步的了解。本篇我们来讲一下之前没讲的,在虚幻引擎预初始化(GEngineLoop.PreInit)、初始化(GEngineLoop.Init)、循环(GEngineLoop.Tick)等阶段中调用的一些比较重要的函数。
一.GEngineLoop.PreInit:
还记得吗引擎循环类(GEngineLoop)的预初始化(PreInit)主要调用两个函数:预初始化的预启动函数(PreInitPreStartupScreen)和预初始化的后启动函数(PreInitPostStartupScreen)。其中PreInitPreStartupScreen调用了启动虚幻应用程序函数(AppInit),PreInitPostStartupScreen调用了用来加载需要的核心模块的函数(LoadStartupCoreModules)。
1.AppInit:
AppInit函数启动应用程序,内部做一些和平台有关的初始化操作,包括初始化时间、文件系统、日志系统、相关配置等。下面是简略源码,解释都在注释中:
bool FEngineLoop::AppInit()
{
//……
//特定平台的预初始化
FPlatformMisc::PlatformPreInit();
//平台应用的预初始化
FPlatformApplicationMisc::PreInit();
//记录启动时间
GSystemStartTime = FDateTime::Now().ToString();
//切换到执行文件目录
FPlatformProcess::SetCurrentWorkingDirectoryToBaseDir();
//在分析命令行完成后初始化文件管理器(file manager)
IFileManager::Get().ProcessCommandLineOptions();
//此处省略了一些命令行处理……
//初始化日志目录树
IFileManager::Get().MakeDirectory(*FPaths::ProjectLogDir(), true);
//注释翻译:获取最小文件名,此时我们有足够的信息将其保存日志文件夹名字
FCString::Strcpy(MiniDumpFilenameW, *IFileManager::Get().ConvertToAbsolutePathForExternalAppForWrite(*FString::Printf(TEXT("%sunreal-v%i-%s.dmp"), *FPaths::ProjectLogDir(), FEngineVersion::Current().GetChangelist(), *FDateTime::Now().ToString())));
//在磁盘中初始化日志系统
FPlatformOutputDevices::SetupOutputDevices();
//……
//初始化配置系统(config system)
FConfigCacheIni::InitializeConfigSystem();
//……
//加载哪些加载阶段设置为尽早(EarliestPossible)的插件和项目
ProjectManager.LoadModulesForProject(ELoadingPhase::EarliestPossible)
PluginManager.LoadModulesForEnabledPlugins(ELoadingPhase::EarliestPossible)
//此时配置已经初始化完成,开始设置堆栈运行选项
FPlatformStackWalk::Init();
//……
//根据路径检测某些项目和插件是否更新,并尝试编译它们
ProjectManager.CheckModuleCompatibility(IncompatibleFiles);
PluginManager.CheckModuleCompatibility(IncompatibleFiles, IncompatibleEngineFiles);
//……
//测试若存在需要的模块,他通过IDE加载它
FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *CompileForbidden, TEXT("Missing Modules"));
//继续执行之前询问是否编译
FPlatformMisc::MessageBoxExt(EAppMsgType::YesNo, *CompilePrompt, *FString::Printf(TEXT("Missing %s Modules"), FApp::GetProjectName())
//……
//处理命令行和配置信息
FLogSuppressionInterface::Get().ProcessConfigAndCommandLine();
//此时配置系统(config system)已经完成初始化,开始初始化音视频系统(audio system)
[[IOSAppDelegate GetDelegate]InitializeAudioSession] ;
//加载那些加载阶段设置为预加载(pre-init)的插件和项目
ProjectManager.LoadModulesForProject(ELoadingPhase::PostConfigInit)
PluginManager.LoadModulesForEnabledPlugins(ELoadingPhase::PostConfigInit)
//……
//初始化头显
PreInitHMDDevice();
//……
//根据命令行参数启动日志打印
GLogConsole->Show(true);
//打印所有初始化日志
FApp::PrintStartupLogMessages();
//……
//初始化色彩列表
GColorList.CreateColorMap();
//触发代理函数,通知初始化一些其他系统
FCoreDelegates::OnInit.Broadcast();
//……
}
2.LoadStartupCoreModules:
LoadStartupCoreModules函数内部实现基本都是在加载模块,包括运行时模块(Runtime)、平台需要的模块、编辑器相关的模块、UMG/Slate相关模块、音效相关模块、AI相关模块等。可以发现许多模块是我们听说过的。简略源码如下:
bool FEngineLoop::LoadStartupCoreModules()
{
//……
//加载所有运行时(Runtime)模块
FModuleManager::Get().LoadModule(TEXT("Core"));
FModuleManager::Get().LoadModule(TEXT("Networking"));
//加载一些平台需要的模块
FPlatformApplicationMisc::LoadStartupModules();
//加载Messaging模块
FModuleManager::LoadModuleChecked<IMessagingModule>("Messaging");
//加载MRMesh,根据注释来看这个模块用来支持场景重建(Scene Reconstruction)
FModuleManager::LoadModuleChecked<IMRMeshModule>("MRMesh");
//加载UnrealEd、EditorStyle、LandscapeEditorUtilities等模块
FModuleManager::Get().LoadModuleChecked("UnrealEd");
FModuleManager::LoadModuleChecked<IEditorStyleModule>("EditorStyle");
FModuleManager::Get().LoadModuleChecked("LandscapeEditorUtilities");
//加载SlateCore、Slate、SlateReflector模块,这些都是UI类模块,其中SlateReflector用来初始化WidgetSnapshotService
FModuleManager::Get().LoadModule("SlateCore");
FModuleManager::Get().LoadModule("Slate");
FModuleManager::Get().LoadModule("SlateReflector");
//加载UMG模块,该模块在运行时(runtime)或将游戏项目中的资源进行处理和打包时(cooking)使用;
//加载UMG/UMGEditor模块时为蓝图(blueprints)的编译做准备
FModuleManager::Get().LoadModule("UMG");
/***************************************************************************************
* 加载所有Development模块
***************************************************************************************/
//加载MessageLog和FunctionalTesting模块
FModuleManager::Get().LoadModule("MessageLog");
FModuleManager::Get().LoadModule("FunctionalTesting");
//加载行为树编辑器(BehaviorTreeEditor)模块,打包行为树资源需要该模块
FModuleManager::Get().LoadModule(TEXT("BehaviorTreeEditor"));
//加载GameplayTasksEditor模块,能力任务(Ability tasks)就是基于GameplayTasks的
FModuleManager::Get().LoadModule(TEXT("GameplayTasksEditor"));
//加载音效模块并注册对应资源
IAudioEditorModule * AudioEditorModule = &FModuleManager::LoadModuleChecked<IAudioEditorModule>("AudioEditor");
AudioEditorModule->RegisterAssetActions();
//加载StringTableEditor模块并注册对应资源
FModuleManager::Get().LoadModule("StringTableEditor");
//加载VREditor模块
FModuleManager::Get().LoadModule(TEXT("VREditor"));
//加载EnvironmentQueryEditor(EQS)模块,在静态资源初始化和资源打包(cooking)时使用
FModuleManager::Get().LoadModule(TEXT("EnvironmentQueryEditor"));
//加载IntroTutorials和Blutility模块
FModuleManager::Get().LoadModule(TEXT("IntroTutorials"));
FModuleManager::Get().LoadModule(TEXT("Blutility"));
/***************************************************************************************
* 加载运行时客户端需要的模块
***************************************************************************************/
//加载Overlay模块
FModuleManager::Get().LoadModule(TEXT("Overlay"));
//加载MediaAssets模块
FModuleManager::Get().LoadModule(TEXT("MediaAssets"));
//加载ClothingSystemRuntimeNv模块
FModuleManager::Get().LoadModule(TEXT("ClothingSystemRuntimeNv"));
//加载ClothingSystemEditor模块
FModuleManager::Get().LoadModule(TEXT("ClothingSystemEditor"));
//加载PacketHandler和NetworkReplayStreaming模块
FModuleManager::Get().LoadModule(TEXT("PacketHandler"));
FModuleManager::Get().LoadModule(TEXT("NetworkReplayStreaming"));
//……
}
注:虚幻引擎加载模块几乎都是使用FModuleManager
二.GEngineLoop.Init:
这里先提一下UEngine,该类表示引擎实例类型,它有两个子类,分别是编辑器模式下的UEditorEngine和非编辑器模式下的UGameEngine,在这一小节中我们主要聚焦在非编辑器模式下的函数实现。
在非编辑器模式下执行引擎循环类的初始化接口的过程中,会调用引擎实例的初始化接口(UGameEngine::Init)和开始接口(UGameEngine::Start),其中Start会初始化GameInstance,加载地图和初始化世界和世界中的Actor,而UGameEngine::Init会调用父类的实现(UEngine::Init)。
注:由于Start内容较多且比较重要,本章主要讲UGameEngine和UEngine的Init,至于Start会留待下一篇讲
1.UGameEngine::Init:
UGameEngine::Init代码量不多,缩减之后加上注释也就四十行左右。该函数在一开始就调用了父类实现(UEngine::Init),之后加载用户的游戏设置、初始化GameInstance、创建世界管理器(FWorldContext)、初始化UGameViewportClient和ULocalPlayer。
这里说一下UGameViewportClient和ULocalPlayer的关系。可以把ULocalPlayer看作屏幕前的用户,把UGameViewportClient看作屏幕,本质是处理音效、渲染和输入的接口类。
注:GameInstance在UE4.4开始出现,是从UGameEngine中分离出来的 UGameEngine::Init简略源码如下:
void UGameEngine::Init(IEngineLoop* InEngineLoop)
{
//……
//调用基类函数
UEngine::Init(InEngineLoop);
//……
//加载和引用用户的游戏设置
GetGameUserSettings()->LoadSettings();
GetGameUserSettings()->ApplyNonResolutionSettings();
//创建游戏实例(GameInstance)并初始化
GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
GameInstance->InitializeStandalone();
//创建WorldContext,这部分在UE4.25中是注掉的
//WorldContext用来管理世界(World),游戏模式下WorldContext只有唯一个,编辑器模式有两个WorldContext,一个管理编辑器的World,一个PIE(Play In Editor)的World
FWorldContext& InitialWorldContext = CreateNewWorldContext(EWorldType::Game);
//……
//初始化客户端游戏视口、初始化并赋给WorldContext
ViewportClient = NewObject<UGameViewportClient>(this, GameViewportClientClass);
ViewportClient->Init(*GameInstance->GetWorldContext(), GameInstance);
GameViewport = ViewportClient;
GameInstance->GetWorldContext()->GameViewport = ViewportClient;
//下面这部分代码应该在创建UI之前执行
{
//把创建的客户端视口绑定到新视口
CreateGameViewport(ViewportClient);
//初始化本地玩家(LocalPlayer)
ViewportClient->SetupInitialLocalPlayer(Error)
//触发视口创建代理函数
UGameViewportClient::OnViewportCreated().Broadcast();
}
//设置引擎实例已初始化标志
bIsInitialized = true;
}
2.UEngine::Init:
之前我们说子类的初始化接口调用了父类实现,现在我们来说说它的源码。UEngine::Init代码量比子类更多,缩减之后也有大约一百行。该函数做一些通用的初始化,包括初始化子系统、默认地图处理、Slate相关处理、加载一些引擎类、读取配置、创建世界(UWorld)等。老规矩,上菜:
void UEngine::Init(IEngineLoop* InEngineLoop)
{
//……
EngineLoop = InEngineLoop;
//收集并初始化子系统(Subsystems)
EngineSubsystemCollection.Initialize(this);
//通过检测命令行,确定用户是否设置默认地图(default map)
if (FParse::Value(FCommandLine::Get(), TEXT("DEFAULTMAP="), MapName, UE_ARRAY_COUNT(MapName)))
{
//……
UGameMapsSettings::SetGameDefaultMap(MapString);
}
//……
//添加垃圾回收(GC)代理函数
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().AddStatic(UEngine::PreGarbageCollect);
//初始化头显设备
InitializeHMDDevice();
//初始化眼睛追踪设备
InitializeEyeTrackingDevice();
//……
//当Slate被使用时(不在专属服务器上),初始化Slate的渲染(此时在RHI已经初始)
FSlateApplication& CurrentSlateApp = FSlateApplication::Get();
CurrentSlateApp.InitializeSound(TSharedRef<FSlateSoundDevice>(new FSlateSoundDevice()));
//……
//加载引擎类
LoadObject<UClass>(UEngine::StaticClass()->GetOuter(), *UEngine::StaticClass()->GetName(), NULL, LOAD_Quiet | LOAD_NoWarn, NULL);
//读取引擎配置文件(Engine.ini),获取默认材质(DefaultMaterial)和etc
LoadConfig();
//……
//该函数加载所有引擎引用的对象(包括材质、Texture、UConsole、UGameViewportClient等)
InitializeObjectReferences();
//……
//为编辑器创建一个WorldContext并创建一个新的World
FWorldContext& InitialWorldContext = CreateNewWorldContext(EWorldType::Editor);
InitialWorldContext.SetCurrentWorld(UWorld::CreateWorld(EWorldType::Editor, true));
GWorld = InitialWorldContext.World();
//初始化音效设别管理器
InitializeAudioDeviceManager();
//网络校验,确保网络能在该项目版本正常使用
const UGeneralProjectSettings& ProjectSettings = *GetDefault<UGeneralProjectSettings>();
FNetworkVersion::SetProjectVersion(*ProjectSettings.ProjectVersion);
//……
//添加travel和network失败时的代理函数,可以覆盖该代理以添加自己的处理代码
OnTravelFailure().AddUObject(this, &UEngine::HandleTravelFailure);
OnNetworkFailure().AddUObject(this, &UEngine::HandleNetworkFailure);
OnNetworkLagStateChanged().AddUObject(this, &UEngine::HandleNetworkLagStateChanged);
//添加在线子系统代理函数
FOnlineExternalUIChanged OnExternalUIChangeDelegate;
OnExternalUIChangeDelegate.BindUObject(this, &UEngine::OnExternalUIChange);
UOnlineEngineInterface::Get()->BindToExternalUIOpening(OnExternalUIChangeDelegate);
//初始化内存可视化系统的数据
GetBufferVisualizationData().Initialize();
//……
//初始化引擎分析器(engine analytics provider)
FEngineAnalytics::Initialize();
//加载引擎运行时模块(engine runtime modules)
{
FModuleManager::Get().LoadModule("ImageWriteQueue");
FModuleManager::Get().LoadModuleChecked("StreamingPauseRendering");
FModuleManager::Get().LoadModuleChecked("MovieScene");
FModuleManager::Get().LoadModuleChecked("MovieSceneTracks");
FModuleManager::Get().LoadModule("LevelSequence");
}
//……
//触发资源管理器加载工作的完成事件
AssetManager->FinishInitialLoading();
//……
//这里省略添加引擎状态的代码……
//……
//记录头显设别的分析
RecordHMDAnalytics();
//……
}
三.GEngineLoop.Tick:
本小节仍然聚焦在非编辑器模式下,引擎循环类的Tick接口每帧调用引擎实例的Tick(UGameEngine::Tick)。该接口每帧更新许多模块:日志、刷新窗口、子系统,渲染等,还触发一些模块的Tick,简略源码如下:
void UGameEngine::Tick(float DeltaSeconds, bool bIdleMode)
{
//……
//更新Log
GLog->Flush();
//清除关闭的窗口
CleanupGameViewport();
//根据帧率设置是否降低画面细节
GameViewport->SetDropDetail(DeltaSeconds);
//调用一些子系统(subsystems)的Tick
FEngineAnalytics::Tick(DeltaSeconds);
FStudioAnalytics::Tick(DeltaSeconds);
//……
//根据需要触发无缝地图、客户端、服务端的Tick
TickWorldTravel(Context, DeltaSeconds);
//调用World的Tick
Context.World()->Tick(LEVELTICK_All, DeltaSeconds);
//更新天光(SkyLight)
USkyLightComponent::UpdateSkyCaptureContents(Context.World());
//根据需要生成玩家,提供一个刷新玩家的机会
GamePlayer->Exec(GamePlayer->GetWorld(), *(FString("CAUSEEVENT ") + InitialExec), *GLog);
//根据需要阻塞住World的异步加载 Block on async loading if requested.
BlockTillLevelStreamingCompleted(Context.World());
//更新关卡流
Context.World()->UpdateLevelStreaming();
//……
//触发游戏对象(GameObject)的Tick
FTickableGameObject::TickObjects(nullptr, LEVELTICK_All, false, DeltaSeconds);
//……
//触发media模块的Tick
IMediaModule* MediaModule = FModuleManager::LoadModulePtr<IMediaModule>(MediaModuleName);
MediaModule->TickPostEngine();
//触发游戏视口(GameViewport)的Tick
GameViewport->Tick(DeltaSeconds);
//显示这一帧游戏窗口中的内容
GameViewportWindow.Pin()->ShowWindow();
FSlateApplication::Get().RegisterGameViewport(GameViewportWidget.ToSharedRef());
//渲染所有需要渲染的东西 Render everything.
RedrawViewports();
//触发需要在所有渲染完成后做的任务
GetRendererModule().PostRenderAllViewports();
//触资源流Tick,更新信息
IStreamingManager::Get().Tick(DeltaSeconds);
//触发音效Tick,更新音效
GameAudioDeviceManager->UpdateActiveAudioDevices(bIsAnyNonPreviewWorldUnpaused);
//更新渲染线程相关命令 rendering thread commands
//触发GRenderingRealtimeClock、GRenderTargetPool、FRDGBuilder的Tick
GRenderingRealtimeClock.Tick(DeltaSeconds);
GRenderTargetPool.TickPoolElements();
FRDGBuilder::TickPoolElements();
//广播EngineTick完成事件
BroadcastPostEditorTick(DeltaSeconds);
//触资产注册(FAssetRegistryModule)模块Tick, 更新资产注册信息
FAssetRegistryModule::TickAssetRegistry(DeltaSeconds);
}
四.小结:
本章进一步讲了引擎的预初始化、初始化和循环几个阶段。但初始化还有个Start没讲,这在下篇再来讲,话不多所,下篇见。