• UE5的引擎初始化流程


    UE5的引擎初始化流程

    首先跟着UE的官方文档[1]获取到UE的源代码,然后在参考GitHub上repo的readme,将UE引擎从源码build出来。以Windows平台为例,先找到引擎的入口函数:

    int32 WINAPI WinMain(_In_ HINSTANCE hInInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ char* pCmdLine, _In_ int32 nCmdShow)
    {
    	int32 Result = LaunchWindowsStartup(hInInstance, hPrevInstance, pCmdLine, nCmdShow, nullptr);
    	LaunchWindowsShutdown();
    	return Result;
    }
    

    可以看到入口函数很简单,就分为startup和shutdown两部分,那么想必引擎的主循环是在这个startup里。startup函数主要负责初始化各种调用环境,解析命令行参数,调用真正的main函数:

    LAUNCH_API int32 LaunchWindowsStartup( HINSTANCE hInInstance, HINSTANCE hPrevInstance, char*, int32 nCmdShow, const TCHAR* CmdLine )
    {
    	// Setup common Windows settings
    	SetupWindowsEnvironment();
    
    	int32 ErrorLevel			= 0;
    	hInstance				= hInInstance;
    
    	if (!CmdLine)
    	{
    		CmdLine = ::GetCommandLineW();
    
    		// Attempt to process the command-line arguments using the standard Windows implementation
    		// (This ensures behavior parity with other platforms where argc and argv are used.)
    		if ( ProcessCommandLine() )
    		{
    			CmdLine = *GSavedCommandLine;
    		}
    	}
    
        ErrorLevel = GuardedMain( CmdLine );
    
    	return ErrorLevel;
    }
    

    简化后的main函数如下:

    int32 GuardedMain( const TCHAR* CmdLine )
    {
        // Super early init code. DO NOT MOVE THIS ANYWHERE ELSE!
    	FCoreDelegates::GetPreMainInitDelegate().Broadcast();
    
    	// make sure GEngineLoop::Exit() is always called.
    	struct EngineLoopCleanupGuard 
    	{ 
    		~EngineLoopCleanupGuard()
    		{
    			// Don't shut down the engine on scope exit when we are running embedded
    			// because the outer application will take care of that.
    			if (!GUELibraryOverrideSettings.bIsEmbedded)
    			{
    				EngineExit();
    			}
    		}
    	} CleanupGuard;
    
    	int32 ErrorLevel = EnginePreInit( CmdLine );
    
    	// exit if PreInit failed.
    	if ( ErrorLevel != 0 || IsEngineExitRequested() )
    	{
    		return ErrorLevel;
    	}
    
    #if WITH_EDITOR
        if (GIsEditor)
        {
            ErrorLevel = EditorInit(GEngineLoop);
        }
        else
    #endif
        {
            ErrorLevel = EngineInit();
        }
    
    	// Don't tick if we're running an embedded engine - we rely on the outer
    	// application ticking us instead.
    	if (!GUELibraryOverrideSettings.bIsEmbedded)
    	{
    		while( !IsEngineExitRequested() )
    		{
    			EngineTick();
    		}
    	}
    
    #if WITH_EDITOR
    	if( GIsEditor )
    	{
    		EditorExit();
    	}
    #endif
    	return ErrorLevel;
    }
    

    梳理一下大体的逻辑,在一切初始化逻辑开始之前,函数会先把这个消息广播出去,如果有更早需要执行的逻辑,可以注册到这个委托上。另外,为了保证引擎的退出逻辑一定会被调用到,UE在这里定义了一个局部的struct,这个struct定义了一个析构函数,析构函数的实现就是调用引擎的退出逻辑。有了这个局部struct,再定义一个局部的对象,这个对象不是动态分配的,那么C++会保证它离开作用域时就会销毁它,从而调用到引擎的退出逻辑。这也是C++ RAII的常见做法了。

    再往下,可以发现引擎的初始化分为PreInit和Init两个步骤。Init会根据是否是Editor环境,调用不同的逻辑。在初始化完成后,就进入到了引擎的主循环。

    顺便一提,PreInit,Init,Tick,Exit都是简单的包装函数,它们的实现就是调用全局变量GEngineLoop的相应接口。

    int32 EngineInit()
    {
    	int32 ErrorLevel = GEngineLoop.Init();
    
    	return( ErrorLevel );
    }
    
    LAUNCH_API void EngineTick( void )
    {
    	GEngineLoop.Tick();
    }
    
    LAUNCH_API void EngineExit( void )
    {
    	// Make sure this is set
    	RequestEngineExit(TEXT("EngineExit() was called"));
    
    	GEngineLoop.Exit();
    }
    

    PreInit初始化了各种底层模块[4],非常繁多。

    UE5的引擎初始化流程1

    除此之外,还完成了许多模块的加载:

    UE5的引擎初始化流程2

    Init阶段,UE会根据是否为editor环境调用EditorInit或者EngineInit。而EditorInit本身的实现里就是包含EngineInit的。Init简化后的版本如下。

    int32 FEngineLoop::Init()
    {
    	UClass* EngineClass = nullptr;
    	if( !GIsEditor )
    	{
    		// We're the game.
    		FString GameEngineClassName;
    		GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("GameEngine"), GameEngineClassName, GEngineIni);
    		EngineClass = StaticLoadClass( UGameEngine::StaticClass(), nullptr, *GameEngineClassName);
    		GEngine = NewObject(GetTransientPackage(), EngineClass);
    	}
    	else
    	{
    #if WITH_EDITOR
    		// We're UnrealEd.
    		FString UnrealEdEngineClassName;
    		GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("UnrealEdEngine"), UnrealEdEngineClassName, GEngineIni);
    		EngineClass = StaticLoadClass(UUnrealEdEngine::StaticClass(), nullptr, *UnrealEdEngineClassName);
    		GEngine = GEditor = GUnrealEd = NewObject(GetTransientPackage(), EngineClass);
    #endif
    	}
    
        GEngine->ParseCommandline();
        GEngine->Init(this);
        FCoreDelegates::OnPostEngineInit.Broadcast();
    
        if (!IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostEngineInit) || !IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostEngineInit))
        {
            RequestEngineExit(TEXT("One or more modules failed PostEngineInit"));
            return 1;
        }
    
    	// Call module loading phases completion callbacks
    	SetEngineStartupModuleLoadingComplete();
        GEngine->Start();
    
    	GIsRunning = true;
    
        FCoreDelegates::OnFEngineLoopInitComplete.Broadcast();
        return 0;
    }
    

    Init阶段主要做了如下的事情[3]:

    1. 根据当前是否为编辑器环境,检查引擎配置文件以确定应该使用哪个GameEngine类;
    2. 然后创建该类的一个实例并将其作为全局UEngine实例。也就是GEngine
    3. 创建之后,进行初始化,完成时会触发一个全局委托;
    4. 加载已配置为延迟加载的任何项目或插件模块;
    5. 加载完成后,再触发一个完成的回调;
    6. 正式启动引擎;
    7. 发送引擎启动的委托。

    自此,引擎的初始化流程就宣告结束了,后面就进入到了tick的阶段。

    Reference

    [1] 下载虚幻引擎源代码

    [2] 从零开始手敲次世代游戏引擎(五)

    [3] UE5 – 引擎运行流程(从main到BeginPlay)

    [4] The Unreal Engine Game Framework: From int main() to BeginPlay

  • 相关阅读:
    狂神说Go语言学习笔记(三)
    小米集团收入增长失速已久:穿越寒冬,雷军的路走对了吗?
    人工智能——图像处理和Python深度学习的全教程(建议收藏)
    MySQL进阶—从零到入土
    文本编辑器vi--常用命令查阅版(记得收藏)
    《Java并发编程之美》
    RuntimeError: Error compiling objects for extension手把手带你解决(超详细)
    【Verilog语法】比较不同计数器的运算方式,其中有一个数是延迟打一拍的效果,目的是使得两个计数器的结果相同。
    Python函数式编程(三)操作符函数(operator)
    SAP系统里的统驭科目
  • 原文地址:https://blog.csdn.net/weixin_45776473/article/details/140027123