UE4学习笔记:Gameplay框架及其模块梳理(上篇)

本随笔用于记录UE4项目中Gameplay框架的执行流程脉络,最主要的目的是熟悉游戏项目的完整Gameplay框架,大致了解这些Gameplay框架的组成部分,以及这些部分在游戏运行过程中的生命周期和对Gameplay框架所做的贡献。本随笔整理自Epic Games中文社区经理大钊编写的知乎专栏:InsideUE4Gameplay架构部分。

随笔作者还在学习阶段,对UE4的使用和理解还不是十分透彻,难免出现书写上和技术上的问题,若您在随笔中发现这类型的问题,欢迎在评论区或私信与我讨论

导读

  • 每个小节的标题都以<蓝图类型名字>(<C++类型名字>)的格式命名。
  • 本随笔标题编写顺序即是UE4引擎Gameplay框架的一种大致关系树。关系树指一种类型是另一种类型的成员变量而构成的“包含”关系的树形结构,而非实际代码的继承树。

Object(UObject)

  • 几乎是虚幻引擎里面所有C++类和蓝图类的直接或间接基类。
  • UObject为所有派生类提供了垃圾回收(Garbage Collection),反射(Reflection),元数据(MetaData),序列化(Serialization)等基础功能,让所有派生类能够在C++代码层面实现与虚幻引擎更密切地配合。

Actor(AActor)

  • 既然是3D引擎,那自然少不了要在场景里面摆放物体或在场景里生成物体的步骤,因此UObject派生了一个可放置于场景中的类型——Actor。
  • 除了能够被摆放到场景里以外,Actor还提供了网络复制(Replication)、生成(Spawn)、Tick等功能。

    思考:为何Actor不自带一个Transform?

    • 虽然Actor主要是可被放置的场景中的类型,但是并不代表该类型“可见”,例如游戏模式(AGameMode)、游戏状态(AGameState)等这类类型依然可放置(或者更确切地描述是“生成”)在场景中,但是它们都是不可见的。因此Actor更确切的定义是场景中的“元素”,小到一块石头模型,大到游戏规则,都可以是Actor。
    • UE4引擎里默认的蓝图类型的Actor(而不是C++类型的Actor)提供了可供移动的位置(Location)、可供旋转的旋转(Rotation)和可供放大缩小的缩放(Scale)属性,这三种属性定义了该Actor在场景的位置等信息,由上文可得知单纯的Actor是不应该包含这类“变换(Transform)”属性的,因此我们也可以得知实现Actor变换功能是Actor内部另一个模块的功能,也就是下一小节将要介绍的“组件(Component)”实现的功能。
    • 需要注意的是,虽然Actor本身在细节面板里提供了变换属性值,但是在实际的代码里面其实是转发到了USceneComponent* RootComponent这个组件对象里的。同理,Actor能够接收玩家的输入,也是因为内部转发到了UInputComponent* InputComponent的原因。
    • 不添加变换功能是为了让Actor类型变得更加纯粹,既然变换功能可要可不要的话,不添加变换功能既可以减少Actor类型的赘余功能,还能减少内存占用,因此Actor类型并不会自带变换功能。

Component(UActorComponent)

  • 我们可以在上文提到的Actor里编写功能代码,但是如果我们把所有功能代码都写进一个Actor里面,会让这个Actor变得臃肿不堪,因此UE4引擎提供了一个类型,允许将Actor的功能代码封装进这个新类型里,并让该类型成为Actor的组成部分,然后又能反作用于Actor,这样既能能实现代码的复用,又减轻了Actor的负担,这个新类型就是“组件(Component)”类型。
  • 像上文提到的USceneComponent、UInputComponent,这些都是组件类型。
  • 就像一棵树木会有一个根一样,每个Actor也会有一个“根组件”,也就是底层代码里面的成员变量“USceneComponent* RootComponent”
  • 组件最基础的类型是“UActorComponent”,该类型是通过直接继承基类“UObject”得来的。该组件支持一些可在场景中生成但是“不可见”的功能,如UMovementComponent提供的移动功能。
  • UActorComponent最重要的派生类是“USceneComponent”,因为该派生类在基类UActorComponent的基础上添加了“变换”功能,允许我们在场景里面调整该组件(甚至是调整使用该组件的Actor)的各种位置、旋转、缩放信息。可以说一切在场景里面“可见”的组件,都是由USceneComponent继承而来的。如UMeshComponent、UStaticMeshComponent组件分别提供了可见的展示骨骼网格体和静态网格体的功能。
  • 需要注意的是组件的两大基础类型“UActorComponent”和“USceneComponent”,前者不能支持互相嵌套,而后者支持互相嵌套。

    思考:为何ActorComponent不能互相嵌套?而在SceneComponent一级才提供嵌套?

    • 随笔作者认为,对于某些适合封装起来复用但是又不需要坐标转换功能的功能代码,如UMovementComponent提供的移动功能,就像UE4推崇的是组件的强模块性,因此这类组件不依赖其他模块,同时也不需要被其他组件依赖,也就是实现组件的功能“单一性”,因此在UActorComponent层上并不提供嵌套的功能,UE4更多鼓励我们组件和组件之间互相“搭配”而不是“依赖”,但是因为很多时候我们也必须使用这样的嵌套功能,所以会以UActorComponent为基类派生出另外一个常用的类型,即USceneComponent类。

Level(ULevel)

  • 丰富多彩的Actor和Component构成了一个个功能强大的对象,现在需要有一个容器能够存放下这些所有的对象,存放这些所有元素的对象就是UE4提供的ULevel类型。
  • 关卡(Level)类型在C++代码里命名可知,Level类型也是继承自UObject类型的。
  • 既然是继承自UObject类型,那自然也会继承了UObject提供的各种特性,其中也包括“编写脚本”的能力,因此每个ULevel对象也都自带了一个“ALevelScriptActor”对象用来实现“关卡蓝图”的功能。
  • 关卡本身也需要支持一些可自定义的属性,例如设置关卡里的光照贴图、本关卡使用的游戏模式等等,因此该类型也自带着一个“书记官”——“AWorldSettings”类型用于记录这些每个关卡本身的可自定义属性。
  • 关卡本身支持添加多个子关卡,又因为每个关卡都可以定义自己的游戏模式等属性,因此当一个“持久性关卡(Persistent Level)”里面被添加了多个子关卡时,实际使用到的“AWorldSettings”对象只会是持久性关卡的那一个。
  • 关卡的作用是用来存放Actor及其派生类,因此关卡本身会有一个变量“TArray<AActor*> Actors”用来存放关卡内所有生成的Actor,“ALevelScriptActor”和“AWorldSettings”也理所当然地被保存到该数组里面。

    思考:既然ALevelScriptActor也继承于AActor,为何关卡蓝图不设计能添加Component?

    • 从实质上来说,ALevelScriptActor是可以添加组件的,甚至引擎已经在ALevelScriptActor里面添加了UInputComponent来实现对用户输入的反应,引擎仅仅只是将“组件界面”给隐藏起来了。
    • 因此UE4引擎将组件界面隐藏是希望能够将ULevel类型的性质看成是一个“Actors的容器”,其作用更多的应该是用来容纳Actor,而不应该在关卡本身上添加过多功能。

World(UWorld)

  • 很多时候仅仅只有一个关卡是不够的,如果我们的游戏场景尺寸非常巨大,要将这些庞大数量的Actor全部都塞进一个关卡里,势必会造成关卡的臃肿和维护的困难,若我们将这个巨大的场景根据一定规则划分,并能够实现在我们需要的时候读取一部分内容,或者释放掉我们不需要的内容,无论是在性能上还是开发过程都会起到良好的作用。因此UE4引擎提供了一个能够容纳多个关卡的类型——World。
  • UWorld类型是从ULevel类型直接继承而来的,并添加了多个数组用于保存子关卡及其各自的Actors,以及保存了指向“Persistent Level”和“Current Level”的指针。
  • UWorld使用的关卡就是“Persistent Level”,也就是项目编辑器视口打开了的持久性关卡,而UWorld使用的AWorldSettings也就是持久性关卡的AWorldSettings。但并不是说其他子关卡的AWorldSettings就完全没有用,部分配置例如光照配置,就是使用的各自关卡AWorldSettings的设置而不是照搬持久性关卡的设置。
  • UWorld里面可以访问得到所有关卡(包括持久性关卡和子关卡)里的所有Actor,但是并不是说UWorld直接保存这些Actors,而仅仅只是通过遍历ULevel然后再遍历其Actor。

WorldContext(FWorldContext)

  • 在UE4引擎中,有些时候(例如开发的时候)并不会只存在一个World,因此UE4引擎提供了一个类型来管理多个World——FWorldContext。需要注意这个类型以F开头,也就是说该类型不再派生自UObject或AActor。
  • 对于独立运行的游戏,WorldContext只有唯一的一个(Game WorldContext);对于编辑器模式,则是一个WorldContext给编辑器(Editor Context),一个WorldContext给PIE(PIE WorldContext),甚至还会有其他的WorldContext,如编辑器视图里面还没有运行的游戏场景的World(Preview World)
  • FWorldContext类型的成员变量UWorld* ThisCurrentWorld会指向当前的World。当需要从一个World切换到另一个World的时候(如点击“播放”按钮之后,UE4引擎从编辑器视图的Preview World切换到PIE World),FWorldContext就会用来保存切换过程信息和目标World的上下文信息。
  • 需要注意的是,因为WorldContext本身会自己执行并解决World之间的协作问题,所以我们不应该在外部直接去操纵这个类型。

    思考:为什么Level的切换信息不放在World里而要放在WorldContext里?

    • 每一个World只能有一个持久性关卡(Persistent Level),因此在打开新关卡(而不是添加新关卡)的时候UE4实际上是把当前的World释放掉,然后再新建World并把指定关卡设置为持久性关卡。如果Level切换信息放到了World类型里面保存的话,那我们在释放当前World信息的时候就必须先把切换信息复制一份以防丢失了,如此的话还不如直接保存到WorldContext里面以省略一步复制信息的步骤。

GameInstance(UGameInstance)

  • 如果继续从这种关系树往上层查找,我们可以找到用来保存FWorldContext对象和整个游戏信息的新类型——UGameInstance。
  • UGameInstance类型除了会保存FWorldContext* WorldContext,还保存了当前游戏里所有的Local Player、Game Session等信息。
  • 我们在切换Level的时候,其内的各种数据都会被释放然后重新生成,也就是说会丢失数据,哪怕是管理Level的World也是(只要切换Persistent Level,UWorld都会被释放然后重新生成新UWorld再来存放Persistent Level,从而造成数据丢失),因此UGameInstance就非常适合用于编写独立于所有Level和World之外的功能。

Engine(UEngine)

  • 在继续往关系树上层查找的话就会找到最上层的一个类——UEngine,因为也是很基础的类,再加上开发过程中会经常访问到该类型,因此UE4引擎也在代码全局范围内定义了一个该类型的全局变量:UEngine* GEngine供开发者直接调用。该最基础的类型分化成了两个子类:UGameEngine和UEditorEngine。
  • UGameEngine保存了唯一的一个UGameInstance* GameInstance指针,这是符合情理的,因为当我们在实际运行游戏(而不是运行项目或在项目里PIE)时,整个游戏只会有一个GameInstance,因此UGameEngine就直接保存了该GameInstance的指针。
  • UEditorEngine包含了两个UWorld指针:一个是UWorld* PlayWorld指针,一个是UWorld* EditorWorld,这也说明了我们的编辑器其实就是UE4引擎创建的一个World,一个“游戏”,然后我们在这个“游戏”里面去创建新的游戏。无论是哪一个World,我们都可以通过其对象查找到其WorldContext,然后再查找到其GameInstance,也就是说在编辑器模式下其实是有两个GameInstance的。

番外:GamePlayStatics(UGameplayStatics)

  • 该类型并不包含在Gameplay框架关系树里面,而是单独的一个蓝图函数库类型。
  • 知道了UE4引擎的Gameplay框架内容,我们就可以直接访问到每一层对应的类型,并进行获取或更改属性的操作,UE4引擎为了给开发人员提供便利,将一些常用功能封装进了该蓝图函数库里,例如GetPlayerController、SpawActor和OpenLevel等方法。

总结

  • 在本随笔里我们梳理了UE4引擎Gameplay框架的一个大致关系树:Object -> Actor+Component -> Level -> World -> WorldContext -> GameInstance -> Engine,这颗关系树已经阐明了一个游戏项目的各个组成部分,但是还没有详细介绍每个部分都是用什么样的“逻辑”去驱动的。在下一篇随笔里就会详细的介绍这些组成部分。
posted @ 2022-07-14 15:31  U_N_Owen  阅读(1966)  评论(0编辑  收藏  举报