Unreal随笔系列2: 初始化流程&Gameplay基础类

2023-03-08 15:19:44 浏览数 (1)

导语

近期排查一个问题时,将Unreal的启动的初始化流程和基础的Gameplay类又review了下。 将相关内容整理,随笔记录下来。

Unreal程序入口点和主循环

Unreal使用C 作为基础开发语言;作为一个引擎,它的代码结构庞杂,功能繁多。但如果从整个计算机体系看,它仅是操作系统的一个进程;作为C 编写的可执行程序,它启动后,代码执行的入口必然是main函数。

在Windows环境下,使用Visual Studio调试,我们在FEngineLoop的PreInitPreStartupScreen函数增加断点。可以得到如下堆栈:

代码语言:javascript复制
    FEngineLoop::PreInitPreStartupScreen(const wchar_t * CmdLine)
    FEngineLoop::PreInit(const wchar_t * CmdLine) 
    EnginePreInit(const wchar_t *) 
    GuardedMain(const wchar_t * CmdLine) 
    GuardedMainWrapper(const wchar_t * CmdLine)
    LaunchWindowsStartup(...) 
    WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * pCmdLine, int nCmdShow)

在调用栈的最底层,正是所有Windows程序的入口点。

不同平台的程序入口点分别位于如下代码目录。

代码语言:javascript复制
Engine/Source/Runtime/Launch/Private/${platform}

如果跳过为了可移植性编写的不同操作系统的入口点函数,引擎程序本身的入口点可以认为是GuardedMain。(实现位于Runtime/Launch/Private/Launch.cpp文件)。

GuardedMain函数实现的主要流程如下:

代码语言:javascript复制
GuardedMain( const TCHAR* CmdLine )
{
    <...>
    EnginePreInit
    <...>
    EngineInit
    <...>
    while( !IsEngineExitRequested() )
    {
        EngineTick();
    }   
    <...>
}

这个函数和大多数的服务器程序类似:先执行初始化,然后进入所谓的程序循环,在收到引擎退出的请求后,则终止执行。

在介绍上述流程中的初始化步骤前,我们回顾下:

Gameplay的基础类

Unreal构建游戏玩法的基础类罗列如下。对于有志于引擎,或者Unreal游戏开发的同学,花一周左右的时间,对下面的类及其使用基本耳熟能详。

代码语言:javascript复制
UEngine   UGameInstance(Outer)  FWorldContext UWorld(OwningGameInstance) ULevel(OwningWorld)

AGameMode  AGameSession  AGameState

AActor(Outer) UActorComponent

APawn AController

UNetConnection UPlayer

简单的讲, UEngine,UGameInstance,UWorld,ULevel是抽象等级比较高的Gameplay管理类。

UEngine是引擎功能的抽象,负责完成整个逻辑循环的实现。ULevel是游戏对象(AActor)的集合。一般来说,UWorld由一个Persistent Level和若干Streaming Levels构成。对于拥有不同的场景的游戏,比如手游的大厅界面和单局游戏,对应着不同的UWorld。 UGameInstance的抽象层级,介于UEngine和UWorld之间。对于GameEngine来说,基本和UEngine具有相同的生命周期,原生实现仅有一个UGameInsance实例;只有在EdiotrEngine中,原生实现会有多个多个不同的UGameInsance实例。

World创建之后,会创建对应的AGameMode实例负责游戏玩法的状态转移,它会创建AGameSession和AGameState辅助自己完成相应工作。在玩家登录时,它会Spawn出玩家控制的角色ACharacter(或者APawn),以及玩家控制器(AController)。

Unreal引擎初始化过程

上面的陈述比较笼统。读者可以结合下面整理的初始化流程,熟悉下各Gameplay对象初始化的时机。以及彼此之间的关系。

代码语言:javascript复制
GuardedMain(Launch.cpp)
    EnginePreInit
        FEngineLoop::PreInit
            FEngineLoop::PreInitPreStartupScreen
                FEngineLoop::AppInit()
                    FCoreDelegates::OnInit.Broadcast()
                        InitUObject()
                            StaticUObjectInit();
                                GObjTransientPkg = NewObject<UPackage>(nullptr, TEXT("/Engine/Transient"), RF_Transient);   
            FEngineLoop::PreInitPostStartupScreen               
    EngineInit
        FEngineLoop::Init
            GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
            UGameEngine::Init
                GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);
            UGameEngine::Start
                UGameInstance::StartGameInstance() 
                    UEngine::Browse
                        UEngine::LoadMap
                            NewWorld = UWorld::FindWorldInPackage(WorldPackage);
                            WorldContext.World()->SetGameMode(URL);
                                UGameInstance::CreateGameModeForURL
                                    World->SpawnActor<AGameModeBase>(GameClass, SpawnInfo);
                            UWorld::InitializeActorsForPlay  
                                AGameModeBase::InitGame
                                    World->SpawnActor<AGameSession>
                            UWorld::BeginPlay
                                AGameMode::StartPlay()  
                            FCoreUObjectDelegates::PostLoadMapWithWorld.Broadcast
    EngineTick循环
        FEngineLoop::Tick()
            UGameEngine::Tick
                UWorld::Tick
                    UWorld::RunTickGroup
                        FTickTaskManager::RunTickGroup
                            <...>
                                FActorTickFunction::ExecuteTick
                                    AActor::TickActor

这里赘述下几个比较关键的节点

1. PreInit的时候, 会创建TransientPkg。

2. FEngineLoop::Init会首先创建Engine类的实例;在Engine初始化过程中会创建GameInstance类的实例。

3. FEngineLoop::Init其后会调用Engine::Start。这里会执行LoadMap的操作,期间会创建UWorld,并Spawn GameMode。

4. 在LoadMap的最后,会使用PostLoadMapWithWorld事件通知关心该事件的对象。

5. 在完成了初始化,进入Tick循环。

初始化流程的定制

对于业界现有的单局化的游戏开发来说,LoadMap承载的只是单局内的静态资源,对于每局都有一定随机性的游戏设计,需要在LoadMap完成后,继续一些动态的初始化流程。另外为了优化性能,游戏一般会引入预创建的过程。动态初始化和预创建,可以通过对以上流程的理解,安排在合适的时机:比如PostLoadMap,或者放到Tick内。

这些定制化的流程,可能带来游戏流程的变化。比如等待这些定制化流程完成的阶段。比如等待所有玩家登录的阶段。

这里仅做抛砖引玉,读者可以结合自己实际工作,自行思考下定制化的初始化流程实现。

玩家登录后的初始化过程

以上是整个游戏的初始化流程。下面我们看下玩家的初始化流程。

对于联机游戏,玩家登录的初始化流程是在如下堆栈之上完成的。

代码语言:javascript复制
    UWorld::NotifyControlMessage(UNetConnection * Connection, unsigned char MessageType, FInBunch & Bunch)
    UControlChannel::ReceivedBunch(FInBunch & Bunch)
    UChannel::ReceivedSequencedBunch(FInBunch & Bunch)
    UChannel::ReceivedNextBunch(FInBunch & Bunch, bool & bOutSkipAck)
    UChannel::ReceivedRawBunch(FInBunch & Bunch, bool & bOutSkipAck)
    UNetConnection::ReceivedPacket(FBitReader & Reader, bool bIsReinjectedPacket)
    UNetConnection::ReceivedRawPacket(void * InData, int Count)
    [外部代码]  
    Invoke(void(UNetDriver::*)(float))
    UE::Core::Private::Tuple::TTupleBase<TIntegerSequence<unsigned int>>::ApplyAfter(void(UNetDriver::*)(float) &)
    TBaseUObjectMethodDelegateInstance<0,UNetDriver,void __cdecl(float),FDefaultDelegateUserPolicy>::ExecuteIfSafe(float <Params_0>)
    TMulticastDelegate<void __cdecl(float),FDefaultDelegateUserPolicy>::Broadcast(float <Params_0>)
    UWorld::Tick(ELevelTick TickType, float DeltaSeconds) 行 1373    C  
    UGameEngine::Tick(float DeltaSeconds, bool bIdleMode) 行 1831    C  

引擎会在tick中调用NetDriver的命令处理。连接的登录请求包,最终由UWorld::NotifyControlMessage来处理。

其后的玩家创建流程如下:

代码语言:javascript复制
UWorld::NotifyControlMessage
    UWorld::SpawnPlayActor
        AGameModeBase::Login
            SpawnPlayerController
            InitNewPlayer
                UpdatePlayerStartSpot
                    <...>
                        ChoosePlayerStart_Implementation
        AGameModeBase::PostLogin
            HandleStartingNewPlayer
                RestartPlayer
                    <...>
                        AGameModeBase::SpawnDefaultPawnAtTransform_Implementation
                StartMatch

玩家初始化的关键点有两个:

1. 执行GamoMode的Login流程,主要是创建PlayerController,并选择出生点。

2. 执行GamoMode的PostLogin流程,这里会创建玩家控制的Character或者Pawn, 同时根据GameMode状态决定是否可以进入下一状态(StartMatch)。

同样的,玩家的登录流程也可以定制。比如增加玩家鉴权,比如拉取玩家装备。这里也仅作抛砖引玉,不细致展开。

结语

本文对Unreal的主程序入口和Gameplay基础类做了一定剖析,并详细整理了引擎初始化,和玩家登录后的初始化流程。 希望对于做Unreal Gameplay开发的同学,减少相关代码的学习时间,并对定制化的游戏流程设计产生帮助。

随笔系列说明

23年新挖一个《Unreal随笔系列》的坑。所谓随笔,就是研究过程中的一些想法随时记录;细节可能来不及考证,甚至一些想法可能也不太成熟,有失偏颇;希望读者也可以帮忙指正和讨论。这个系列主要求量,希望每个月给自己布置一些研究小课题,争取今年发满12篇。

0 人点赞