【ue4】【架构】游戏框架

游戏框架

UE4 作为一个游戏引擎, 不仅完成了一个游戏引擎的本分, 还替游戏开发者着想,设计了一套用于游戏开发的框架,

这个游戏框架包含以 UObject 为基类的游戏性类

这些游戏性类构造了一个比较完整的游戏世界,只是留下了空白供使用者填充

由此也可以看出, UE4 的开发模式是基于__继承__而, 而非基于组件的 (u3d)

World 之下

ULevel

ULevel 作为游戏中的关卡, 承载着显示所有游戏中的物体 (AActor) 的使命 -- 属于 MVC 模式中的 View

拥有一个存储所有 AActor 的数组 -- Actors

拥有一个关卡蓝图 (ALevelScriptActor) 的引用

拥有一个关卡设置 (AWorldSettings) 的引用

【Tips】 关卡设置由于历史原因才叫 AWorldSettings, 叫 ALevelSettings 更合理

ALevelScriptActor

继承自 AActor

ALevelScriptActor 提供当前关卡级的逻辑实现 -- 属于 MVC 模式中的 Controller

用来实现当前场景中各个 Actor 之间的交互等问题

【Tips】 只与当前关卡相关的动作 (如在某个位置触发事件) 等才用关卡蓝图, 可复用的逻辑应用类蓝图

【Tips】 关卡蓝图应用于本Level逻辑的实现, 而 GameMode 才是整个完整的游戏逻辑的实现所在

【Tips】 是一个看不见的 Actor

AWorldSettings

AWorldSettings 记录着当前关卡中的各种规则属性 -- 属于 MVC 模式中的 Model

用来配置当前关卡的规则, 包括当前所使用的 游戏模式 (GameMode)

【Tips】 AWorldSettings 的名字由于历史原因这么叫,理论上叫 ALevelSettings 更为合理

AActor

属于 MVC 中的 View

AActor 是真实存在于游戏中的物体, 也包括不可见的游戏规则状态信息等幽灵

Actor 是一个树型结构, 一个 Actor 可以有许多 Children Actor

重写 Actor 的 Tick() 函数, 在游戏的主 Tick() 函数里就会通过多态依次调用

  • AActor
    • APawn
      • ACharacter
    • AController
    • APlayerStartActor
    • ALevelScriptActor
    • AInfo
    • AStaticMeshActor
    • ASkeletalMeshActor
    • ACameraActor

Actor 的生命周期基本为 -- BeginPlay -> Tick -> EndPlay

【Tips】 继承自 Actor 的类带有 A 前缀

【Tips】 有些 Actor 是不用显示在游戏中的

APawn

一种特殊的 AActor

可以接受外部的输入 -- InputComponent

可以将外部输入转化为移动及其他 -- MovementComponent

可以由 AController 控制

支持物理碰撞 PhysicsCollision

一些特殊的 APawn

ADefaultPawn -- 默认的 Pawn 方便使用

ASpectatorPawn -- 观战的 Pawn -- 提供一个不带重力的漫游 (USpectatorPawnMovement)

ACharacter -- 人形的 Pawn -- CapsuleComponent (胶囊体) + SkeletalMesh

AController

属于 MVC 中的 Controller

APawn 的控制器 -- 玩家控制的角色本身的行为逻辑

具有动态关联 APawn 的能力 -- Possess 和 UnPossess

Tick 机制

可以在场景中移动

支持事件响应

支持网络同步

【Tips】 可以脱离 APawn 存在 -- 可延迟选择 Posses 哪个 APawn

【Tips】 AController 拥有比 APawn 更长的生命周期 -- 可存放 APawn 消失也想保存下来的数据

APlayerState

属于 MVC 中的 Model

保存一个玩家使用某个 AController 需要的可网络复制的信息

如当前关卡的玩家得分 -- 关卡内的统计数据等

【Tips】 APlayerState 是与 UPlayer 对应的

UActorComponent

UActorComponent 作为一种功能的接口, 提供给 AActor 组装使用

  • UActorComponent
    • USceneComponent
      • UPrimitiveComponent
        • UMeshComponent
          • UStaticMeshComponent
      • UChildActorComponent
    • UInputComponent
    • UMovementComponent

一般一个 AActor 都有一个 USceneComponent 作为 RootComponent

USceneComponent 顾名思义即场景中的组件, 它有两个主要功能

提供 Transform -- 这样 AActor 才有位置

提供嵌套 -- 其他的 UActorComponent 是不支持嵌套的

【Tips】 Actor 不带 Transform 信息 -- 使用 SceneComponent 添加

World 之中

UWorld

UE 的游戏场景由多个关卡 (Level) 组成, 而 UWorld 则是 Level 的集合及管理者 -- MVC 中的 View

存储所有关卡的引用 - Levels 数组里保存当前已经加载过的 - StreamingLevels 数组里保存整个World的 Levels 配置列表 - PersistentLevel 表示主关卡 - CurrentLevel 表示当前关卡 -- 运行时只能指向 PersistentLevel

控制关卡的加载方式 - Persistent -- 一开始就加载进 World - Streaming -- 后续动态加载进 World

【Tips】 UE4 可以采用 World Composition + 原点偏移 + 基于距离的流式加载 的方式实现无缝大地图

AGameMode

游戏模块 即当前游戏的玩法

是整场游戏的逻辑实现地 -- MVC 中的 Controller

登记常用 Class -- Pawn, HUD, PlayerController, GameState, PlayerState, Spectator

生成游戏实体 -- Pawn Controller 的生成和数目管理等

控制游戏进度 -- SetPause, RestartPlayer 等

切换Level时的决策 -- 哪些Actor需要保存到下一个 Level

【Tips】 切换关卡时会重新生成新的 GameMode

【Tips】 应用于游戏本身的玩法, 玩家的行为控制交给 APlayerController 管理

AGameState

保存当前的状态数据 -- 如任务数据等 -- MVC 中的 Model

可以用来在网络中传播同步 (GameMode 中的不能)

保存玩家的状态列表

【Tips】 AGameMode AGameState AWorldSettings APlayerState 都继承自 AInfo -- 一种可以不在场景中显示出来的 AActor

World 之上

UEngine

位于 Engine\Source\Runtime\Engine\Classes\Engine.h

UEngine 是整个引擎的最高统治者, 控制着整个引擎的生命周期

UEngineLoop 配合,控制整个游戏及引擎的 Init Tick Exit

UEngine 负责着对编辑器或者游戏特别重要的系统, 还定义了某些默认的类, 并保存着一些类型的所有对象, 如 WorldList 保存所有的 World

UEngine 只有一个全局变量 GEngine

1
2
3
4
5
// Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp 258
/**
* Global engine pointer. Can be 0 so don't use without checking.
*/
ENGINE_API UEngine* GEngine = NULL;

UEngine 有两个派生类,

分别用于 编辑器 和 游戏运行时

UEditorEngine -- Engine\Source\Editor\UnrealEd\Classes\Editor\EditorEngine.h

UGameEngine -- Engine\Source\Runtime\Engine\Classes\Engine\GameEngine.h

可以新建继承自 UGameEngine 的类, 并在 DefaultEngine.ini 配置文件中指定其为引擎类, 便可通过此类扩展引擎类的功能

UGameInstance

位于 Engine\Source\Runtime\Engine\Classes\Engine\GameInstance.h

在 UE4 的游戏世界里,世界 World 是不止一个的, 不同的 World 有不同的功能

如 Game World 表示游戏运行的场景, PIE 表示在编辑器中运行的游戏场景等

Engine\Source\Runtime\Engine\Classes\Engine\EngineTypes.h 里声明了世界类型的枚举量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Engine\Source\Runtime\Engine\Classes\Engine\EngineTypes.h 796

namespace EWorldType
{
enum Type
{
/** An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels */
None,

/** The game world */
Game,

/** A world being edited in the editor */
Editor,

/** A Play In Editor world */
PIE,

/** A preview world for an editor tool */
EditorPreview,

/** A preview world for a game */
GamePreview,

/** A minimal RPC world for a game */
GameRPC,

/** An editor world that was loaded but not currently being edited in the level editor */
Inactive
};

}

目前来说, UE4 只能同时运行一个 World, 所以需要有一个地方来保存当前的 World 信息, 以及控制 World 的切换,

这个地方就是 WorldContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Engine\Source\Runtime\Engine\Classes\Engine\Engine.h 292

USTRUCT()
struct FWorldContext
{
GENERATED_USTRUCT_BODY()

/**************************************************************/

TEnumAsByte<EWorldType::Type> WorldType;

FName ContextHandle;

/** URL to travel to for pending client connect */
FString TravelURL;

/** TravelType for pending client connects */
uint8 TravelType;

/** URL the last time we traveled */
UPROPERTY()
struct FURL LastURL;

/** last server we connected to (for "reconnect" command) */
UPROPERTY()
struct FURL LastRemoteURL;

UPROPERTY()
UPendingNetGame * PendingNetGame;

UPROPERTY()
class UGameInstance* OwningGameInstance;

/** The PIE instance of this world, -1 is default */
int32 PIEInstance;

/** The Prefix in front of PIE level names, empty is default */
FString PIEPrefix;

/**************************************************************/
// ...

FORCEINLINE UWorld* World() const
{
return ThisCurrentWorld;
}

private:

UWorld* ThisCurrentWorld;
};

GameInstance 就是管理 WorldContext 的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Engine\Source\Runtime\Engine\Classes\Engine\GameInstance.h 110

UCLASS(config=Game, transient, BlueprintType, Blueprintable)
class ENGINE_API UGameInstance : public UObject, public FExec
{
GENERATED_UCLASS_BODY()

protected:
struct FWorldContext* WorldContext;

/** List of locally participating players in this game instance */
UPROPERTY()
TArray<ULocalPlayer*> LocalPlayers;

public:

/** virtual function to allow custom GameInstances an opportunity to set up what it needs */
virtual void Init();

/** virtual function to allow custom GameInstances an opportunity to do cleanup when shutting down */
virtual void Shutdown();

// ...

}

换句话说, GameInstance 比 World 的层次更高

它保存了当前的WorldContext和整个游戏的信息

所以我们用继承 GameInstance 类来处理独立于某些关卡的、应用于整个游戏范围的逻辑

引擎的初始化流程工作

LocalPlayer 的增删管理

重新为某个关卡修改 GameMode

全局的配置、UI、逻辑等

USaveGame

有时候一些全局的变量,往往是需要持久化保存下来的, 而 GameInstance 虽然可以保存在 Level 关卡之外持续存在的变量, 但是对于整个游戏来说需要离线存储的数据, 就不好使用 GameInstance 来保存了。

所以 USaveGame 的作用就类似于离线存档

我们只要继承一下 USaveGame, 然后添加我们想要离线存储下来的字段就可以了

USaveGame 继承自 UObject 的序列化机制自然就会将我们的字段序列化保存下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Engine\Source\Runtime\Engine\Classes\GameFramework\SaveGame.h

UCLASS(abstract, Blueprintable, BlueprintType)
class ENGINE_API USaveGame : public UObject
{
/**
* @see UGameplayStatics::CreateSaveGameObject
* @see UGameplayStatics::SaveGameToSlot
* @see UGameplayStatics::DoesSaveGameExist
* @see UGameplayStatics::LoadGameFromSlot
* @see UGameplayStatics::DeleteGameInSlot
*/

GENERATED_UCLASS_BODY()
};

可以看到, USaveGame 实际上只是一个 UObject 的空壳, 而所以的存档接口都暴露在 UGamepalyStatic 类里

【Tips】 UGameplayStatic 类是一个蓝图函数库, 说白了就是一堆可让蓝图调用的静态函数

UPlayer

玩家即输入。

之所以把 UPlayer 放在和 UGameInstance USaveGame 一起, 是因为很多时候, 游戏的输入模式是脱离于场景的, 可能多个场景切换来切换去, 而输入模式是不变的

所以 UPlayer 应该也是立足于 World 之上的存在, 这也是为什么 UPlayer 是继承自 UObject 而非 Actor 的原因吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Engine\Source\Runtime\Engine\Classes\Engine\Player.h

UCLASS(MinimalAPI, transient, config=Engine)
class UPlayer : public UObject, public FExec
{
GENERATED_UCLASS_BODY()

/** The actor this player controls. */
UPROPERTY(transient)
class APlayerController* PlayerController;

// ...

public:

/**
* Dynamically assign Controller to Player and set viewport.
*/
ENGINE_API virtual void SwitchController( class APlayerController* PC );

/**
* Gets the player controller in the given world for this player.
*/
ENGINE_API APlayerController* GetPlayerController(UWorld* InWorld) const;
};

UPlayer 有两个派生类

ULocalPlayer -- 本地玩家 -- 控制 APlayerController 的生成

UNetConnection -- 远程连接玩家

APlayerController 即为 UPlayer 在 游戏场景中的话事者, 但是 APlayerController 的输入则是在它接受 UPlayer 里产生的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Engine\Source\Runtime\Engine\Private\PlayerController.cpp

void APlayerController::SetPlayer( UPlayer* InPlayer )
{
// ...

// Set the viewport.
Player = InPlayer;
InPlayer->PlayerController = this;

// initializations only for local players
ULocalPlayer *LP = Cast<ULocalPlayer>(InPlayer);
if (LP != NULL)
{
// Clients need this marked as local (server already knew at construction time)
SetAsLocalPlayerController();
LP->InitOnlineSession();
InitInputSystem(); // 此句为输入的初始化
}
else
{
NetConnection = Cast<UNetConnection>(InPlayer);
if (NetConnection)
{
NetConnection->OwningActor = this;
}
}

UpdateStateInputComponents();

// notify script that we've been assigned a valid player
ReceivedPlayer();
}

上面的代码说明 APlayerController 调用 SetPlayer() 调用的 InitInputSystem() 才是其可以获得输入的源头

【Tips】一般不在 UPlayer 里写逻辑, 但我们仍然可以继承它并这么做。

游戏启动流程

UE4 为了实现跨平台, 使用一个 GuardedMain() 来封装不同平台上的 main() 函数

GuardedMain() 位于 Engine\Source\Runtime\Launch\Private\Launch.cpp

大体的启动流程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int32 GuardedMain() 
{
// Init
PreInit();
if(GIsEditor) { GEngine = NewObject<UEditorEngine>(); }
else { GEngine = NewObject<UGameEngine>(); }
GEngine.Init();
PostInit();

// MainLoop
while(!GIsRequestingExit) { PreTick(); Tick(); PostTick(); }

// Exit
Exit();

}

void GEngine::Init()
{
// Create GameInstance
GameInstance = NewObject<UGameInstance>(this, GameInstanceClass);

}