对Gameplay框架的理解

这里发现了一个很Good的博主

https://www.cnblogs.com/u-n-owen/p/16425358.html  //讲述了Gmaeplau框架  很直白  很详细

 

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
  • UE4学习笔记:Gameplay框架及其模块梳理(下篇)

     

    Actor层面

    Component(UActorComponent)

    • Component表达的是“功能”的概念,例如说赐予某个Actor根据WASD键移动、能够让某个Actor跳跃等功能,而不应该涉及“游戏业务逻辑”,例如达成某项目的之后即可让游戏通关。后者虽然也是某种功能,但是该功能是和游戏本身紧密联系的,但是前者的功能是“脱离于游戏本身”的功能。
    • Component实现的功能是需要能够直接利用到我们任何一个其他游戏上面的,也就是强解耦性。如果某个Component在移植过程中需要修改部分甚至是全部代码,那就需要考虑该Component的设计是否合理,以及是否符合“Component”强解耦性功能的定义。

    Actor(AActor)

    • 如果说Component表达的是“功能”的概念,那么Actor要表达的就是“游戏业务逻辑”的概念,而UE4引擎已经使用Actor作为基类派生出了许多适合实现游戏业务逻辑的类型,如AGameMode、AGameState等类型,其中也包括了两个很重要的类型:面向对象派生下来的Pawn与Character,支持组合的Controller。

    Pawn(APawn)

    • 很多时候我们需要一种类型能够响应我们的输入,特别是当我们想要控制一个对象的时候,如当我们输入的时候对象会如何移动、当我们输入的时候对象会如何格挡等,能够实现这些功能并响应我们(玩家们)的需求的类型,就是APawn类型。APawn是直接从AActor派生出来的类型。

    • APawn定义了三个基本的模块方法接口供我们使用:可被Controller控制、PhysicsCollision表示和MovementInput的基本响应接口。

      思考:为何APawn还需要被一个额外的类型Controller控制,而不是直接在APawn里面直接控制?

      • 从本质上来说用户确实可以直接在APawn里面接收用户的输入来操纵APawn,但是UE4引擎更加希望将这两种事件进行分别处理,两种事件分别是“可操控对象的表现”和“可操控对象的控制”,APawn应该更加着重于当接收到玩家输入的时候会产生什么样的“表现”,因此也就会有一个额外的类型——即AController类型——专注于如何接收并处理玩家的输入。Controller相关的类型会在本篇下文介绍。

      思考:为何Actor也可以接收玩家的输入?

      • APawn可以接收玩家输入是在情理之中的,但是实际上输入事件的功能逻辑却是在AActor里面定义的,在官方文档里面说明的“输入处理程序”也表面了游戏里面最先接收输入的对象是启用了输入事件(该条件也隐含了Actor默认不接受输入)的Actor,然后才是Controller、然后是Level Script、最后才到Pawn,而且需要注意的是,在这个输入处理顺序当中,如果某一层接收到了玩家的输入,那么之后所有的对象都将不会接收到玩家的输入,相当于是被“截取”了。
      • 对于游戏玩家来说,“输入”很多时候就是键盘输入、手柄输入等,因此在玩家可操作对象APawn上或Controller上接收输入可能更符合我们的想法,但在实际的项目当中,我们还有可能需要去实现诸如“触屏”、“声控”之类的输入功能,这些功能也归纳在输入里面,但是并不需要用到APawn作为载体,因此对于这种不确定实际输入的类型,UE4引擎干脆直接在AActor上实现输入的接口,这样无论遇到什么样的输入需求我们都能实现。

      DefaultPawn和SpectatorPawn(ADefaultPawn和ASpectatorPawn)

      • 很多时候我们会使用到APAwn,但是还需要我们自己手动一个个添加移动组件并设置控制方法,感觉很繁琐,因此UE4引擎索性直接了一个已经“组装”好基础组件的Pawn类型供用户使用,这个类型就是直接继承于APawn的ADefaultPawn类型。
      • 有时候我们在游戏里面会需要一些“观战者”角色,就是可以跳脱出常规角色的操控方法,而在场景里面随时随地“漫游”,因此UE4引擎也提供了一个特殊的类型——ASpectatorPawn类型,该类型直接继承自ADefaultPawn,并自己定义了移动的方法,让玩家能够像在空中“飞翔”一样自由漫游场景。

    Character(ACharacter)

    • 可操纵角色中很多时候都会以“人形”的姿态展现,并且还会需要使用到人物动画、人物模型等类似的资源,因此UE4引擎提供了一种APawn的强化版类型,也就是ACharacter类型。
    • 该类型在APawn已经提供的功能基础之上,还提供了可供展示骨骼网格体的组件“SkeletalComponent”,接近于人形的碰撞体“CapsuleCollision”和能够模拟人类移动方式的移动组件“CharacterMovementComponent”。如果说APawn是适合于玩家操纵的类型,那么ACharacter就是适合于玩家操纵的人形类型。

    Controller(AController)

    • 如上文所说,APawn负责“可操纵对象的表现”,因此我们还需要一个类型能够负责“可操纵对象的控制”,这个类型就是由AActor直接继承而来的AController类型。
    • AController定义了关联Pawn的方法:void Possess(APawn* InPawn)和void UnPossess(),用于掌控指定的Pawn,这样就可以更灵活地动态操纵某个Pawn亦或者取消操纵。
    • 因为AController继承自AActor,自身也能够接收到来自用户的输入,因此能够让用户直接在AController的里面定义该如何处理这些输入。
    • 需要注意的是,在项目里面我们也可以在任意一个Pawn上设置操纵策略,例如当这个Pawn被放置到场景里面的时候会自动生成对应的Controller并Possess,或者是当这个Pawn生成还没有被放置的时候就Possess,或者任意一种情况,或者什么都不做。

      思考:Controller和Pawn必须1:1吗?

      • 在源码里面一个Controller仅仅只能保存一个Pawn,也就是说在代码层面,UE4引擎仅支持同一时刻一个Controller仅能操控一个Pawn。
      • 但是我们可以通过在我们自己的继承自Controller的类里面保存多个Pawn来实现一个Controller对应多个Pawn的功能。
      • Controller于Pawn的关系,再次阐明了UE4引擎在设计时候的一个重要理念:对于可要可不要、可以通过其他方式实现的功能,UE4引擎会更加倾向于舍弃这些功能的实现,而让这些功能交由开发者来实现。

      思考:Controller的位置有什么意义?

      • 默认的Controller类型是自带一个SceneComponent组件的,这也就说明了Controller本身自带位置信息,但是Controller仅仅是用来处理输入逻辑的,那为什么还需要一个位置信息?其实带入到第三人称游戏里面就很好理解了,有的时候我们需要我们玩家的视角在上下左右四个方位上查看周围场景,但是我们又想在查看过程中不去让玩家控制的角色跟随着转动,也就是说Controller自带位置的意义在于将”玩家的位置”和“角色的位置”区别开来。

      思考:哪些逻辑应该写在Controller当中?

      • 正如上文所说,AController着重表达“可操控对象的控制”的概念,而APawn着重表达“可操纵对象的表现”的概念,因此在AController当中最需要处理的就是“当我们接收到玩家输入后要怎样处理”的逻辑。
      • 如果是可以抽象为同一类型的逻辑操作,例如操纵对象的前后左右移动,那么我们可以将该逻辑写进AController里;如果是某些特定于固定类型,例如某一类型操纵对象可以开炮,那么开炮逻辑我们就可以编写到APawn里面而不是AController里面。
      • 从对象生命周期上来说,AController比APawn的周期要长,在游戏过程中会遇到操纵对象死亡的情况,一般来说操纵对象死亡之后就会被Destroy掉,然后重新Spawn一个新的对象,这时对象身上的属性都会被重置掉,因此我们在编写一些不需要被重置的功能时就可以将其写进AController里面。

      PlayerState(APlayerState)

      • AController本身可以通过定义各种各样的变量来实现存储玩家信息的功能,但是UE4引擎更加细致的提供了一个独立去AController的类型供用户来实现存储玩家信息的功能,这个类型就是APlayerState。
      • 使用APlayerState类型就可以根据需要来定义变量实现存储功能,而且相较于直接存储在AController里面的方式,存储在APlayerState里面可以让玩家信息数据于AController“分离”,因为AController类型里面仅仅只是存储了一个指向APlayerState类型的指针,实际上APLayerState是生成在Level里面的,也就是说是和APawn、AController是平级的,这样无论是重新生成APawn还是AController,APlayerState里面的数据也都会一直存在。
      • 将PlayerState和PlayerController分开还有一个好处,那就是在网络游戏中,如果玩家因为网络波动问题掉线了,其Controller和Pawn都会被释放掉,这时可以让服务器暂存PlayerState,等玩家上线之后再挂载到玩家身上,就可以实现一个无缝地联机体验。
      • 虽然官方在代码里面注释说只有玩家需要PlayerState,而NPC不需要,但实际上存储APlayerState的变量是定义在了AController类型里面,而不是其直接派生类APlayerController或AAIController(这两个类将在本随笔的下一节进行讲解),因此也说明了做为NPC的AI也是可以拥有PlayerState的。这也很好理解,如果能让NPC读取到玩家的PlayerState然后据此做出一些决策的话,也能够成为游戏机制的一环的。

        思考:哪些数据应该放在PlayerState中?

      • 从类型名字上就可以得知,该类型应该存储的是玩家的信息,诸如玩家的得分情况,因此像是游戏的信息(竞技游戏中当前对局的比分等)就不应该放到PlayerState里面,而应该放到GameState里面(GameState类型将会在本随笔下文部分介绍)。
      • 需要注意的是因为PlayerState实际上是生成在关卡里面的,切换关卡的时候PlayerState会被释放的,所以PlayerState实际上代表的是“当前关卡中玩家的信息”,如果是需要横跨关卡、代表整个游戏过程的数据,那最好是存储到GameInstance里面。

    PlayerController(APlayerController)

    • 在大部分游戏中,角色对象可分为两类:一类是玩家可直接操纵的角色,另一类是玩家不可操纵,而是由AI来操纵的角色。UE4引擎提供了从AController直接继承而来的类型APlayerController来代表其操控者为玩家。
    • 相较于其直接基类AController,APlayerController显然考虑到了玩家们的需要,因此添加了摄像机管理、存储当前玩家(UPlayer)等功能。

      思考:哪些逻辑应该放在PlayerController中?

      • PlayerController最适合编写那些实现由玩家直接操控的对象所需要的输入逻辑,例如接收通过键盘或者是手柄产生的输入并进行正确的逻辑判断(如要不要响应输入,什么时候该响应什么时候不该响应等)。

    AIController(AAIController)

    • 在上一小节提到角色对象可分为两类,其中一类就是不由玩家操控、而是由AI操控的角色。可实现AI操控功能的类型就是从AController直接继承而来的类型AAIController。
    • 相较于直接基类AController,AAIController并不需要APlayerController那样的摄像机、存储当前玩家等功能,相反,该类型添加了能够支持AI系统的AI组件:运行行为树、使用黑板数据、探索周围环境等。

      思考:哪些逻辑应该放在AIController里面?

      • 就像Controller控制Pawn一样,AIController也应该编写一些功能逻辑以操控其控制的Pawn或Character。
      • 从实现上来说,我们可以直接在AIController里面实现硬编码,从而实现“AI逻辑”,但是最推荐的方法是Epic Games实现的行为树系统及黑板组件。

    Level层面

    GameMode(AGameMode)

    • 我们已经知道,无论是UWorld还是ULevel,其主要职责还是将所有需要使用到的Actor包含进去,也就是对象的“显示”,但是一个游戏只有显示是不够的,还需要有一个“游戏规则”才能够让一场游戏进行下去,这个负责指定游戏规则并“控制”整个World以及所有Level的类型,就是AGameMode类型。
    • GameMode登记了一些例如Pawn、PlayerController、PlayerState等等的基础类型,这样让开发者在设计游戏规则的时候能够更加便捷,。
    • 为了控制整个游戏的执行进度,诸如SetPause、RestartPlayer这类方法也都是实现在了GameMode对象里面。
    • GameMode也控制了World的切换,甚至当我们启用了bUseSeamlessTravel之后,可以重载GameMode和PlayerController里面的GetSeamlessTravelActorList方法来指定哪些Actor不会被释放而是直接被“传送”到下一个World。
    • GameMode甚至可以控制网络游戏中每个玩家的加入情况。
    • 在联网游戏当中,GameMode仅仅只会存在于服务器上。

      思考:配置不同GameMode的多个Level组成的World采用的是哪一个World的WorldSettings?

      • 一个World里面只会生成一个GameMode,否则整个游戏肯定会乱套了。
      • UE4引擎仅仅只会在加载World时,加载那个World的Persistent Level使用的GameMode,哪怕是后续再加载流送子关卡,整个World也仅仅只会有Persistent Level使用的那个GameMode。

      思考:当我们切换World(也就是切换Persistent Level)时,GameMode是否保持一致?

      • 只要是切换了World,那么当前World里面所有已经生成的对象都会被释放掉(包括GameMode、GameState等),然后再重新生成新World里面的GameMode,也就是说GameMode并不会保持一致。
      • (随笔作者注:GameMode里面有个属性bUseSeamlessTravel,可以实现World部分对象的传递,但是其中并不包括GameMode对象。关于bUseSeamlessTravel属性还需要后续实践来了解)

    GameState(AGameState)

    • 我们已经知道APlayerState是用来存储每个玩家的信息状态的,那么对于一个游戏,我们也可以有一个类型用来保存游戏的信息(比如说各个队伍获得的总分数等),这个类型就是AGameState。AGameState同APlayerState、AWorldSettings一样都是直接继承于AInfo。
    • AGameState类型里面的成员变量FName MatchState和相关的回调时用于在网络中传播传播同步游戏状态的,因为GameState在网络游戏中是存在于服务器上且能被复制到客户端的。
    • AGameState还保存了所有玩家的APlayerState。

    GameSession(AGameSession)

    • 该类型主要是用来实现网络联机游戏当中的Session,本文重点不在于网络,对于该网络相关类型不做过多介绍,因此暂时忽略。

    AGameModeBase和AGameStateBase

    • 原版的AGameMode类型和AGameState类型内置了许多网络联机游戏才合适的接口,例如匹配状态、玩家总人数等,但是这些信息都只是网络联机游戏才需要的,为了能够更加适应其他游戏类型,UE4将AGameMode和AGameState里面更加抽象的一些逻辑抽离了出来,分别生成了新的类型——AGameModeBase和AGameStateBase,如果我们在实现我们自己的Game Mode和Game State时候不需要很复杂的逻辑,那么我们就可以选择这两种类型的“Base”版本来作为我们自定义类型的基类。

    Player层面(处于Actor层和Game层之间的一个中间层)

    • 无论是Actor层面的对象还是Level层面的对象,我们都是在搭建一个可供游玩的环境,当我们的可游玩环境设计好了之后,自然是需要我们玩家自身加入进去,这里的“玩家”并不是指玩家控制的APawn或ACharacter,也不是指APlayerController,而是指实实在在的、我们玩家自身在这个Gameplay框架里面的代表

    Player(UPlayer)

    • 指代实际玩家对象的类型就是由UObject直接继承而来的UPlayer,之所以不继承自AActor是因为Actor及其派生类都必须被放置到场景里面才能够正常使用,而且在切换Level(即切换World)的时候其内的所有Actor都会被释放,但实际上Player类型应该是独立于任何Level,并且长期存在的对象,因此最适合继承自UObject。
    • UPlayer类型定义了一个APlayerController* PC成员变量,也提供了设置该变量的接口,这也就是说引擎会为每个Player都设置对应的输入响应,这也很符合“玩家”的概念。

    LocalPlayer(ULocalPlayer)

    • 顾及到UE4引擎会被使用在到网络联机游戏上,因此引擎也对玩家进行了一个分类:分为“本地玩家”和“远程玩家”。其中用来表示本地玩家的类型就是从UPlayer直接继承而来的ULocalPlayer。
    • ULocalPlayer相比其基类UPlayer多了Viewport相关的功能逻辑。作为本地玩家肯定需要将我们的输入反馈到本地的输出设备上(例如屏幕)。
    • 因为在Player层面上面就到了Game层面,因此在UGameInstance类型里面会包含一个统计了所有本地玩家的数组TArray<ULocalPlayer*> LocalPlayers。
    • UE4内部创建玩家的过程可以表示为:UE4在创建GameInstance的时候,会先默认创建出一个GameViewportClient,然后在内部再转发到GameInstance的CreateLocalPlayer创建出LocalPlayer之后再创建相应的PlayerController,在PlayerController生成过程中会在内部调用InitPlayerState,至此就将Player和PlayerState全都联系起来了。

      思考:为何不在ULocalPlayer里面编写逻辑?

      • 虽然Player类型代表了很重要的每个玩家对象,但实际上UE4引擎并没有直接的LocalPlayer蓝图类,甚至官方文档里面也没有详细描述如何去创建自己的ULocalPlayer类。原因很可能是每个玩家“操纵”的对象已经根据功能性解耦成了很多独立的模块:用以表示“玩家表现”的APawn和ACharacter,用以表现“玩家操控”的AController和APlayerController,以及用于“接收玩家输入”的UPlayerInput等,因此在表现一个“玩家”的对象上————也就是ULocalPlayer和UPlayer————也就不需要再去添加额外的功能逻辑,而是通过这些独立的模块来完成“玩家”的功能逻辑。
      • 实际上UE4引擎还是提供了使用玩家自定义ULocalPlayer的设置,不过只能够创建C++类而不能创建蓝图类,当我们自己的LocalPlayer类编写好了之后只需要在UE4编辑器主界面上的“编辑(Edit)->项目设置(Project Settings)->引擎(Engine)->常规设置(General Settings)->默认类型(Default Class)->本地玩家类型(Local Player Class)”里选择我们自己的自定义类,这样就能让引擎在生成LocalPlayer的时候创建我们自定义的类型。

    NetConnection(UNetConnection)

    • 如上文所说,在网络联机游戏上玩家可以分为“本地玩家”和“远程玩家”,其中ULocalPlayer代表了本地玩家,那么代表远程玩家的就是本小节将要介绍的同样直接继承自UPlayer类型的UNetConnection类型。
    • “远程玩家”仅仅只是相对于“本地玩家”而言的,因为远程玩家实质上并不存在于本地设备上,而是通过将服务器上的信息复制到本地上来进行展示的,因此该类型相较于ULocalPlayer类型,少了需要输出相关的逻辑。

    Game层(仅次于Engine层)

    GameInstance(UGameInstance)

    • 我们已经知道在整个引擎的Engine层里是可以允许多个GameInstance存在的,除游戏本身的GameInstance以外,编辑器的、PIE的都能算独立的GameInstance,因此这很有可能也是这种类型被命名为“Game Instance(游戏实例)”而不是单纯的“Game”的原因。而GameInstance都是由Engine层(UGameEngine和UEditorEngine)进行管理,这就将“Game”的概念和“Engine”的概念区分开了。

      思考:哪些逻辑应该写在GameInstance?

      • Game层的GameInstance在Gameplay关系树里面是高于ULevel和UWorld的一个层,因此GameInstance非常适合用于实现切换Level相关的逻辑。
      • 虽然UE4引擎已经默认实现了创建Player的逻辑,但如果我们想要一些实现自定义生成Player的功能逻辑的话,GameInstance可以提供很多常用的接口。
      • UE4引擎的UI系统是一套独立于World的系统,并不像APlayerController、APawn、AGameMode等类型会生成在关卡当中,也就是说需要将UI相关的逻辑实现在Level层之上,GameInstance也因此非常适合用来实现UI相关的逻辑。 (随笔作者注:从我个人的开发经验上来说,我们使用UMG来编写UI,然后会直接在具体的Actor里面去管理UI的生成和销毁以及部分接口的调用,但很多时候会发现很多不适合于项目需求的地方,大钊这部分的文章给了我一个新的使用UI的方式,在以后的开发过程中可以结合着使用试试。)
      • 适用于全局配置的读取(例如游戏画质设置、操控设置等)。
      • 就像PlayerState是用于存储单个玩家信息,GameState用于存储单个世界(UWorld)的信息,如果我们需要存储一些生命周期大于UWorld和ULevel的信息,那我们可以选择直接在GameInstance里面创建变量。但是需要注意的是,GameInstance的生命周期也仅仅只是游戏启动一直到游戏关闭的这段“临时”周期,如果我们还需要数据能够存储到本地,供我们下一次启动时候读取从而实现类似“游戏存档”功能的话,最好使用SaveGame。

    番外:SaveGame(USaveGame)

    • 该类型并不属于本随笔上述的Actor层、Level层或Game层,而是一个直接从UObject类直接继承而来的类型。
    • USaveGame本质上就只是一个提供给UE4用来序列化和反序列化的对象,因此本身并不需要任何控制,我们只需要在其类型内定义我们需要保存的数据即可。
    • 实际控制USaveGame的序列化和反序列化操作的是蓝图函数库GameplayStatics提供的接口:CreateSaveGameObject、LoadGameFromSlot等。

    番外:Subsystem(USubsystem)

    • Subsystem(子系统)是一套(而不是一个)可以定义自动实例化和自动释放的框架,说白了就是UE4引擎能够在合适的时候生成实例,也会在合适的时候释放实例的对象。
    • 该对象直接继承自UObject,因此其类型名字就为USubsystem。但当我们在使用子系统的时候我们并不会直接继承自USubsystem,而是继承自它的五大子类:UEngineSubsystem、UEditroSubsystem、UGameInstanceSubsystem、ULocalPlayerSubsystem和UWorldSubsystem。
    • 子系统的最方便的地方在于其“自动托管”的生命周期,也就是说五大子系统的所有派生类都能够在特定的场合里生成实例,也能够在特定的场合里销毁这些实例,当我们掌握五大子系统的不同生命周期之后,可以任意选择适合我们的子系统作为直接基类来派生我们自己定义的子系统类。
    • Subsystem的创建和使用于通常的UObject派生类一致,只需要在创建C++类的时候,选择一个子系统作为其基类,然后再在类体里面定义我们需要的变量和方法即可。就算是蓝图反射机制(如“BlueprintCallable”)也完全可以使用到我们自定义的子系统上————子系统本质上就只是一个UObject!

      子系统加载的流程

      • 子系统在UE4引擎里面的生成流程可以大致归结如下:
        1. UE4引擎在创建了UGameInstanceSubsystem(其他类型的子系统也是)之后,会创建所有继承自该类型的派生类的所有对象,需要注意的是这些对象都是单例,也就是说每种类型只会生成一个对象,类似于“全局变量”。
        2. UGameInstanceSubsystem初始化时,将在其子类的子系统上调用Initialize()
        3. UGameInstanceSubsystem关闭时,将在其子类的子系统上调用Deinitialize()
        4. 此时将放弃对子系统的引用,如果不再有子系统被引用,则其将被垃圾回收。

      子系统的优点

      • 使用子系统而不是我们自己去创建一个管理类(继承自UGameInstance等等)会为我们带来非常多的便利:
        1. 更方便的管理。不需要我们去手写我们自定义的单例模式管理类,不需要去判断单例,因此会降低我们需要编写的代码量。
        2. 模块化。普通的UGameInstance类,我们在编写逻辑的时候很有可能全部都会塞进这个类型里面,但是子系统允许我们继承自UGameInstanceSubsystem生成很多个自定义子系统,每个子系统都会被生成,因此我们可以很方便将原本臃肿的逻辑给模块化,使代码看起来更加简洁。
        3. 生命周期更加细腻。在五大子系统类型上,不同的子系统类型有着不同的生命周期,需要选择适合我们需求的子系统作为基类派生出我们自定义的类型,远比笼统的使用UGameInstance派生的类型使用一致、但有时候不是必须的生命周期要来得便利。
        4. 上下文菜单访问。虽然子系统看起来和“全局变量”、“全局函数”很相似,但是和这类可以在任意位置调用的变量、函数不同的是,我们在获取对应子系统的时候,获取函数会自动检查当前对象里能否调用到我们的子系统,从而返回实际的子系统对象或空指针。例如我有一个UWorldSubsystem子系统,但是我却在UObject或是UUserWidget里面尝试获取该子系统,即使子系统是确实存在的,但我们实际上只能够获得一个空指针。

      子系统的生命周期

      • 五类子系统的生命周期和其五类Outer对象的生命周期是几乎一致的:
        1. UEngineSubsystem的Outer对象是UEngine GEngine*,代表引擎。无论是Editor模式还是Runtime模式全局范围内只会存在一个该对象。从进程启动开始时候创建,到进程结束时候销毁。
        2. UEditorSubsystem的Outer对象是UEditorEngine GEditor*,代表编辑器。在Editor模式下只会存在一个。从编辑器开始启动开始创建,到编辑器结束销毁。
        3. UGameInstanceSubsystem的Outer对象是UGameInstance GameInstance,代表一场游戏,无论是Editor模式还是Runtime模式都只会存在一个。从游戏启动开始创建,到游戏结束销毁。需要注意的是*Editor模式下只有在点击Play按钮,也就是PIE模式下才会创建,因此也会在点击Stop按钮,也就是结束PIE模式的时候销毁,在这个过程中UGameInstanceSubsystem会不断生成销毁,因为其Outer对象UGameInstance* GameInstance也是在不断生成销毁的。
        4. ULocalPlayerSubsystem的Outer对象是ULocalPlayer LocalPlayer,代表本地玩家,其数量会根据玩家的数量不同而不同,单人游戏中就只会存在一个。会随着第一个LocalPlayer的创建而创建,也会随着最后一个LocalPlayer的销毁而销毁*。
        5. UWorldSubsystem的Outer对象是UWorld World*,代表一个世界。在Runtime模式下只会存在一个,但是会随着关卡的切换(即Persistent Level的切换)而生成销毁,在Editor模式下会存在多个,因为不仅仅是PIE算一个World,编辑器视口里,甚至编辑器本身也会算作是一个World!
        • 总结来说,就是每个子系统都会随着其第一个Outer对象的创建而创建,也会随着最后一个其Outer对象的销毁和销毁。
        • 五类子系统中有两类子系统是属于UDynamicalSubsystem的:UEngineSubsystem和UEditorSubsytem。之所以被称为“动态子系统”是因为这两类子系统及其派生类都是根据其所在模块的加载而创建,随着模块的卸载而销毁

      如何获取到子系统对象

      • 在我们定义好我们自己的子系统类之后,其生成和销毁完全是有引擎控制的,我们只需要通过一些方式即可获取到子系统对象:
        1. UEngineSubsystem
        UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>();
        
        1. UEditorSubsystem
        UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>();
        
        1. UGameInstanceSubsystem
        UGameInstance* GameInstance = ...;
        UMyGameSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameSubsystem>();
        
        1. ULocalPlayer
        ULocalPlayer* LocalPlayer = ...;
        UMyPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyPlayerSubsystem>();
        
        1. UWorldSubsystem
        UWorld* World = ...; 
        UMyWorldSubsystem* MySubsystem=World->GetSubsystem<UMyWorldSubsystem>();
        

      思考:如何用好子系统?

      • Subsystem就是Gameplay级别的Component。就像是Actor可以通过Component来扩展功能一样,Gameplay框架也可以通过Subsystem来扩展功能,并依赖Gameplay框架里面的对象。如果我们需要实现一系列依赖于Gameplay里面的对象(例如依赖UGameInstance,但是又想扩展UGameInstance的功能),就可以选择在Subsystem里面添加逻辑。
      • USubsystem只是一个普通的UObject。一切我们可以在UObject上进行的操作,例如定义变量、定义函数、暴露给蓝图等等的操作,我们在USubsystem里面也同样可以实现。
      • 取代Manager类。我们自己编写Manager类的话,需要很小心的去管理Manager类的生命周期,而USubsystem类已经帮我们掌控好了生命周期,因此我们可以更专心地去编写游戏逻辑。

 

https://zhuanlan.zhihu.com/p/547894317 //一个对于虚幻官方文档比较文本的一个文章   https://zhuanlan.zhihu.com/p/22924838  //也是一个

posted @ 2022-10-27 21:49  蜡笔小新紫南  阅读(397)  评论(0编辑  收藏  举报