趁着国庆假期把 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 并初始化执行登录流程,流程大概如下:

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