找回密码
 立即注册
查看: 717|回复: 2

[UE4/UE5][C++] 虚幻引擎框架或者虚幻运行原理

[复制链接]
发表于 2022-12-6 10:40 | 显示全部楼层 |阅读模式
The Unreal Engine Game Framework: From int main() to BeginPlay - YouTube
你可以完全不看这篇文章,而看上面这个视频。如果你看不懂英语,可以尝试看一下文章。这只是我的笔记。UE5代码还是发生了一些改变,但是变化不是太大。这个视频非常好,没有一句废话,全是干货。但是就是有一点难,看完下来晕头转向的。稍不留神就不知道在说什么了。我画了一个很长很长的流程图去说明这个过程,但是这个流程图我感觉依然很难被理解,就像视频作者那个视频总结一样。一开始我都是自己在虚幻源码中找这些关键代码过程,但是后来因为扭伤不能去用我的台式电脑,只能截取视频作者的代码。这篇笔记有可能漏洞百出,希望网友多多包涵。
虚幻引擎运行原理 | ProcessOn免费在线作图,在线流程图,在线思维导图 |
游戏循环

游戏编程中最常见的,最基础的概念就是游戏循环。
int main() {
        init();
        while (!g_exit_requested) {
                poll_input();
                update();
                render();
        }
        shutdown();
}
初始化游戏,然后玩家输入,根据玩家输入更新世界,然后将世界渲染到屏幕上。当关闭游戏时,会做一些清洗工作。
但是虚幻不一样,你并不能直接参与游戏循环。如果你写过虚幻的游戏代码,你就会知道你其实是通过继承一些像GameMode,Character这样的类来自定义自己的功能。



常用类结构

虚幻引擎中的游戏循环

根据游戏引擎版本不同,代码有可能不一样。
你可以在Launch.cpp里找到虚幻的游戏循环,他叫
FEngineLoop        GEngineLoop;
初始化游戏引擎,tick,退出
/**
* PreInits the engine loop
*/
int32 EnginePreInit( const TCHAR* CmdLine )
{
        int32 ErrorLevel = GEngineLoop.PreInit( CmdLine );

        return( ErrorLevel );
}

/**
* Inits the engine loop
*/
int32 EngineInit()
{
        int32 ErrorLevel = GEngineLoop.Init();

        return( ErrorLevel );
}
/**
* Ticks the engine loop
*/
LAUNCH_API void EngineTick( void )
{
        GEngineLoop.Tick();
}
/**
* Shuts down the engine
*/
LAUNCH_API void EngineExit( void )
{
        // Make sure this is set
        RequestEngineExit(TEXT("EngineExit() was called"));

        GEngineLoop.Exit();
}
preInit过程

preInit其实在决定加载什么包。比如uplugin,uproject里就有一些要加载的包。



rider插件的uplugin

Moduledescriptor.h的ELoadingPhase决定了哪个module需要在开始阶段被加载。
/**
* Phase at which this module should be loaded during startup.
*/
namespace ELoadingPhase
{
        enum Type
        {
                /** As soon as possible - in other words, uplugin files are loadable from a pak file (as well as right after PlatformFile is set up in case pak files aren't used) Used for plugins needed to read files (compression formats, etc) */
                EarliestPossible,

                /** Loaded before the engine is fully initialized, immediately after the config system has been initialized.  Necessary only for very low-level hooks */
                PostConfigInit,

                /** The first screen to be rendered after system splash screen */
                PostSplashScreen,

                /** Loaded before coreUObject for setting up manual loading screens, used for our chunk patching system */
                PreEarlyLoadingScreen,

                /** Loaded before the engine is fully initialized for modules that need to hook into the loading screen before it triggers */
                PreLoadingScreen,

                /** Right before the default phase */
                PreDefault,

                /** Loaded at the default loading point during startup (during engine init, after game modules are loaded.) */
                Default,

                /** Right after the default phase */
                PostDefault,

                /** After the engine has been initialized */
                PostEngineInit,

                /** Do not automatically load this module */
                None,

                // NOTE: If you add a new value, make sure to update the ToString() method below!
                Max
        };
}
搞明白虚幻引擎到底在初始化什么,我们必须知道虚幻引擎的一些包,如下图。你的一些设置文件决定了以下有些包需要被加载。一些包比另一些包要重要,一些包只能加载在特定的平台上加载,一些包在特定情况下加载。



虚幻的一些模块

这是一个加载顺序



大致加载顺序

首先加载底层的一些模块,一些底层的关键系统被初始化和关键类型被定义。然后你的项目和插件的模块需要被提前加载的在此时被加载。接着一些更高级的关键系统被初始化,然后你的项目和插件模块在此时被加载。最后一步才是你的代码被加载的地方。
你的模块如何被加载?

首先虚幻引擎注册任何你的模块里的UObject,这使得虚幻反射系统能意识到这些类并为这些类建立CDO,class default object(类的默认对象?)。在默认状态下,CDO是你类的记录和用做以后继承的原型。因此,如果你定义了一个自定义的actor类型或者gamemode,或者任何被UClass所声明的类,engine loop都会分配一个默认的实例,然后运行其类的构造函数并传入CDO的父类作为模板。这也是为什么构造函数不应该有任何玩法代码的原因之一。构造函数只是建立类的通用细节,而不是去修改类的任何内容。在这些你的类全部被注册后,引擎调用你的start module方法(startmodule与你的shutdownmodule相匹配),做一些与模块生命周期相联系的初始化。
所以在preinit阶段,虚幻引擎加载了一切必须的引擎,项目,插件的模块。虚幻引擎注册了来自这些模块的类,初始化必须的底层系统。



GameModeBase的构造函数



start module

Init过程

Init相对非常直接。如果我们将其简化一点点,它通过一个叫Uengine的类来处理事情。它是目录UE_5.1\Engine\Source\Runtime\Engine\Classes\Engine\Engine.h文件下的一个类。用rider的ctrl+左键点击一下看看。




创建Gengine实例

EngineLoop在init函数中检查到底哪一个GameEngine类需要被使用。然后EngineLoop创建了一个这个GameEngine类的实例和将它作为全局的Uengine实例。这个Uengine实例可以通过全局变量Gengine去访问,这个变量同样在Engine.h里被声明。



Engine.h声明的GEngine

当Gengine被创立,然后就被初始化了。一旦初始化完成,engine loop就会用一个全局委托去表明这个Engine已经被初始化了。然后就会加载被设置为晚加载的项目或者插件模块。最终,engine开始运行,初始化也被完成。



初始化方法



全局委托去广播



加载模块



引擎开始运行

Engine.h里的Browse和LoadMap

engine这个类做了很多事情,但主要的方法是Browse和LoadMap



engine主要功能

我们现在去看看这个进程是如何启动和如果让所有的引擎系统初始化。
UnrealEngine.cpp里的LoadMap

这些都在UnrealEngine.cpp里实现。


引擎能够浏览url,这个url可以是一个客户端的服务器地址,也可以是一个本地地图的地址。这个url也可以有些参数加到他们身上。



含有参数的url

当你设置了一个默认地图在你的项目的DefaultEngine.ini。你就告诉游戏引擎去浏览这个地图,当你的引擎启动时。



默认地图

GameEngine.cpp里的初始化方法

我们说到,在gameloop类中初始化调用了Gengine的init方法。也就是在GameEngine.cpp中的UGameEngine::Init中,创建了一些较为重要的实例,GameInstance,GameViewport Client,和LocalPlayer。



GameInstance



GameViewport Client



LocalPlayer

你可以把GameViewport Client想象成一个屏幕,LocalPlayer想象成正在盯着屏幕的人。对于渲染,声音,和输入系统,UGameViewportClient是高级接口。所以你可以认为这个是User和Uengine之间的接口。而GameInstance是从Uengine分离的一个类,去处理关于你的项目的一些特殊功能。在过去这些功能是放在Uengine之中。



4个重要类

init初始化全过程

当这些类被创建后,就开始下一步工作了,见下图。



初始化用的一些方法

在这些的最后一步LoadMap会创建很多东西。比如创建一个Uworld,这个UWorld包含了所有被存储在这个World上的Actor。还有游戏框架等。下图就是那些类在loadMap之前创建,哪些类在loadMap创建。上面是引擎的类的初始化,下面是游戏类的初始化。



初始化不同的阶段

但是虚幻引擎提供了不同map之间的无缝穿越,而且可以保持actor的完整。如果你真正去了一个“新地图”,或者连上了一个新服务器,或者返回到了主菜单,你的所有actor都将被摧毁,世界被清理,这些类将不见了,直到下一个地图。
loadMap基本过程

一开始这个游戏引擎去发送一个全局委派去暗示这个地图将要加载。



全局委派

如果之前有地图在内存已经被加载好了,就会摧毁这个地图。



摧毁之前的地图

当我们到达上图中的starttime时,已经要确保没有旧地图了。LoadMap方法是传入WorldContext的参数。WorldContext是在引擎初始化之时,被gameInstance所创建。WorldContext的是一个永久的类,worldContext用来追踪在此时哪些world被加载。



world context参数

在任何事情被加载之前,gameinstance都有机会加载它想要的资产。



GameInstance加载资产

world和map我感觉是可以互换的词,而world可以包含多个level或者一个level。
Map, Level and World, whats the difference - Development / World Creation - Unreal Engine Forums
如果你的编辑器上有个地图,那这个Uworld已经被载入内存之中了。其中这个Uworld有些level,而这个level有些actor,当你需要把这个Uworld持久化时,它会和他的level还有actor一起被序列化放到磁盘上。也就是你在文件夹里看到的umap文件。而loadmap会从磁盘把umap中的持久化的level,level中的actor,还有worldsettings全部加载到内存之中。



序列化到磁盘上



将磁盘上的umap载入内存中

下面是world一些代码,设置类型,addtoroot防止被垃圾回收。而initworld方法中,则是设置系统像physics,navigation,AI,audio等系统



初始化world

设置世界的gamemode actor



设置gamemode

加载地图,意味着任何“总是加载”的子level还有附带的资产都被加载进去。



加载永久level

为level里的actor初始化gameplay,告诉world游戏要开始了。



initialzeActorForPlay

UWorld::InitializeActorsForPlay
更新关卡组件,这个在UpdateWorldComponents中。这个会注册在这个世界所有actor的component。



更新关卡组件

UActorComponent::RegisterComponentWithWorld
注册actor的component



注册actor组件

将actor的世界引用为加载的世界。



引用世界

注册actor的component,并给了component一些早期初始化。UActorComponent::ExecuteRegisterEvents。如果是一个primitiveComponent,在注册之后,componnet将有一个FPrimitiveSceneProxy,FPrimitiveSceneProxy将被创建和加入Fscene,Fscene是Uworld的渲染线程。



注册component



scene和add primitive component

一旦注册完成之后,world就会调用gamemode的初始化。



gamemode的初始化

gamemode的init方法会使gamemode去放置一个gamesession actor



gamesession actor

注册world里的所有level的所有actor



多初始化actor类

初始化level里的actor。有两个阶段,preinit actor component,init actor component。preinit在注册之后,但是在正式初始化之前。



actor初始化的两个阶段

gamemode也是一个actor,所以它也有preinit actor component。gamemod component 生成了一个GameSTate,GameNetworkManager,最终也初始化了gamestate。



生成gamestate,game network manager

激活gamemode的component和初始化gamemode的component



激活和初始化 component

post initialize是actor完全准备好状态的最早时候,所以在这个方法之中,就可以写一些代码在游戏之前初始化这个component。
AGameMode

gamemode定义了游戏的规则,它也生成了很多gameplay的关键actor。在游戏之中,对于发生什么,gamemode有绝对的权限,而且它只发生在server端。
AGameSession

gamesession用来处理登陆请求,也是在线服务(比如steam和psn)的接口。只在server端。
AGameNetworkManager

AGameNetworkManager是用来设置一些名叫cheat direction和move prediction的东西。这个是方向预测?只在server端。
AGameStateBase

GameState在服务器上创建,只有服务器有权限去改变它。但是它会复制给每个客户端,你想要所有的玩家都知道当前游戏状态。gamestate是你存储与游戏状态相关数据的地方。
我们当前缺失的是我们player actor。
用loadmap的gameinstance的GetLocalPlayerIterator迭代器遍历了gameinstance里的所有actor,一般来说迭代器只有一个。对于一个localplayer,它调用了自己的spawnplayeractor方法。在spawnplayeractor这个方法中,play actor和playercontroller是可以互换的概念。比如在spawnplayeractor这个函数中,就可以生成一个playercontroller。



迭代



localPlayerController

UlocalPlayer和PlayerController

local player是引擎里player的代表,它在UEngine::init里创建。与local player相反的是playerController是player在地图里的代表。
local player实际上是player base class的具体化。还有一种player叫net connection,它是一个连接远程进程的player。
为了让任何进程加入游戏,无论是本地还是网络的。他都不得不经历一个登陆过程。这个过程被game mode把持着。
gamemodebase:prelogin只有远程的连接请求才会被调用。它的责任就是同意或者拒绝登陆请求。一旦我们将player加入游戏中,要么它被同意登陆,要么就是个本地player。



Login返回一个playercontroller

gamemodebase:login  生成了一个player controller actor,然后将它返回给世界。
我们生成了一个actor,所以我们的world已经为游戏开始准备好了。在生成actor的过程中,我们要初始化这个player controller actor。
所以APlayerController::PostInitializeComponents()被调用,然后这个方法会生成一个PlayerState。



生成一个PlayerState

PlayerController和PlayerState和Gamemode和Gamestate非常相似,GameMode只能在服务器,而playerController只能是服务器授权,只对拥有的客户端复制。Gamestate服务器授权,对所有客户端复制。而playerstate同样也是如此。


Uworld的spawnplayeractor




player进入过程

我们通过代码发现,这个过程无疑是new player要加入,如果player申请加入并成功,gamemode就会生成一个playerController,playerController初试化自己的网络,然后再把这个player和controller联系在一起。然后调用gamemode的postLogin来设置一些东西。比如生成一个pawn给playercontroller控制。


Pawn和PlayerController

一个actor在controller代表了运行actor的大脑,而pawn则是在世界里的实际运行体。因此每当有一个新玩家加入游戏,game mode都会为其分配一个pawn给player controller去拥有或者说控制。


游戏框架同样支持game spectator(旁观者),你的player state可以被用来设置表明你的player是否必须旁观。gamemode去把player全部作为旁观者启动。在那种情况下,game mode就不会生成一个pawn,相反,player controller会生成一个spectator pawn,这个spectator pawn可以在游戏世界自由飞行,而不需要与游戏世界交流。如果不生成game pawn,游戏框架会重启player。
这个什么情况?还要重启player?想象你在一个多人射击游戏中,如果一个玩家被杀了,他的pawn就死了,变成了一个尸体躺在那。这个pawn不需要再被控制。但是player controller依然在那。当player已经准备好复活时,pawn又一次要被重新生成。这就是restartPlayer这个方法的作用。



restartplayer

给一个playercontroller,它就会找到一个代表pawn在哪生成的actor。然后再找到哪个pawn class需要被使用。然后就会生成这个pawn class的实例。在默认情况下,gamemode会浏览所有的在地图上的play start actor,并选择其中一个。但是这些都可以在你的gamemode里重写。在任何事件中,一旦一个pawn被生成,它都会和一个player controller相联系,这个playercontroller将控制这个pawn。


接下来,就是最后时刻了。游戏开始。引擎告诉world游戏开始,然后world告诉gamemode游戏开始,gamemode告诉worldsettings游戏开始。worldsettings告诉所有actor游戏开始,同样在蓝图中的begin play也开始了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-12-6 10:42 | 显示全部楼层
学习一下,辛苦
发表于 2022-12-6 10:43 | 显示全部楼层
总结的很好。
赞!
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 07:12 , Processed in 0.093333 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表