【ue4】【总结】阶段学习总结

架构

UE4 从源代码的组织上来看是由__模块 (Module) __组成的

从功能上讲, UE4 对 编辑器(Editor) 和 游戏性(Runtime) 都提供了支持

对于编辑器来说, UE4 提供了丰富的编辑界面,

角色编辑器 (Persona) 动画编辑器 (AnimationEditor) 特效编辑器 (Cascade和Niagara) 材质编辑器 脚本编辑器 (蓝图) 等等

同时还可以自定义插件, 以及使用开放的蓝图节点工具和UI工具 Slate 来定制自己的编辑器界面

对于游戏性来说, UE4提供了比较完整的游戏性类, 以实现对整个游戏生命周期的控制

这些游戏性类从宏观上看也是遵循 MVC 模式的

工程目录

Build -- 编译过程中产生的 可执行文件 及其他

Config -- 配置文件 -- 控制引擎行为 -- 游戏目录下的 Config 配置会覆盖引擎的

Content -- 资源文件 模型 地图 动画 蓝图 -- .uasset (地图为 .umap)

Source -- 源码

Saved -- 自动保存的内容 -- 配置.ini 日志(崩溃日志)

Plugins -- 插件 -- 与源代码内容类似, 但可能包含 Content

Shaders -- 着色器

Module -- 模块

每一个 UE 项目, 包括 UE 引擎项目本身,都是由一个或多个__模块(Module)__组成

且至少有一个主模块, 其他模块都是扩展模块

在编译后, 每一个模块都是一个单独的 dll (也有lib), 供运行时可选择性地相互链接

【Tips】 每一个插件也是由一个或多个模块组成

模块的结构

每一个模块大概包含以下文件

Classes -- 包含所有游戏性类的 .h 文件

Private -- 包含所有 .cpp 文件 -- 包括游戏性类的和模块本身的

Public -- 包含模块本身的头文件

ModuleName.Build.cs -- 包含模块的编译信息

【Tips】 Classes 文件夹不是必须的 【Tips】 每个模块至少有一个 .h 和一个 .cpp

模块的依赖

模块的 Build.cs 文件里可以指定其所依赖的其他模块 -- 定义 ModuleRules

每一个模块也可以在其他模块的 Build.cs 里被其他模块所依赖

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
// CPTPS.Build.cs
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class CPTPS : ModuleRules
{
public CPTPS(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

PublicDependencyModuleNames.AddRange(
new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"HeadMountedDisplay"
});

PrivateDependencyModuleNames.AddRange( new string[] { /* ... */ });

PrivateIncludePaths.AddRange( new string[] { /* ... */ });

PrivateIncludePathModuleNames.AddRange( new string[] { /* ... */ });

}
}

游戏项目中的 .Target.cs (一般有两个, 可自己新增) 声明了游戏和编辑器下需要如何将不同模块链接成目标代码

它定义了 TargetRules

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CPTPS.Target.cs
// Copyright 1998-2018 Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;
using System.Collections.Generic;

public class CPTPSTarget : TargetRules
{
public CPTPSTarget(TargetInfo Target) : base(Target)
{
Type = TargetType.Game;
ExtraModuleNames.Add("CPTPS");
}
}

游戏项目中的模块必须在 .uproject 文件里统一定义才能被 UE 识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// CPTPS.uproject

{
"FileVersion": 3,
"EngineAssociation": "{EBDB0310-437E-3F3E-D665-17822D8A4B5C}",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "CPTPS",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "BlueprintGraph",
"Type": "Runtime",
"LoadingPhase": "Default"

}
]
}

【Tips】 插件中的模块也要在插件的 .uplugin 文件里统一定义 【Tips】 模块大致分为 运行时模块 (Runtime) 和 编辑器模块 (Editor)

模块的建立

当新建一个模块时,我们需要手动创建 ModuleName.h ModuleName.cpp ModuleName.Build.cs 三个文件

并按照上面的规则建立好模块间的依赖关系

最重要的是, 每一个模块必须有一个实现了 IModuleInterface 的类

并将其作为暴露给外部使用的接口(指针)

可以看出, 这个接口是用来操作模块本身整体的, 即加载, 初始化, 卸载及一些标记状态量等

除此之外,为了满足每个模块的dll (或lib) 能被顺利地找到, 即把这个接口暴露出来给其他地方使用, 需要声明一些样板宏, 即 IMPLEMENT_MODULE

IMPLEMENT_MODULE(FDefaultModuleImpl, ModuleName) // 一般的模块声明 FDefaultModuleImpl为空实现

IMPLEMENT_MODULE(FModuleName, ModuleName) // 有自己的初始化逻辑的模块 ModuleNameModule为子类

IMPLEMENT_GAME_MODULE(FModuleName, ModuleName) // 包含游戏逻辑的模块

IMPLEMENT_PRIMARY_GAME_MODULE(FModuleName, ModuleName, "ModuleName") // 当前项目的主模块

【Tips】 必须声明一个主模块 【Tips】 一般用 FModuleName 为类名继承 IModuleInterface, 而 ModuleName 则为实际模块名

模块的引用

在其他地方引用某个模块,只需要调用 FModuleManager 类的 LoadModule() 方法即可

1
FLevelEditorModule& LEModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>("LevelEditor");

【Tips】 LoadModule() 不论是动态链接还是静态链接, 都会通过模块名称正确地找到模块的接口。 【Tips】 LoadModuleChecked() 会检查返回的模块接口是否合法

游戏框架

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);

}

对象系统

UE4 的对象系统是实现反射系统的基础

而反射系统又支撑着 GC 蓝图编辑器 网络通信 等各个模块

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
// Engine\Source\Runtime\Core\Public\UObject\UObjectHierarchyFwd.h

class UObjectBase;
class UObjectBaseUtility;
class UObject;
class UField;
class UEnum;
class UProperty;
class UByteProperty;
class UUInt16Property;
class UUInt32Property;
class UUInt64Property;
class UInt8Property;
class UInt16Property;
class UIntProperty;
class UInt64Property;
class UBoolProperty;
class UFloatProperty;
class UDoubleProperty;
class UObjectPropertyBase;
class UObjectProperty;
class UClassProperty;
class UInterfaceProperty;
class UWeakObjectProperty;
class ULazyObjectProperty;
class USoftObjectProperty;
class USoftClassProperty;
class UNameProperty;
class UStructProperty;
class UStrProperty;
class UTextProperty;
class UArrayProperty;
class UDelegateProperty;
class UMulticastDelegateProperty;
class UMapProperty;
class USetProperty;
class UEnumProperty;
class UStruct;
class UFunction;
class UClass;
class UScriptStruct;
class FLinker;
class FLinkerLoad;
class FLinkerSave;
class UPackage;
class USystem;
class UTextBuffer;
class UPackageMap;
class UObjectRedirector;

UObject

是 UE 所有对象的基类

对象标记 ObjectFlags

类型信息 ObjectClass

对象引用管理 --增删改查

序列化

GC

UE4 中有一个全局的列表 GUObjectArray 用来保存当前引擎实例中所有继承自 UObject 的对象, 实现了一个全局的对象管理。

序列化

序列化是指将对象转换成__字节流__, 从到存储对象 (或传输对象) 到其他地方 (内存或磁盘等)

用于保存对象的 结构 或 状态

UE4 使用 访问者模式 (Vistor Pattern) 实现序列化机制

FArchive 作为 Vistor, 是真正进行序列化操作的地方

UObject 的 Serialize(FArchive& Ar) 作为 Client 供 Vistor 访问

【Tips】 FArchive 通过重载 << 来实现对 UObject 中数据的访问

GC

触发条件

入口函数 UEngine::ConditionalCollectGarbage() 每帧都会执行

默认情况下每 60 秒(可修改)执行 TryCollectGarbage 尝试进行一次 GC

主动触发函数

UEngine::ForceGarbageCollection() -- 下一帧将强制执行GC

UEngine::DelayGarbageCollection() -- 下一帧一定不执行GC

执行流程

第一步, 可达性检查

维护一个可达列表 -- RootSet

全置为不可达

从根节点出发遍历所有对象

如果是 PendingKill,则不加入RootSet, 否则加入

【Tips】 省略了一些细节

第二步, 对象销毁

所有不可达的对象, 都要被销毁

销毁的过程即是回收内存的过程

这个过程是 分时分帧 进行处理的 -- 使用了许多全局变量保存状态

UProperty

原子型成员变量

描述了成员变量在类内存中的偏移量, 字节大小, 类型等信息

UStruct

复合型成员变量

同 UProperty 作用类似, 但是用更多的字段以描述复合型的成员变量

其子类中最重要的就是 UClass 和 UFunction

UClass

用于描述一个类的反射信息

不仅可以用来描述 原生的c++类 (NativeClass), 也可以用来描述蓝图使用的类 (BluprintClass)

UFunction

用于描述一个 UObject 对象的成员函数

包括 Native Function 和 Script Function

函数名

参数

返回值

局部变量

代码块

【Tips】 UScriptStruct 为 c++ 的 struct

UHT 和 UBT

UHT (UnrealHeaderTool) 会根据我们在声明类型时打的标签宏解析并生成对的的 c++ 反射代码

这些生成的代码文件大部分名为 xx.generated.hxx.gen.cpp

位于 Intermediate\Build\Win64\UE4Editor\Inc 文件夹下

而 UBT (UnrealBuildTool) 会首先调用 UHT 来生成反射代码, 然后根据再逐个编译 (选择不同平台配置进行编译) 每个 Module 并根据每个 Module 的 Build.csTarget.cs 来处理依赖关系

渲染系统

UE4 的渲染器是在其自身的渲染线程中执行的

这个渲染线程通常落后于游戏线程1到2帧

所以有一些类会将游戏线程上的状态信息连接到渲染线程中来 (通常以F开头)

FScene -- 表示游戏场景, 是 UWorld 的渲染器版本

FPrimitiveSceneProxy -- UPrimitiveComponent 的渲染器版本, 为渲染线程映射 UPrimitiveComponent 状态

FPrimitiveSceneInfo -- FPrimitiveSceneProxy 的状态, 只存在于渲染器

FViewInfo -- 当前屏幕视图的渲染器 对应游戏模块中的 FSceneView

FSceneViewState -- FViewInfo 的状态, 与游戏中 UPlayer 一一对应

FLightSceneProxy 与 FLightSceneInfo -- 跟 Primitive差不多

FPrimitiveSceneProxy 存在于游戏模块中,有两个 API 比较重要

DrawDynamicElements -- 在与之相关的任何 Pass 中绘制该拥有动态相关性的代理物体

DrawStaticElements -- 在与游戏线程相连时绘制该拥有静态相关性的代理物体

这也说明 UE4 对几何体的渲染是分了 Static 和 Dynamic 分别来处理的

  • Static Render
    • FPrimitiveSceneProxy 被放入场景时, 会调用其 DrawStaticElements 来收集 FStaticMeshElements, 然后创建对应的 Draw Policy 放到 FSceneDraw List 里去。
  • Dynamic Render
    • 在判定 FPrimitiveSceneProxy 可见后即调用 DrawDynamicElements() 来收集 FMeshElements

【Tips】 Proxy 的意思为渲染模块在游戏模块中出现的代理, 实际存在于引擎模块中, 而非渲染模块中

材质在渲染时也需要连接游戏线程和渲染线程的接口

FMaterial -- 材质的接口, 提供属性的查询等 -- 纯虚类

FMaterialResoruce -- FMaterial 的一个具体实现, 对应 UMaterial

FMaterialRenderProxy -- 提供给渲染线程使用的代理

渲染流程

=============================================

【Pass_0】 Depth Only Pass 【Pass_1】 Base Pass 【Pass_2】 Issue Occlusion Queries 【Pass_3】 ShadowMap 【Pass_4】 Lighting 【Pass_5】 Fog 【Pass_6】 Post Processing

============================================= 【Tips】 基于 Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.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
34
35

class FDeferredShadingSceneRenderer : public FSceneRenderer
{
public:

// Pass_0
bool RenderPrePassViewDynamic(FRHICommandList& RHICmdList, const FViewInfo& View, const FDrawingPolicyRenderState& DrawRenderState);
bool RenderPrePassView(FRHICommandList& RHICmdList, const FViewInfo& View, const FDrawingPolicyRenderState& DrawRenderState);

// Pass_1
bool RenderBasePass(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, IPooledRenderTarget* ForwardScreenSpaceShadowMask);
bool RenderBasePassView(FRHICommandListImmediate& RHICmdList, FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, const FDrawingPolicyRenderState& InDrawRenderState);

// Pass_2
void RenderOcclusion(FRHICommandListImmediate& RHICmdList);
void FinishOcclusion(FRHICommandListImmediate& RHICmdList)

// Pass_3
void RenderShadowDepthMaps(FRHICommandListImmediate& RHICmdList);
void RenderShadowDepthMapAtlases(FRHICommandListImmediate& RHICmdList);

// Pass_4
virtual void Render(FRHICommandListImmediate& RHICmdList) override;
void RenderLights(FRHICommandListImmediate& RHICmdList);

// Pass_5
virtual void Render(FRHICommandListImmediate& RHICmdList) override;
bool RenderFog(FRHICommandListImmediate& RHICmdList, const FLightShaftsOutput& LightShaftsOutput);

// Pass_6
virtual void Render(FRHICommandListImmediate& RHICmdList) override;


}

【Pass_0】 Depth Only Pass

深度处理阶段

绘制 Depth 到 Depth_Buffer

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
/**
* Renders the scene's prepass for a particular view
* @return true if anything was rendered
*/
bool RenderPrePassView(FRHICommandList& RHICmdList, const FViewInfo& View, const FDrawingPolicyRenderState& DrawRenderState)
{
// ...
{ bDirty |= RenderPrePassViewDynamic(); }
return bDirty;
}

/**
* Renders the scene's prepass for a particular view
* @return true if anything was rendered
*/
bool RenderPrePassViewDynamic(FRHICommandList& RHICmdList, const FViewInfo& View, const FDrawingPolicyRenderState& DrawRenderState)
{
foreach (mesh)
{
// 判断可见性代码
// ...
if (可见) { FDepthDrawingPolicyFactory::DrawDynamicMesh(); }
}
return true;
}

【Tips】 DrawingPolicyFactory 为绘制规则的工厂类

【Pass_1】 Base Pass

绘制 不透明的 和 Masked Material属性的 Mesh

填充 G_Buffer

UE4 中的 G_Buffer 结构 (来知乎专栏_虚幻4渲染编程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Engine\Source\Runtime\Renderer\Private\BasePassRendering.cpp

/**
* Renders the scene's base pass
* @return true if anything was rendered
*/
bool RenderBasePass(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, IPooledRenderTarget* ForwardScreenSpaceShadowMask)
{
// ...
bDirty |= RenderBasePassView(RHICmdList, View, BasePassDepthStencilAccess, DrawRenderState);
return bDirty;
}

/** Renders the basepass for a given View. */
bool RenderBasePassView(FRHICommandListImmediate& RHICmdList, FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, const FDrawingPolicyRenderState& InDrawRenderState)
{
bDirty |= RenderBasePassStaticData(RHICmdList, View, DrawRenderState);
RenderBasePassDynamicData(RHICmdList, View, DrawRenderState, bDirty);
return bDirty;
}

此时 Shader文件可从下面代码

1
2
3
4
5
6
7
8
// Engine\Source\Runtime\Renderer\Private\BasePassRendering.cpp 113

#define IMPLEMENT_BASEPASS_PIXELSHADER_TYPE(LightMapPolicyType,LightMapPolicyName,bEnableSkyLight,SkyLightName) \
typedef TBasePassPS<LightMapPolicyType, bEnableSkyLight> TBasePassPS##LightMapPolicyName##SkyLightName; \
IMPLEMENT_MATERIAL_SHADER_TYPE(template<>,TBasePassPS##LightMapPolicyName##SkyLightName, \
TEXT(" /Engine/Private/BasePassPixelShader.usf"), \
TEXT("MainPS"),SF_Pixel);

/Engine/Private/BasePassPixelShader.usf 696

1
2
3
4
5
6
7
8
9
10
11
// is called in MainPS() from PixelShaderOutputCommon.usf
void FPixelShaderInOut_MainPS(
FVertexFactoryInterpolantsVSToPS Interpolants,
FBasePassInterpolantsVSToPS BasePassInterpolants,
in FPixelShaderIn In,
inout FPixelShaderOut Out)
{
// ...
// Fill G_Buffer
// ...
}

这就是 Bass Pass 的 着色器入口了

【Pass_2】 Issue Occlusion Queries

遮挡查询, 为下一帧的可见性判断提供信息

ue4 的遮挡查询是对包围盒进行深度测试来判断的

1
2
void RenderOcclusion(FRHICommandListImmediate& RHICmdList) {}
void FinishOcclusion(FRHICommandListImmediate& RHICmdList) {}

【Pass_3】 ShadowMap

阴影计算

1
2
void RenderShadowDepthMaps(FRHICommandListImmediate& RHICmdList) {}
void RenderShadowDepthMapAtlases(FRHICommandListImmediate& RHICmdList) {}

【Pass_4】 Lighting

光照计算

首先预处理 延迟贴花 SSAO 等, 然后再进行光照计算

UE4 的光照计算采用 TBDR (Tiled Based Deferred Rendering)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
// ...
// Pre-lighting composition lighting stage
// e.g. deferred decals, SSAO
// ...

// Render lighting.
RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_Lighting));
RenderLights(RHICmdList);
RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_AfterLighting));

// Pre-lighting composition lighting stage
// e.g. LPV indirect
// ...
}

然后是处理光照的着色器入口

1
2
3
4
5
6
7
8
9
10
// Engine\Shaders\Private\TiledDeferredLightShaders.usf 55

[numthreads(THREADGROUP_SIZEX, THREADGROUP_SIZEY, 1)]
void TiledDeferredLightingMain(
uint3 GroupId : SV_GroupID,
uint3 DispatchThreadId : SV_DispatchThreadID,
uint3 GroupThreadId : SV_GroupThreadID)
{
// ...
}

【Pass_5】 Fog

雾计算

非透明表面逐像素计算 Fog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
// ...
// Draw fog.
if (ShouldRenderFog(ViewFamily))
{
SCOPE_CYCLE_COUNTER(STAT_FDeferredShadingSceneRenderer_RenderFog);
RenderFog(RHICmdList, LightShaftOutput);
ServiceLocalQueue();
}
}

/** Renders the scene's fogging. */
bool RenderFog(FRHICommandListImmediate& RHICmdList, const FLightShaftsOutput& LightShaftsOutput)
{
// ...
}

【Pass_6】 Post Processing

后处理效果

1
2
3
4
5
6
7
8
9
10
11
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
// ...

// Resolve the scene color for post processing.
ResolveSceneColor(RHICmdList);

GetRendererModule().RenderPostResolvedSceneColorExtension(RHICmdList, SceneContext);

CopySceneCaptureComponentToTarget(RHICmdList);
}

【Tips】 UE4 移动端似乎只有 Forward Rendering

应用

插件

UE4 支持插件的独立编译和自由移植 -- 即插即用

一个插件可以渗透 Editor 的地方

菜单栏

工具栏

右链菜单项

模式里的预放置体

Detail 面板

可自定义自己的编辑器

插件也是由 模块 (Module) 组成的

插件的项目文件并非 .uproject 而是 .uplugin

插件也分为 引擎插件 (附在引擎源文件中) 和 游戏插件 (附在游戏源文件中)

插件可以同时拥有 Eidtor 类型的 Module 和 Runtime 类型的 Module

插件可以引用任何模块 (引擎的和游戏的), 但是引擎插件或者编辑器插件最好不要引用游戏的模块 (无法移植)

一个插件也可以引用另一个插件, 但是最好不要这么做 (也不好移植)

开发 Editor 使用的插件可以使用 Slate 作为 UI 开发

Slate

Slate 是一种完全自定义的、平台无关的用户界面架构

UE的所有可视化工具都是基于Slate UI 编写的, 而在游戏中推荐使用的 UMG UI 则是 Slate 的可视化编辑接口, 即在 Slate 的基础上又封装了一层可视化的接口。

Slate 的实质是利用 宏和C++运算符重载 来实现的布局语法

Slate 是基于事件和委托的, 也是一种 IMGUI

Slate 为了做某件事而产生的工具集称作 ToolKit, 由下面元素组成

FSlateApplication -- Slate 的最高层管理器 -- 管理所有顶层窗口

SWindow -- 顶层窗口 -- 针对不同平台对应 FGenericWindow

Tab -- 标签页 -- 通过 TabManager 管理

Dock -- 给 Tab 提供悬浮功能

除此之外, Slate 还为窗口准备了部局 Layout

Stack 类布局 -- 多个 Tab 共享一个区域, 通过 Tab 头切换

Splitter 类布局 -- 可对本身的区域进行划分

【Tips】 Layout 可以通过配置表来设置布局

自定义编辑器

Slate 可供用户像蓝图结点那样定义出自己的结点, 从而实现一个类似于蓝图的自制编辑器

UEdGraphNode

蓝图中的一个包含 Input 和 Output 的函数, 或者材质编辑器中的一个节点, 都是一个 GraphNode

Pin

节点的 引脚, 通过 Pin 可以连接两个节点

一个 Node 有多个 Pin, 总体来说 Pin 分 Input 和 Output 两种

Pin 只能由 Output 连 Input

UEdGraph

所有的 Node 合成一张图表即为 UEdGraph, 如一张蓝图, 一个材质等

UEdGraphSchema

每个 UEdGraph 都包括一个 UEdGraphSchema

UEdGraphSchema 表示制表, 决定当前的 Graph 创建节点的规则

它也包括一系列的 FEdGraphSchemaAction, 对应不同结点的创建

特效

【ue4】【使用】特效系统Cascade与Niagara

【ue4】【原理】特效系统Cascade与Niagara

动画

动画资源

Animation Sequence -- 动画序列

Animation Composite -- 拼接多个动画序列 -- 没什么卵用

Blend Space -- 混合空间 -- 可由参数控制动作在不同的维度融合,

Aim Offset -- 偏移目标 -- 常用于人物瞄准时的各个角度

Montage -- 动画蒙太奇 -- 支持动画的连接、分段、循环等等

其中蒙太奇动画可设置 Slot 以供在动画状态机里留下的 Slot Pose 调用, 但是其调用通常由 Montage 相关的 API 主动调用, 适用于释放主动技能等。

【Tips】 BlendSpace 和 AimOffset 和 Montage 都可有一至多个动画序列制作而成

动画重定向

类似于 u3d 中 Humanolid 的 Retargeting

使用相同骨骼资源的 Skeletal Mesh 可以设置 Retargeting 选项

Animation -- 骨骼的 Transform 信息来自动画文件

Skeleton -- 骨骼的 Transform 信息来自骨骼文件

Animation Scaled -- 骨骼的 Transform 信息来自动画文件, 但根据骨骼进行缩放

Animation Relative -- 不知道

【Tips】 一般将 Root、IK 和 Weapon 骨骼设置成 Animation 【Tips】 一般将 Pelvis骨骼 (或根节点) 设置成 Animation Scaled

对于使用不同骨骼资源的 Skeletal Mesh, 也可以使用 Rig 文件将两者进行映射

动画逻辑

动画的逻辑实现在 动画蓝图 (Animation Bluprint) 里

动画蓝图 (ABP) 有两种比较重要的图表 -- 事件图表和动画图表

事件图表将外部驱动动画运行的数值(如人物速度)或状态(如人物跳)传入动画蓝图里

而动画图表里则是由动画状态机驱动而获得不同的姿势 Pose

状态机

类似于 u3d 的 Animator Controller

动画事件

动画事件 (Animation Notify) 可以在 动画序列 或 Montage 中运行设置

也可在动画状态机状态跳转线上进行设置

动画事件可在事件图表中绑定触发时的事件

【Tips】有时候动画可能会被忽然打断,注意在此情况下在动画序列或 Montage 还未播的地方设置的 Notify 是不会被响应的

动画逻辑重用

Child Animation Blueprint

主要用于重用所有动画逻辑

仅能替换不同的动画文件

Sub Animation Blueprint

可分解动画逻辑到不同的蓝图中去

但是要注意的是

一个动画子蓝图只能在状态机中被引用一次, 即不能出现在两个地方

父动画蓝图获得不到子动画蓝图的实例, 不能直接调用子动画蓝图的函数和变量

似乎只有在动画图表中才可以将父动画蓝图中的数据赋值给子动画蓝图, 前提是子动画蓝图中暴露出了这个变量

所以如果想要分离比较复杂的逻辑, 如比说

想让父动画蓝图也能调用子动画蓝图的函数

那么就需要自己设计方案了, 此处有一种方案为

子动画蓝图可以获取到父动画蓝图的实例 (通过其所属AActor间接获得)

那么子动画蓝图就可以访问父动画蓝图的函数

在父动画蓝图里设置函数执行的标记位 (bool型), 通过动画状态机付给子动画蓝图

子动画蓝图在 Tick 中不断监听标记位, 一但标记位为 true 便执行函数, 同时调用父动画蓝图的函数清除标记位, 保证一次 true 只执行一次函数

动画融合

前面讲到的 BlendSpace AimOffset Montage 也属性动画融合

Montage

Montage 常用 API

除此之外, 在动画图表里还有一些用于动画融合的节点

Blend 节点

根据 Alpha 的值混合 姿势 A 和 姿势 B

1
混合姿势 =  A* Alpha + B * (1 - Alpha)

Blend Poses By 节点

根据 bool 或 int 值选择执行哪一个姿势

其实是一种选择结构, 并无太多融合的意味

Layered Blend By Per Bone 节点

基于每个骨骼的分层骨骼混合

这种混合方式可以只对某些骨骼进行混合,其他骨骼保持原姿势。

对哪些骨骼进行混合,以及混合的权重,都可以由我们自己决定。

比较典型的用法是在奔跑的时候切换武器,就可以将上半身用 站立时切换武器的动画 进行混合, 而下半身保持原奔跑姿势不变。

【ue4】【使用】动画融合_分层骨骼混合

Apply Additive

暂时不懂-。-

渲染

材质编辑器

官链_材质编辑器用户指南

Exp -- 旋转 UV

材质表达式 -- 即材质图表中的节点, 用来实现不同材质效果的逻辑

材质函数 -- 材质图表中可重用的节点集合 -- 用以封装可重用的功能

HLSL Code

我们可以打开 HLSL Code 面板查看整个材质的 HLSL 代码

这个代码是位于 Engine\Shaders\Private\MaterialTemplate.ush 的一个 Shader 模板, 而我们材质中的每一个节点, 都是一些 HLSL 代码片段

当我们编译的时候, 每个节点的 HLSL 代码都会被编译到这个 Shader 模块里, 生成最终的完整 Shader 代码

由此可见, 我们也可以直接修改这个模板, 如直接将自己的 .ush 文件 include 进去

我们可以通过 Custom 节点来写我们自己的 HLSL 代码

Custom 结点可对任意数量的输入执行操作, 并输出操作结果 -- 可能会影响效率 (阻止常量折叠)

例如 我们将一张 贴图 与 纯红色 进行混合

就可以新建一个 CustomNode, 然后输入以下代码

1
2
3
4
5
6
float3 frag_color;

float3 tex_color = Texture2DSample(tex, texSampler, UV);
frag_color = alpha * tex_color + (1.0 - alpha) * color;

return frag_color;

需要注意的是, CustomNode 只支持代码块和参数的传递, 并不支持函数的传递, 但是我们可以通过修改 CustomNode 的源码使其支持函数的引用, 这样做的好处是我们可以在别的地方写 shader, 再通过配置函数表引用过来。

自定义 Shading Model

对于每一个材质而言, 都有一个枚举量 Shading Model 可供选择

这个枚举量预置了当前的 Shader 模型, 即一些宏定义状态的选择

这会导致材质的主节点会有一些 Pin 被禁用 或激活

那么如果我们按照 UE 的这个枚举量的工作逻辑, 给它再加一个枚举状态, 作为我们自定义的 Shading Model, 并使用额外的 Pin 来处理我们自已定义的 Shader 了

Shader

Global Shader

全局着色器是对固定几何体 (如屏幕四边形) 执行操作但不需要与材质进行交互的着色器

如阴影过滤, 全局后处理, 全局雾 等, 都可以用全局着色器来渲染

【Tips】 一个全局着色器类型在内存中只能有一个

要想实现全局着色器, 必须实现一个继承自 FGlobalShader 的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FMyGShader : public FGlobalShader
{
public:
FMyGShader(){}
FMyGShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FGlobalShader(Initializer)
{
SimpleColorVal.Bind(Initializer.ParameterMap, TEXT("SimpleColor"));
}

void SetParameters(FRHICommandListImmediate& RHICmdList, const FLinearColor &MyColor)
{
SetShaderValue(RHICmdList, GetPixelShader(), SimpleColorVal, MyColor);
}

static bool ShouldCache(EShaderPlatform Platform) { return true; }
virtual bool Serialize(FArchive& Ar) override;

private:
FShaderParameter SimpleColorVal;
}

然后分别再继承 FMyGShader 实现 顶点着色器类 和 像素着色器类

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
class FMyGShaderVS : public FMyGShader
{
DECLARE_SHADER_TYPE(FMyGShaderVS, Global);

public:

/** Default constructor. */
FMyGShaderVS() {}

/** Initialization constructor. */
FMyGShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FLensDistortionUVGenerationShader(Initializer) { }

};

class FMyGShaderPS: public FMyGShader
{
DECLARE_SHADER_TYPE(FMyGShaderPS, Global);

public:

/** Default constructor. */
FLensDistortionUVGenerationPS() {}

/** Initialization constructor. */
FLensDistortionUVGenerationPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer) : FLensDistortionUVGenerationShader(Initializer) { }

};

除此之外还要写好着色器中的内容到 .usf 中, 并 使用 IMPLEMENT_SHADER_TYPE 与之关联

1
2
IMPLEMENT_SHADER_TYPE(, FMyGShaderVS, TEXT(".../MyGShader.usf"), TEXT("MainVS"), SF_Vertex)
IMPLEMENT_SHADER_TYPE(, FMyGShaderPS, TEXT(".../MyGShader.usf"), TEXT("MainPS"), SF_Pixel)

Material Shader

材质着色器是特定于某些过程的着色器

需要访问材质的某些属性 -- 即必须针对每个材质进行编译

但是并不需要访问任何网格 (Mesh) 属性

建立 Material Shader 的过程和建立 Global Shader 的过程类似, 不过要将 FGlobalShader 换成 FMaterialShader

Mesh Material Shader

网格材质着色器不仅依赖于特定的材质, 而且依赖于网格的类型

所以它必须针对每一个 材质和顶点工厂的组合 进行编辑

Post Process

UE4 提供了内置的后处理 (Post Process) 阶段 -- 渲染管线的最后一个阶段

UE4 提供的后处理效果

HDR 与 ToneMapping (色调映射 HDR->LDR)

SSAO

MotionBlur -- 动态模糊

AntiAliasingQuality -- 搞锯齿 -- FSAA -- FXAA TAA MSAA

Depth of Field -- 景深

Eye Adaption -- 人眼自适应 -- 自动曝光

Lens Flare -- 镜头眩光

SSR -- 屏幕空间反射 -- 解决动态反射问题

【Tips】 UE 的 SSAO 是在 DeferredShading 中做的, 所以 手机上没有

配置方式

UE4 的后处理配置方式有2种

渲染选项 -- 后处理的渲染选项在 Scalability.ini 里 -- r.xxx -- 全局的默认后处理设置

后处理体积 PostProcessVolomn -- PPV

只有在后处理体积内的空间才使用当前 PPV 的后处理配置

即仅当游戏时 Camera 移动到某个 PPV 内部时, 该 PPV 的配置才被启用

【Tips】 如果Camera 位于多个 PPV 中, 则根据优先级进行线性插值

后处理材质

可以在 PPV 中指定后处理材质来实现自定义的后处理效果

将一个材质的 Materail Domain 设置为 Post Porcess 便会将其设置为后处理材质

此时输出节点只能连接 Emissive Color

而输入节点则可以定义一个材质节点 SceneTexture, 并从中选择当前屏幕的各种 buffer

【Tips】 UE 官方并不推荐使用 后处理材质 -- 影响效率 -- 尽可能使用ppv提供的内置功能

及其他

UMG

UMG 作为官方推荐的 UI 系统, 是在 Slate 上又封装了一层

支持可视化的设计界面, 支持 UI 的复用

在控件蓝图 (Widget Blueprint) 里进行编辑

一般只关注于 UI 的显示, 至于事件的绑定及逻辑上的交互, 大都放在游戏逻辑里去写

如果当前的 UI 与 地图的切换无关, 那最好就放到 World 之上那层去实现逻辑

在 AActor 蓝图里, 也可以给 WidgetComonent 指定 控件父类, 从而实现3D场景中交互的UI

UE4 内置了 寻路体积 (NavMeshBoundsVolume) 来实现某个体积区域的 NavMesh 覆盖

可以按 P 键查看当前 NavMesh 覆盖的地方

可以使用 NavLink 增加跳跃点

我理解的 NavMesh 寻路的原理无非是生成寻路网格,然后跑A*

行为树

BehaviorTree -- 行为树

BlackBoard -- 黑板 -- 用于共享数据

Decorator -- 先决条件

Service -- 服务

Task -- 任务 -- 执行节点 -- 行为树中的叶子节点

AIController -- AI控制器 -- 联系行为树和黑板并运行行为树的地方

Root 节点

根节点, 只能有一个附着点 (孩子节点)

Composites

Selector 节点 -- 从左到右在其子项中运行,直到其中一个成功则返回

Sequence 节点 -- 从左到右在其子项中运行,直到其中一个失败则返回

Simple Parallel 节点 -- 可使单个主任务节点在整颗行为树旁执行

Service

附着在 Composites 节点上 (之后)

常用于检查和更新黑板

Task

是行为树的叶子节点

是任务的执行节点

Decorators

附着于 Composites 或 Task (之前)

决定其附着的节点能不能被执行 -- 相当于行为树中的条件语句

参考资料

知乎专栏_InsideUE4

知乎专栏_虚幻4渲染编程

UWA博客_虚幻引擎学习之路