InsideUE5阅读笔记GamePlay架构篇
《InsideUE4》基础概念 - 知乎 (zhihu.com)
UE5文件目录
Binaries:存放编译生成的二进制文件(gitignore)
Config:配置文件
Content:蓝图、资源等
DervedDataCache(DDC):引擎针对平台特化后的资源版本
Intermediate:各种中间文件
- Build中间文件
- UHT预处理的.generated.h/.cpp文件
- VS项目文件
- AssetRegistryCache缓存文件,uasset资源注册表
Saved:自动保存文件,日志,烘焙数据等
Source:代码文件
命名约定
- T:模板类TArray,TMap,TSet
- U:UObject派生类
- A:AActor派生类
- S:SWidget派生类
- I:抽象接口
- E:枚举类
- b:bool变量
- F:其他类型——FString,FName
UObject
UE游戏世界的所有类基类,提供元数据、反射生成、GC垃圾回收、序列化、编辑器可见,Class Default Object等。动态创建。
一句话介绍:大多数与游戏逻辑、资源管理相关的类都继承自UObject,UObject为这些类提供了反射、序列化、GC等功能,AActor就是其中之一。Actor通过组装Component来实现一些具体的功能,同时也可以通过UChildActorComponent实现父子嵌套(SceneComponent有transform,所以理论上SC会被当成RootComponent,而且父子嵌套的实质也是SceneComponent之间的嵌套)。同样继承于UObject的ULevel中含有所有Actor的引用,Level也就是关卡,也可以理解为大地图上的一块区域,多个Level组成一个World。再往上走,World由WorldContext来进行切换,切换world的本质是切换PersisitentLevel。world之上就是GameInstance了。
AActor
继承自UObject,额外提供Replication(网络复制),Spawn(生命周期),Tick(更新)功能。此外,Actor提供TArray存储子Actor。
Actor不提供Transform,因为有许多不需要带有坐标的object也是Actor,比如世界设置对象,游戏模式对象等,UE不需要这些没有坐标的对象独立划出空间来存储“坐标”来浪费空间。
Component
UActorComponent的简称,是所有Compnents的基类。
在AActor中,有一个TSet<UActorComponent*> OwnedComponents,保存着这个Actor所拥有的所有Component,一般其中会有一个SceneComponent作为RootComponent。
Actor中支持包含多个SceneComponent,也就是一个Actor其实可以代表多个实体
TArray<UActorComponent*> InstanceComponents,存储被实例化的Components。
如果Actor要想被放进Level中,必须要有实例化USceneComponent(源码中声明为“RootComponent”),在这个Component中,包含Transform和SceneComponent相互嵌套功能。
虚幻引擎遵循组合优于继承的概念,所以只有带有Transform的SceneComponent拥有嵌套功能,其他组件不能相互继承嵌套。
Actor之间有父子关系的概念,但是Actor并不关心父子关系,Actor只负责对象的网络同步,创建销毁等功能,父子关系由SceneComponent负责,因为他又Transform,在3D坐标世界上就能理解了。所以Actor的类“AddChildren”的方法“Child:AttachToActor“是间接通过调度”Child:AttachToComponent“来创建父子关系的。
Level
由多个Level组成一个World,是Actor的容器,供玩家活动的场景,包含静态网格体(Static Mesh)、体积(Volume)、光源(Light)、蓝图(Blueprint)。
Level的释放和加载是动态的,类似于Unity引擎中的”场景“,Level可以视为闯关类游戏中的会切换场景的关卡,也可以视为吃鸡游戏中将大地图动态进行划分的区域(不可视区域没必要加载)。这样的粒度也方面团队协作。
- ALevelScriptActor:顾名思义即Level自己的脚本,可以在这个脚本内获取关卡内所有的Actor,可以编写这个Level的相关游戏逻辑。
- AWorldSettings:AInfo类的类都不直接放入world中,这个worldsetting只和本Level相关,内含各种Level相关的配置设置,例如GameMode等
- AInfo:那些再world中没有物理展示的Actor基类,保存world中设置数据的管理类,也可以作为实现复制意图的Actor使用。
ALevelScriptActor意思是关卡的脚本Actor,用于执行Level中的逻辑操作,而WorldSetting继承于AInfo,用于操作游戏世界的设置。
在Level中,WorldSetting记录本Level中的所有规则属性,Level记录了Level内的所有Actor。而Actor内也存储WorldSettings和LevelScriptActor。
ULevel内的Actor排序
ULevel内有一个TArray<AActor*> Actors
字段存储关卡内所有Actor,遍历Actors会很花时间,所有Level内有一个排序Actor的方法ULevel::SortActorList()
,他会单独把网络对象和非网络对象分开,网络对象放在后面,并使用索引进行标记,可以加速网络复制的检测速度。(AWorldSettings是静态数据提供者,是放在最前列的)
于Unity的异同
Unity中和Level很像的场景概念叫Scene
- Scene之间独立,一个Scene包含它自己需要的对象、组件、光照等,切换场景时完全卸载;Level可以由多个Sublevel组成,多个Sublevel流式加载到同一个主关卡。虽然Unity也支持流式加载,但Unity的Scene加载需要在代码中手动管理控制,没有向UE那样直观。
UE5中被World Partition替换,自动将世界分块,根据玩家视角与距离动态加载卸载
- Scene本身没有逻辑,要完成一些关卡相关的逻辑必须单独挂GameObject加脚本逻辑;Level有单独的关卡蓝图脚本系统,如ALevelScriptActor。
World
在World中用SubLevel的方式将Level拼接起来。
PersisitentLevel:一开始就加载进来的Level
StreamingLevels:动态加载进来的Level
Levels:当前已经加载进来的Level
当然可以一口气把单独的Level放进PersisitentLevel里,也可以按照流式加载进行区分,如果是后者则World里有一个叫CurrentLevel的引用,在编辑器里指向其他Level,而运行时只指向PersistentLevel。
相关WorldSetting以PersistentLevel为主,也有单独为特定Level配置的设置。
Level之间不会共享Actor的引用,但是碰撞时全局的,也就是说所有的Actor物理实体是在World当中的。
WorldContext
namespace EWorldType
{
enum Type
{
None, // An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels
Game, // The game world
Editor, // A world being edited in the editor
PIE, // A Play In Editor world
Preview, // A preview world for an editor tool
Inactive // An editor world that was loaded but not currently being edited in the level editor
};
}
各种不同的World类型
WorldContext就是管理这些不同World的工具,负责World之间切换的上下文,也负责Level之间切换的操作信息。
GameInstance
继承于UObject。
保存WorldContext和其他的整个游戏的信息,贯穿游戏始终,在整个游戏内保持一致。
惯常的做法是继承GameInstance,并编写那些能作用于整个游戏范围的逻辑,比如玩家的单机分数之类,这些是不随关卡退出而刷新的,比如联网逻辑或者全局游戏设置等。以此来”扩展“GameInstance的功能,可以在UE中配置自定义的GameInstance。
Engine
分化为两个子类:UGameEngine和UEditorEngine。
UGameEngine指游戏运行时逻辑的引擎类,当游戏打包启动的时候负责游戏的核心逻辑。
UEditorEngine指编辑器状态的逻辑引擎类,负责创建和编辑游戏内容。
启动PIE状态由UEditorEngine负责,但是启动之后的PIE状态本身就是由UGameEngine负责。
GamePlayStatics
蓝图静态方法的暴露类,方便操作蓝图数据库。
Pawn
Possess然后传递Input
继承于AActor。即“可操控的棋子”。
pawn定义了三个模板方法接口:
- 可被Controller控制
- PhysicsCollision表示
- MovementInput基本响应接口
对于Input,Actor本身能接受input事件,如wasd事件,但是接受事件之后如何进行响应Actor就没有继续进行处理了。而Pawn额外包装了一层MovementInput,可以理解为默认地增加了“按下w之后会向前移动”的逻辑接口。
一句话介绍:Pawn就是一个特殊的Actor,它具备“可被控制”、“物理Collision表示”、“移动响应MovementInput”接口三个基本功能,可以对这三个功能进行定制化,派生出“默认的DefaultPawn”、”观战SpectatorPawn“、”人型Character“。AController也是特殊的Actor,用于控制Pawn,可以派生出APlayerController和AAIController。APlayerState继承于AInfo,AInfo是没有物理表示的AActor,APlayerState于AController绑定在一群,充当网络复制的数据状态。
DefaultPawn
默认的Pawn,带有默认的DefaultPawnMovementComponent、spherical CollisionComponent和StaticMeshComponent。
SpectatorPawn
继承于DefaultPawn,将Movement改为了“USpectatorPawnMovement(不带重力漫游)”,关闭了StaticMesh显示,定义了一个“观战摄像机”行为。
Character
继承于Pawn,人型pawn。
相当于带人型骨架的Pawn,同样带有移动、碰撞体、mesh三部分,不过是人型的。
Controller
继承于AActor。
Controller于Pawn是一一对应的,也就是说如果碰上需要一次性操控多个Pawn,或者多个Ctr控制单个Pawn的情况,需要进行特殊处理。有种鼓励人们去进行扩展的意思,毕竟一对一的模式更加方便引擎自身进行项目管理和错误筛查。
和Unity的不同:
Unity是GameObject+Component的模式,自由组合,对此并没有任何定义,也就是说控制器、角色本体、甚至MVC模式设计模式本身都需要从0开发。这给了设计者非常大的自由性。
UE规定好UObject、Actor、Component的各自职责,衍生出了Pawn+Controller的角色控制模式,更加强调类之间的层级关系。
Controller与Pawn:
- Pawn与Controller都掌管Actor“运动“的概念,其中Pawn更强调”运动的能力“,例如碰撞检测、动画播放、位置更新等;而Controller更强调”运动的决策“,也就是运动自身的逻辑,什么时候该走,该往哪个方向走。
- Pawn更强调能力,也就是说Pawn更适合编写特地的一些功能逻辑,例如战车与坦克,坦克能开炮,战车不能,所以坦克Pawn需要编写开跑功能;而他们两者都能驾驶,所以可以使用同一个Controller来控制他们两个。
- Controller更面向玩家,Pawn因为某些情况”死掉了“,例如血条清空等,那么Pawn自身就析构了,而Controller还存在,毕竟实际上玩家还是有权利控制控制器的,只不过可能得不到反馈罢了。所以如果有持久性的例如玩家得分需要进行记录,那么可以放在Controller当中进行逻辑处理。
APlayerState
继承于AInfo。AInfo应该不陌生,也就是”没有实体的Actor“。
Controller可以存取玩家状态,就是通过其内部引用的APlayState实现的。
APlayerState与Pawn、Controller是平级的关系,虽然和Controller绑定,但其实有多少实际的玩家才会生成多少APlayerState。
APlayerState独立成一个Actor还能保证网络同步时的数据稳定性,比如UE的网络架构中,如果断开连接,那么Controller就消失了,再次连接上时重新使用APlayerState的数据进行重连就好了。
APlayerState在切换关卡的时候也会被释放掉,所以和GameInstance作区分,照开发实际需求来就好。
APlayerController
继承于AController
- Camera管理——可以知道Camera是挂在Controller上而不是Pawn了
- Input系统
- UPlayer关联(LocalPlayer或者UNetConnection)
- HUD显示
- Level切换(网络切换Level通过PCtr进行RPC调用后转发到自己的World)
- Voice
AAIController
继承于AController
- Navigation导航
- AI行为树等其他AI组件
- Task系统
GameMode
由AInfo派生。管控Level相关逻辑。
- Class登记与实体Spawn:反射出的UClass类型信息就登记在此,用于方便Spawn出新对象。
- 游戏进度:重启游戏、暂停游戏的支持。SetPause、ResartPlayer函数进行逻辑控制。
- Level切换:可以指定是否播放开场动画。
- 多人游戏状态:MatchState指定开始状态结束状态。
Level Blueprint与GameMode:
- 前者注重关卡内特殊表现,后者注重游戏本体逻辑
- 简而言之就是通用玩法可以放GameMode里
- GameMode只在Server中有
- GameMode与PlayerController职责很像,都是负责游戏玩法,连着和不冲突
一句话介绍:除了PlayerState,同样也有其他派生自AInfo的类,对应Level的State叫做”AWorldSetting“,对应一局游戏的State叫做”GameState“。GameMode更侧重于管控Level的逻辑,比如游戏进度等。每个Level都可以独立设置GameMode,也可以直接配置全局的GameMode。GameState代表了一局游戏中的数据,例如玩家一局中的得分等,而GameInstance表示的是贯穿一整个游戏程序的数据。
GameState
由AInfo派生,和APlayerState对应,保存当前游戏的状态数据,比如任务数据等。
与GameInstance做区分,GS是一局游戏的数据,而GI是整个游戏程序的数据
UPlayer
看到这里的时候不由得感到震惊,感觉UE做的GamePlay框架真的很多
继承于UObject。是Actor与Game层之间的中间层。
级别比World更高,因为哪怕World不断切换,玩家是一成不变的。UPlayer当中置有PlayerController的引用,所以可以理解为两者是相互关联的(即引擎会为Player做输入相应)。UPlayer不代表游戏中的存在,而是确确实实代表玩家本身在Gameplay中的类。
一句话介绍:GameInstance不仅保存World,也存储Player。LocalPlayer代表本地玩家,NetConnection代表远端的连接,负责同步远端的PlayerState。LocalPlayer再GameInstance除四害的适合就默认创建出来,而远程的玩家通过远程同步数据,并不具备输出能力。
ULocalPlayer
UPlayer的派生,即本地玩家。GameInstance具有ULocalPlayer的列表,支持遍历访问实现本地玩家相关操作。
为了顺从UE越来越多的网络联机需求,单独分出来Local的玩家类,比UPlayer多出来了Viewport的功能逻辑,在本地屏幕上给出操作反馈。
LocalPlayer创建流程:
1.初始化GameInstance默认创建GameViewportClient(管理渲染窗口、处理用户输入,与游戏视图和渲染系统交互)。
2.内部转发到GI的CreateLocalPlayer创建LocalPlayer,再创建PlayerController。
3.调用Ctr内部的InitPlayerState,将PlayerState与LocalPlayer对应上来。
4.网络联机中远程同步过来的PlayerState通过Replicated过来。
UNetConnection
UPlayer的派生,代表远程同步过来的玩家对象。
相比于ULocalPlayer,少了输出相关的逻辑。
GameInstance
继承于UObject。在GameEngine里创建。
- 引擎初始化,init、shutDown
- Player的创建,CreateLocalPlayer,GetLocalPlayers
- GameMode的重载
- OnlineSession的管理
GameInstance可以被上层的Engine实例出多个的。(理论上)
- PIE:指在编辑器中按下“Play”后在编辑器的上下文当中运行的游戏,可以实现动态更改场景蓝图变量等。(存有GI)
- 独立启动游戏:指脱离编辑器运行的游戏,通过编译/打包后运行的可执行文件。(存有GI)
- 编辑器模式:直接在编辑器中进行操作,可以做到对场景、关卡、甚至动画等的实时浏览功能,但是没有输入相应。(不存有GI)
GI一般能做的业务逻辑:
- 切换Level相关
- 自定义生成Player
- UI相关(一般开发都是由专门的Actor负责管理UMG,但是使用GameInstance管理也是一种思路)
- 全局配置(游戏设置)
- 从游戏启动到游戏结束中需要存储的全局变量
SaveGame
GameInstance适合存储关卡外存储的数据变量,如果由需要存进内存中的持久性数据,可以考虑继承USaveGame,通过UObject的序列化机制来实现属性字段的序列化保存。
相关接口:
- CreateSaveGameObject
- LoadGameFromSlot