[原创]UE—GamePlay启动流程

趁着国庆假期把 GamePlay 框架又梳理了一番, 遂尝试将梳理的脉络用文字表达出来, 借此加深理解, 倘若能为他人解惑那更是善莫大焉; 当前引擎版本: UnrealEngine4.25.3

游戏引擎的GamePlay框架伪代码基本如下:

int main()
{
    init();
    while(!exit_requested)
    {
        update(); //input,gamelogic,render...
    }
    exit();
}
//CodeSnippet.1

UE的GamePlay框架也是遵循这个伪代码展开; 我们打开UE的引擎源码, 从 GuardedMain() 函数开始 (为了便于理解流程,本文所有代码段都对源码进行一定的删改 )

//Launch.cpp 
int32 GuardedMain(const TCHAR* CmdLine)
{
    struct EngineLoopCleanupGuard 
    { 
       ~EngineLoopCleanupGuard() 
       {
          GEngineLoop.Exit(); 
       }
    } CleanupGuard;
 
    //run early Initialization,load engine modules
    GEngineLoop.PreInit(CmdLine);
 
    //Create and initizlize a UEngine, run late Initialization,start the game
    GEngineLoop.Init();
 
    //kick off rendering,tick the engine,update RHI
    while(!IsEngineExitRequested())
    {
	 GEngineLoop.Tick();
    }
} 
//CodeSnippet.2

CodeSnippet.2 和 CodeSnippet.1 的区别就是退出函数。 GuardedMain() 定义了一个局部变量 CleanupGuard,在函数结束以后系统会调用 ~EngineLoopCleanupGuard(), 从而退出游戏

GEngineLoop.PreInit() 函数的主要内容就是按照顺序加载游戏相关的引擎、项目和第三方插件等模块, 代码如下:

//LaunchEngineLoop.cpp
int32 FEngineLoop::PreInit(const TCHAR* CmdLine)
{
    LoadCoreModules(); //CoreUObject
    LoadPreInitModules(); //Engine,Renderer,AnimGraphRuntime,RenderAPI...
    LoadCustomModules(); //EarliestPossible,PostConfigInit,PostSplashScreen...
    LoadStartupCoreModules(); //Core,Networking,SlateCore,Slate,Overlay...
    ...
    return 0;
}
//CodeSnippet.3

加载完当前模块会进行模块注册等相关内容(CDO也是在此阶段生成), 预初始化执行完就开始初始化,CodeSnippet.2 的 GEngineLoop.Init() 就会被执行,代码如下:

//LaunchEngineLoop.cpp
int32 FEngineLoop::Init()
{
    FString GameEngineClassName;
    GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("GameEngine"), GameEngineClassName, GEngineIni);
    UClass* EngineClass = StaticLoadClass( UGameEngine::StaticClass(), nullptr, *GameEngineClassName);
   
    //create UGameEngine
    GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
    check( GEngine );
   
    //create GameInstance; Client: create ViewpointClient and LocalPlayer
    GEngine->Init(this);
    UEngine::OnPostEngineInit.Broadcast();
    FCoreDelegates::OnPostEngineInit.Broadcast();
   
    //initialize any late-loaded modules(加载哪些设置在postEngineinit阶段才加载的模块)
    IProjectManager::Get().LoadModulesForProject(ELoadingPhase::PostEngineInit);
    IPluginManager::Get().LoadModulesForEnabledPlugins(ELoadingPhase::PostEngineInit));

    //start the game,typically this load the default map
    GEngine->Start();
    GIsRunning = true;
    FCoreDelegates::OnFEngineLoopInitComplete.Broadcast();
    return 0;
}
//CodeSnippet.4

本文分析的是非编辑器环境,因此 创建的 UEngine 就是 UGameEngine, 创建完就执行初始化 GEngine->Init(this); 代码如下:

//GameEngine.cpp
void UGameEngine::Init(IEngineLoop* InEngineLoop)
{
    //create GameInstance
    FSoftClassPath GameInstanceClassName = GetDefault<UGameMapsSettings>()->GameInstanceClass;
    UClass* GameInstanceClass = LoadObject<UClass>(NULL, *GameInstanceClassName.ToString()));
    GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
 
    //create FWorldContext and dummy World,Init Subsystem
    GameInstance->InitializeStandalone();
	
    //Create ans Initialize the viewport client
    if(GIsClient)
    {
        ViewportClient = NewObject<UGameViewportClient>(this, GameViewportClientClass);
        ViewportClient->Init(*GameInstance->GetWorldContext(), GameInstance);
        GameViewport = ViewportClient;
        GameInstance->GetWorldContext()->GameViewport = ViewportClient;
        CreateGameViewport(ViewportClient);

        //Create a ULocalPlayer and associate it with viewpoint client
        FString Error;
        ViewportClient->SetupInitialLocalPlayer(Error);
        UGameViewportClient::OnViewportCreated().Broadcast();
    }
}
//CodeSnippet.5

CodeSnippet.5 主要就是创建 UGameInstance 并初始化。 此时开始出现 Client 和 Server ( 本文特指 Dedicated Server,暂不考虑 Listen Server) 的区别

Server

CodeSnippet.5 执行完毕,Server 端拥有的对象: UGameEngine , UGameInstance , UWorldContext以及创建的一个临时世界 ; 接下来就是执行 CodeSnippet.4 内的 GEngine->Start() , 它最终会调用 UEngine::LoadMap() 代码如下:

//UnrealEngine.cpp
bool UEngine::LoadMap( FWorldContext& WorldContext, FURL URL, class UPendingNetGame* Pending, FString& Error )
{
    //Let any interested parties konw that the current world is about to be unloaded
    FCoreUObjectDelegates::PreLoadMap.Broadcast(URL.Map);
   
    //Clear Current World
    // Destory player-controlled Pawns and PlayerControllers
    //Route the EndPlay event to all actors
    //Clean up the world, destory all actors
   
    //Notify the GameInstance of the map change in case it wants to load assets
    WorldContext.OwningGameInstance->PreloadContentForURL(URL);
   
    //Load our persistent level's map package and get top-level UWorld
    //UWorld,ULevel,Actor 等在编辑器已经生成好,打包的时候被序列化到了.umap文件内, 加载完反序列化就能拿到所有内容
    //也包括WordSettings对象(以及给它设置的各种对象配置)
    UPackage*  WorldPackage = LoadPackage(nullptr, *URL.Map, LOAD_None));;
    UWorld*    NewWorld = UWorld::FindWorldInPackage(WorldPackage);
   
    //Give the world a reference to the GameInstance
    NewWorld->SetGameInstance(WorldContext.OwningGameInstance);
    GWorld = NewWorld;
   
    //just-load UWorld and get it ready for gameplay
    WorldContext.SetCurrentWorld(NewWorld);
    WorldContext.World()->WorldType = WorldContext.WorldType;
    WorldContext.World()->AddToRoot();
    WorldContext.World()->InitWorld(); //物理,寻路导航,AI以及声音系统进行设置
    WorldContext.World()->SetGameMode(URL); //让GameInstance在世界生成GameMode,Server-Only
   
    //Load any per-map packages and make sure "always aloaded" sub-levels are fully loaded
    LoadPackagesFully(WorldContext.World(), FULLYLOAD_Map, WorldContext.World()->PersistentLevel->GetOutermost()->GetName());
    WorldContext.World()->FlushLevelStreaming(EFlushLevelStreamingType::Visibility);

    //Register components, then initialize all actors and their components
    {
       FRegisterComponentContext Context(WorldContext.World());
       WorldContext.World()->InitializeActorsForPlay(URL, true, &Context);
       Context.Process();
    }

    //Spawn play actors(i.e. PlayerControllers) for all active local players
    for(auto It = WorldContext.OwningGameInstance->GetLocalPlayerIterator(); It; ++It)
    {
        FString Error2;
        (*It)->SpawnPlayActor(URL.ToString(1),Error2,WorldContext.World());
    }
   
    //如果是DS或者监听服,那么创建Socket监听客户端请求
    if (Pending == NULL && (!GIsClient || URL.HasOption(TEXT("Listen"))))
    {
       if (!WorldContext.World()->Listen(URL))
       {
          UE_LOG(LogNet, Error, TEXT("LoadMap: failed to Listen(%s)"), *URL.ToString());
       }
    }

    //Route the BeginPlay event to indicate that all actors are present and initialized
    WorldContext.World()->BeginPlay();

    //let listeners konw that the map change is done
    PostLoadMapCaller.Broadcast(WorldContext.World());

    double StopTime = FPlatformTime::Seconds();
    UE_LOG(LogLoad, Log, TEXT("Took %f seconds to LoadMap(%s)"), StopTime - StartTime, *URL.Map);
    FLoadTimeTracker::Get().DumpRawLoadTimes();
    WorldContext.OwningGameInstance->LoadComplete(StopTime - StartTime, *URL.Map);

    // Successfully started local level.
    return true;
} 
//CodeSnippet.6

根据URL加载对应的 URL.ump , 然后将 URL.ump 内的所有对象反序列化出来。因此我们就有了新的UWorld,ULevel 以及场景内包含的 Actors

接下来执行的代码是 SetGameMode(URL) 它会创建一个很重要的对象: GameModeBase。它代表着游戏的规则,比如:游戏时长,玩家数量,输赢判定等

再接下来执行的代码是 InitializeActorsForPlay(URL, true, &Context) 它的作用是初始化当前关卡内所有Actors; 包括注册 Actors 的组件,执行 Actors 的三个初始化方法 PreInitializeComponents,InitializeComponents,PostInitializeComponents 等

GameModeBase 继承自 Actor , 因此它的 PreInitializeComponents 方法也会被调用, 代码如下:

//GameModeBase.cpp
void AGameModeBase::PreInitializeComponents()
{
    Super::PreInitializeComponents();
   
    //Spawn a GameState actor
    UWorld* World = GetWorld();
    FActorSpawnParameters SpawnInfo;
    SpawnInfo.Instigator = GetInstigator();
    SpawnInfo.ObjectFlags |= RF_Transient; // We never want to save game states or network managers into a map                            
    GameState = World->SpawnActor<AGameStateBase>(GameStateClass, SpawnInfo);
   
    //Associate the GameState with the world
    World->SetGameState(GameState);
    GameState->AuthorityGameMode = this;

    //spawn a GameNetWorkManager, Server Only
    AWorldSettings* WorldSettings = World->GetWorldSettings();
    World->NetworkManager = World->SpawnActor<AGameNetworkManager>(WorldSettings->GameNetworkManagerClass, SpawnInfo);
   
    //Initialize the GameState based upon the initial configuration of the GameMode
    InitGameState();
} 
//CodeSnippet.7

CodeSnippet.7 主要是生成 GameState , NetworkManager ; GameState 用来存储当前游戏模式的基础数据

Actor 在调用 InitializeComponents 方法时,如果Actor勾选了bAutoActivate ,则会执行激活Actor 。所以如果你想在Actor的 BeginPlay 之前做一些操作的话,最好的时机就是 PostInitializeComponents 方法内

接下来执行的代码是 Listen(URL), 启动 Server 的网络监听; 再就是调用所有Actors 的 BeginPlay

至此 Server 分析完毕,它目前具备了一个游乐场所需要的一切设备,梦幻的场景( UWorld,ULevel ), 有趣的游戏规则(UGameMode), 游乐场看门老头( Listen(URL) ) ,满心欢喜的在等待着 Client 加入这场游戏盛宴

Client

CodeSnippet.4 内的 GEngine->Init(this) 方法( CodeSnippet.5 ), 在Client端它会生成 GameViewportClient(可以想象成屏幕本身:它本质上就是作为渲染、声音以及输入系统的高级接口,也是用户与引擎之间的交互接口) 和 LocalPlayer(可以想象成坐在屏幕前的用户)

而CodeSnippet.4 内的 GEngine->Start()方法在 Client 它会调用到 UEngine::Browse() , 代码如下:

//UnrealEngine.cpp
EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
{
    if( URL.IsLocalInternal() )
    {
       //Server: Create GameMode,GameSession,GameState,GameNetWorkManager
       return LoadMap( WorldContext, URL, NULL, Error ) ? EBrowseReturnVal::Success : EBrowseReturnVal::Failure;
    }
   
    if( URL.IsInternal() && GIsClient )
    {
       //Client, Create UPendingNetGame,UNetDriver,send "MIT_Hello" to Server
       WorldContext.PendingNetGame = NewObject<UPendingNetGame>();
       WorldContext.PendingNetGame->Initialize(URL);
       WorldContext.PendingNetGame->InitNetDriver();
       return EBrowseReturnVal::Pending;
    }
 }
//CodeSnippet.8

CodeSnippet.8 的主要功能是创建 UpendingNetGame 并初始化执行登录流程,流程大概如下:

UE4 登录流程
登录ds服务器流程

Create Pawn In Server

那么Server 和 Client , 分别是什么时候开始生成玩家的呢?

Server 是在收到 NMT_Join 消息, UWorld::SpawnPlayActor 生成 PlayerController, 代码如下:

APlayerController* UWorld::SpawnPlayActor(UPlayer* NewPlayer, ENetRole RemoteRole, const FURL& InURL, const FUniqueNetIdRepl& UniqueId, FString& Error, uint8 InNetPlayerIndex)
{
    AGameModeBase* const GameMode = GetAuthGameMode()

    //Give the GameMode a chance to accept the login
    APlayerController* const NewPlayerController = GameMode->Login(NewPlayer, RemoteRole, *InURL.Portal, Options, UniqueId, Error);

    //Possess the newly-spawned player
    NewPlayerController->NetPlayerIndex = InNetPlayerIndex;
    NewPlayerController->SetRole(ROLE_Authority);
    NewPlayerController->SetReplicates(RemoteRole != ROLE_None);
    NewPlayerController->SetPlayer(NewPlayer); //NewPlayer type is UNetConnection

    //Spawn Pawn
    GameMode->PostLogin(NewPlayerController);
    return NewPlayerController;
}
//CodeSnippet.9

在 NewPlayerController 的构造方法执行之后 PostActorConstruction方法会被调用,它内部会依次调用 PreInitializeComponents,InitializeComponents,PostInitializeComponents。 与 PlayerController 对应的 PlayerState 则会在 PostInitializeComponents 方法内生成并执行初始化

//PlayerController.cpp
void APlayerController::PostInitializeComponents()
{
    if ( !IsPendingKill() && (GetNetMode() != NM_Client) )
    {
       // create a new player replication info
       FActorSpawnParameters SpawnInfo;
       SpawnInfo.Owner = this;
       SpawnInfo.Instigator = GetInstigator();
       SpawnInfo.ObjectFlags |= RF_Transient; 

       const AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
       PlayerState = World->SpawnActor<APlayerState>(GameMode->PlayerStateClass, SpawnInfo);
    } 
} 
//CodeSnippet.10

而PlayerState 创建以后,也会执行 PostInitializeComponents 方法,将自己放入 GameStateBase 中; 这样 GameStateBase 就能获取到所有加入到游戏的玩家信息(通过 PlayerArray), 通过玩家信息又能获取到对应的 PlayerController, 通过 PlayerController, 就能获取到 UPlayer (type is UNetConnection)

生成PlayerControllery以后,接下来调用GameMode->PostLogin, 这里就会生成最终我们控制的Pawn

Create Pawn In Client

Client 在收到 NMT_Welcome 消息后开始加载地图,CodeSnippet.6内 SpawnPlayActor() 被调用,之后的流程和 Server 一致

小结

GamePlay的初始化流程分为两个大的阶段:

  • LoadMap 之前: 生成 GameEngine, GameInstance, WorldContext, GameViewportClient(Client Only) , LocalPlayer(Client Only)
  • LoadMap 之后: URL.umap资源序列化以后的对象(UWorld, ULevel, AActor, UActorComponent…), GameModeBase(Server Only), GameSession (Server Only) , GameStateBase, GameNetworkManager (Server Only) , PlayerController, PlayerState, Pawn

LoadMap 之前生成的对象只有一个, GameInstance 的数量在 PIE 模式下有点特殊, 会根据 Number of Players 生成多个(Play Offline/Play As Listen Server: n , Play As Client:n+1)

至此,GamePlay启动流程基本结束。如果对文中一些类的具体用途以及为什么如此设计,则可以翻阅大钊老师的专栏《InsideUE5》,里面详细讲解了GamePlay框架中最重要的几个类的设计原理以及具体用途

本文的内容主要来自youtube上一位大佬的视频讲解(有好心人搬到了B站), 最好的学习方法就是照着视频的内容,一步步看源码然后记录下来,记载成文加深记忆和理解

youtube:The Unreal Engine Game Framework: From int main() to BeginPlay
bilibili:UE4游戏框架:从int main()到BeginPlay