ue4框架


游戏编程中最基本的概念

也是最简单的概念之一-----游戏循环的概念

当你运行游戏时,做一些初始化设置,然后运行一个循环,只要玩家想继续玩:每一帧,你处理输入,跟新游戏世界的状态,渲染结果显示在屏幕上,当玩家关闭游戏的时候,会进行一些清理工作,然后就完成了.

int main()
{
    init();
    while(!g_exit_requested)
    {
        poll_input();
        update();
        render();
    }
    shutdown();
}

但是,我们在虚幻引擎中编写游戏代码,并不会直接处理游戏循环.

我们不是从main函数开始,而是从定义GameMode子类并重载InitGame之类的函数开始.或者,编写一次性的Actor和Component类并重载它们的BeginPlay或者Tick函数已添加我们的逻辑.这些实际上是我们要做的事情,引擎会为我们处理其它所有的事情,这不就是我们想要的结果吗.

作为程序员,虚幻引擎也为我们提供了很多功能和灵活性,引擎是开源的,可以通过多种不同的方式进行扩展,即使是初学者,当然也希望对引擎的GameFramework有一个初步的了解:GameMode/GameState/PlayController/Pawn和PlayerState等类.

熟悉引擎的方式之一就是查看源代码并了解它是如何启动游戏引擎的.

可是打开源代码,发现很难找到main入口点,就算找到游戏代码运行的路径,会看到有不同的系统在起作用,支持多个不同的平台,有大量的条件编译正在进行以支持不同的构建配置.有单独的游戏和渲染线程,还有构建在之上的面向对象的抽象类,核心游戏循环的功能受所有这些复杂性都易于管理.

如果开始查看初始化引擎的代码,会发现非常的不可思议,当引擎启动的时候,在进入那些更高级别的抽象之前,他会运行数千行代码来做很多很多设置,全局状态和初始化各种系统的小事情, 这是优化的20多年的,一些混乱的复杂性是不可避免的,这就好像是大爆炸之后的初始时刻.在一块非常小的表面区域内发生了大量的事情,并且许多系统相互重叠,当我们使用InitGame或者BeginPlay-以及我们编写的游戏代码时,宇宙以及在慢慢扩展,事情已经形成了有序的形式.

但是我任务打破混乱并查看引擎如何从程序的入口点到实际运行的代码可能是有帮助的.

这一切都是从启动模块开始,在这里我们会看到为不同平台定义的主要功能.

最终,在Launch.cpp中找到了访问这个GuardedMain(函数的方法)

在这里 ,我们能看到一个基本的循环;

#include "LaunchEngineLoop.h"

FEngineLoop	GEngineLoop;
bool GIsRequestingExit= false;

int32 GuardeMain(const TCHER* CmdLine)
{
    int32 ErrorLevel = GEngineLoop.PreInit( CmdLine );
       
	if ( ErrorLevel != 0 || GIsRequestingExit() )
	{
		return ErrorLevel;
	}
    ErrorLevel= GEngineLoop.Init();
    
    while( !GIsRequestingExit )
    {
        GEngineLoop.Tick();
    }
    
}

引擎的主循环在一个名为FEnglineLoop的类中实现.

我们可以看到引擎有一个PreInit()阶段,然后引擎完全初始化Init();然后每帧Tick(),最后退出Exit().

GEnglineLoop.PreInt(CmdLine)是大多数模块加载的位置;

当我们制作具有C++源代码的游戏项目或者插件的时候,可以在.uproject或者.uplugin文件中定义一个或者多个源模块,并且可以指定LoadingPhase来指定可是加载该模块.

引擎也分为不同的源模块.

有些模块比其它模块更重要,有些只在某些平台或某些情况下加载,因此模块系统有助于确保代码库不同部分之间的依赖关系是可管理的;

PreInit 阶段

当引擎循环开始其 PreInit 阶段时,它会加载一些低级引擎模块,以便初始化基本系统并定义基本类型。

然后,如果我们的项目或任何启用的插件具有处于这些早期加载阶段的源模块,那么接下来会加载这些模块。

之后,会加载大量更高级别的引擎模块。

之后,我们来到加载项目和插件模块的默认点。

接下来,.通常我们的游戏的 C++ 代码首先被注入到以前只是虚幻引擎的通用实例的地方。我们的游戏模块是在所有基本引擎功能都已加载和初始化的时候产生的,但在任何实际游戏状态创建之前。

那么当模块加载的时候发生了什么?

首先,引擎注册在该模块中定义的任何 UObject 类。这使得反射系统知道这些类,并且它还为每个类构造一个 CDO 或类默认对象。CDO 是我们的类在其默认状态下的记录,它用作进一步继承的原型。因此,如果我们定义了自定义 Actor 类型、自定义 Game Mode 或任何在其前面使用 UCLASS 声明的内容; 引擎循环分配该类的默认实例,然后运行其构造函数,将父类的 CDO 作为模板传入。这就是构造函数不应该包含任何与游戏相关的代码的原因之一:它实际上只是为了建立类的通用细节,而不是为了修改该类的任何特定实例。

注册完所有类后,引擎会调用模块的 StartupModule 函数,该函数与 ShutdownModule 匹配,让我们有机会处理需要与模块生命周期相关联的任何初始化。

所以此时,引擎循环已经加载了所有必需的引擎、项目和插件模块,它从这些模块中注册了类,并初始化了所有需要就位的低级系统。 这完成了 PreInit阶段,所以我们可以进入 Init 函数。

Init 阶段

引擎循环的 Init函数比较简单。

如果我们稍微简化一下,我们可以看到它把事情交给了一个叫做 UEngine 的类。

在此之前,当我说“引擎”时,我们一直在谈论带有小写 e 的引擎:基本上,我们正在启动的可执行文件,由我们自己编写的代码组成。

在这里,我们将介绍 THE Engine,即大写 E引擎。 引擎是一个软件产品,它包含一个名为 Engine 的源模块,在该模块中有一个名为 Engine.h 的头文件,在该头文件中定义了一个名为 UEngine 的类,它在 UEditorEngine 和 UGameEngine 两种风格中实现。

在游戏的初始化阶段,FEngineLoop检查引擎配置文件以确定应该使用哪个 GameEngine 类。

然后它创建该类的一个实例并将其作为全局 UEngine 实例供奉,可通过在 Engine/Engine.h 中声明的全局变量 GEngine 访问。

一旦创建了引擎,它就会被初始化,我们稍后会详细介绍;


完成后,引擎循环会触发一个全局委托以指示引擎现已初始化,

然后它会加载已配置为延迟加载的任何项目或插件模块。

最后,引擎启动,初始化完成。

那么 Engine 类实际上是做什么的呢? 它做了很多事情,但它的主要职责在于这里的这组大而肥的功能,包括 Browse 和 LoadMap。

我们已经了解了该过程如何启动并初始化所有引擎系统,但为了进入实际游戏并开始玩,我们必须加载到地图中,正是 UEngine 类为我们实现了这一点 .引擎能够浏览到一个 URL,该 URL 可以表示要作为客户端连接的服务器地址,也可以表示要在本地加载的地图的名称。URL 也可以添加参数。

当我们在项目的 DefaultEngine.ini 文件中设置默认地图时,就是在告诉引擎在启动时自动浏览到该地图。

当然,在开发版本中,我们还可以通过在命令行中提供 URL 来覆盖该默认地图,并且我们还可以在游戏过程中使用 open 命令浏览到不同的服务器或地图。

那么让我们看看Engine初始化。

引擎在加载地图之前进行自我初始化,它通过创建几个重要对象来实现:GameInstance、GameViewportClient 和 LocalPlayer。

我们可以将 LocalPlayer 视为代表坐在屏幕前的用户,我们可以将视口客户端视为屏幕本身:它本质上是渲染、音频和输入系统的高级接口,所以它 表示用户和引擎之间的接口。

UGameInstance 类是在 Unreal 4.4 中添加的,它是从 UGameEngine 类中分离出来的,用于处理一些以前在引擎中处理的更特定于项目的功能。

所以在引擎初始化之后,我们有一个 GameInstance、一个 GameViewportClient 和一个 LocalPlayer。

一旦完成,游戏就可以开始了:这是我们最初调用 LoadMap 的地方。在 LoadMap 调用结束时,我们将拥有一个UWorld,其中包含保存到地图中的所有 Actor,我们还将拥有一些新生成的 Actor,它们构成了 GameFramework 的核心:其中包括 游戏模式、游戏会话、游戏状态、游戏网络管理器、玩家控制器、玩家状态和棋子。区分这两组对象的关键因素之一是生命周期。

概括地说,有两个不同的生命周期需要考虑:加载地图之前发生的一切,然后是加载之后发生的一切。LoadMap 之前发生的一切都与进程的生命周期相关联。

其他所有内容(例如 GameMode、GameState和 PlayerController)都是在地图加载后创建的,并且只要我们在该地图中玩游戏,它们就会一直存在。该引擎确实支持所谓的“无缝旅行”,我们可以在其中转换到不同的地图,同时保持某些演员完好无损。但是如果你直接浏览到新地图,或者连接到不同的服务器,或者返回到主菜单 - 那么所有的演员都被摧毁了,世界被清理了,这些类在你加载之前都是不存在的 另一张地图。

那么让我们看看 LoadMap 中发生了什么。 这是一个复杂的功能,但如果我们将其简化为基本功能,它就不难理解了。

首先,引擎会触发一个全局委托以指示地图即将更改。

然后,如果已经加载了一张地图,它会清理并摧毁那个世界。 我们现在最关心的是初始化,所以我们只是挥手致意。

长话短说,当我们到达这里时,已经没有世界了。然而,我们所拥有的是一个世界背景。该对象由游戏实例在引擎初始化期间创建,它本质上是一个持久对象,用于跟踪当前加载的任何世界。

在加载任何其他内容之前,GameInstance有机会预加载它可能需要的任何资产,默认情况下,这不会做任何事情。

接下来我们需要给自己一个UWorld。

如果我们在编辑器中处理地图,则编辑器会在内存中加载一个 UWorld,以及一个或多个 ULevel,其中包含我们放置的 Actor。

当我们保存永久关卡时,该 World、它的关卡以及它的所有 Actors 会被序列化为一个地图包,该地图包会以 .umap 文件的形式写入磁盘。

因此,在 LoadMap 期间,引擎会找到该地图包并加载它。 此时,世界、其持久关卡和该关卡中的演员(包括 WorldSettings)已被重新加载到内存中。


所以我们有一个世界,现在我们必须初始化它。

引擎为 World 提供对 GameInstance 的引用,然后使用对 World 的引用初始化全局 GWorld 变量。

然后将 World 安装到 WorldContext 中,初始化其世界类型(在本例中为 Game),并将其添加到根集中,以防止其被垃圾收集。

InitWorld 允许世界设置物理、导航、人工智能和音频等系统。

当我们调用 SetGameMode 时,World 会要求 GameInstance在世界中创建一个 GameMode actor。一旦 GameMode 存在,引擎就会完全加载地图,这意味着任何始终加载的子关卡以及任何引用的资源都会被加载。


接下来,我们来到 InitializeActorsForPlay。这就是引擎所说的“让世界充满乐趣”。

在这里,世界在几个不同的循环中迭代所有演员。第一个循环向世界注册所有参与者组件。

每个 Actor 中的每个 ActorComponent都被注册,这对组件做了三件重要的事情:首先,它为它提供了一个对它被加载到的世界的引用。

接下来,它调用组件的OnRegister 函数,使其有机会进行任何早期初始化。而且,如果它是一个 PrimitiveComponent,那么在注册之后,该组件将创建一个 FPrimitiveSceneProxy并将其添加到 FScene,这是 UWorld 的渲染线程版本。


注册组件后,世界会调用 GameMode 的 InitGame 函数。

这会导致 GameMode产生一个 GameSession actor。

之后,我们有另一个循环,其中世界逐级进行,并且每个级别都初始化其所有参与者。这发生在两次传递中。


在第一轮中,关卡调用每个 Actor 的 PreInitializeComponents 函数。 这使 Actor 有机会在其组件注册之后但在其组件初始化之前相当早地初始化自己。

GameMode 和其他任何角色一样都是演员,因此这里也调用了它的 PreInitializeComponents 函数。发生这种情况时,GameMode 会生成一个 GameState对象并将其与 World 相关联,它还会生成一个 GameNetworkManager,然后最终调用游戏模式的 InitGameState 函数。



最后,我们再次循环遍历所有参与者,这一次调用 InitializeComponents,然后是 PostInitializeComponents。

InitializeComponents 循环遍历所有 Actor 的组件并检查两件事:

如果组件启用了 bAutoActivate,则该组件将被激活。

如果组件启用了 bWantsInitializeComponent,那么它的 InitializeComponent函数将被调用。

PostInitializeComponents 是Actor 处于完全成型状态的最早时间点,因此它是放置在游戏开始时初始化 Actor 的代码的常见位置。

至此,我们的 LoadMap 调用几乎完成:所有 Actors 都已加载并初始化,World 已开始播放,我们现在有一组 Actors 用于管理游戏的整体状态:

GameMode 定义了规则 游戏的核心,它催生了大多数核心游戏角色。它是游戏过程中发生的事情的最终权威,并且仅存在于服务器上。GameSession 和 GameNetworkManager也是仅限服务器的。网络管理器用于配置作弊检测和运动预测等内容。对于在线游戏,GameSession批准登录请求,并用作在线服务(例如 Steam 或 PSN)的接口。GameState 是在服务器上创建的,只有服务器有权更改它,但它会复制到所有客户端:所以它是我们存储与游戏状态相关的数据的地方,我们希望所有玩家都能够 知道关于。

所以现在世界已经完全初始化了,我们有了代表我们游戏的游戏框架actors。 我们现在所缺少的只是代表我们玩家的游戏框架参与者。

在这里,LoadMap 迭代我们的 GameInstance 中存在的所有 LocalPlayers:通常只有一个。 对于该 LocalPlayer,它调用SpawnPlayActor 函数。 请注意,“PlayActor”在此处可与“PlayerController”互换:此函数生成一个 PlayerController。

正如我们所见,LocalPlayer 是引擎对玩家的表示,而 PlayerController 是游戏世界中玩家的表示。

LocalPlayer 实际上是基本 Player 类的特化。 还有另一个名为 NetConnection 的 Player 类表示从远程进程连接的播放器。

为了让任何玩家加入游戏,无论是本地还是远程,都必须经过登录过程。

该过程由 GameMode 处理。GameMode 的 PreLogin 函数仅在远程连接尝试时调用:它负责批准或拒绝登录请求。

一旦我们批准将玩家添加到游戏中,无论是因为远程连接请求被批准还是因为玩家是本地玩家,都会调用 Login。

Login 函数生成一个 PlayerControllerActor 并将其返回给 World。

当然,由于我们是在世界为播放而设置之后生成一个演员,所以该演员在生成时会被初始化。这意味着我们的 PlayerController 的PostInitializeComponents 函数会被调用,然后它会生成一个 PlayerState actor。

PlayerController 和 PlayerState与 GameMode 和 GameState 类似,它们是游戏(或玩家)的服务器权威表示,相应的状态对象包含每个人都应该知道的有关游戏(或玩家)的数据。

生成 PlayerController 后,World 将完全初始化它以进行联网并将其与 Player 对象相关联。完成所有这些后,游戏模式的 PostLogin函数就会被调用,让游戏有机会进行任何由于该玩家加入而需要进行的设置。

默认情况下,游戏模式将尝试在 PostLogin 上为新的 PlayerController 生成 Pawn。

Pawn 只是控制器可以拥有的一种特殊类型的 actor。PlayerController 是基本 Controller 类的特化,还有另一个称为 AIController 的子类,用于非玩家角色。这是 Unreal 中的一个长期惯例:如果我们有一个演员根据自己的自主决策过程在世界各地移动 - 无论是人类玩家做出决策并将其转化为原始输入,还是 AI 做出更高级别的决策 关于去哪里和做什么——那么你通常有两个演员。

Controller 代表了驱动 Actor 的智能,而 Pawn 只是 Actor 在世界中的表示。

因此,当新玩家加入游戏时,默认的 GameMode 实现会生成一个Pawn 供新的 PlayerController 拥有。游戏框架也支持旁观者:我们的 PlayerState 可以配置为指示玩家应该旁观,或者我们可以配置 GameMode 以将所有玩家初始为旁观者。 在这种情况下GameMode 不会生成 Pawn,而是 PlayerController 将生成自己的SpectatorPawn,允许它在不与游戏世界交互的情况下飞来飞去。否则,在 PostLogin 上,游戏模式将执行所谓的“重新启动玩家”。 想想多人射击游戏中的“重启”:如果玩家被杀,他们的 Pawn 就死了——它不再被控制; 它只是像尸体一样徘徊,直到被摧毁。但是 PlayerController 仍然存在,当玩家准备好重生时,游戏需要为他们生成一个新的 Pawn。

这就是 RestartPlayer 所做的:给定一个 PlayerController,它会找到一个表示应该在哪里生成新 Pawn 的 actor,

然后它会确定要使用哪个 Pawn 类,并生成该类的一个实例。

默认情况下,游戏模式会查看已放置在地图中的所有 PlayerStart Actor,并从中选择一个。 但是所有这些行为都可以在我们自己的 GameMode 类中被覆盖和自定义。无论如何,一旦生成了 Pawn,它将与 PlayerController 相关联,并且 PlayerController 将拥有它。

现在,回到 LoadMap,我们已经为游戏真正开始做好了一切准备。 剩下要做的就是路由 BeginPlay 事件。

引擎告诉世界,世界告诉游戏模式,

游戏模式告诉世界设置,世界设置循环所有演员。


每个 Actor 都会调用其 BeginPlay 函数,该函数又会在所有组件上调用 BeginPlay,并在蓝图中触发相应的 BeginPlay 事件。

完成所有这些后,游戏就完全启动并运行了,LoadMap 可以完成,我们已将其纳入我们的游戏循环。

让我们快速地再过一遍,只是为了复习。

当我们以最终打包形式运行游戏时,我们正在运行一个进程。

该进程的入口点是一个主函数,主函数运行引擎循环。引擎循环处理初始化,然后它在每一帧打勾,当它完成时,它会关闭一切。

现在我们最关心的是初始化期间发生的事情。我们的项目或插件代码运行的第一个点将是我们的模块加载时。

这可能在多个点发生,具体取决于 LoadingPhase,但通常发生在 PreInit 结束时。当你的模块被加载时,所有的 UObject类都会被注册,并且默认对象会通过构造函数被初始化。 然后调用模块的 StartupModule 函数,这是我们可以挂接到委托以设置稍后调用的其他函数的第一个地方。

Init 阶段是我们开始设置引擎本身的地方。简而言之,我们创建一个 Engine 对象,对其进行初始化,然后开始游戏。

为了初始化引擎,我们创建了一个GameInstance 和一个 GameViewportClient,然后我们创建了一个 LocalPlayer 并将它与 GameInstance 关联起来。


有了这些基本对象,我们就可以开始加载游戏了。我们确定要使用哪个地图,浏览到该地图,然后让 GameInstance知道何时完成。
我们的其余启动过程发生在 LoadMap 调用中。首先我们找到我们的地图包,然后我们加载它:这会将放置在持久关卡中的任何演员带入内存,它还为我们提供了一个 World 和一个 Level 对象。

我们找到 World,我们给它一个 GameInstance 的引用,我们在 World 中初始化一些系统,然后我们生成一个 GameMode Actor。

之后,我们完全加载地图,引入任何始终加载的子关卡和任何需要加载的资产。

一切都装满了,我们开始为世界带来乐趣。我们首先为每个关卡中的每个 Actor 注册所有组件;

……然后我们初始化 GameMode,它反过来生成一个 GameSession Actor。

然后我们初始化世界上所有的 Actor。首先,我们在每个关卡的每个 Actor 上调用 PreInitializeComponents:当 GameMode 发生这种情况时,它会生成一个 GameState和一个 GameNetworkManager,然后它会初始化 GameState。

然后,在另一个循环中,我们初始化每个关卡中的每个 Actor:这会为所有需要它的组件调用 InitializeComponent(并且可能是 Activate),然后我们的 Actor 就完全成型了。


一旦世界被打开来玩,我们就可以将我们的 LocalPlayer 登录到游戏中。在这里,我们生成了一个 PlayerController,它又为自己生成了一个 PlayerState并将该 PlayerState 添加到 GameState;

.. 然后我们使用 GameSession 注册该玩家并缓存一个初始起始点。

随着 PlayerController 的生成,我们现在可以初始化它以用于网络并将其与我们的 LocalPlayer 关联

......然后我们继续 PostLogin,假设一切都已设置好,我们可以重新启动播放器,这意味着我们找出他们在哪里 应该从世界开始,我们确定要使用哪个 Pawn 类,然后我们 Spawn 并初始化一个 Pawn。


然后我们让 PlayerController拥有 Pawn,我们有机会为玩家控制的 pawn 设置默认值。

最后,我们要做的就是路由 BeginPlay 事件。这导致 BeginPlay在 World 中的所有 Actors 上被调用,它注册tick 函数并在所有组件上调用 BeginPlay,

最后,我们的 BeginPlay Blueprint 事件被触发。

至此,我们完成了地图的加载,正式开始了游戏,并且完成了引擎循环的初始化阶段。

我们在这里涵盖了很多内容,所以这里只是总结几点:

我们查看了 GameModeBase和 GameStateBase 类,而不是 GameMode 和 GameState。 这些基类是在 Unreal 4.14 中添加的,以便从游戏模式中剔除一些 Unreal-Tournament 风格的功能。

虽然 GameModeBase 包含所有基本的游戏模式功能,但 GameMode 类添加了“比赛”的概念,比赛状态在 BeginPlay 之后发生变化。 这可以处理整个游戏流程,例如在所有玩家准备好之前进行观赛,决定游戏何时开始和结束,以及为下一场比赛过渡到新地图。

我们还查看了 Pawn 类,但GameFramework 还定义了一个 Character 类,它是 Pawn 的一种特殊类型,包含几个有用的特性。

一个角色有一个主要用于运动扫描的碰撞胶囊,它有一个骨架网格,所以它被假定为一个动画角色,

它有一个 CharacterMovementComponent - 它与 Character 类紧密耦合并做了一些非常有用的事情:

最重要的是,角色移动是开箱即用的复制,具有客户端移动预测。如果我们正在制作多人游戏,这是一个非常有用的功能。

角色还可以使用动画播放中的根运动并将其应用到演员,并进行复制。

Character Movement 还处理导航和寻路,因此我们可以让AIController 拥有一个 Character,并且它可以移动到我们告诉它的导航网格上的任何位置,而无需运行自己的导航查询。

最后,Character Movement 实现了完整的运动选项,包括步行、跳跃、跌落、游泳和飞行; 并且有许多不同的参数可用于调整 movemenet 行为。我们可以在较低级别上利用大部分功能,至少在 C++ 中是这样,但 Character 类是一个很好的起点。 请记住,如果我们保持默认角色设置不变,那么我们的游戏只会感觉像是一个虚幻教程项目,而这并不是它自己的过错。因此,最好先抽象地考虑一下我们希望游戏的运动感觉如何,然后相应地调整运动参数。

因此,我们查看过的所有这些类(UWorld 和 ULevel 除外)都在这里供我们根据需要进行扩展。

我们已经看到 Unreal 如何拥有这个成熟的游戏框架,它具有处理在线集成、登录请求和网络复制等事情的成熟设计。

这意味着我们可以非常轻松地开箱即用地开发多人游戏,并且引擎的设计允许我们在几乎任何级别添加自定义功能。

如果我们最感兴趣的是制作简单的纯单人游戏,那么游戏框架的复杂性对我们来说可能毫无意义。

请记住,这纯粹是选择加入:例如,如果我们在加载地图之前不需要做任何特殊的事情,那么我们可能不需要自定义 GameInstance 类,默认的 GameInstance 实现就可以了 它的工作和远离你的方式。

不过,我仍然认为了解这些类的用途很有用,因为一旦我们知道自己在做什么,就不会花费任何费用来按预期使用它们,而且我们通常会得到更简洁的设计 方式。

例如,如果我们有一些关于玩家的信息需要跟踪,那么我们可以在许多不同的地方放置这些数据。

对于多人游戏,我们需要明智地选择,否则我们可能会发现无法在我们需要的地方访问数据。 如果我们正在制作单人游戏,我们几乎可以选择任何对象,包括 GameMode,而最糟糕的情况是我们必须遵循笨拙的引用链才能在需要时获取数据。

但是,无论我们制作的是哪种游戏,批判性地思考数据的结构以及不同对象之间的交互方式都是一个好主意。从长远来看,它会让你成为一个更好的程序员。

还值得指出的是,通过继承扩展这些类并不是将我们自己的功能添加到引擎的唯一方法。

如果我们只需要运行一些代码来响应引擎所做的事情,最简单的方法就是将回调函数绑定到代表该事件的委托。特别是,引擎定义了几组不同的静态委托,我们可以随时绑定到这些委托。这包括 CoreDelegates、CoreUObjectDelegates、GameViewportDelegates、GameDelegates 和 WorldDelegates。

从 Unreal 4.22 开始,引擎还具有“子系统”功能,可以轻松添加模块化功能。 我们所要做的就是定义一个扩展这些子系统类型之一的类,引擎将自动创建与相应对象的生命周期相关联的子系统实例。

例如,插件可能会通过让我们使用在该插件中定义的自定义 GameInstance 来为我们的项目添加自定义功能。

这会起作用,但你会被锁定在那个类中:如果有第二个插件可以做同样的事情,你会很不走运。

使用 GameInstanceSubsystem 而不是自定义 GameInstance 可以解决这个问题,对于模块化、独立的功能来说,这通常是一种更简洁的方法。

以上就是虚幻引擎如何启动游戏的介绍。

相关